大话数据结构 第八章 查找 (读书笔记)

来源:互联网 发布:win10 查看80端口占用 编辑:程序博客网 时间:2024/06/05 21:55

第八章 查找

8.1开场白

8.2查找概论

8.3顺序表查找

8.3.1顺序表查找算法O(n)

顺序查找的算法实现如下:

/*顺序查找,a为数组,n为要查找的数组长度,key为要查找的关键字*/

int  Sequential_Search(int *a, int n, int key)

{

int i;

for(i = 1; i <= n; i++)

{

if(a[i] == key)

 return i;

}

return 0;

}

8.3.2顺序表查找优化

/*有哨兵顺序查找*/

int Sequential_Serach2(int *a, int n, int key)

{

int i;

a[0] = key;/*设置a[0]为关键字,我们称之为“哨兵”*/

i = n;  /*循环从数组尾部开始*/  

while(a[i] != key)

{

i--;

}

return 0;  /*返回0说明查找失败*/

}

 

 

 

8.4有序表查找

8.4.1折半查找(二分查找)O(logn)

前提是线性表中的记录必须是关键码有序(通常为从小到大有序),线性表必须采用顺序存储。

/*折半查找*/

int Binary_Search(int *a, int n, int key)

{

int low, high, mid;

low = 1;  /*定义最低下标为记录首位*/

high = n;  /*定义最高下标为记录末位*/

while(low <= high)

{

mid = (low + high) / 2;/*折半*/

if(key > a[mid]) /*若查找值比中值大*/

low = mid + 1;  /*最高下标调整到中位下标小一位*/

else if(key < a[mid])/*若查找值比中值小*/

 high = mid - 1; /*最高下标调整到中位下标大一位*/

return mid;   /*若相等则说明mid即为查找到的位置*/

}

return 0;

}

8.4.2插值查找O(logn)

/*插值查找(Interpolation_Search)是根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法,其核心就在于插值的计算公式(key - a[low])/(a[high] - a[low])*/

Int Interpolation_Search(int *a, int n, int key)

{

int low, high, mid;

low = 1;  /*定义最低下标为记录首位*/

high = n;  /*定义最高下标为记录末位*/

while(low <= high)

{

mid =   low + (key - a[low]) / (a[high]-a[low]) * (high - low);/*插值变化之处*/

if(key > a[mid]) /*若查找值比中值大*/

low = mid + 1;  /*最高下标调整到中位下标小一位*/

else if(key < a[mid])/*若查找值比中值小*/

 high = mid - 1; /*最高下标调整到中位下标大一位*/

return mid;   /*若相等则说明mid即为查找到的位置*/

}

return 0;

}

 

8.4.3斐波那契查找

斐波那契数列(Fibonacci Search)是利用了黄金分割原理来实现的。

/*斐波那契查找*/

int Fibonacci__Search(int *a, int n, int key)

{

int low, high, mid, i, k;

low = i;  /*定义最低下标为记录首位*/

high = n;/*定义最高下标为记录末位*/

k = 0;

while(n > F[k] - 1) /*计算n在斐波那契数列的位置*/

k++;

for(i = n; i < F[k] - 1; i++) /*将不满的数值补全*/

a[i] = a[n];

while(low <= high)

{

mid = low + F[k-1] -1; /*计算当前分隔的下标*/

if(key < a[mid]) /*若查找记录小于当前分隔记录*/

{

high = mid - 1; /*最高下标调整到分隔下标mid-1处*/

k = k - 1;/*斐波那契数列下标减1*/

}

else if(key > a[mid]) /*若查找记录大于当前分隔记录*/

{

low = mid + 1;/*最低下标调整到分隔下标mid+1处*/

k = k - 2; /*斐波那契数列减两位*/

}

else

{

if(mid <= n)

return mid; /*若相等则说明mid即为查找到的位置*/

else

return n; /*若mid>n说明是补全数值,返回n*/

}

}

return 0;

}

 

注:斐波那契数列查找优于折半查找,可惜如果是最坏情况,比如在这里key=1,那么始终都处于左侧长半区在查找,则查找效率要低于折半查找。

 

 

 

 

8.5线性索引查找

8.5.1稠密索引

稠密索引是指在线性索引中,将数据集中在每个记录对应一个索引项。

