920 likes | 1.05k Views
第 8 章 查找. 在英汉字典中查找某个英文单词的中文解释;在新华字典中查找某个汉字的读音、含义;在对数表、平方根表中查找某个数的对数、平方根;邮递员送信件要按收件人的地址确定位置等等。可以说查找是为了得到某个信息而常常进行的工作。 计算机、计算机网络使信息查询更快捷、方便、准确。要从计算机、计算机网络中查找特定的信息,就需要在计算机中存储包含该特定信息的表。如要从计算机中查找英文单词的中文解释,就需要存储类似英汉字典这样的信息表,以及对该表进行的查找操作。本章将讨论的问题即是“信息的存储和查找”。
E N D
第8章 查找 • 在英汉字典中查找某个英文单词的中文解释;在新华字典中查找某个汉字的读音、含义;在对数表、平方根表中查找某个数的对数、平方根;邮递员送信件要按收件人的地址确定位置等等。可以说查找是为了得到某个信息而常常进行的工作。 • 计算机、计算机网络使信息查询更快捷、方便、准确。要从计算机、计算机网络中查找特定的信息,就需要在计算机中存储包含该特定信息的表。如要从计算机中查找英文单词的中文解释,就需要存储类似英汉字典这样的信息表,以及对该表进行的查找操作。本章将讨论的问题即是“信息的存储和查找”。 • 查找是许多程序中最消耗时间的一部分。因而,一个好的查找方法会大大提高运行速度。另外,由于计算机的特性,象对数、平方根等是通过函数求解,无需存储相应的信息表。
第8章 查找 • 8.1 查找的基本概念 • 8.2 静态查找表 • 8.3 动态查找表 • 8.4 哈希表
8.1 查找的基本概念 查找表:是由具有同一类型(属性)的数据元素(记录)组成的集合。分为静态查找表和动态查找表两类。 静态查找表:仅对查找表进行查找操作,而不能改变的表; 动态查找表:对查找表除进行查找操作外,可能还要进行向表中插入数据元素,或删除表中数据元素的表。 关键字:数据元素的某个数据项的值,用它可以标识列表中的一个或一组数据元素。如果一个关键字可以唯一标识列表中的一个数据元素, 则称其为主关键字,否则为次关键字。当数据元素仅有一个数据项时, 数据元素的值就是关键字。
查找: 根据给定的关键字值,在特定的查找表中确定一个其关键字与给定值相同的数据元素,并返回该数据元素在查找中的位置。若找到相应的数据元素, 则称查找是成功的,否则称查找是失败的,此时应返回空地址及失败信息,并可根据要求插入这个不存在的数据元素。显然,查找算法中涉及到三类参量: ① 查找对象K(找什么); ② 查找范围L(在哪找); ③ K在L中的位置(查找的结果)。其中①、②为输入参量, ③为输出参量,在函数中,输入参量必不可少,输出参量也可用函数返回值表示。
平均查找长度:为确定数据元素在列表中的位置, 需和给定值进行比较的关键字个数的期望值,称为查找算法在查找成功时的平均查找长度。对于长度为n的列表, 查找成功时的平均查找长度为: 其中Pi为查找列表中第i个数据元素的概率,Ci为找到列表中第i个数据元素时,已经进行过的关键字比较次数。由于查找算法的基本运算是关键字之间的比较操作,所以可用平均查找长度来衡量查找算法的性能。
数据元素类型说明 在手工绘制表格时,总是根据有多少数据项,每个数据项应留多大宽度来确定表的结构,即表头的定义。然后,再根据需要的行数,画出表来。在计算机中存储的表与手工绘制的类似,需要定义表的结构,并根据表的大小为表分配存储单元。
本章以后讨论中,涉及的关键字类型和数据元素类型统一说明如下:本章以后讨论中,涉及的关键字类型和数据元素类型统一说明如下: typedef float KeyType; typedef int KeyType; typedef char* KeyType; typedef struct { KeyType key;// 关键域 …… // 其它字段 } ElemType
对两个关键字的比较约定如下: • //对数值型关键字 #define EQ(a,b) ((a)==(b)) #define LT(a,b) ((a)<(b)) #define LQ(a,b) ((a)<=(b)) • //对字符型关键字 #define EQ(a,b) (!strcmp((a),(b))) #define LT(a,b) (strcmp((a),(b))<0) #define LQ(a,b) (strcmp((a),(b))<=0)
8.2 静态查找表 静态查找表结构 静态查找表是数据元素的线性表,可以是基于数组的顺序存储或以线性链表存储。 /* 顺序存储结构 */ typedef struct{ ElemType *elem; /*数组基址 */ int length; /* 表长度 */ }SSTABLE;
//链式存储结构结点类型 typedef struct NODE{ ElemType elem;// 结点的值域 struct NODE *next;//下一个结点指针域 }NodeType;
8.2.1 顺序查找 • 顺序查找又称线性查找,是最基本的查找方法之一。其查找方法为:从表中最后一个记录开始,逐个进行记录的关键字和给定值的比较,若某个记录的关键字和给定值比较相等,则查找成功,并给出数据元素在表中的位置;反之,若直至第一个记录,其关键字和给定值比较都不等,则查找失败,给出失败信息。
以顺序存储为例,数据元素从下标为1的数组单元开始存放,0号单元留空。以顺序存储为例,数据元素从下标为1的数组单元开始存放,0号单元留空。 int Search _ Seq (SSTable ST,KeyType key) { /*在表ST中查找关键字为kex的数据元素,若找到返回该元素在数组中的下标,否则返回0 */ ST.elem[0].key = key;//”哨兵“ for( i = ST.length ; !EQ(ST.elem[i].key , key); -- i); /* 从标尾端向前找 */ return i; }// Search _ Seq
性能分析 • 就上述算法而言,对于n个数据元素的表,给定值key与表中第i个元素关键码相等,即定位第i个记录时,需进行n-i+1次关键码比较,即Ci=n-i+1。则查找成功时,顺序查找的平均查找长度为: 设每个数据元素的查找概率相等,即Pi=1/n,则等概率情况下有 查找不成功时,关键码的比较次数总是n+1次。 算法中的基本工作就是关键码的比较,因此,查找长度的量级就是查找算法的时间复杂度,其为O(n)。
许多情况下,查找表中数据元素的查找概率是不相等的。为了提高查找效率,查找表需依据查找概率越高,比较次数越少;查找概率越低,比较次数就较多的原则来存储数据元素。许多情况下,查找表中数据元素的查找概率是不相等的。为了提高查找效率,查找表需依据查找概率越高,比较次数越少;查找概率越低,比较次数就较多的原则来存储数据元素。 • 顺序查找缺点是当n很大时,平均查找长度较大,效率低;优点是对表中数据元素的存储没有要求。另外,对于线性链表,只能进行顺序查找。
8.2.2 有序表的折半查找 • 有序表即是表中数据元素按关键码升序或降序排列。 • 折半查找的思想为:在有序表中,取中间元素作为比较对象,若给定值与中间元素的关键码相等,则查找成功;若给定值小于中间元素的关键码,则在中间元素的左半区继续查找;若给定值大于中间元素的关键码,则在中间元素的右半区继续查找。不断重复上述查找过程,直到查找成功,或所查找的区域无数据元素,查找失败。
【步骤】 ①low=1;high=length; // 设置初始区间 ②当low>high时,返回查找失败信息 // 表空,查找失败 ③low≤high,mid=(low+high)/2; // 取中点 a. 若key<ST.elem[mid].key,high=mid-1;转②// 查找在左半区进行 b. 若key>ST.elem[mid].key,low=mid+1;转②// 查找在右半区进行 c. 若key=ST.elem[mid].key,返回数据元素在表中位置// 查找成功
算法描述: int Search _ Bin(SSTable ST,KEY key) { /* 在表ST中查找关键码为key的数据元素,若找到返回该元素在表中的位置,否则,返回0 */ int mid,flag=0; low=1;high=ST.length; /* ①设置初始区间 */ while(low<=high) /* ②表空测试 */ {mid=(low+high)/2; /* ③得到中点 */ if(EQ(key,ST.elem[mid].key) return mid;//找到待查元素 else if(LT(key,ST.elem[mid].key) high=mid-1;// 调整到左半区 else low=mid+1; } return 0; }// Search _ Bin
折半查找的性能分析 • 从折半查找过程看,以有序表的中点为比较对象,并以中点将表分割为两个子表,对定位到的子表继续这种操作。所以,对表中每个数据元素的查找过程,可用二叉树来描述,称这个描述查找过程的二叉树为判定树。
56 19 05 80 21 64 88 13 37 75 92 折半查找的性能分析 关键字序列:05,13,19,21,37,56,64,75,80,88,92 • 显然,找到有序表中任一记录的过程,对应判定树中从根结点到与该记录相应的结点的路径,而所做比较的次数恰为该结点在判定树上的层次数。因此,折半查找成功时,关键字比较次数最多不超过判定树的深度。 查找key=21 由于判定树的叶子结点所在层次之差最多为1,故n个结点的判定树的深度与n个结点的完全二叉树的深度相等,均为[log2n]+1。这样,折半查找成功时,关键字比较次数最多不超过[log2n]+1。
56 19 05 80 21 64 88 13 37 75 92 折半查找的性能分析 相应地,折半查找失败时的过程对应判定树中从根结点到某个含空指针的结点的路径,因此,折半查找失败时,关键字比较次数最多也不超过判定树的深度[log2n]+1。 查找key=85 折半查找时查找不成功的过程就是走了一条从根结点到外部结点的路径,和给定值进行比较的关键字个数等于该路径上内部结点个数。
折半查找的性能分析 • 为便于讨论,假定表的长度n=2h-1,则相应判定树必为深度是h的满二叉树,h=log2(n+1)。又假设每个记录的查找概率相等, 则折半查找成功时的平均查找长度为
8.2.3静态树表的查找 • 当有序表中各记录的查找概率相等时,用折半查找其性能最优。如果有序表中各记录的查找概率不等,情况又如何呢?
静态树表的查找 • 例如:假设有序表中含5个记录,并且各记录的查找概率分别为p1=0.1, p2=0.2, p3=0.1, p4=0.4, p5=0.2。对此有序表进行折半查找,查找成功时的平均查找长度为2.3,但如果在查找时令给定值先和第4个记录的关键字进行比较,比较不相等时再继续在左或右子序列中进行折半查找,则查找成功的平均查找长度为1.8。说明当有序表中各记录的查找概率不等时,按判定树进行折半查找,其性能未必是最优的,此时应如何进行查找呢?
静态树表的查找 • 如果只考虑查找成功的情况,则使查找性能最佳的判定树是其带权内路径长度之和PH值 取最小值的二叉树。其中n为二叉树上结点的个数(即有序表的长度);hi为第i个结点在二叉树上的层次数;结点的权wi=cpi(i=1,2,…n),其中pi为结点的查找概率,c为某个常量。称PH值去最小的二叉树为静态最优查找树。由于构造它花费的时间代价较高,我们来构造近似最优查找树(次优查找树)的有效算法。
构造次优查找树的方法 • 设按关键字有序的记录序列为: rl, rl+1,… rh (1) 与每个记录相应的权值为 wl, wl+1,… wh 在(1)中取第i个记录构造根结点ri,使得 • 取最小值,然后分别对子序列rl, rl+1,… ri-1和ri+1, ri+2,… rh构造两可次优查找树,分别设为根结点的左右子树。
为便于计算 ,引入累计权值和 并设wl-1=0和swl-1=0,则 =|(swh-swi)-(swi-1-swl-1)| =|(swh+swl-1)-swi-swi-1|
构造次优查找树的递归算法 void SecondOptimal(BiTree &T,ElemType R[],float sw[],int low,int high) {i=low; mon=abs(sw[high]-sw[low]; dw=sw[high]+sw[low-1]; for(j=low+1;j<=high;++j) if(abs(dw-sw[j]-sw[j-1])<min) { i=j; min=abs(dw-sw[j]-sw[j-1]) } T=(BiTree)malloc(sizeof(BiNode)); T->data=R[i]; //生成结点 If(i==low) T->lchild=NULL; //左子树为空 Else SecondOptimal(T->lchild,R,sw,low,i-1); //构造左子树 If(i==high) T->rchild=NULL; //右子树为空 Else SecondOptimal(T->rchild,R,sw,i+1,high); //构造右子树 }// SecondOptimal
8.2.4索引顺序表的查找 • 分块查找又称索引顺序查找,这是顺序查找的一种改进方法。 • 条件:1、将表分为若干子表; 2、建立一个索引表,每个索引项包含关键字项和指针项; 3、索引表按关键字有序,表或者有序或者分块有序(指第二个子表中所有记录的关键字均大于第一个子表中的最大关键字,第三个子表中所有记录的关键字均大于第二个子表中的最大关键字,依次类推) 。
查找方法: 1、先确定待查记录所在的块(可以顺序查找也可折半查找)。 2、在块中查找记录(只能顺序查找)。
性能分析: 一般情况下,为进行分块查找,可以将长度为n的表均匀地分成b块,每块含s个记录,即b=n/s;又假定表中每个记录的查找概率相等,若用顺序查找确定所在的块,则
8.3动态查找表 • 动态查找表的特点是:表结构本身是在查找过程中动态生成的,即对于给定值key,若表中存在其关键字等于key的记录,则查找成功返回,否则插入关键字等于key的记录
8.3.1二叉排序树 一、二叉排序树定义 二叉排序树(Binary Sort Tree)或者是一棵空树;或者是具有下列性质的二叉树: ⑴若左子树不空,则左子树上所有结点的值均小于根结点的值;若右子树不空,则右子树上所有结点的值均大于根结点的值。 ⑵左右子树也都是二叉排序树 二叉排序树(BST)又称二叉搜索树,二叉查找树或二叉检索树。
二、二叉排序树查找过程 从其定义可见,二叉排序树的查找过程为: ①若查找树为空,查找失败。 ②查找树非空,将给定值key与查找树的根结点关键码比较。 ③若相等,查找成功,结束查找过程,否则, a.当key小于根结点关键码,查找将在以左子女为根的子树上继续进行,转① b.当给key大于根结点关键码,查找将在以右子女为根的子树上继续进行,转①
以二叉链表作为二叉排序树的存储结构,则查找过程算法程序描述如下:以二叉链表作为二叉排序树的存储结构,则查找过程算法程序描述如下: BiTree SearchBST(BiTreeT, KeyType key) {/*在根指针T所指二叉排序树中, 递归查找某关键字等于key的元素, 若查找成功,则返回指向该元素结点指针, 否则返回空指针 */ if( (!T) ||EQ(key,T->data.key)) return NULL; //查找结束 else if LT(key ,T->data. key) return( SearchBST(T->lchild, key)); //在左子树中继续查找 else return (SearchBST(T->rchild, key)); //在右子树中继续查找 }// SearchBST
三、二叉排序树插入操作和构造一棵二叉排序树三、二叉排序树插入操作和构造一棵二叉排序树 • 先讨论向二叉排序树中插入一个结点的过程:设待插入结点的关键码为key,为将其插入,先要在二叉排序树中进行查找,若查找成功,按二叉排序树定义,待插入结点已存在,不用插入;查找不成功时,则插入之。因此,新插入结点一定是作为叶子结点添加上去的。并且是查找不成功时查找路径上访问的最后一个结点的左孩子或右孩子结点。 • 构造一棵二叉排序树则是逐个插入结点的过程。
63 63 63 63 63 63 63 63 63 70 70 70 70 70 70 70 70 42 42 42 42 42 55 55 55 55 55 55 55 90 90 90 90 90 90 90 90 90 67 67 67 67 67 67 98 98 98 98 83 83 83 10 10 45 二叉排序树插入操作和构造一棵二叉排序树 例:记录的关键码序列为:63,90,70,55,67,42,98,83,10,45则构造一棵二叉排序树的过程如下: 插入63 插入90 插入70 插入55 插入42 φ 插入67 63 插入98 插入83 插入10 插入45
二叉排序树插入操作和构造一棵二叉排序树 已知一个关键字值为key的结点s, 若将其插入到二叉排序树中,只要保证插入后仍符合二叉排序树的定义即可。插入可以用下面的方法进行: ① 若二叉排序树是空树,则key成为二叉排序树的根;② 若二叉排序树非空, 则将key与二叉排序树的根进行比较,如果key的值等于根结点的值,则停止插入;如果key的值小于根结点的值,则将key插入左子树;如果key的值大于根结点的值,则将key插入右子树。相应的递归算法如下:
二叉排序树插入操作和构造一棵二叉排序树 void InsertBST(BiTree *bst, KeyType key) /*若在二叉排序树中不存在关键字等于key的元素, 插入该元素 */ { if (*bst==NULL) /*递归结束条件*/ { s=(BSTree)malloc(sizeof(BSTNode)); /*申请新的结点s*/ s-> key=key; s->lchild=NULL; s->rchild=NULL; *bst=s; return; } else if EQ(key ,(*bst)->key) return; else if LT(key ,(*bst)->key) InsertBST(&((*bst)->lchild), key); /*将s插入左子树*/ else if (key > (*bst)->key) InsertBST(&((*bst)->rchild), key); /*将s插入右子树*/ }
四.二叉排序树删除操作 从二叉排序树中删除一个结点,不能把以该结点为根的子树都删去, 只能删掉该结点,并且还应保证删除后所得的二叉树仍然满足二叉排序树的性质不变。也就是说,在二叉排序树中删去一个结点相当于删去有序序列中的一个结点。 删除操作首先要查找,已确定被删结点是否在二叉排序树中。若不在 ,则不做任何操作;否则,假设要删除的结点为p,结点p的双亲结点为f,并假设结点p是结点f的左孩子(右孩子的情况类似)。以下分三种情况进行讨论。
⒈ *p结点为叶结点,由于删去叶结点 后不影响整棵树的特性,所以,只 需将被删结点的双亲结点相应指针 域改为空指针。
*f *f F *f *f F F F 删除p *p P *p 删除p *p *p P Pr Pl Pl pr • ⒉若p结点只有左子树,或只有右子树,则可将p的左子树或右子树直接改为其双亲结点f的左子树,即:f->lchild=p->lchild(或f->lchild=p->rchild); free(p);
f f F F p p S P c 删除结点P c PR C PR C q q Cl Q Cl s s Q Sl Ql S Ql Sl • ⒊*p结点既有左子树Pl又有右子树Pr,可按中序遍历保持有序进行调整。 首先找到p结点在中序序列中的直接前驱s结点, 然后用s结点的值替代p结点的值,再将s结点删除,原s结点的左子树改为s的双亲结点q的右子树:p->data=s->data;q->rchild= s->lchild;free(s);
BiTNode * DelBST(BiTree t, KeyType k) /*在二叉排序树t中删去关键字为k的结点*/ { BiTNode *p=t, *f=NULL, *s , *q; while(p) /*查找关键字为k的待删结点p*/ { if EQ(p->key,k ) break; /*找到, 则跳出查找循环*/ f=p; /*f指向p结点的双亲结点*/ if LT( k, p->key ) p=p->lchild; else p=p->rchild; } if (p==NULL) return t; /*若找不到, 返回原来的二叉排序树*/
if(p->lchild==NULL) /*p无左子树*/ { if(f==NULL) t=p->rchild; /*p是原二叉排序树的根*/ else if (f->lchild==p) /*p是f的左孩子*/ f->lchild=p->rchild ; /*将p的右子树链到f的左链上*/ else /*p是f的右孩子*/ f->rchild=p->rchild ; /*将p的右子树链到f的右链上*/ free(p); /*释放被删除的结点p*/ }
else /*p有左子树*/ { q=p; s=p->lchild; while(s->rchild) /*在p的左子树中查找最右下结点*/ {q=s; s=s->rchild; } if(q==p) q->lchild=s->lchild ; /*将s的左子树链到q上*/ else q->rchild=s->lchild; p->key=s->key; /*将s的值赋给p*/ free(s); } return t; } /*DelBST*/
二叉排序树的查找分析 • 在二叉排序树上查找其关键字等于给定值的结点的过程,恰是走了一条从根到该结点的过程,和给定值比较的关键字个数等于结点所在的层次数,因此,和折半查找类似,与给定值比较的关键字个数不超过树的深度。然而,折半查找长度为n的表的判定树是唯一的,而含有n个结点的二叉排序树却不唯一。即含有n个结点的二叉排序树的平均查找长度和树的形态有关。 思考:含有n个结点的二叉排序树的最好平均查找长度是多少?最差又是多少? 最好:log2n 最差:(n+1)/2
8.3.2平衡二叉树(AVL树) • 平衡二叉树或者是一棵空树,或者是具有下列性质的二叉排序树:它的左子树和右子树都是平衡二叉树,且左子树和右子树高度之差的绝对值不超过1。 • 平衡因子:结点的左子树深度减去它的右子树深度。 • 引入平衡二叉排序树的目的是为了提高查找效率。
在平衡二叉树上插入或删除结点后,可能使树失去平衡,因此,需要对失去平衡的树进行平衡化调整。设a结点为失去平衡的最小子树根结点,对该子树进行平衡化调整归纳起来有以下四种情况:
p 0 0 2 1 p B A A B AR BL h-1 BL BR BR AR h-1 LL x x 一. 右单旋转(LL型) rc=p->lchild; p->lchild=lc->rchild; Lc->rchild=p;p=lc; 具体算法P236 :算法9.9 【调整策略】 调整后的子树除了各结点的平衡因子绝对值不超过1,还必须是二叉排序树。 LL型失衡的特点是:A->bf=2,B->bf=1。