查找

查找

1. 概念

平均查找长度:

查找成功时的平均查找长度:

2. 线性结构的查找

查找结构:

   1 typedef struct{ 
   2     int key;
   3     info_type otherinfo;
   4 }record_type;
   5 typedef record_type elem_type;
   6 typedef struct  {
   7     elem_type  elem[MAXSIZE];
   8     int size;
   9 }seq_list;

2.1. 顺序查找

   1 int seq_search(seq_list *r, int key) {
   2    int i;
   3    for(i = 0; i < r->size; i++)
   4       if(r->elem[i].key == key)
   5          return i;
   6    return -1;
   7 }

查找成功时的平均查找长度:(n+1)/2

查找不成功时的平均查找长度:n

2.2. 折半查找

折半查找要求元素先排序

   1 int bin_search(seq_list *r, int key) {
   2    int low = 0, high = r->size;
   3    while(low < high) {
   4       int mid = (low + high) / 2;
   5       if(r->elem[mid].key == key)
   6          return mid;
   7       else if(r->elem[mid].key < key)
   8          low = mid + 1;
   9       else
  10          high = mid;
  11    }
  12    return -1;
  13 }

查找成功时的平均查找长度

3. 树型结构查找

3.1. 二叉排序树

二叉排序树是一棵空二叉树或者满足以下条件的二叉树:

二叉排序树表示

   1 typedef struct datatype {
   2    int key;
   3 }datatype;
   4 struct node {
   5    datatype data;
   6    struct node *left, *right;
   7 };

二叉排序树的插入

   1 struct node *insert_bst(struct node *root, datatype elem){
   2     if(root == NULL) {
   3         root = malloc(sizeof(struct node));
   4         root->data = elem;
   5         root->left = root->right = NULL;
   6         return root;
   7     }
   8     if(root->data.key < elem.key)
   9         root->right = insert_bst(root->right, elem);
  10     else if(root->data.key > elem.key)
  11         root->left = insert_bst(root->left, elem);
  12 }

二叉排序树的查找

   1 struct node *search(struct node *root, int key) {
   2     if(root == NULL)
   3         return NULL;
   4     else if(root->data.key < key)
   5         return search(root->right, key);
   6     else
   7         return search(root->left, key); 
   8 }

二叉排序树的删除结点

3.2. 二叉排序树的平衡

AVL平衡

红黑平衡

3.3. B树

4. 哈希表

4.1. 基本概念

设所有可能出现的关键字集合记为U(简称全集)。实际发生(即实际存储)的关键字集合记为K(|K|比|U|小得多)。

散列方法是使用函数h将U映射到表T[0..m-1]的下标上(m=O(|U|))。这样以U中关键字为自变量,以h为函数的运算结果就是相应结点的存储地址。从而达到在O(1)时间内就可完成查找。其中:

  1. h:U→{0,1,2,…,m-1} ,通常称h为散列函数(Hash Function)。散列函数h的作用是压缩待处理的下标范围,使待处理的|U|个值减少到m个值,从而降低空间开销。
  2. T为哈希表(Hash Table)。
  3. h(Ki)(Ki∈U)是关键字为Ki结点存储地址(亦称哈希地址)。
  4. 将结点按其关键字的散列地址存储到散列表中的过程称为哈希(Hashing)

散列表的冲突现象

两个不同的关键字,由于散列函数值相同,因而被映射到同一表位置上。该现象称为冲突(Collision)或碰撞。发生冲突的两个关键字称为该散列函数的同义词(Synonym)。

例:上图中的k2≠k5,但h(k2)=h(k5),故k2和K5所在的结点的存储地址相同。

冲突不可能完全避免

通常情况下,h是一个压缩映像。虽然|K|≤m,但|U|>m,故无论怎样设计h,也不可能完全避免冲突。因此,只能在设计h时尽可能使冲突最少。同时还需要确定解决冲突的方法,使发生冲突的同义词能够存储到表中。

