首页 > 技术文章 > 数据结构学习(五)--查找

maopneo 2020-11-11 14:40 原文

(一)查找

根据给定的某个值,在查找表中确定一个其关键字等于给定数值的数据元素(或记录)。

1. 定义

查找表

由同一个数据类型的数据元素构成的集合。

静态查找表l

制作查找操作的查找表。

① 查找某个特定的元素是否存在在查找表中。

② 检索特定元素的各种属性。

动态查找表

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

① 查找时插入数据元素。

② 查找时删除数据元素。

关键字

数据元素中某个数据项的值,又称为键值。也称为关键字。把能唯一标识一个数据记录的键值称为主关键码。

 

2. 顺序表查找

顺序表查找又叫线性查找,是基本的查找技术。查找的过程如下:从表中第一个元素或者最后一个元素,逐个对元素的关键字和给定值进行比较,若某个记录的关键字和给定值相等,则查找成功。若查找表中所有元素比对结束后仍然没有找到记录,表示查找失败。

(1) 顺序查找算法

遍历查找表,判断元素的关键字和和给定值是否相等。相等则返回元素位置下标,不相等继续循环。查找完成还没匹配成功,返回0.

(2) 顺序查找优化

顺序查找过程中,每次需要判断下标是否越界,然后和关键字比较。可以通过哨兵的方法较少越界判断。

数组第一个位置一般放需要比较的值,为哨兵。从数据最后一个数值向前移动,依次和哨兵比较。如果不相等则进入循环体i--,否则跳出,返回i。【数据元素在数组下标为1-N的数组中,如果匹配到了,则返回值>0,如果没匹配上,在哨兵位置必然匹配上,返回0

3. 有序查找

查找表数据原本有序,对于有序的查找表,有些其它的查找方法。

(1) 折半查找

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

折半查找的基本思想是:有序表中,取中间的记录做比较。给定的值和中间的记录的关键字相等,则比较成功。如果中间的记录关键字大于给定的值,则在左半区继续按照二分查找。如果中间的记录小于关键字,则在中间记录的右半区继续二分查找。不断重复上述过程,直到查找成功,或者所有查找区域都无此记录。查找失败为止。

折半查找又称为二分查找,折半查找的作用对象是有序的查找表,也就是说,我们的查找表是已经排好序的。之所以称为折半查找,是因为在每次关键字比较时,如果不匹配,则根据匹配结果将查找表一份为二,排除没有关键子的那一半,然后在含有关键字的那一半中继续折半查找。

 

查找过程如下

1、查找初始化为low=1high=length。其中lowhigh是两个位置指示器,分别指向当前序列的第一个和最后一个值;

2、对当前有序序列做如下处理:

1)求当前序列的中间位置:mid=low+length/2mid为中间值的位置指示器;

2)将要查找的查询关键字与mid指示的值进行比较。若相等,查找结束,返回中间位置的值mid;若大于该值,则将查找范围缩短为该序列的后半部分,此时改变low=mid+1,而high值不变;若小于该值,则将查找范围缩短为该序列的前半部分,此时改变high=mid-1,而low值不变。

3)重复执行过程(2)直至找到待查关键字,返回此值的位置mid,否则返回0

 

代码实现如下

  1. //二分查找法(折半查找法)  
  2. public static int halfSearch(int[] arr,int number){  
  3. int low=0;  //最小下标  
  4. int high=arr.length-1;   //最大下标  
  5. int mid = 0;  //中间下标  
  6. while (low<high){   // 循环条件为 low<high
  7. //没找到,更新范围继续找  
  8. mid = (low+high)/2;  
  9. if (arr[mid]>number){   //number在mid的左边  
  10. high= mid-1;  //改变最大下标  
  11. }else if(arr[mid]<number){  //number在mid的右边  
  12. low= mid+1;  //改变最小下标  
  13. }else{  
  14. return  mid;  
  15. }  
  16. }  
  17. return -1;  
  18. }  

 

 

(2) 插值查找