对于稠密索引这个索引表来说,索引项一定是按照关键码有序的排列。

 

8.5.2分块索引

分块有序,是把数据集的记录分成了若干块,并且这些块需要满足两个条件

u 块内无序

u 块间有序

我们定义的分块索引项结构分为三个数据项:

u 最大关键码

u 存储了块中的记录个数,以便于循环时使用;

u 用于指向块首数据元素的指针,便于开始对这一块记录进行遍历。

在分块索引表中查找,就是分两步进行:

① 在分块索引表中查找关键字所在的块。由于分块索引表示块间有序的,因此很容易利用折半/插值等算法得到结果。

② 根据首指针找到相应的块,并在块中顺序查找关键码。因为块中可以是无序的,因此只能顺序查找。

 

 

8.5.3倒排索引

索引项的通用的结构:

u 次关键码

u 记录表号

其中记录号表存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的主关键字)。这样的索引方法就是倒排索引(inverted index)。倒排索引源于实际应用中需要根据属性(或字段/次关键码)的值来查找记录。这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

8.6二叉树排序

二叉树排序树(Binary Sort Tree),又称为二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树。

u 若它的左子树不空,则左子树上所有结点的值均小于它的根结构的值。

若它的右子树不空,则右子树上所有 结点的值均大于它的根结点的值。

u 它的左、右子树叶分别为二叉排序树。

注:构造一棵二叉树排序树的目的,其实并不是为了排序,而是为了提高查找和插入删除关键字的速度。

 

8.6.1二叉排序树查找操作

/*二叉树的二叉链表结点结构定义*/

typedef struct BiTNode  /*结点结构*/

{

int data; /*结点数据*/

struct BiTNode *lchild, *rchild; /*左右孩子指针*/

}BiTNode, *BiTree;   

/*递归查找二叉排序树T中是否存在key*/

/*指针f指向T的双亲,其初始调用值为NULL*/

/*若查找成功,则指针p指向该数据元素结点,并返回NULL*/

/*否则指针p指向查找路径上访问的最后一个结点并返回false*/

Status SearchBST(BiTree T, int key, BiTree f, BiTree *p)

{

if(!T) /*查找不成功*/

{

*p = f;

return FALSE;

}

else if(key == T->data) /*查找成功*/

{

*p = T;

return TRUE;

}

else if(key < T->data)

return SearchBST(T->lchild, key, T, p) /*在左子树继续查找*/

else

return SearchBST(T->rchild, key, T, p) /*在右子树继续查找*/

}

 

 

 

 

 

 

8.6.2二叉排序树插入操作

/*当二叉排序树T中不存在关键字等于key的数据元素时*/

/*插入key并返回TRUE,否则返回FALSE*/

Status InsertBST(BiTree *T, int key)

{

BiTree p, s;

if(!SearchBST(*T, key, NULL, &p))/*查找不成功*/

{

s = (BiTree)malloc(sizeof(BiTree))

s->data = key;

s->lchild = s->rchild = NULL;

if(!p)

*T = s; /*插入s为新根结点*/

else if(key < p->data)

p->lchild = s; /*插入s为左孩子*/

else

p->rchild = s; /*插入s为右孩子*/

return TRUE;

}

else  

return FALSE;/*树中已有关键字相同的结点,不再插入*/

}

 

补充:构建一棵二叉排序树,代码如下:

int i;

int a[10] = {62, 88, 58, 47, 35, 73, 51, 99, 37, 93};

BiTree T = NULL;

for(i = 0; i < 10; i++)

{

InsertBST(&T, a[i]);

}

8.6.3二叉排序树删除操作

根据我们对删除结点三种情况的分析:

u 叶子结点

u 仅有左或右子树的结点

u 左右子树都有的结点,我们来看代码,下面这个算法是递归方式对二叉排序树T查找key,查找到时删除。

/*若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点*/

/*并将返回TRUE,否则返回FALSE*/

Status DeleteBST(BiTree *T, int key)

{

if(!*T) /*不存在关键字等于key的数据元素*/

return FALSE;

else

{

if(key == (*T)->data) /*找到关键字等于key的数据元素*/

return Delete(T);

if(key < (*T)->data)

return DeleteBST((*T)->lchild, key);

if(key > (*T)->data) 

return DeleteBST((*T)->rchild, key);

}

}

