查找

目录

 

1 基本概念

2 顺序表查找

3 折半查找

4 线性索引查找

4.1 稠密索引

4.2 分块索引

4.3 倒排索引

5 二叉排序树

5.1 二叉排序树查找操作

5.2 二叉排序树插入操作

5.3 二叉排序树删除操作

5.4 二叉排序树总结

6 平衡二叉树(AVL树)

6.1 平衡二叉树实现原理

6.2 平衡二叉树实现算法

7 多路查找树(B树)

7.1 2-3树

7.1.1 2-3树的插入实现

7.1.2 2-3树的删除实现

7.2 2-3-4树

7.3 B树

7.4 B+树

8 散列表查找(哈希表)

8.1 散列函数的构造方法

8.2 处理散列冲突的方法


1 基本概念

查找表是由同一类型的数据元素(或记录)构成的集合。

静态查找表:只作查找操作的查找表。

动态查找表:在查找过程红同时插入查找吧中国呢不存在的数据元素,或者从查找表中删除已经存在的某个数据元素。

2 顺序表查找

顺序查找又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功过,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。

3 折半查找

折半查找又称二分查找,它的前提是线性表中的记录必须是关键码有序,线性表必须采用顺序存储。

折半查找的记录思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右搬去继续查找,不断重复这个过程,直到查找成功,或所有查找区域无记录,查找失败为止。

/* 折半查找 */
int binarySearch(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]) {    /* 若查找值比中值小 */
          high = mid - 1;    /* 最高下标调整到中位下标小一位 */
      } else if(key > a[mid]) {  /* 若查找值比中值大 */
          low = mid + 1;     /* 最低下标调整到中位下标大一位 */
      } else {
          return mid;    /* 若相等则说明mid即为查找到的位置 */
      }
   }
   return 0;
}

4 线性索引查找

索引是为了加快查找速度而设计的一种数据结构,索引就是把一个关键字与它对应的记录相关联的过程。一个索引由若干个索引项构成,每个索引项至少应包含关键字和其对应的记录在存储其中的位置等信息。索引技术是组织大型数据库以及磁盘文件的一种重要技术,索引按照结构可以分为线性索引、树形索引和多级索引。

线性索引就是将索引项集合组织成线性结构,也称为索引表。

4.1 稠密索引

稠密索引:稠密索引是指线性索引中,将数据集中的每个记录对应一个索引项,对于稠密索引这个索引表来说,索引项一定是按照关键码有序的排列。索引项有序也就意味着,查找关键字时,可以使用折半查找算法,大大提高效率。

稠密索引因为索引项与数据集的记录个数相同,所以空间代价很大,为来减少索引项的个数,可以对数据集进行分块,使其分块有序,然后再对每一块建立一个索引项,从而减少索引项的个数。

4.2 分块索引

分块索引:对于分块有序的数据集,将每块对应一个索引项

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

  • 块内无序,即每一块内的记录不要求有序,如果能够让块内有序对查找来说更理想,不过这就要付出大量时间和空间的代价,因此通常不要求块内有序。
  • 块间有序,块间有序,才有可能在查找时带来效率

分块索引的索引项结构分三个数据项:

  • 最大关键码,它存储每一块中的最大关键字,这样的好处就是可以使得在它之后的下一块中的最小关键字也能比这一块最大的关键字要大;
  • 存储了块中的记录个数,以便于循环时使用;
  • 用于指向块数据元素的指针,便于开始对这一块中记录进行遍历。

在分块索引表中查找,分两步:

  • 在分块索引表中查找要查关键字所在的块,由于分块索引表是块间有序的,因此很容易利用折半得到结果。
  • 根据块首指针找到相应的块,并在块中顺序查找关键码,因为块中可以是无序的,因此只能顺序查找。

4.3 倒排索引

倒排索引,索引项的通用结构是:次关键码,记录号表。

其中记录号表存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的关键字),这样的索引方法就是倒排索引。倒排索引源于实际应用中需要根据属性的值来查找记录,这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址,由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引。

