1.32k likes | 1.51k Views
第 8 章 查找. 8.1 基本概念 8.2 静态查找表 8.3 动态查找表 8.4 哈希表及其查找. 8.1 基本概念. 1 、查找表和查找 查找表 :被查找的对象是由一组结点组成的表 (Table) 或文件,而每个结点则由若干个数据项组成。并假设每个结点都有一个能唯一标识该结点的关键字。 查找 :给定一个值 K ,在含有 n 个结点的表中找出关键字等于给定值 K 的结点。若找到,则查找成功,返回该结点的信息或该结点在表中的位置;否则查找失败,返回相关的指示信息。. 8.1 基本概念. 2 、查找的分类
E N D
第8章 查找 8.1 基本概念 8.2 静态查找表 8.3 动态查找表 8.4 哈希表及其查找
8.1 基本概念 1、查找表和查找 查找表:被查找的对象是由一组结点组成的表(Table)或文件,而每个结点则由若干个数据项组成。并假设每个结点都有一个能唯一标识该结点的关键字。 查找:给定一个值K,在含有n个结点的表中找出关键字等于给定值K的结点。若找到,则查找成功,返回该结点的信息或该结点在表中的位置;否则查找失败,返回相关的指示信息。
8.1 基本概念 2、查找的分类 按查找条件分:按主关键字查找、按次关键字查找。 按查找数据存放存储器分:内查找、外查找。 内查找:整个查找过程都在内存中进行。 外查找:查找过程中需要访问外存。 按查找目的分:静态查找、动态查找。 若在查找的同时对表做修改操作,则相应的表称之为动态查找表(Dynamic Search Table),否则称之为静态查找表(Static Search Table)。
8.1 基本概念 3、如何进行查找 由于查找表中的数据元素之间不存在明显的组织规律,因此不便于查找。 为了提高查找的效率,需要在查找表中的元素之间人为地加入某种确定关系,即用另外一种结构来表示查找表,而查找的方法则取决于查找表的结构。 4、查找方法的评价 ① 查找速度; ② 查找过程中占用存储空间的多少;
8.1 基本概念 ③ 查找算法的复杂程度; ④ 平均查找长度ASL。 5、平均查找长度ASL 平均查找长度:查找过程中对关键字需要执行的平均比较次数,是衡量一个查找算法效率优劣的标准之一。 其中:① n:结点的个数;
8.1 基本概念 ② pi:查找第i个结点的概率。若不特别声明,认为每个结点的查找概率相等,即 pl=p2…=pn=1/n ③ ci:找到第i个结点所需进行的比较次数。 注意:为了简单起见,假定表中关键字的类型为整数。 定义 8.1 typedef int KeyType;/*KeyType应由用户定义*/
8.2 静态查找表 线性表上进行查找的方法主要有三种: • 顺序查找 • 二分查找 • 分块查找
8.2.1 顺序查找(Sequential Search) 1、存储结构要求 顺序查找方法既适用于线性表的顺序存储结构,也适用于线性表的链式存储结构(使用单链表作存储结构时,扫描必须从第一个结点开始)。 2、基本思想 从表的一端开始,顺序扫描线性表,依次将扫描到的结点关键字和给定值K相比较。若当前扫描到的结点关键字与K相等,则查找成功;若扫描结束后,仍未找到关键字等于K的结点,则查找失败。扫描线性表可以从表尾开始,也可以从表头开始。
8.2.1 顺序查找(Sequential Search) 3、查找实例 在顺序表L=(3,9,14,21,33,47,55,73,80,97)中查找关键字55的过程图如图8.1示:
8.2.1 顺序查找(Sequential Search) 4、基于顺序结构的顺序查找算法 (1)类型说明 定义 8.2 typedef struct{ KeyType key; InfoType otherinfo; /*此类型依赖于应用*/ }NodeType; typedef NodeType SeqList[n+1]; /*0号单元用作哨兵*/
8.2.1 顺序查找(Sequential Search) (2)查找算法 算法 8.1 顺序查找算法一 int SeqSearch(Seqlist R,KeyType K) {/*在顺序表R[1..n]中顺序查找关键字为K的结点,成功时返回结点位置,失败时返回0*/ int i; R[0].key=K; /*设置哨兵于表头*/ for(i=n;R[i].key!=K;i--); /*从表后往前找*/ if(i>0) return i; /*若i为0,表示查找失败,否则R[i]是要找的结点*/ else return 0; }
8.2.1 顺序查找(Sequential Search) 算法 8.2 顺序查找算法二 int SeqSearch(Seqlist R,KeyType K) {/*在顺序表R[0..n-1]中顺序查找关键字为K的结点,成功返回结点位置,失败返回0*/ int i; R[n].key=K; /*设置哨兵于表尾*/ for(i=0;R[i].key!=K;i++); /*从表后往前找*/ if(i<n) return i; /*若i在0~n-1之间,则R[i]是要找的结点,返回i*/ else return 0; /*否则查找失败,返回0*/ } /*SeqSearch*/
8.2.1 顺序查找(Sequential Search) (3)算法分析 成功时的顺序查找的平均查找长度: pi=1/n(1≤i≤n)时, ASLsq= (n+…+2+1)/n=(n+1)/2。 查找概率不等,则 pn≥pn-1≥……≥p2≥p1时ASLsq取极小值。 查找概率不定时,改进:每次查找后,将刚找到的记录移至表尾。
8.2.1 顺序查找(Sequential Search) 对算法的修改:每当查找成功,就将找到的结点和其后继(若存在)结点交换。 顺序查找的优点:算法简单,且对表的结构无任何要求,无论是用向量还是用链表来存放结点,无论结点之间是否按关键字有序,它都同样适用。 顺序查找的缺点:查找效率低,因此,当n较大时不宜采用顺序查找。
8.2.2 二分查找(Binary Search) 1、存储要求 二分查找:又称折半查找,它是一种效率较高的查找方法。 二分查找要求:线性表是有序表,并且使用向量作为表的存储结构。不失一般性,不妨假设该有序表是递增有序的。 2、二分查找的基本思想 (设R[low..high]是当前的查找区间) (1)确定该区间的中点位置:mid= (low+high)/2
8.2.2 二分查找(Binary Search) (2)将待查K值与R[mid].key比较: 若相等,则查找成功并返回此位置, 不相等,则须确定新的查找区间,方法如下: 若R[mid].key>K,则新的查找区间是左子表R[1..mid-1]。 若R[mid].key<K,则新的查找区间是右子表[mid+1..n] 。 (3)重复过程(2),直至找到关键字为K的结点,或者直至当前的查找区间为空(即查找失败)时为止。
8.2.2 二分查找(Binary Search) 3、二分查找算法 算法 8.3 二分查找算法 int BinSearch(SeqList R,KeyType K) { /*在有序表R[1..n]中进行二分查找,成功时返回结点的位置,失败时返回零*/ int low=1,high=n,mid; /*置当前查找区间上、下界的初值*/ while(low<=high) /*当前查找区间R[low..high]非空*/ { mid=(low+high)/2; if(R[mid].key==K) return mid; /*查找成功返回*/ if(R[mid].key>K) high=mid-1; /*继续在R[low..mid-1]中查找*/ else low=mid+1; /*继续在R[mid+1..high]中查找*/ } return 0; /*当low>high时表示查找区间为空,查找失败*/ }
8.2.2 二分查找(Binary Search) 4、二分查找算法的执行过程 例:在序列(3,9,14,21,33,47,55,73,80,97)中查找关键字Key分别是21和70的查找过程如图所示: key=21的查找过程,找到
8.2.2 二分查找(Binary Search) 4、二分查找算法的执行过程 例:在序列(3,9,14,21,33,47,55,73,80,97)中查找关键字Key分别是21和70的查找过程如图所示: key=70的查找过程,失败
8.2.2 二分查找(Binary Search) 5、二分查找的性能分析 对于成功的查找,如查找21,二分查找可以用图8.3(a)的判定树来描述其查找过程。比较的次数与判定叉树的层次对应,如查找21共比较了4次,才找到④号元素,而④元素在判定树上的位置正好位于第4层,因此,二分查找的过程与判定树查找一致,因此二分查找的成功查找次数至多为(log2n)+1。
8.2.2 二分查找(Binary Search) 对于不成功的查找,如查找70,可以用图8.3(b)的判定树来描述其查找过程。比较的次数超出判定叉树的层次时,查找失败,如查找70共比较了5次,从而超过了判定树的深度,因此,二分查找的过程与判定树查找一致,可以证明查找失败时查找次数至多为Г(log2n)+1˥。
8.2.2 二分查找(Binary Search) 假设查找每个元素的概率相等,即pi=1/n,则平均查找长度为: 因此,二分查找的时间复杂度为O(log2n)。
8.2.2 二分查找(Binary Search) 6、二分查找的优点和缺点 虽然二分查找的效率高,但是要将表按关键字排序。而排序本身是一种很费时的运算。既使采用高效率的排序方法也要花费O(nlogn)的时间。 二分查找只适用顺序存储结构。为保持表的有序性,在顺序结构里插入和删除都必须移动大量的结点。因此,二分查找特别适用于那种一经建立就很少改动、而又经常需要查找的线性表。 对那些查找少而又经常需要改动的线性表,可采用链表作存储结构,进行顺序查找。链表上无法实现二分查找。
8.2.3 索引查找(Blocking Search) • 索引顺序查找(Blocking Search)又称分块查找。 • 它是一种性能介于顺序查找和二分查找之间的查找方法。 • 在建立顺序表的同时,建立一个索引项,包括两项:关键字项和指针项。索引表按关键字有序,顺序表则为分块有序。
8.2.3 索引查找(Blocking Search) 1、索引查找表存储结构 (1)“分块有序”的线性表。 表R[1..n]均分为b块,前b-1块中,每块结点个数为 ,第b块的结点数小于等于s;每一块中的关键字不一定有序,但前一块中的最大关键字必须小于后一块中的最小关键字,即表是“分块有序”的。 (2)索引表 抽取各块中的最大关键字和起始位置构成一个索引表ID[l..b],即:ID[i](1≤i≤b)中存放第i块的最大关键字及该
8.2.3 索引查找(Blocking Search) 块在表R中的起始位置。由于表R是分块有序的,所以索引表是一个递增有序表。
8.2.3 索引查找(Blocking Search) 2、查找的基本思想 • 首先查找索引表,确定待查的块。因为是分块有序,所以查找索引表时采用二分查找,以确定待查的结点在哪一块; • 然后在已确定的块中进行顺序查找,由于块内无序,只能用顺序查找。 3. 算法实现 用数组存放待查记录,每个数据元素至少含有关键字域,建立索引表,每个索引表结点含有最大关键字域和指向本块第一个结点的指针。
8.2.3 索引查找(Blocking Search) 4. 索引顺序查找示例 线性表L=(21,8,17,19,14,31,33,22,25,40,52, 61, 78, 46),在其中查找25的过程,如图8.5所示。
8.2.3 索引查找(Blocking Search) 5. 算法分析 (1)平均查找长度ASL ①若以二分查找来确定块,分块查找成功时的平均查找长度 ASLblk=ASLbs+ASLsq≈log2(b+1)- 1+(s+1)/2 ≈log2(n/s+1)+s/2 ②若以顺序查找确定块,分块查找成功时的平均查找长度 ASL'blk=(b+1)/2+(s+1)/2=(s2+2s+n)/(2s) (2)块的大小 在实际应用中,分块查找不一定要将线性表分成大大小相等
8.2.3 索引查找(Blocking Search) 的若干块,可根据表的特征进行分块。例如一个学校的学生登记表,可按系号或班号分块。 (3) 结点的存储结构 各块可放在不同的线性表中,也可将每一块存放在一个单链表中。 (4)索引顺序查找的优点 优点是: ①在表中插入或删除一个记录时,只要找到该记录所属的块,就在该块内进行插入和删除运算。
8.2.3 索引查找(Blocking Search) ②因块内记录的存放是任意的,所以插入或删除比较容易,无须移动大量记录。 代价是: 增加一个辅助数组的存储空间和初始表分块排序运算。
8.2.4 线性表查找方法的比较 1、顺序查找 顺序存储和链表存储皆可。优点是算法简单且对表的结构无任何要求;缺点是查找效率低;适用于n较小的表的查找和查找较少但改动较多的表。 2、二分查找 只用于顺序存储结构。优点是查找效率高;缺点是要求线性表按键值有序排列,且只适用顺序存储结构;特别适用于一经建立就很少改动又经常需要查找的线性表。 3、分块查找 顺序存储和链表存储皆可。优点是在表中插入和删除记录时,只要找到该元素所属的块,就在该块内进行插入和删除运算。因块内记录的存放是任意的,故插入和删除较容易,不需移动大量元素;缺点是要增加一个辅助数组的存储空间和将初始表分块排序的运算;适用于有分块特点的记录。
8.3 动态查找表 • 二叉排序树 • 平衡二叉树 • 2-3树 • B-树和B+树 • 键树
8.3.1 二叉排序树 1、二叉排序树的定义: 二叉排序树或者是空树,或者是满足如下性质的二叉树: ① 若其左子树非空,则左子树所有结点值均小于根结点的值; ② 若其右子树非空,则右子树所有结点值均大于根结点的值; ③ 左、右子树本身各是一棵二叉排序树。 上述性质简称二叉排序树性质(BST性质),故二叉排序树是满足BST性质的二叉树。
8.3.1 二叉排序树 二叉排序树(Binary Sort Tree)又称二叉查找(搜索)树(Binary Search Tree)。 2、二叉排序树的特点 ① 二叉排序树中任一结点x,其左(右)子树中任一结点y(若存在)的关键字必小(大)于x的关键字。 ② 二叉排序树中,各结点关键字是唯一的。 ③ 按中序遍历该树所得到的中序序列是一个递增有序序列。
8.3.1 二叉排序树 2、二叉排序树的特点 例如,如图8.7所示的是二叉排序树,它的中序序列为有序序列:11, 15, 18, 20, 30, 33, 36, 41。
8.3.1 二叉排序树 3、 二叉排序树的存储结构 定义 8.3 typedef int KeyType; /*假定关键字类型为整数*/ typedef struct node { /*结点类型*/ KeyType key; /*关键字项*/ InfoType data; /*data为记录中其他数据项总称*/ struct node *lchild,*rchild; /*左右孩子指针*/ } BSTNode; typedef BSTNode *BSTree /*BSTree是二叉排序树的类型*/
8.3.1 二叉排序树 4. 二叉排序树上的运算 (1) 二叉排序树的插入和生成 ①二叉排序树插入新结点的过程 (a)若二叉排序树T为空,则为待插入的关键字key申请一个新结点,并令其为根; (b)若二叉排序树T不为空,则将key和根的关键字比较: (i)若二者相等,则说明树中已有此关键字key,无须插入。 (ii)若key<T->key,则将key插入根的左子树中。 (iii)若key>T->key,则将它插入根的右子树中。 子树中的插入过程与上述的树中插入过程相同。如此进行下去,直到将key作为一个新的叶结点的关键字插入到二叉排序
8.3.1 二叉排序树 树中,或者直到发现树中已有此关键字为止。 ②二叉排序树插入新结点的递归算法 (算法 8.4) int Insert_BST( BSTree *p, KeyType k,BSTree *f) {/*在以p为根结点的BST中插入关键字k。插入成功返回1,否则返回0*/ if( p==NULL) /*原树为空,新插入的记录为根结点*/ { p=(BSTree *)malloc(sizeof(BSTree)); p->key=k; p->lchild=p->rchild=NULL; return 1; } else if( k==p->key) /*树中存在相同关键字的结点,返回0*/ return 0; else if( k<p->key) return Insert_BST(p->lchild, k,p); /*插入到p的左子树中*/ else return Insert_BST(p->rchild, k,p); } /*插入到p的右子树中*/
8.3.1 二叉排序树 ③二叉排序树插入新结点的非递归算法(算法 8.5 ) void InsertBST(BSTree *Tptr,KeyType key) { /*若二叉排序树 *Tptr中没有关键字为key,则插入,否则直接返回*/ BSTNode *f,*p=*Tptr; /*p的初值指向根结点*/ while(p) /*查找插入位置*/ { if(p->key==key) return; /*树中已有key,无须插入*/ f=p; /*f保存当前查找的结点*/ if (key<p->key)p=p->lchild /*若key<p->key,则在左子树中查找*/ else p=p->rchild; } /*否则在右子树中查找*/ p=(BSTNode *)malloc(sizeof(BSTNode)); p->key=key; p->lchild=p->rchild=NULL; /*生成新结点*/ if(*TPtr==NULL) *Tptr=p; /*原树为空,新插入的结点为新的根*/ else /*原树非空*/ if(key<f->key) f->lchild=p ; /* p作为f左孩子插入*/ else f->rchild=p; }/*或作为f右孩子插入*/
8.3.1 二叉排序树 ④二叉排序树的生成算法(算法 8.6 ) 从空的二叉排序树开始,每输入一个结点数据,就调用一次插入算法将它插入到当前已生成的二叉排序树中。 BSTree CreateBST(void) { /*输入一个结点序列,建立一棵二叉排序树,将根结点指针返回*/ BSTree T=NULL; /*初始时T为空树*/ KeyType key; scanf("%d",&key); /*读人一个关键字*/ while(key) /*假设key=0是输人结束标志*/ { InsertBST(&T,key); /*将key插入二叉排序树T*/ scanf("%d",&key);/*读入下一关键字*/ } return T; /*返回建立的二叉排序树的根指针*/ }
8.3.1 二叉排序树 ⑤二叉排序树的(输入实例(5,2,7,3,1,14,9))
8.3.1 二叉排序树 (2) 二叉排序树的查找 • 二叉排序树的查找十分方便, 其平均查找长度明显小于一般的二叉树。 • 二叉排序树的查找从根结点出发, 当访问到树中某个结点时, 如果该结点的关键字值等于给定的关键字值, 就宣布查找成功。 反之, 如果该结点的关键字值大(小)于已给的关键字值, 下一步就只需考虑查找左(右)子树了。 换言之, 每次只需查找左或右子树的一枝便够了, 效率明显提高。
8.3.1 二叉排序树 ①二叉排序树查找递归算法(算法 8.7) BSTNode *SearchBST(BSTree T,KeyType key) {/*在二叉排序树T上查找关键字为key的结点,成功时返回该结点位置,否则返回NUll*/ if(T==NULL||key==T->key) /*递归的终结条件*/ return T; /*T为空,查找失败;否则成功,返回找到结点位置*/ if(key<T->key) return SearchBST(T->lchild,key); /*继续在左子树中查找*/ else return SearchBST(T->rchild,key);/*继续在右子树中查找*/ }
8.3.1 二叉排序树 ② 算法分析:在二叉排序树上进行查找时的平均查找长度和二叉树的形态有关。 • 在最坏情况下,二叉排序树是通过把一个有序表的n个结点依次插入而生成的,此时所得的二叉排序树蜕化为一棵深度为n的单支树,它的平均查找长度和线性表的顺序查找相同,亦是(n+1)/2。 • 在最好情况下,二叉排序树在生成的过程中,树的形态比较匀称,最终得到的是一棵形态与二分查找的判定树相似的二叉排序树,此时它的平均查找长度大约是log2n。 • 插入、删除和查找算法的时间复杂度均为O(log2n)。
8.3.1 二叉排序树 (3)二叉排序树的删除的步骤 ①进行查找。查找时,令p指向当前访问到的结点,parent指向其双亲(其初值为NULL)。若树中找不到被删结点则返回0,否则被删结点是*p。 ②删去*p。删*p时,应将*p的子树(若有)仍连接在树上且保持BST性质不变。按*p的孩子数目分三种情况进行处理。
8.3.1 二叉排序树 *p是叶子(即它的孩子数为0)。无须连接*p的子树,只需将*p的双亲*parent中指向*p的指针域置空即可。
8.3.1 二叉排序树 • *p只有一个孩子*child。只需将*child和*p的双亲直接连接后,即可删去*p。 • 注意:*p既可能是*parent的左孩子也可能是其右孩子,而*child可能是*p的左孩子或右孩子,故共有4种状态。
8.3.1 二叉排序树 • *p有两个孩子。先令q=p,将被删结点的地址保存在q中;然后找*q的中序后继*p,并在查找过程中仍用parent记住*p的双亲位置。*q的中序后继*p一定是*q的右子树中最左下的结点,它无左子树。因此,可以将删去*q的操作转换为删去的*p的操作,即在释放结点*p之前将其数据复制到*q中,就相当于删去了*q。
8.3.2 平衡二叉树 1.平衡二叉树的定义: 形态匀称的二叉排序树称为平衡二叉树(balanced binary tree)。 其严格定义是:一棵空树是平衡二叉树; 若T是一棵非空二叉树, 其左、 右子树为TL和TR,令hl和hr分别为左、右子树的深度,相应地定义hl-hr为二叉平衡树的平衡因子(balance factor)。当且仅当①|hl-hr|≤1时,即任一结点的平衡因子的绝对值都不大于1,则T是平衡二叉树, ②TL、TR都是平衡二叉树。如图8.13所示。 平衡二叉树上所有结点的平衡因子可能是-1, 0, 1。