冲突的频繁程度除了与h相关外,还与表的填满程度相关。

设m和n分别表示表长和表中填人的结点数,则将α=n/m定义为散列表的装填因子(Load Factor)。α越大,表越满,冲突的机会也越大。通常取α≤1。

4.2. 散列函数的构造方法

散列函数的选择有两条标准:简单和均匀。

假定关键字是定义在自然数集合上。

平方取中法

具体方法:先通过求关键字的平方值扩大相近数的差别,然后根据表长度取中间的几位数作为散列函数值。又因为一个乘积的中间几位数和乘数的每一位都相关,所以由此产生的散列地址较为均匀。

相应的散列函数用C实现很简单: int Hash(int key){ //假设key是4位整数

除余法

该方法是最为简单常用的一种方法。它是以表长m来除关键字,取其余数作为散列地址,即 h(key)=key%m。该方法的关键是选取m。选取的m应使得散列函数值尽可能与关键字的各位相关。m最好为素数。

折叠法

相乘取整法

该方法包括两个步骤:首先用关键字key乘上某个常数A(0<A<1),并抽取出key.A的小数部分;然后用m乘以该小数后取整。即:

该方法最大的优点是选取m不再像除余法那样关键。比如,完全可选择它是2的整数次幂。虽然该方法对任何A的值都适用,但对某些值效果会更好。Knuth建议选取

   1 int Hash(int key){
   2   double d=key *A//不妨设A和m已有定义
   3   return (int)(m*(d-(int)d));//(int)表示强制转换后面的表达式为整数
   4 }

随机数法

选择一个随机函数,取关键字的随机函数值为它的散列地址,即

其中random为伪随机函数,但要保证函数值是在0到m-1之间。

4.3. 处理冲突的方法

通常有两类方法处理冲突:开放定址(Open Addressing)法和拉链(Chaining)法。前者是将所有结点均存放在散列表T[0..m-1]中;后者通常是将互为同义词的结点链成一个单链表,而将此链表的头指针放在散列表T[0..m-1]中。

开放定址法

用开放定址法解决冲突的做法是:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的地址则表明表中无待查的关键字,即查找失败。

注意:

开放定址法的一般形式为: hi=(h(key)+di)%m 1≤i≤m-1 其中:

  1. h(key)为散列函数,di为增量序列,m为表长。
  2. h(key)是初始的探查位置,后续的探查位置依次是hl,h2,…,hm-1,即h(key),hl,h2,…,hm-1形成了一个探查序列。
  3. 若令开放地址一般形式的i从0开始,并令d0=0,则h0=h(key),则有:hi=(h(key)+di)%m 0≤i≤m-1 探查序列可简记为hi(0≤i≤m-1)。

开放定址法要求散列表的装填因子α≤l,实用中取α为0.5到0.9之间的某个值为宜。

按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、二次探查法、双重散列法等。

线性探查法(Linear Probing)

该方法的基本思想是:将散列表T[0..m-1]看成是一个循环向量,若初始探查的地址为d(即h(key)=d),则最长的探查序列为:

探查过程终止于三种情况:

  1. 若当前探查的单元为空,则表示查找失败(若是插入则将key写入其中);
  2. 若当前探查的单元中含有key,则查找成功,但对于插入意味着失败;
  3. 若探查到T[d-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。

利用开放地址法的一般形式,线性探查法的探查序列为:

二次探查法(Quadratic Probing)二次探查法的探查序列是:

即探查序列为d=h(key),d+12,d+22,…,等。该方法的缺陷是不易探查到整个散列空间。

双重散列法(Double Hashing):该方法是开放定址法中最好的方法之一,它的探查序列是:

该方法使用了两个散列函数h(key)和h1(key),故也称为双散列函数探查法。

拉链法

拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组 T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于 1,但一般均取α≤1。

与开放定址法相比,拉链法有如下几个优点:

  1. 拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
  2. 由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
  3. 开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
  4. 在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

查找 (2008-02-23 15:35:52由localhost编辑)