这段代码和前面的二叉排序树查找几乎完全相同,唯一的区别就在于第8行,

此时执行的是Delete方法,对当前结点进行过删除操作,用其前驱结点来替换(当然也可以用后继结点来替换),我们来看Delete代码。

/*从二叉排序树中删除结点p,并重接它的左或右子树*/

Status Delete(BiTree *p)

{

BiTree q, s;

if((*p)->rchild == NULL) /*右子树空则只需要重接它的左子树*/

{

q = *p; *p = *p->lchild; free(q);

}

else if((*p)->lchild == NULL) /*左子树空则只需要重接它的右子树*/

{

q = *p; *p = *p->rchild; free(q);

}

else /*左右子树均不为空*/

{

q = *p; s = (*p)->lchild;

while(s->rchild) /*转左,然后向右到尽头(找待删结点的前驱)*/

{

q = s; s = s->rchild;

}

(*p)->data = s->data; /*s指向被删结点的直接前驱*/

if(q != *p)

q->rchild = s->lchild; /*重接q的右子树*/

else

q->lchild = s->lchild; /*重接q的左子树*/

free(s);

}

return TRUE;

}

8.6.4二叉排序树总结

总之,二叉排序树是以链接的方式存储,保持了链接存储结构在执行插入或删除操作时不用移动元素的优点,只要找到的合适的插入和删除位置后,仅需要修改链接指针即可。插入删除的时间性能比较好。而对于二叉排序树的查找,走的就是从根结点到要查找的结点的路径,其比较次数等于给定值的结点在二叉排序树的层数。极端情况,最少为1次,即根结点就是要找的结点,最多也不会超过树的深度。也就是说,二叉排序树的查找性能取决于二叉排序树的形状。可问题就在于,二叉排序树的形状是不确定的。因此我们希望对一个集合按二叉排序树查找,最好是把它构成一棵平衡二叉排序树,这样我们就引申出另一个问题,如何二叉排序树平衡的问题。

 

8.7平衡二叉树(AVL树)

平衡二叉树(Self-Balancing Binary Search Tree 或 Height-Balanced Binary Search Tree),是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1。

它是一种高度平衡的二叉排序树,意思是说,要么它是一棵空树,要么它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。我们将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor),那么平衡二叉树上所有结点的平衡因子只可能是-1,0和1。只要二叉树上有一个接地单的平衡因子的绝对值大于1,则该二叉树就是不平衡的。

距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,我们称为最小不平衡子树。

 

8.7.1平衡二叉树实现原理

平衡二叉树构建的基本思想就是在构建二叉排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。

在构建平衡树过程中,如果BF的值为正,因此我们将整个树进行右旋(顺时针旋转)。

如果BF的值为负值,所以我们对这棵最小平衡子树进行左旋(逆时针旋转)。假如发现两个BF一正一负,符号并不统一时,就需要对结点先进行一次旋转以使得符号相同后,再反向旋转一次才能够完成平衡操作。

 

8.7.2平衡二叉树实现算法

 

/*二叉树的二叉链表结点结构定义*/

typedef struct BiTNode  /*结点结构*/

{

int data;  /*结点数据*/

int bf;/*结点的平衡因子*/

struct BiTNode *lchild, *rchild; /*左右孩子指针*/

} BiTNode, *BiTree;

对于右旋操作,代码如下:

void R_Rotate(BiTree *p)

{

BiTree L;   

L = (*p)->lchild;  /*L指向p的左子树根结点*/

(*p)->lchild = L->rchild; /*L的右子树挂接为p的左子树*/

L->rchild = (*p);

*p = L; /*p指向新的根结点*/           

}

左旋操作,代码如下:

/*对于p为根的二叉排序树作左旋处理*/  

/*处理之后p指向新的树根结点,即旋转处理之前的右子树的根结点0*/

void L_Totate(BiTree *p)

{

BiTree R;  

R = (*p)->rchild;   /*R指向p的右子树根结点*/

(*p)->rchild = R->lchild;/*R的左子树挂接为p的右子树*/

R->rchild = (*p);

(*p) = R;/*p指向新的根结点*/

}

现在我们来看左平衡旋转处理的函数代码:

#define LH +1 /*左高*/

#define EH 0  /*等高*/

#define RH -1 /*右高*/

/*对以指针T所指结点为根的二叉树作左平衡旋转处理*/