插值查找时二分查找的一种优化。二分查找中间mid的取值为(low

+high/2 = low + (high-how) /2 = low+weight(high-low) 其中weight1/2。因为1/2并不是最佳值。

 

插值查找的weight  (key-a[low])/(a[high]-a[low])

(3) 斐博那楔查找

4. 线性索引查找

索引就是把一个关键字和它对应的记录关联的过程。一个索引有若干个索引项构成。每个索引项至少包含关键字其对应的记录在存储器中的位置等信息。索引技术是组织大型数据以及磁盘文件的一种重要技术。

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

线性索引分为 稠密索引、分块索引和倒排索引。

(1) 稠密索引

线性索引中,将数据集中每个数据记录都对应一个索引项。【每个记录都有一个索引项】,索引项一定按照关键字有序排列。

(2) 分块索引

稠密索引因为索引项和数据记录一样多,因此,空间代价很大。为了解决这个问题,可以对数据集进行分块。使其块间有序,然后每个块建立一个索引项。从而减少索引项的个数。

分块有序是把数据的记录分成若干块。并且这些块需满足两个条件。(1块内无序2块间有序

分块索引的索引项结构分为三个数据项。最大关键码块中存储的记录数块首个数据元素指针【便于对这一块数据进行遍历

 

分块索引查找分两步:块间折半查找,块内顺序查找。

(1) 在分块索引中查找关键字所在的块。【索引块间有序,利用折半查找,可以很快确定关键字所在的块】

(2) 根据块首元素地址可以找到对应的块。根据元素个数count,块内无需,可以按照顺序查找的方式查找。

(3) 倒排索引

通常的索引是在源中查找关键字的。查找源中关键字和给定值相等的记录。而倒排索引是从关键字中找到对应的源的。个人理解,和散列的思路类似。从key直接能得到对应的源。

5. 二叉排序树

有序的顺序存储,使用二分法查找比较好。这是建立在静态查找的基础之上的。如果有序的数据需要动态查找。即在查找的过程中需要插入未找到的数据元素或者删除查找到的数据元素,顺序存储的数组是不能很好解决需要。需要使用一种叫二叉排序树的数据结构。个人理解:是二分查找的一种链式存储。

(1) 定义

二叉排序树又称为二叉查找树。它或者是一颗空树,或者是具有以下性质的二叉树。

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

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

l 它的左右子树都是二叉排序树。

(2) 二叉排序树查找操作

关键字和二叉树根节点的值域的数值比较,如果相等就查询成功。如果关键字小于根结点的值,就递归查找根结点的左孩子。如果关键字大于根结点的值。就递归查找根结点的左孩子。

  1. /* 递归查找二叉排序树T中是否存在key, */  
  2. /* 指针f指向T的双亲,其初始调用值为NULL */  
  3. /* 若查找成功,则指针p指向该数据元素结点,并返回TRUE */  
  4. /* 否则指针p指向查找路径上访问的最后一个结点并返回FALSE */  
  5. Status SearchBST(BiTree T, int key, BiTree f, BiTree *p)   
  6. {    
  7. if (!T) /*  查找不成功 */  
  8. {   
  9. *p = f;    
  10. return FALSE;   
  11. }  
  12. else if (key==T->data) /*  查找成功 */  
  13. {   
  14. *p = T;    
  15. return TRUE;   
  16. }   
  17. else if (key<T->data)   
  18. return SearchBST(T->lchild, key, T, p);  /*  在左子树中继续查找 */  
  19. else    
  20. return SearchBST(T->rchild, key, T, p);  /*  在右子树中继续查找 */  

 

(3) 二叉排序树的插入操作

基本算法:基于查找函数,如果返回为false,则代表没有找到结点。新创建结点s,结点值域赋值为关键字key。和查找位置的双亲结点P的结点数值比较。如果key<双亲的data,则新结点sP的左子树。如果key>双亲结点pdata,则新结点sP的右子树。代码实现如下:

  1. /*  当二叉排序树T中不存在关键字等于key的数据元素时, */  
  2. /*  插入key并返回TRUE,否则返回FALSE */  
  3. Status InsertBST(BiTree *T, int key)   
  4. {    
  5. BiTree p,s;  
  6. if (!SearchBST(*T, key, NULL, &p)) /* 查找不成功 */  
  7. {  
  8. s = (BiTree)malloc(sizeof(BiTNode));  
  9. s->data = key;    
  10. s->lchild = s->rchild = NULL;    
  11. if (!p)   
  12. *T = s;         /*  插入s为新的根结点 */  
  13. else if (key<p->data)   
  14. p->lchild = s;  /*  插入s为左孩子 */  
  15. else   
  16. p->rchild = s;  /*  插入s为右孩子 */  
  17. return TRUE;  
  18. }   
  19. else   
  20. return FALSE;  /*  树中已有关键字相同的结点,不再插入 */  
  21. }  
(4) 二叉排序树的删除操作

实现分为两层:

第一层:判断key值和树根结点是否相等。如果相等在删除结点,并重接左右子树。如果key<根结点的数值,则递归调用二叉树删除方法,删除左子树。如果key>根结点的数值,则递归调用二叉树删除方法,删除右子树。

第二层:删除结点,并重接左右子树的方法实现。

删除结点又以下三种可能。(1)叶子结点【直接删除】(2)仅有左或者右结点【如果只有左子树,删除后左子树调补到被删除的位置上,如果只有右子树,右子树添补到被删除的结点】(3左右子树都有结点【找整棵树先序遍历的前驱结点s替换被删除的结点p的数据,假设s还有左子树s->lchild,且s的原来双亲结点为q,如果p=q则将s的左子树接到q的右孩子。q->rchild = s->lchild ,如果p==q ,则s的左子树接到q的左孩子,q->lchild=s->lchild。删除稍稍复杂一些。使用直接前驱结点替换被删除的结点。然后调整之前前驱的左子树,重新接入到二叉树。

 

  1. /* 从二叉排序树中删除结点p,并重接它的左或右子树。 */  
  2. Status Delete(BiTree *p)  
  3. {  
  4. BiTree q,s;  
  5. if((*p)->rchild==NULL) /* 右子树空则只需重接它的左子树(待删结点是叶子也走此分支) */  
  6. {  
  7. q=*p; *p=(*p)->lchild; free(q);  
  8. }  
  9. else if((*p)->lchild==NULL) /* 只需重接它的右子树 */  
  10. {  
  11. q=*p; *p=(*p)->rchild; free(q);  
  12. }  
  13. else /* 左右子树均不空 */  
  14. {  
  15. q=*p; s=(*p)->lchild;  
  16. while(s->rchild) /* 转左,然后向右到尽头(找待删结点的前驱) */  
  17. {  
  18. q=s;  
  19. s=s->rchild;  
  20. }  
  21. (*p)->data=s->data; /*  s指向被删结点的直接前驱(将被删结点前驱的值取代被删结点的值) */  
  22. if(q!=*p)  
  23. q->rchild=s->lchild; /*  重接q的右子树 */   
  24. else  
  25. q->lchild=s->lchild; /*  重接q的左子树 */  
  26. free(s);  
  27. }  
  28. return TRUE;  
  29. }  
  30. /* 若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点, */  
  31. /* 并返回TRUE;否则返回FALSE。 */  
  32. Status DeleteBST(BiTree *T,int key)  
  33. {   
  34. if(!*T) /* 不存在关键字等于key的数据元素 */   
  35. return FALSE;  
  36. else  
  37. {  
  38. if (key==(*T)->data) /* 找到关键字等于key的数据元素 */   
  39. return Delete(T);  
  40. else if (key<(*T)->data)  
  41. return DeleteBST(&(*T)->lchild,key);  
  42. else  
  43. return DeleteBST(&(*T)->rchild,key);  
  44. }  
  45. }  

6. 平衡二叉树AVL

平衡二叉树,是一种二叉排序树,其中每个节点的左子树和右子树的高度至多等于一。他是一种高度平衡的二叉排序树。将二叉树的左子树深度减去右子树深度的值称为平衡因子。二叉树所有结点的平衡因子只可能是-101

理解:平衡二叉树是在二叉排序树的基础之上,添加约束。每个结点的左子树和右子树的深度只差只能为[-101]

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

平衡二叉树对动态查找很好使用。但是,如果不平衡,会造成深度比较高,效率不高。所以,平衡二叉树AVL本质上是平衡排序二叉树。平衡使得二叉树的深度有保障。

(1) AVL树实现原理

构建平衡二叉树的基本思想就是:在构建过程中,每当插入一个结点时,检查是否破坏了树的平衡性,若是,则找出最小不平衡树,进行相应的调整。

平衡二叉树的结点数据结构设计。除了二叉树结点的数据域(数据),两个指针域(左子树指针、右子树指针)外,多了一个平衡因子的数据项bf。如下:

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

根据之前提到的基本思想,为调整最小不平衡树,首先要了解两种最基本的操作:左旋操作和右旋操作

① 基本操作

l 右旋

如下图中左边的最小不平衡二叉树,进行右旋操作即可变为右边中的平衡二叉树。

 

需要右旋,说明左侧子树深度高,L需要作为跟结点。所以,P需要称为L的右子树。L的右子树为p结点的前序遍历的前驱。所以,L的右子树称为P的左子树。之后将P作为L的右子树。

  1. /* 对以p为根的二叉排序树作右旋处理。 */  
  2. /* 处理之后p指向新的树根结点。即旋转处理之前的左子树的根结点 */  
  3. void R_Rotate(BiTree *P)  
  4. {   
  5. BiTree L;  
  6. L=(*P)->lchild; /*  L指向P的左子树根结点 */   
  7. (*P)->lchild=L->rchild; /*  L的右子树挂接为P的左子树 */   
  8. L->rchild=(*P);  
  9. *P=L; /*  P指向新的根结点 */   

l 左旋

同上所述,左旋操作的图示及代码,如下所示

 

  1. /* 对以P为根的二叉排序树作左旋处理, */  
  2. /* 处理之后P指向新的树根结点。即旋转处理之前的右子树的根结点0  */  
  3. void L_Rotate(BiTree *P)  
  4. {   
  5. BiTree R;  
  6. R=(*P)->rchild; /*  R指向P的右子树根结点 */   
  7. (*P)->rchild=R->lchild; /* R的左子树挂接为P的右子树 */   
  8. R->lchild=(*P);  
  9. *P=R; /*  P指向新的根结点 */   
  10. }  

② /右平衡旋转

l 左平衡旋转

左平衡旋转说明新结点添加在左子树。最小不平衡子树TBF必然是2。整体必然需要右转。如果T的左孩子的BF==1,则最小不平衡子树T需要右转。如果T的左孩子的BF==-1,说明新结点在T左孩子的右子树上。说明需要双旋。先对T的左孩子的左旋,然后对整个T进行右旋。

  1. /*  对以指针T所指结点为根的二叉树作左平衡旋转处理 */  
  2. /*  本算法结束时,指针T指向新的根结点 */  
  3. void LeftBalance(BiTree *T)  
  4. {   
  5. BiTree L,Lr;  
  6. L=(*T)->lchild; /*  L指向T的左子树根结点 */   
  7. switch(L->bf)  
  8. { /*  检查T的左子树的平衡度,并作对应平衡处理 */   
  9. case LH: /*  新结点插入在T的左孩子的左子树上,要作单右旋处理 */   
  10. (*T)->bf=L->bf=EH;  
  11. R_Rotate(T);  
  12. break;  
  13. case RH: /*  新结点插入在T的左孩子的右子树上。要作双旋处理 */   
  14. Lr=L->rchild; /*  Lr指向T的左孩子的右子树根 */   
  15. switch(Lr->bf)  
  16. { /*  改动T及其左孩子的平衡因子 */   
  17. case LH: (*T)->bf=RH;  
  18. L->bf=EH;  
  19. break;  
  20. case EH: (*T)->bf=L->bf=EH;  
  21. break;  
  22. case RH: (*T)->bf=EH;  
  23. L->bf=LH;  
  24. break;  
  25. }  
  26. Lr->bf=EH;  
  27. L_Rotate(&(*T)->lchild); /*  对T的左子树作左旋平衡处理 */   
  28. R_Rotate(T); /*  对T作右旋平衡处理 */   
  29. }  

 

l 右平衡旋转

需要对最小不平衡子树的右子树进行平衡旋转操作。说明新结点添加在右子树上。

右平衡旋转说明新结点添加在右子树。最小不平衡子树TBF必然是-2。整体必然需要右转。如果T的右孩子的BF==-1,说明新结点在T右孩子的右子树上,则最小不平衡子树T需要左转。如果T的左孩子的BF==1,说明新结点在T右孩子的左子树上。说明需要双旋。先对T的右孩子的右旋,然后对整个T进行左旋。代码如下:

  1. /*  对以指针T所指结点为根的二叉树作右平衡旋转处理, */   
  2. /*  本算法结束时,指针T指向新的根结点 */   
  3. void RightBalance(BiTree *T)  
  4. {   
  5. BiTree R,Rl;  
  6. R=(*T)->rchild; /*  R指向T的右子树根结点 */   
  7. switch(R->bf)  
  8. { /*  检查T的右子树的平衡度。并作对应平衡处理 */   
  9. case RH: /*  新结点插入在T的右孩子的右子树上。要作单左旋处理 */   
  10. (*T)->bf=R->bf=EH;  
  11. L_Rotate(T);  
  12. break;  
  13. case LH: /*  新结点插入在T的右孩子的左子树上,要作双旋处理 */   
  14. Rl=R->lchild; /*  Rl指向T的右孩子的左子树根 */   
  15. switch(Rl->bf)  
  16. { /*  改动T及其右孩子的平衡因子 */   
  17. case RH: (*T)->bf=LH;  
  18. R->bf=EH;  
  19. break;  
  20. case EH: (*T)->bf=R->bf=EH;  
  21. break;  
  22. case LH: (*T)->bf=EH;  
  23. R->bf=RH;  
  24. break;  
  25. }  
  26. Rl->bf=EH;  
  27. R_Rotate(&(*T)->rchild); /*  对T的右子树作右旋平衡处理 */   
  28. L_Rotate(T); /*  对T作左旋平衡处理 */   
  29. }  
  30. }  

 

(2) AVL树实现算法

AVL树的insert函数和二叉树的insert函数类似。在插入的基础之上判断高度是是否发生变化。Taller发生变化,需要对子树进行调整。

  1. /*  若在平衡的二叉排序树T中不存在和e有同样关键字的结点,则插入一个 */   
  2. /*  数据元素为e的新结点。并返回1,否则返回0。若因插入而使二叉排序树 */   
  3. /*  失去平衡,则作平衡旋转处理。布尔变量taller反映T长高与否。 */  
  4. Status InsertAVL(BiTree *T,int e,Status *taller)  
  5. {    
  6. if(!*T)  
  7. { /*  插入新结点。树“长高”,置taller为TRUE */   
  8. *T=(BiTree)malloc(sizeof(BiTNode));  
  9. (*T)->data=e; (*T)->lchild=(*T)->rchild=NULL; (*T)->bf=EH;  
  10. *taller=TRUE;  
  11. }  
  12. else  
  13. {  
  14. if (e==(*T)->data)  
  15. { /*  树中已存在和e有同样关键字的结点则不再插入 */   
  16. *taller=FALSE; return FALSE;  
  17. }  
  18. if (e<(*T)->data)  
  19. { /*  应继续在T的左子树中进行搜索 */   
  20. if(!InsertAVL(&(*T)->lchild,e,taller)) /*  未插入 */   
  21. return FALSE;  
  22. if(taller) /*   已插入到T的左子树中且左子树“长高” */   
  23. switch((*T)->bf) /*  检查T的平衡度 */   
  24. {  
  25. case LH: /*  原本左子树比右子树高。须要作左平衡处理 */   
  26. LeftBalance(T); *taller=FALSE; break;  
  27. case EH: /*  原本左、右子树等高,现因左子树增高而使树增高 */   
  28. (*T)->bf=LH; *taller=TRUE; break;  
  29. case RH: /*  原本右子树比左子树高,现左、右子树等高 */    
  30. (*T)->bf=EH; *taller=FALSE; break;  
  31. }  
  32. }  
  33. else  
  34. { /*  应继续在T的右子树中进行搜索 */   
  35. if(!InsertAVL(&(*T)->rchild,e,taller)) /*  未插入 */   
  36. return FALSE;  
  37. if(*taller) /*  已插入到T的右子树且右子树“长高” */   
  38. switch((*T)->bf) /*  检查T的平衡度 */   
  39. {  
  40. case LH: /*  原本左子树比右子树高。现左、右子树等高 */   
  41. (*T)->bf=EH; *taller=FALSE; break;  
  42. case EH: /*  原本左、右子树等高,现因右子树增高而使树增高  */  
  43. (*T)->bf=RH; *taller=TRUE; break;  
  44. case RH: /*  原本右子树比左子树高,须要作右平衡处理 */   
  45. RightBalance(T); *taller=FALSE; break;  
  46. }  
  47. }  
  48. }  
  49. return TRUE;  
  50. }

 

 

7. 多路查找树B

每个结点的孩子数可以多于两个,且每个结点可以存储多个元素。B树为了存储设备或者磁盘而设计的一种平衡查找树

(1) 2-3

其中每个结点都具有两个孩子(称为2结点)或者三个孩子(称为3结点)。一个2结点包含一个元素两个孩子(或者没有孩子)。一个3结点包含2个元素3个孩子(或者没孩子),2-3树的所有叶子都在同一层次。

(2) 2-3-4

2-3-4树是2-3树的一种扩展。包括4结点的使用。一个4结点包括小中大三个元素和4个孩子(或者没孩子-叶子结点)。为了方便说明子树和元素之间的大小关系。假设4结点又3个元素(从左向右依次为:a1a2a3)和4个孩子(child1child2child3child4)。Child1<a1, a1<child2<a2, a2<child3<a3, a3<child2

(3) B

B树是一种平衡的多路查找树。节点最大的孩子数目称为B树的阶(order)。2-3树是3B树,2-3-44B树。

B树的数据结构主要用在内存和外部存储器的数据交互中

 

定义

一棵m阶的B树具有以下特征。

(1) 树中每个孩子最多包含m个孩子。

(2) 除根节点外,其它结点至少有[ceil(m / 2)(代表是取上限的函数)]个孩子。

(3) 若根节点不是叶子结点,至少有两个孩子。

(4) 所有的叶子结点都出现在同一层中。叶子结点不包含任何关键信息

(5) B+

B+树是在B树的基础之上,加入了新的元素组织形式。在B树中,每个元素都只出现一次,可能在叶子结点上,也可能在分支结点上。而在B+树中,出现在分支结点上的元素会被当做他们在该分支结点的中序后继者(叶子结点)中再次出现。另外,每个叶子结点都会保存一个指向下一个叶子结点的指针。

 

好处:如果随机查找,可以按照B树的查找方式去查找。不过,查到分支结点找到的关键字只是索引,需要到包含该关键字的终端结点(叶子结点)获得。如果需要从小到大顺序查找,可以从最左侧叶子结点出发,不经过分支结点,沿着指针指向下一个叶子结点的指针就可以遍历所有关键字。

8. 散列(哈希Hash)表查找

是根据键(Key)而直接访问在内存存储位置的数据结构。关键字和存储位置之间的对应关系为ff称为散列函数(哈希函数)。通过散列技术将记录存储在一块连续的存储空间中,这块存储空间-数组称为哈希表。

散列技术既是一种存储方法,也是一种查找方法。相当于其它查找方法而言,散列技术没有依次比较的过程,从而提升了效率。

(1) 散列函数的构造方法

l 直接定址法 f(key)=a*key+b

l 平方取中法。

l 折叠法

除留余数法 f(key) =key mod p (p<=m)

l 随机数法

(2) 处理散列冲突方法

l 开放定值法

l 再散列函数法

链址法 相同f(key)的值放在同一个链表中。Fkey)的位置存放链表的表头。如果冲突,需要在链表中依次匹配,直至最后一个结点。匹配元素。

l 公共溢出区法:对于溢出的部分,不排序放入溢出表(顺序存储的数组)中。寻找发现冲突,则需要在溢出表中整体遍历,顺序查找。

(3) 散列的查找

推荐阅读