900 likes | 997 Views
第九章 查找. 查找:亦称检索,从大量数据元素中找到所需要的特定数据元素的过程。 对象:由一组结点组成的数据集合,每个结点由若干个数据项组成,能唯一标识该结点的数据项为记录关键字。. 查找的概念. 查找表:同一类型的数据元素(记录)构成的集合。 集合中的数据元素之间存在着完全松散的关系,所以查找表是一种非常灵活的数据结构。 对查找表经常进行的操作有: 1 、查找某个特定的数据元素是否在表中。 2 、检索某个特定的数据元素的各种属性。 3 、在表中插入一个数据元素。 4 、从表中删除某个数据元素。 静态查找表:对查找表只进行查找操作。
E N D
第九章 查找 查找:亦称检索,从大量数据元素中找到所需要的特定数据元素的过程。 对象:由一组结点组成的数据集合,每个结点由若干个数据项组成,能唯一标识该结点的数据项为记录关键字。
查找的概念 查找表:同一类型的数据元素(记录)构成的集合。 集合中的数据元素之间存在着完全松散的关系,所以查找表是一种非常灵活的数据结构。 对查找表经常进行的操作有: 1、查找某个特定的数据元素是否在表中。 2、检索某个特定的数据元素的各种属性。 3、在表中插入一个数据元素。 4、从表中删除某个数据元素。 静态查找表:对查找表只进行查找操作。 动态查找表:不仅进行查找操作,而且在查找过程中还伴随着插入、删除某个数据元素的操作。
查找的概念 关键字(key):是数据元素(或记录)的某个数据项的值,用它可标识(识别)一个数据元素(或记录)。 主关键字:可以唯一标识一个数据元素或记录的关键字。 次关键字:用以标识或识别若干个记录的关键字。 标识:是一个记录赖以存在的基础。 识别:通过它可以找到对应的记录。 设置主关键字的目的:保证查找记录的唯一性。 查找:根据给定的某个值,在查找表中确定一个其关键字值等于给定值的数据元素或记录(在列表中的位置)的过程。 查找的结果: a.查到该记录,称为查找成功,给出其在列表中的位置或其它信息。 b.查不到元素,称为查找失败,表中无此记录,可输出相应信息或把元素插入表中。
查找的概念 如何查找? 对表进行查找的方法取决于表中数据元素依何种关系(这种关系是人为地加上的)组织在一起的。 电话号码簿是依用户分类(集体或个人)且依笔划(或拼音)顺序排列。查找方法是先依类顺序查找,再在类中进行顺序查找。 英汉字典查找:英汉字典是按单词的字母在字母表中的顺序排列的。可以采用索引和顺序查找。 查找所讨论的数据对象是数据集合,数据元素之间仅存在着“同属一个集合”的松散关系,给查找带来不便。因此,需要在数据元素之间人为地加上一些关系,以便按某种规律进行查找,即以另一种数据结构(线性表)来表示查找表。
ASL= 其中Pi为查找第i个元素的概率,且有: =1。 查找的概念 查找算法性能评价 衡量一个算法好坏的度量为: 时间复杂度:衡量算法执行的时间度量。 空间复杂度:数据结构所占存储空间和附加空间。 通常用平均查找长度来作为衡量查找算法好坏的依据。 定义:为确定记录在查找表中的位置,需与给定值进行比较的关键字个数的期望值称为查找算法在查找成功时的平均查找长度。 对于含有n个元素的表,查找成功的平均查找长度为: 一般认为查找每个元素的概率相等,即Pi=1/n,Ci为查找第i个元素所用到的比较次数。
查找的概念 1、典型的关键字类型说明: typedef float KeyType; typedef int KeyType; typedef char *KeyType; 2、数据元素类型定义: typedef struct { KeyType key; … }ElemType; 3、两个关键字比较约定: #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)
静态查找表 静态表抽象数据类型 ADT Staticsearchtable { 数据对象D:具有相同特性的数据元素的集合。 数据关系R:数据元素同属于一个集合。 数据操作: create(&ST,n); 构造一个含有n个数据元素的静态查找表ST。 destroy(&ST); 撤消ST。 search(ST,key); 在ST中查找关键字等于key的数据元素,若找到则返回元素的值或在ST中的位置,否则为空。 traverse(ST); }
顺序表的查找 对象:以顺序表表示的静态查找表。 假定:记录关键字无序的顺序分配的线性表。 数据类型定义: typedef struct { elemtype *elem; int length; }SStable 基本思想:从表的一端开始,顺序扫描静态表,依次将给定值与扫描结点关键字进行比较,直到两者相符,找到该元素为止。若查遍表仍未找到,则查找失败。 实现技巧:把给定值K作为第0个元素(该元素不是查找表的元素)的关键字。这样不必判断是否已查找完整个表。第0个元素起到了监视哨的作用。这样可节省大量测试时间。
顺序表的查找 算法: int search(SStable ST,KeyType key) { ST.elem[0].key=key; //0 位置本身为空单元 for(i=ST.length;!EQ(ST.elem[i].key,key);- -i); return i; } //若存在返回在静态表中的位置 i ,否则 i=0 线性链表结构 LinkListLinkSearch(LinkList S;KeyType key) //S--头指针,p--被查找元素的指针 { p=s; while( (p&&(p->key!=key) p=p->next; return(p) }
ASL= =(n*(n+1)/2n= 顺序表的查找 查找操作的性能分析: 空间复杂度:O(1),增加了一个元素的辅助空间。 时间复杂度:假定记录随机分布,每个记录查找概率,即Pi=1/n。则 若查找不成功,比较次数肯定是n+1。 成功查找所需平均次数为: 顺序查找的优点:对表的结构无任何要求(勿需排序),算法简单且适应面广。 缺点:效率低(当表很长时,线性查找的效率很低)
有序表的查找 对象:有序表,即设元素按关键字非递减(或非递增)顺序排序。 基本思想:设R[low..high]是当前查找区间 首先选取区间的中点位置记录,比较其关键字: 若相等,则查找成功,返回此位置值;否则若所找记录关键字大,则取后半部记录,否则取其前半部记录。 重复上面过程,直到找到或证明无此记录为止。 这种有序表的查找方法称为折半查找或二分查找。
有序表的查找 令:low--所查部分的下限。 high--所查部分的上限。 mid--用作比较的记录号。 设:表长为n 初值:low=1,high=n 步骤:a. mid=(int) ((low+high)/2) b. key==R[mid].key 成功。退出。 key<R[mid].key high=mid-1转(a) key>R[mid].key low=mid+1转(a) 未找到的标志:low>high。
有序表的查找 例:设表元素的关键字为: 3 10 13 17 40 43 50 70 k=43 第一步:low=1,high=8,mid=4 k>17 令 low=mid+1=5 第二步:low=5,high=8,mid=6 k=43找到。 k=5 第一步:low=1,high=8,mid=4 k<17 令 high=mid-1=3 第二步:low=1,high=3,mid=2 k<10 令 high=mid-1=1 第三步:low=1,high=1,mid=1 k>3 令 low=mid+1=2 此时low>high,查找失败。
有序表的查找 算法: int search(SStable ST,KeyType key) { 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; }
17 10 43 3 13 40 50 70 <3 4-9 11-12 14-16 18-39 41-42 44-49 51-69 <71 有序表的查找 算法分析:可用二叉树来描述查找路径,查找过程实际上是确定被查找元素在二叉树(逻辑上的)中的位置。即当前查找区间的中点位置作为二叉树的根,左子表和右子表分别作为根的左子树和右子树,由此得到的二叉树称为二叉查找的判定树或比较树。判定树的形态只与表结点的个数有关,与输入实例的关键字的取值无关。 成功的查找过程恰好是走了一条从根结点到被查找结点的路径(图中的内部结点)。 若查找失败,查找过程则是经历了一条从根结点到某个外部结点的的路径 判定树的深度: K=int(log2n)+1 与完全二叉树的相同点:前k-1层是满的
有序表的查找 算法分析:查找比较的最大次数为判定树的深度log2(n+1)。 平均查找长度为:ASL=(n+1)/n log2(n+1)-1 设s=20+2.21+3.22+…+(h-1).2h-2+h.2h-1 则2s=21+2.22+…+(h-2).2h-2+(h-1).2h-1+h.2h s=2s-s=h .2h-(20+21+22+…+2h-2+2h-1) =h .2h-(2h-1) = log2(n+1).(n+1)-n 当n很大时,ASL log2(n+1)-1可以作为二分查找成功时的平均查找长度,它的时间复杂度为O(log2n)。 不成功查找的比较次数也不超过判定树的深度。 结论:折半查找比线性查找的效率高得多,当元素个数固定时,是一种值得推荐的好方法。若要经常插入和删除元素,为保持有序性,必然伴随大量元素的移动。 若查找过程有元素的插入和删除,宜采用二叉排序树结构 。
22 48 86 1 7 13 最大关键字 起始地址 索引表 索引顺序表的查找—分块查找 方法:将表中元素均匀地分成块,块间按大小排序,块内不排序。 辅助数据结构:建立一个关键字表(也称索引表),按大小顺序存放每块中最大或最小关键字值。 关键字表:相当于一个块索引,间接地描述了块间关系。
容易证明,当s取 时,分块查找可取最小值 +1 索引顺序表的查找—分块查找 基本思想:先建立索引(最大(最小)关键字表)。 要查找关键字为key的元素,先按折半查找获得key对应记录所在的块,再按线性查找在块中找到Key对应的记录。 例:查找key=60的记录,在关键字表中对应着86,即第三块,再在第三块中按线性查找找到key为60的记录。 查找效率:设有n个元素,每块s个元素, 查找平均次数=折半查找平均次数+线性查找平均次数=┌lg(n/s+1)┐-1+(s+1)/2≈lg(n/s+1)+s/2 查找平均次数=索引顺序查找平均次数+线性查找平均次数=(n/s+1)/2+(s+1)/2=(n/s+s+2)/2 介于折半查找和线性查找之间。
动态查找表 特点:表结构本身是在动态查找过程中生成的,即对于给定值key,若表中存在着关键字等于key的记录,则查找成功。否则,插入关键字等于key的记录。 动态表抽象数据类型: ADT Staticsearchtable { initDtable(&DT); //初始化 destroyDtable(&DT); //撤消 searchDtable(DT,key); //查找 insertDtable(&DT,e); //插入 deleteDtable(&DT,key) //删除 traverse(DT); //遍历 }
二叉排序树 二叉排序树又称为二叉查找(搜索)树。其定义为: 二叉排序树或者是一棵空树,或者是具有下列性质的二叉树: 1、若它的左子树非空,则左子树上所有结点的值均小于它的根结点的值。 2、若它的右子树非空,则右子树上所有结点的值均大于它的根结点的值。 3、它的左、右子树本身也分别是二叉排序树。 上述性质简称为二叉排序树性质(BST性质)。
二叉排序树 BST性质说明: 1、任一结点x,其左子树的关键字必小于x的关键字,其右子树的关键字必大于x的关键字。按此定义,要求各结点关键字是唯一的。若要求有相同关键字,可修改(1)的“小于”为“小于等于”,或修改(2)的“大于”为“大于等于”。修改(2)可以保证其稳定性。 2、按中序遍历该二叉树所得到的序列是一个递增有序序列。 二叉排序树是用非线性结构表示有序线性表的一种方法。
二叉排序树 数据类型定义: typedef int KeyType; typedef struct node{ struct {KeyType key; InfoType otherinfo; }data; Struct node *lchild,*rchild; }BSTNode; typedef BSTNode *BSTree;
二叉排序树的查找 基本思想:当二叉排序树非空时,首先将给定值与根结点的关键字比较,若相等则查找成功,否则将依据给定值与根结点关键字的大小关系,分别在左子树或右子树上继续进行查找。算法: BSTNode *SearchBST(BSTree T,KeyType key) { if(T==NULL || key==T->data.key) return T; if(key<T->data.key) return(SreachBST(T->lchild,key); else return(SreachBST(T->rchild,key); } 查找成功,则查找过程走了一条从根结点到查找点的路径。 与折半查找的区别:折半查找的对象是顺序存储结构的有序表,判定树是唯一的。
二叉排序树的插入 在二叉排序树中插入新结点,保证插入后仍满足BST的性质。 插入过程:若二叉排序树T为空,则为待插入的关键字key申请一个新结点,并令其为根;否则将key与根的关键字比较: 若二者相等,则说明树中已有此关键字,无需插入; 若key<T->data.key,将其插入到左子树中, 否则 将其插入到右子树中。 插入左右子树的过程与上面插入过程相同。 上面插入过程首先是一个查找过程,若树中有相同结点则不插入;若不存在则插入。 为了使查找过程适合插入等的运算需要,可对其进行改写。
二叉排序树插入算法 Status SearchBST(BSTree T,KeyType key,BSTree f,BSTree &p) { if(!T) {p=f;return FALSE;} else if(EQ(key,T->data.key)) { p=T, return TRUE;} else if(LT(key,T->data.key)) reurn(SreachBST(T->lchild,key,T,p)); else return(SreachBST(T->rchild,key,T,p)); } Status insertBST(BTree T,Elemtype e) { if (!SearchBST(T,e.key,NULL,p) { new(s); s->data=e; s->lchild=s->rchild=Null; if (!p) T=s; // 原树为空 else if LT (e.key,p->data.key) p->lchild=s; else p->rchild=s; return TRUE; } return FALSE; }
10 20 50 13 17 16 二叉排序树的生成算法 BSTree CreateBST(SeqList R) { BSTree T; int i; for(i=0;i<R.length;i++) InsertBST(&T,R.elem[i]); return (T); } 例:R={15,20,10,13,17,50,16} 15
二叉排序树的中序遍历 对二叉排序树按中序遍历输出每个结点的值,就得到了从小到大的排列顺序。所以对任意一个关键字序列构造其二叉排序树的过程就是对关键字进行排序的过程,称这种排序为树排序。树排序的执行时间为O(nlgn)。 构造二叉排序树的目的是为了查找,所以又常称为二叉排序树或二叉查找树。
二叉排序树结点的删除 从二叉排序树中删除一个结点,相当于删除有序序列中的一个结点,只要保证删除后的二叉排序树仍具有BST性质。 假设被删除结点为p,其双亲为parent,则有: 1、p是叶子结点,无须连接p的子树,只需将parent指向p的指针域置空即可。 2、p只有一个孩子,无论哪种情况,都将用parent指向p的孩子的指针代替parent指向p的指针值。 3、p有两个孩子,删除p结点后用p的中序直接前驱来代替p。中序前驱结点是p的左子树中最右结点。删除方法是将p的中序前驱结点的值复制到p中,然后按1或2的方法删除p的中序前驱结点。
p p p s 二叉排序树结点的删除 只需将 p 连接到待删结点的左子树,并释放 p 所指向的结点 只需将 p 连接到待删结点的右子树,并释放 p 所指向的结点 寻找左子树中最右S ,将 S 结点的数据复制到 P,然后删除 S 结点.
二叉排序树 viod Delete (BSTree &p) { if (!p->rchild) {q=p;p=p->lchild;free(q);} //p 无右孩子 else if(!p->lchild) {q=p;p=p->rchild;free(q);} //p 无左孩子 else //p 左右孩子都有 { q=p;s=p->lchild; //寻找左子树中最大值的结点 S while (s->rchild) {q=s;s=s->rchild ;} p->data=s->data; // S的值复制到 P if (q!=p) q->rchild=s->lchild; else q->lchild=s->lchild; free(s); } }
12 45 24 24 53 37 93 45 12 37 53 93 二叉排序树的查找分析 在二叉排序树查找其关键字等于给定值的结点的过程,恰是走了一条从根结点到查找点的路径的过程。 与折半查找的区别:折半查找的对象是顺序存储结构的有序表,判定树是唯一的,其比较的次数不超过树的深度。 含有n个结点的二叉排序树却不唯一,结点的插入先后次序不同,所构成的二叉排序树不同。 (12,24,37,45,53,93) (45,24,53,12,37,93)
二叉排序树的查找分析 二叉排序树是按结点的输入先后顺序插入而形成的,最好的情况接近二叉判定树,其平均查找长度为O(lgn),最坏的情况蜕变为一棵单支树,平均查找长度为(n+1)/2。就平均情况而言,插入、删除和查找的执行时间均不会超过O(h),h为树的高度。 二叉排序树的查找分析: 理想的情况,其二叉排序树的深度为int(lgn)+1。 最坏的情况,其深度为n,即为单支树。当记录已按升序或降序排列时,所产生的二叉排序树为单右支树,或为单左支树。 为了提高查询速度,必须使二叉排序树尽可能保持某种“平衡”,即构造所谓的平衡二叉树。
二叉排序树的查找分析 二叉排序树只需修改指针就可实现结点的插入和删除。 二叉判定树是用一个向量作为存储结构,涉及插入、删除要维护表的有序性,其代价是O(n)。因此: 有序表是静态查找表时,采用向量存储结构,用二分查找。 有序表是动态查找表时,则选择二叉排序树作为其存储结构。
-1 平衡因子 1 -1 1 0 0 0 二叉平衡树 平衡二叉树是由阿德尔森一维尔斯和兰迪斯(Adelson-Velskii and Landis)于1962年提出的,所以又称为AVL树。 定义:平衡二叉树或为空树,或为如下性质的二叉排序树: (1)左右子树深度之差的绝对值不超过1; (2)左右子树仍然为平衡二叉树. 平衡因子 BF=左子树深度 -右子树深度。 若其绝对值超过1,则该二叉排序树就是不平衡的。
-1 0 0 0 0 0 0 0 此处插入将失衡 二叉平衡树 2 插入结点 当插入结点时, BF 为1或 -1 的最小子树有可能失去平衡,就要进行适当的调整,重新恢复平衡. 例如: 插入不会失衡 失衡分为四种基本情形,下面分别讨论。
2A 0B 顺时针旋转 1B 0A AR AR BL BR BL BR 二叉平衡树—LL型 在结点 A 的左子树的左子树中插入一结点,A结点的BF 变为 2。 AR、BL、BR的高度均为h-1
-2A -1B 逆时针旋转 0B 0A AL BL BR BR BL AL 二叉平衡树—RR型 在结点 A 的右子树的右子树中插入一结点,A结点的BF 变为 -2。 AL、BL、BR的高度均为h-1。
2A 2A 1.逆时针旋转 2C AR -1B AR 0B 1C BL CL CR BL CL CR 二叉平衡树—LR型 在结点 A 的左子树的右子树中插入一结点,A结点的BF 变为 2。 变换方法:先左后右。 AR、BL的高度为h-1 CL、CR的高度为h-2
2A 2C 2.顺时针旋转 AR 0B BL CL CR 二叉平衡树—LR型 0C 0B -1A BL CL CR AR
-2A -2A 1.顺时针旋转 1B -1C AL AL -1B 1C CL BR CL CR BR CR 二叉平衡树—RL型 在结点 A 的右子树的左子树中插入一结点,A结点的BF 变为- 2. AL、BR的高度为h-1 CL、CR的高度为h-2
0C -2A 0A -1B 2.逆时针旋转 -1C AL CL AL BR CR -1B CL BR CR 二叉平衡树—RL型
二叉平衡树 ● LL型:在结点A的左子树的左子树上插上结点,使A结点失去平衡。方法:以A的左子树根结点B为轴进行顺时针旋转。A作为B的右子树,B的右子树作为A的左子树。 ●RR型:在结点A的右子树的右子树上插上结点,使A结点失去平衡。方法:以A的右子树根结点B为轴进行逆时针旋转。A作为B的左子树,B的左子树作为A的右子树。 ●LR型:在结点A的左子树的右子树上插上结点,使A结点失去平衡。方法:先以A的左子树的右子树根结点C为轴进RR处理,再按LL型进行处理。 ●RL型:在结点A的右子树的左子树上插上结点,使A结点失去平衡。方法:先以A的右子树的左子树根结点C为轴进LL 处理, 再按RR型进行处理。
B_树 当查询的数据放在外存,磁盘、光盘的读写时间要比随机存取的内存代价大得多。其二,外存的存取是以“页”为单位的,一页的大小通常是1024字节或2048字节。 1972年R.Bayer和E.M.Cright提出了一种B_树的多路平衡查找树,以适合磁盘等直接存取设备上组织动态查找表。 B_树上算法的执行时间主要由读、写磁盘的次数来决定。因此B_树的结点规模一般以一个磁盘页为单位。一个结点包含的关键字及其孩子个数取决于磁盘页的大小。 一棵度为m的B_树称为m阶B_树。一个结点有k个孩子时,必有k-1个关键字才能将子树中所有关键字划分为k个子集。
B_树的定义 一棵m(m≥3)阶的B_树或为空树,或是满足如下性质的m叉树: (1)树中每个结点至多有m棵子树; (2)若根结点不是叶子结点,则至少有两棵子树; (3)除根结点之外的所有非叶结点至少有┌m/2┐棵子树。 (4)所有非终端结点中包含下列数据域: (n,A0,K1,A1,K2,A2,…Kn,An) 其中:n((┌m/2┐-1)≤n≤m-1)为关键字个数; Ki(i=1,2,…,n)为关键字,Ki<Ki+1(i=1,2,…,n-1) Ai(i=0,1,2,…,n)为指向孩子结点的指针。 指针Ai-1所指子树所有结点的关键字均小于Ki An所指子树所有结点的关键字均大于Kn (5)所有叶子结点都出现在同一层次上,并且不带信息。
B_树的定义 例:一棵4阶深度为4的B_树 F F F F F F F F F F F F 4阶B_树:关键字个数最少为(4/2-1)=1,最多3 结点指针个数最少为4/2=2 至多为4
B_树的查找 B_树的查找过程:根据给定值查找结点和在结点的关键字中进行查找交叉进行。 首先从根结点开始重复如下过程: 若比结点的第一个关键字小,则查找在该结点第一个指针指向的结点进行; 若等于结点中某个关键字,则查找成功; 若在两个关键字之间,则查找在它们之间的指针指向的结点进行; 若比该结点所有关键字大,则查找在该结点最后一个指针指向的结点进行; 若查找已经到达某个叶结点,则说明给定值对应的数据记录不存在,查找失败。
B_树结点数据类型定义 #define m 3 typedef struct BTNode{ int Keynum; //结点中关键字的个数 struct BTNode *parent; //指向父结点的指针 KeyType key[m+1]; //关键字向量,0号单元未用 struct *ptr[m+1]; //子树指针向量 }BTNode,*Btree; Typedef struct { BTNode *pt; //指向找到的结点 int i; //在结点中的关键字序号 int tag; //1:查找成功;0:查找失败 } Result; //查询结果数据类型
B_树查找算法 Result SearchBTree(BTree T,KeyType k) { p=T;q=NULL;found=FALSE;i=0; while(p&&!found) { i=Search(p,k);//在结点内查找 if(i>0&&p->key[i]==k) found=TRUE; else {q=p;p=p->ptr[i];} } if(found) return(p,i,1); else return(q,i,0); }
B_树查找分析 B-树查找包含两种基本动作: ●在B-树上查找结点 ●在结点中找关键字 前一操作在磁盘上进行,后一操作在内存进行。因此查找效率主要由前一操作决定。 在磁盘上查找的次数取决于关键字结点在B-树上的层次数。 定理:若n≥1,m≥3,则对任意一棵具有n个关键字的m阶B-树,其树高度h至多为logt((n+1)/2)+1,t= m/2 。
B_树的插入 B_树的生成从空树开始,逐个插入关键字而得。 B_树结点的关键字个数≥┌m/2┐-1,因此每插入一个关键字不是在树中增添一个叶结点,而是最底层的某个非终端结点中插入一个关键字。若插入后关键字个数不超过m-1,则插入完成,否则要产生结点的“分裂”。 分裂方法:将前┌m/2┐-1个关键字和对应的指针留在原结点中,将第┌m/2┐+1个至第m个关键字和对应的指针放入新结点中,将第┌m/2┐个结点和新结点的指针放入父结点的恰当位置。若父结点的关键字个数不超过m-1,则分裂完成,否则要重复上面分裂。若父结点是根结点,则要产生新的父结点。由于一个结点至多只能分解为两个结点,所以父结点可以允许有两个孩子。