/*本算法结束时,指针T指向新的根结点*/

void LeftBalance(BiTree *T)

{

BiTree L, Lr;

L = (*T)->lchild;  /*L指向T的左子树根结点*/

switch(L->bf)

{ /*检查T的左子树的平衡度,并作相应平衡处理*/

case LH:

 (*T)->bf = L->bf = EH; /*新结点插入在T的左子树上,要作单右旋处理*/

 R_Rotate(T);

 break;

case RH:    /*新结点插入在T的左孩子的右子树上,要作双旋处理*/

 Lr = L->rchild; /*Lr指向T的左孩子的右子树根*/

 switch(Lr->bf) /*修改T及其左孩子的平衡因子*/

 {

 case LH:  (*T)->bf = RH;

 L->bf = EH;

 break;

 case EH:   (*T)->bf = L->bf = EH;

 break;

 case RH:  (*T)->bf = EH;

 L->bf = LH;

 break;

}

 Lr->bf = EH;

 L_Rotate(&(*T)->lchild); /*对T的左子树作左旋平衡处理*/

 R_Rotate(T); /*对T作右旋平衡处理*/

}

}

有了上述这些准备,我们的主函数才算是正式登场了。

/*若在平衡二叉排序树T中不存在和e有相同的关键字的结点,则插入一个*/

/*数据元素为e的新结点并返回1,否则返回0.若因插入而使二叉排序树*/

/*失去平衡,则作平衡旋转处理,布尔变量taller反映T长高与否*/

Status InsertAVL(BiTree *T, int e, Status *taller)

{

if(!*T)

{ /*插入新结点,树“长高”,置taller为TRUE*/

*T= (BiTree)malloc(sizeof(BiTree));

(*T)->data = e;

(*T)->lchild = (*T)->rchild = NULL;

(*T)->bf = EH;

*taller = TRUE;

}

else

{

if(e == (*T)->data)

{ /*树中已存在和e有相同关键字的结点则不再插入*/

*taller = FALSE;

return FALSE;

}

if(e < (*T)->data)

{ /*应继续在T的左子树中进行搜索*/

if(!InsertAVL(&(T)->lchild, e, taller)) /*未插入*/

reutrn FALSE;

if(*taller) /*已插入到T的左子树中且左子树“长高”*/

{

switch((*T)->bf) /*检查T的平衡度*/

{

 case LH: /*原本左子树比右子树高,需要作左平衡处理*/

LeftBalance(T);

*taller = FALSE;

break;

case EH: /*原本左右子树等高,现因左子树增高而树增高*/

(*T)->bf = LH;

*taller = TRUE;

break;

case RH: /*原本右子树比左子树高,现左右子树等高*/

(*T)->bf = EH;

*taller = FALSE;

break;

}

}

}

else

{ /*应继续在T的右子树中进行搜索*/

if(!InsertAVL(&(*T)->rchild), e, taller)/*未插入*/

return FALSE;

if(*taller) /*已插入到T的右子树且右子树“长高”*/

{

switch((*T)->bf) /*检查T的平衡度*/

{

case LH:  /*原来左子树比右子树高,现左,右子树等高*/

(*T)->bf = EH;

*taller = FALSE;

break;

case EH: /*原来左、右子树等高,现因右子树增高而树增高*/

(*T)->bf = RH;

*taller = TRUE;

break;

case RH: /*原本右子树比左子树高,需要作右平衡处理*/

RightBalance(T);

*taller = FALSE;

break;

}

}

}

}

return TRUE;

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

8.8多路查找树(B树)

多路查找树muitl-way search tree),其每一个结点的孩子数可以多于两个,且每一个结点处可以存储多个元素。由于它是查找树,所有元素之间存在某种特定的排序关系。

 

8.8.1  2-3树

l 2-3树是这样的一棵多路查找树:其中的每一个结点都具有两个孩子(我们称它为2结点)或三个孩子(我们称它为3结点)。

一个2结点包含一个元素和两个孩子(或没有孩子),且与二叉排序树类似,左子树包含的元素小于该元素,右子树包含的元素大于该元素。

一个3结点包含一小一大两个元素和三个孩子(或没有孩子),一个3结点要么没有孩子,要么有三个孩子。如果某个3结点有孩子的话,左子树包含小于较小元素的元素,右子树包含大于该元素的元素,中间子树包含介于两个两元素之间的元素。

 