倒排索引的优点显然就是查找记录非常快,基本等于生成索引表后,查找时都不用去读取记录,就可以得到结果。

5 二叉排序树

二叉排序树,又称为二叉查找树,它或者是一棵空树,或者是具有下列性质的二叉树:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结构的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
  • 它的左、右子树也分别为二叉排序树

5.1 二叉排序树查找操作

/* 二叉树的二叉链表结点结构定义 */
typedef struct BitNode { /* 结点结构 */
   int data;   /* 结点数据 */
   struct BitNode *lchild, *rchild;
}BitNode, *BiTree;

/* 递归查找二叉排序树T中是否存在key */
/* 指针f指向T的双亲,其初始调用值为NULL */
/* 若查找成功,则指针p指向该数据元素结点,并返回TRUE */
/* 否则指针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); /* 在右子树继续查找 */
   }
}

5.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(BiTNode));
       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;  /*树中已有关键字相同的结点,不再插入*/
   }
}

5.3 二叉排序树删除操作

二叉排序树的删除,不能因为删除了结点,而让这棵树变得不满足二叉排序树的特性,所以删除需要考虑多种情况。

如果删除的是二叉树中的叶子结点,那是很容易的,毕竟删除叶子结点对整棵树来说,其他结点的结构并未收到影响。例如:

如果删除的结点只有左子树或只有右子树,删除结点后,将它的左子树或右子树整个移动到删除结点的位置即可,可以理解为独子继承父业。例如:

如果要删除的结点即有左子树又有右子树的情况,对二叉树进行中序遍历,找到需要删除的结点p的直接前驱(或直接后继)s,用s来替换结点p,然后在删除此结点s。例如:

 

 

/* 若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点,并返回TRUE,否则返回FALSE */
Status deleteBST(BiTree *T, int key){
   if(!*T) { /* 不存在关键字等于key的数据元素 */
      return FALSE;
   }else{
      if(key == (*T)->data) { /*找到关键字等于key的数据元素*/
         return delete(T);
      }else if(key < (*T)->data) {
         return deleteBST(&(*T)->lchild, key);
      }else {
         return deleteBST(&(*T)-rchild, key);
      }
   }
}

/* 从二叉排序树中删除结点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->lcihld; /*重接q的右子树*/
      }else{
         q->lchild = s->lchild; /*重接q的左子树*/
      }
      free(s);

   }
   return TRUE;
}

5.4 二叉排序树总结

二叉排序树是以链接的方式存储,保持了链接存储结构在执行插入或删除操作时不用移动元素的优点,只要找到合适的插入和删除位置后,仅需修改链接指针即可,插入删除的时间性能比较好,而对于二叉排序树的查找,走的就是从根结点到要查找的结点的路径,其比较次数等于给定值的结点在二叉排序树的层数,极端情况,最少为1次,即根结点就是要找的结点,最多也不会超过树的深度。

 

6 平衡二叉树(AVL树)

平衡二叉树,是一种二叉排序树,其中每一个结点的左子树和右子树的高度差至多等于1。平衡二叉树它是一种高度平衡的二叉排序树,它要么是一棵空树,要么它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上所有结点的平衡因子只可能是-1、0和1,只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。

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

6.1 平衡二叉树实现原理

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

例如:数据a[10]={3,2,1,4,5,6,7,10,9,8}构建二叉排序树。

前两个数3,2按照排序树进行构建,到了第3个数“1”时,发现根结点3的平衡因子变成了2,此时整棵树都变成了最小不平衡树,因此需要调整,因为BF值为正,因此将整个树进行右旋(顺时针旋转),此时结点2成了根结点,3成了2的右孩子,这样三个结点的BF值均为0,非常平衡,如下图:

然后在增加结点4,平衡因子没发生变化,如上图3。增加结点5时,结点3的BF值为-2,说明需要旋转了。由于BF是负值,所以对这棵最小平衡子树进行左旋(逆时针旋转),如下图:

当增加结点10时,结构无变化,如下图10,如果在增加结点9,此时结点7的BF变成了-2,理论上我们只需要旋转最小不平衡子树7、9、10即可,但是如果左旋转后,结点9就成了10的右孩子,这是不符合二叉排序树的特性,此时不能简单的左旋。如下图11:

仔细观察图11,发现根本原因在于结点7的BF是-2,而结点10的BF是1,也就是说,它们两一正一负,符号并不统一,而前面的几次旋转,无论左还是右旋,最小不平衡子树的根结点与它的子结点符号都是相同的,这就是不能直接旋转的关键。那怎么办呢?不统一,不统一就把它们先转到符号统一,于是先对结点9和结点10进行右旋,使得结点10成了9的右子树,结点9的BF为-1,此时就与结点7的BF值符号统一了,这样再以结点7为最小不平衡子树进行左旋,得到如下图:

6.2 平衡二叉树实现算法

/*二叉树的二叉链表结点结构定义*/
typedef struct BiTNode{  /*结点结构*/
   int data;   /*结点数据*/
   int bf;     /*结点的平衡因子*/
   struct BiTNode *lchild, *rchild; /*左右孩子指针*/ 
}BiTNode, *BiTree;

/*对以p为根的二叉排序树作右旋处理*/
/*处理之后p指向新的树根结点,即旋转处理之前的左子树的根结点*/
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_Rotate(BiTree *p){
   BiTree R;
   R = (*p)->rchild;   /*R指向p的右子树根结点*/
   (*p)->rchild = R->lchild;  /*R的左子树挂接为p的右子树*/
   R->lchild = (*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的左孩子的左子树上,要作单右旋处理*/
          (*T)->bf = L-bf = EH;
          r_Rotate(T);
          break;
       case RH: /*新结点插入在T的左孩子的右子树上,要作双旋处理*/
          Lr = L->rchild; /*Lr指向T的左孩子的右子树根*/
          switch(Lr->br){ /*修改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(BiTNode));
      (*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)) { /*未插入*/
             return 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;
}

 

7 多路查找树(B树)

前面讨论过的数据结构,处理数据都是在内存中,因此考虑的都是内存中的运算时间复杂度。但若需要操作的数据集非常大,大到内存已经没办法处理,如数据库、硬盘中上万个文件,在这种情况下,对数据的处理需要不断从硬盘等存储设备中调如或调出内存页面。一旦涉及到这样的外部存储设备,关于时间复杂度的计算就会发生变化,访问该集合元素的时间已经不仅仅是寻找该元素所需比较次数的函数,我们必须考虑对硬盘等外部存储设备的访问时间以及将会对该设备作出多少次单独访问。前面的树结构都是一个结点只能存储一个元素,在元素非常多的时候,要么树的度非常大,要么树的高度非常大,这使得内存存取外存次数非常多,这显然成了时间效率上的瓶颈。

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

7.1 2-3树

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

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

一个3结点包含一小一大两个元素和三个孩子(或没有孩子),这个3结点要么没有孩子,要么就有三个,不能只有一个孩子。如果某个3结点有孩子的话,左子树包含小于较小元素的元素,右子树包含大于较大元素的元素,中间子树包含介于两元素之间的元素。并且2-3树中所有的叶子都在同一层次上。

7.1.1 2-3树的插入实现

对于2-3树的插入来说,与二叉排序树相同,插入操作一定是发生在叶子结点上,可与二叉排序树不同的是,2-3树插入一个元素的过程有可能会对该树的其余结构产生连锁反应。

2-3 树插入可分为3种情况:

  • 对于空树,插入一个2结点即可
  • 插入结点到一个2结点的叶子上,由于其本身就只有一个元素,所以只需要将其升级为3结点即可,
  • 要往3结点中插入一个新元素,因为3结点本身已经是2-3树的结点最大容量(已经有两个元素),因此就需要将其拆分,且将树中两元素或插入元素的三者中选择其一向上移动一层。

例如:

                  

在如上左图插入元素3,根据遍历可知,3比8、4都小,所以就只能考虑插入到叶子结点1的位置,因此就需要将此结点变成3结点如上右图。

如上左图中插入元素5,5比8小,比4大,所以需要插入在拥有6、7元素的3结点位置。问题就在于,6和7结点已经是3结点,不能在加。此时发现它的双亲结点4是个2结点,因此考虑让它升级为3结点,这样它得有三个孩子,于是就想到将6、7结点拆分,让6与4结成3结点,将5称为它的中间孩子,将7称为它的右孩子,如上右图。

通过这个例子,2-3树插入的传播效应导致两根结点的拆分,则树的高度就会增加。

7.1.2 2-3树的删除实现

2-3树的删除也分为3种情况:

所删除元素位于一个3结点的叶子结点上,只需要在该即诶单处删除该元素即可,不会影响到整棵树的其他结构,如下图删除元素9,只需要将此结点改成只有元素10的2结点即可。

所删除的元素位于一个2结点上,即要删除的是一个只有一个元素的结点,如上图删除元素1,那么结点4就不满足平衡树定义了,所以要分如下情况处理:

  • 情形1,此结点的双亲也是2结点,且拥有一个3结点的右孩子,如下图,删除结点1,那么只需要左旋,即6称为双亲,4成为6的左孩子,7是6的右孩子。
  •    
  • 情形2,此结点的双亲是2结点,它的右孩子也是2结点。此时删除结点1,如果直接左旋会造成没有右孩子,因此需要对整棵树变形,办法就是,目标是让结点7变成3结点,那就得让比7稍大的元素8下来,随即就得让比元素8稍大的元素9补充结点8的位置,然后在使用左旋的方式。
  • 情形3,此结点的双亲是一个3结点,此时删除结点10,意味着双亲12、14这个结点不能称为3结点了,于是将此结点拆分,并将12与13合并成为左孩子。
  • 情形4,如果当前树是一个满二叉树的情况,此时删除任何一个叶子都会使得整棵树不能满足2-3树的定义。删除叶子结点8时(其实删除任何一个结点都一样),就不得不考虑要将2-3的层数减少,办法是将8的双亲和其左紫薯6合并为-3个结点,再将14与9合并为3结点。所删除的元素位于非叶子的分支结点,此时通常是将树按中序遍历后得到此元素的前驱或后继元素,考虑让他们来补位即可。

 

如果删除的分支结点是2结点,如下图要删除元素4结点,分析后得到它的前驱是1后继是6,显然由于6、7是3结点,只需要用6来补位即可。

如果要删除的分支结点是3结点的某一元素,如下图要删除元素12、14中元素12,经过分析显然应该是将3结点的左孩子的10上升到删除位置合适。

 

7.2 2-3-4树

2-3-4树就是2-3树的扩展,包括来4结点的使用,一个4结点包含小中大三个元素和四个孩子(或没有孩子),一个4结点要么没有孩子,要么具有4个孩子,如果某个4结点有孩子的话,左子树包含小于最小元素的元素;第二子树包含大于最小元素,小于第二元素的元素;第三子树包含大于第二元素,小于最大元素的元素;右子树包含大于最大元素的元素。

如下图构建2-3-4树

如下图删除结点

7.3 B树

B树是一种-平衡的多路查找树,2-3树和2-3-4树都是B树的特例。即诶单最大的孩子数目成为B树的阶。

一个m阶的B树具有如下属性:

  • 如果根结点不是叶结点,则其至少有两棵子树
  • 每一个非根的分支结点都有k-1个元素和k个孩子,其中\left \lceil m/2 \right \rceil\leq k\leq m。每一个叶子结点n都有k-1个元素,其中\left \lceil m/2 \right \rceil\leq k\leq m
  • 所有叶子结点都位于同一层次

在B树上查找的过程是一个顺指针查找结点和在结点中查找关键字的交叉过程,比如,我们要查找数字7,首先从外存读取得到根结点3、5、8三个元素,发现7不在当中,但在5和8之间,因此就通过A2再读取外存的6、7结点,查找到所要的元素。

在一个典型的B树应用中,要处理的硬盘数据量很大,因此无法一次全部装入内存,因此对B树进行调整,使得B树的阶数与硬盘存储的页面大小相匹配,比如一棵B树的阶为1001,高度为2,它可以存储超过10亿个关键字,那么只需要让根结点持久地保留在内存中,那么在这棵树上,寻找某一个关键字至多需要两次硬盘的读取即可。

7.4 B+树

B+树是应文件系统所需而出的一种B树的变形树。在B树中,每一个元素在该树中只出现一次,有可能在叶子结点上,也有可能在分支结点上,而在B+树中,出现在分支结点中的元素会被当作他们在该废纸结点位置的中序后继再次列出。另外,每一个叶子结点都会保存一个指向后一叶子结点的指针。

一棵m阶的B+树和m阶的B树的差异在于:

  • 有n棵子树的结点中包含有n个关键字;
  • 所有的叶子结点包含全部关键字的信息,及指向含这些关键字记录的指针,叶子结点本身依关键字的大小自小而大顺序链接;
  • 所有分支结点可以看成是索引,结点中仅含有其子树中的最大(或最小)关键字。

8 散列表查找(哈希表)

散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f。把对应关系f称为散列函数,又称为哈希函数。采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。那么关键字对应的记录存储位置称为散列地址。

整个散列表查找的步骤其实就是两步:

  • 在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录。
  • 查找记录时,通过统一的散列函数计算记录的散列地址,按此散列地址访问该记录。

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

8.1 散列函数的构造方法

散列函数的原则:计算简单,散列地址分布均匀

直接地址法:

取关键字的某个线性函数值为散列地址,即:f(key)=a*key+b,(a、b为常数)

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

数字分析法:

从数字中抽取部分来计算散列存储位置的方法,数字分析法常适合处理关键字位数比较大的情况,需要事先知道关键字的分布且关键字的若干位分布较均匀。

平方取中法:先把数字平方,然后取数字中间位用做散列地址,平方取中法比较适合于不知道关键字的分布,而位数又不是很大的情况。

折叠法:折叠法是将关键字从左到右分割成位数相等的几部分,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

除留余数法:

对于散列表长为m的散列函数公式为:f(key) = key mod p,(p<=m)

这方法不仅可以对关键字直接取模,也可在折叠、平方取中后在取模;这个方法关键就在于选择合适的p,p如果选得不好,就可能会容易产生同义词。

随机数法:选择一个随机数,取关键字的随机函数值为它的散列地址,也就是f(key) = random(key)

8.2 处理散列冲突的方法

开放地址法:所谓的开放地址法就是一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。公式:f(key) = (f(key) + d) mod m,(d=1,2,3...n),如果关键字的散列值冲突,就不断的把d换成其他正数,把这种解决冲突的开放地址法称为线性地址探测法。

对于本来都不是同义词却需要争夺一个地址的情况,这种现象称为堆积。为了不让关键字都聚集在某一块区域,可以把公式d改为平方运算,称这种方法为二次探测法。

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

再散列函数法:对散列函数再次使用其他散列函数

链地址法:将所有关键字为同义词的记录存储在一个单链表中,称为同义词子表,在散列表中只存储所有同义词子表的头指针。无论有多少个冲突,都只是在当前位置给单链表增加结点的问题。

公共溢出区法:凡是冲突的关键字都放在一个公共的溢出区来存放。在查找的时候,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表去进行顺序查找。

8.3 散列表查找实现

散列表查询时间复杂度为O(1),当然这个时间复杂度还与散列函数是否均匀,处理冲突的方法,散列表的装填因子相关,不管记录个数n有多大,总可以选择一个合适的装填因子以便将平均查找长度限定在一个范围之内,此时散列查找的时间复杂度就真的是O(1)了,为了做到这一点,通常我们豆浆将散列表空间设置得比查找集合大,此时虽然是浪费一定的空间,但是换来了查询效率的提升。

#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(i=0; i<m; i++) {
       H->elem[i]=NULLKEY;
    }
    return OK;
}

/*散列函数*/
int hash(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;
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值