① 2-3树的插入实现(三种情况)

        1).对于空树,插入一个2结点即可,这很容易理解。

2).插入结点到一个2结点的叶子上。

3).要往3结点中插入一个新元素。因为3结点本身已经是2-3树的结点最大容量(已经有两个元素),因此就需要将其拆分,且将树中两元素或插入元素的三者中选择其一向上移动一层。复杂的情况也正在于此。如果上面一层也是两个元素,那就再往上移动一层。如果一直移动到到根结点还是两个元素的话,那就将根结点拆分,则树的高度会增高。

 

② 2-3树的删除实现(三种情况)

1).所删除元素位于一个3结点的叶子结点上。

2).所删除的元素位于非叶子的分支结点。此时我们通常是将树按中序遍历后得到此元素的前驱或后继元素,考虑让它们来补位即可。

3).所删除的元素位于一个2结点上,即要删除的是一个只有一个元素的结点。如果按照以前树的理解,删除即可,可现在的2-3定义告诉我们这样做是不可以的。因为直接删除后就不满足定义了。

  因此,对于我们删除叶子是2结点的情况,我们需要分四种情况来处理。

情形一:此结点的双亲也是2结点,且拥有一个3结点的右孩子。(删除后左旋)

情形二:此结点的双亲是2结点,它的右孩子也是2结点。(变形,补充,左旋)

情形三:此结点的双亲是一个3结点。(拆分3结点)

情形四:如果当前树是一个满二叉树的情况,此时删除任何一个叶子都会使得整棵树不能满足2-3树的定义。(减层)

 

8.8.2  2-3-4树

其实就是2-3树的概念扩展,包括了4结点的使用。一个4结点包含小中大三个元素和四个孩子(或没有孩子)。

 

 

 

 

8.8.3  B树

B树(B-tree)是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例。结点最大的孩子数目称为B树的阶(order),因此,2-3树是3阶B树。2-3-4树是4阶B树。

至于B树的插入和删除,方式是与2-3树和2-3-4树相类似的,只不过阶数可能会很大而已。

 

8.8.4  B+树

u B+树是应文件系统所需而出的一种B树的变形树。注意严格意义上讲,它其实已经不是第六章定义的树了。在B树中,每一个元素在该树中只出现一次,有可能在叶子结点上。而在B+树中,出现在分支结点中的元素会被当做它们在该分支结点位置的中序后继者(叶子结点)中再次列出。另外,每一个叶子结点都会保存一个指向后一叶子结点的指针。

u 如果我们是需要从最小关键字进行从小到大的顺序查找,我们就可以从最左侧的叶子结点出发,不经过分支结点,而是延着指向下一叶子的指针就可以遍历所有的关键字。

u B+树的插入、删除过程也都与B树类似,只不过插入和删除的元素都是在叶子结点上进行而已。

8.9散列表查找(哈希表)概述

8.9.1散列表查找定义

存储位置 = f(关键字)

散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。若查找集合中存在这个记录,则必定在f(key)的位置上。

我们把这种对应关系f称为散列函数,又称为哈希(Hash)函数。按这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称散列表或哈希表(Hash table)。那么关键字对应的记录存储位置我们称为散列地址。

 

8.9.2散列表查找步骤

散列技术既是一种存储方法,也是一种查找方法。然而它与线性表、树、图等结构不同的是,前面几种结构,数据元素之间都存在某种逻辑关系,可以用连线图表示出来,而散列技术的记录之间不存在逻辑关系,它只与关键字有关联。因此,散列主要是面向查找的存储结构。

散列技术最适合的求解问题是查找与给定值相等的记录。

设计一个简单、均匀、存储利用率高的散列函数是散列技术中最关键的问题。

我们时常会遇到两个关键字key1≠key2,但是却有f(key1)=f(key2),这种现象我们称为冲突(collision)并把key1和key2称为这个散列函数的同义词(synonym)。

8.10 散列表的构造方法

参考原则:计算简单、散列地址分布均匀

8.10.1直接定址法  f(key) = a*key + b;(a,b为常数)

这样的散列函数优点是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合查找表较小且连续的情况。由于这样的限制,在现实应用中,此方法虽简单,但却并不常用。

 

 

8.10.2数字分析法 (抽取)

抽取方法是使用关键字的一部分来计算散列表存储位置的方法,这在散列函数是常常用到的手段。数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑这个方法、

 

8.10.3平方取中法  (先平方在抽取其中几位作为散列地址)

平方取中法比较适合不知道关键字的分布,而位数又不是很大的情况。

 

8.10.4折叠法

折叠法比较适合不知道关键字的分布,适合关键字位数较多的情况。

 

8.10.5除留余数法

f(key)= key mod p (p<=m)

若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。

 

8.10.6随机数法  f(key)= random(key)  (random为随机函数)

 当关键字的长度不等时,采用这个方法构造散列函数比较合适。

 

总之,现实中,应该视不同的情况采用不同的散列函数。我们只能给出一些考虑的因素来提供参考:

① 计算散列地址所需的时间。

② 关键字的长度。

③ 散列表的大小。

④ 关键字的分布情况。

⑤ 记录查找的频率。综合这些因素,才能决策选择哪种散列函数更合适。

 

 

8.11处理散列冲突的方法

8.11.1开放定址法

u 一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。(线性探测法

fi(key) = (f(key)+di) MOD m (di = 1,2,3,....,m-1)

:本来都不是同义词却需要争夺一个地址的情况,我们称这种现象为堆积

 

u 增加平方运算的目的是为了不让关键字聚集在某一块区域。我们称之为二次探测法

fi(key) = (f(key)+di) MOD m(di = 12,-12,22-22...,q2-q2

 

在冲突时,对于位移量di采用随机函数计算得到,我们称之为随机探测法

fi(key) = (f(key)+di) MOD m (di是一个随机数列)

8.11.2再散列函数法

事先准备多个散列函数

fi(key) = RHi(key) (i= 1,2,3,...,k)

 

8.11.3链地址法

将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词子表,在散列表

中只存储所有同义词子表的头指针。链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。当让这也带来了查找时需要遍历单链表的性能损耗。

 

8.11.4公共溢出区法

我们为所有冲突的关键字建立了一个公共的溢出区来存放。在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表去进行顺序查找。如果相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。

 

8.12散列表查找实现

8.12.1散列表查找算法实现

首先需要定义一个散列表的结构以及相关的常数。其中HashTable就是散列表结构。结构中的elem为一个动态数组

#define SUCCESS 1

#define UNSUCCESS 0

#define HASHSIZE 12 /*定义散列表长为数组的长度*/

#define NULLKEY -32768

typedef struct

{

int *elem;  /*数据元素存储基址,动态分配数组*/

int count;  /*当前数据元素个数*/

}HashTable;

int m = 0;/*散列表表长,全局变量*/

有了结构的定义,我们可以对散列表进行初始化。

/*初始化散列表*/ 

Status InitHashTable(HashTable *H)

{

int i;

m = HASHSIZE;

H->count = m;

H->elem = (int *)malloc(m*sizeof(int));

for(int i = 0; i < m; i++)

H->elem[i] = NULLKEY;

return OK;

}

/*散列函数*/

int Hash(int key)

{

return key % m; /*除留余数法*/

}

/*插入关键字进散列表*/

void InsertHash(Hashtable *H, int key)

{

int addr = Hash(key); /*求散列地址*/

while(H->elem[addr] != NULLKEY) /*如果不为空,则冲突*/

addr = (addr + 1) % m; /*开放地址法的线性探测*/

H->elem[addr] = key; /*直到有空位后插入关键字*/

}

/*散列表查找关键字*/

Status SearchHash(HashTable H, int key, int *addr)

{

*addr = Hash(key); /*求散列地址*/

while(H.elem[*addr] != key) /*如果不为空,则冲突*/

{

*addr = (*addr+1) % m; /*开放地址法的线性探测*/

if(H.elem[*addr] == NULLKEY || *addr == Hash(key))

{/*如果循环回到原点*/

return UNSUCCESS; /*则说明关键字不存在·*/

}

}

return SUCCESS;

}

 

 

8.12.2散列表查找性能分析

① 散列函数是否均匀

② 处理冲突的方法

③ 散列表的装填因子

装填因子α= 填入表中记录个数/散列表长度

α标志着散列表的装满的程度)

8.13总结回顾

8.14结尾语

 

 

原创粉丝点击