640 likes | 777 Views
第九章 查找. 基本概念. 查找表 由同一类型的数据元素构成的集合 静态查找表 对查找表的查找仅是以查询为目的,不改动查找表中的数据 动态查找表 可以在查找表中插入不存在的记录,或删除某个已存在的记录 关键字 数据元素(或记录)的某个数据项,能用来标识一个数据元素 查找 指定关键字,在表中查找. 基本概念(续). 查找成功 查找表中存在满足条件的记录 查找不成功 查找表中不存在满足查找条件的记录。 内查找 整个查找过程都在内存中进行。 外查找 在查找过程中需要访问外存。 平均查找长度 ASL —— 查找方法时效的度量
E N D
基本概念 • 查找表 • 由同一类型的数据元素构成的集合 • 静态查找表 • 对查找表的查找仅是以查询为目的,不改动查找表中的数据 • 动态查找表 • 可以在查找表中插入不存在的记录,或删除某个已存在的记录 • 关键字 • 数据元素(或记录)的某个数据项,能用来标识一个数据元素 • 查找 • 指定关键字,在表中查找
基本概念(续) • 查找成功 • 查找表中存在满足条件的记录 • 查找不成功 • 查找表中不存在满足查找条件的记录。 • 内查找 • 整个查找过程都在内存中进行。 • 外查找 • 在查找过程中需要访问外存。 • 平均查找长度ASL——查找方法时效的度量 • 为确定记录在查找表中的位置,需将关键字和给定值比较次数的期望值。 • 查找成功时的ASL计算方法: • n:记录的个数 • pi:查找第i个记录的概率,( 不特别声明时认为等概率 pi =1/n ) • ci:找到第i个记录所需的比较次数
静态查找表 • 顺序表的查找 • 通常查找表中的各元素(或记录)的关键字的值是无序的。 • “哨兵”:数据安排在1~n,给定值放在第0个记录处,从后向前查找,直到找到所查记录为止。记录0像哨兵一样看守着查找表下界,不会越界。 typedef struct { ElemType *elem; int length; } STable;
顺序表算法 不使用哨兵 int seq_search( SSTable l, KeyType key) { for (i = 0;i < l.length; i++) if (l.elem[i] == key) return i; return -1; } 使用哨兵 int seq_search( SSTable l, KeyType key) { l.elem[0].key=key; for (i = l.length; l.elem[i].key != key; i--); return i; }
顺序表算法:采用链表结构 无头单链表 struct ELEM *seq_search( KeyType key) { for (p = head; p != NULL; p = p->next) if (p->key == key) return p; return NULL; } 无头单链表在链尾设置专用的“哨兵”记录,可减少比较次数 struct ELEM *seq_search( KeyType key) { tail->key = key; for (p = head; p->key != key; p = p->next); return p == tail ? NULL : p; }
顺序表算法性能分析 性能分析 • 查找成功时 • ASLs(n)= =(1+2+ ... +n)/n=(n+1)/2 • 查找失败时 • ASLf =n+1
有序表的查找 • 有序表 • 查找表中的各记录的关键字的值是有序的 • 顺序查找 • 不需比较到表尾,只需比较到比给定值大的记录 • 折半查找(二分查找) • 将给定值与中间的记录进行比较 • 若找到则查找成功; • 否则:若比中间记录小,则对前一半子表进行折半查找,反之对后一半子表进行折半查找 • 折半查找的限制 • 顺序表 • 事先排好序 • 折半查找的性能不是查找算法的性能极限
折半查找算法及性能分析 int binsearch (SSTable ST, keytype key) { low= 1; high = ST.length; while (low <= high) { mid = (low + high) / 2; if (key==ST.elem[mid].key) return mid; else if ( key< ST.elem[mid].key) high = mid - 1; else high = mid+1; } return 0; } • 查找性能分析 • 折半查找每次只查表的一半,其所有记录的查找路径构成一棵二叉树,称为折半查找树(或判定树),每次查找只走了该树的一条分支,故平均查找长度不超过log2n + 1
索引表 最大关键字 35 68 93 起始地址 1 10 13 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 顺序表 23 11 16 35 6 28 31 25 19 41 57 68 93 75 88 69 索引顺序表的查找 • 索引顺序表 • 带索引表的顺序表 • 索引部分一定是有序的,这部分可用折半查找等方法 • 顺序表本身不一定有序,要根据顺序表是否有序而选用相应的查找方法 • 分块查找(blocking search) • 将索引部分和表体部分分开查找的方法。 • 索引表的平均查找长度为两部分查找之和。
动态查找表 二叉排序树 平衡二叉树 B-树和B+树
L 122 N 250 C 99 P 300 M 200 E 110 Y 105 230 216 二叉排序树 • 特点:用于频繁进行插入、删除、查找的表。 • 二叉排序树:空或有一个根,根的左子树若非空,则左子树上的所有结点的关键字值 • 均小于根结点的值。根的右子树若非空,则右子树上的所有结点的关键 • 字值均大于根结点的值。根结点的左右子树同样是二叉排序树。
二叉排序树 • 1、查找算法: • 思路:若根结点的关键字值等于查找的关键字,成功。 • 否则,若小于根结点的关键字值,查其左子树。 • 若大于根结点的关键字值,查其右子树。 • 在左右子树上的操作类似。 122 250 99 Bitree SearchBST ( BiTree T, KeyType key ) // 在二叉排序树查找关键字之值为 key 的结点,找到返回该结 // 点的地址,否则返回空。T 为二叉排序树的根结点的地址。 { if ( !T || EQ( key, T ->data. key ) ) return ( T ) ; else if ( LT( key , T ->data. key ) ) return (SearchBST ( T -> lchild, key )); else return (SearchBST ( T -> rchild, key )); } 200 300 110 105 230 216
二叉排序树的插入 • 执行查找算法,找出被插结点的父亲结点 • 判断被插结点是左/右孩子。将被插结点作为叶子插入 • 若二叉树为空。则首先单独生成根结点。 • 注意:新插入的结点总是叶子结点。 • 例:122、99、250、110、300、280作为二叉排序树的结点的关键字值,生成二叉排序树。 122 250 99 300 110 280
二叉排序树插入算法 struct Node *root, **pptr; /* 全局变量 */ Status SearchBst(struct Node *t, KeyType key) { if (t == NULL) return FALSE; else if (key == t->key) return TRUE; else if (key < t->key) { pptr = &t->lchild; return SearchBst(t->lchild, key); } else { pptr = &t->rchild; return SearchBst(t->rchild, key); } } InsertBST (KeyType key) { pptr = &root; if (!SearchBst(root, key)) { *pptr = p = malloc(sizeof(struct Node)); p->key = key; p->lchild = p->rchild = NULL; } }
15 50 20 20 60 30 15 30 70 ASL=(1+2+2+3+3+3)/6=14/6=2.3 50 60 70 ASL=(1+2+3+4+5+6)/6=21/6=3.5 二叉排序树的查找分析 • 最好和最糟情况(折半查找/顺序查找) 下述两种情况下的成功的平均查找长度 ASL 平均情况下,log2n数量级
二叉排序树的删除(1) • (1)叶子结点:直接删除,更改它的父亲结点的相应指针域为空。 • 如:删除数据域为 15、70 的结点。 50 50 20 60 20 60 15 30 70 30
二叉排序树的删除(2) • (2)被删结点的左孩子为空或者右孩子为空: • 如下图所示,删除结点的数据域为 99 的结点。 400 400 99 删除 450 450 122 122 被删结点 500 500 110 250 250 99 105 200 300 200 300 110 230 105 230 216 216
400 被删结点 400 450 110 450 122 500 250 500 99 250 99 替身 200 300 105 200 300 110 替身 230 105 230 216 216 二叉排序树的删除(3a) (3)被删结点的左、右子树皆不空。 维持二叉排序树的特性不变。 在中序遍历中紧靠着被删结点的结点才可以替代被删节点 做法1:左子树中最大的结点做替身, 选左子树最右结点,其右孩子必为空 做法:将替身的数据域复制到被删结点的数据域 将结点110的左孩子作为 110的父结点99的右孩子(选中的结点110右孩子必为空)
被删结点 400 450 122 500 250 99 替身 200 300 110 替身 105 230 216 二叉排序树的删除(3b) 右子树中最小的结点做替身。 选右子树中的最左的结点,其左孩子指针必为空 400 450 200 500 250 99 230 300 110 216 105 做法:将替身的数据域复制到被删结点的数据域。 将结点200的右孩子作为200的父结点250的左孩子。注意:结点 200左孩子必为空
F 被删结点 P PR PR C Q CL CL QL S SL 二叉排序树的删除(3c) 做法2:删除节点,原左子树补上来,原右子树做原左子树最右节点的右子树 (或对称的:原右子树补上来,原左子树做原右子树最左节点的左子树) F C Q QL S SL
二叉排序树的删除算法(1) 全局变量struct NODE **pptr; Status DeleteBST (struct Node *t,KeyType key) { if (t == NULL) // 二叉排序树 t 中不存在关键字为 key 的结点 return FALSE; else if (key == t-> key) DeleteNode(t); // 存在关键字为 key 的结点,进行删除 else if (key < t->key) { pptr = &t->lchild; DeleteBST(t->lchild, key); } else { pptr = &t->rchild; DeleteBST(t->rchild, key); } return TRUE; } 主程序: pptr = &root; DeleteBST(root,key);
二叉排序树的删除算法(2) Status DeleteNode(struct Node *p) // 在二叉排序树中删除地址为 p 的结点,并保持二叉排序树的性质不变。 { if (p->rchild == NULL) *pptr = p->lchild ; else if (p->lchild == NULL) *pptr = p->rchild; else { *pptr = p->lchild; for (s = p->lchild; s->rchild !=NULL; s = s->rchild); s->rchild = p->rchild; } free(p); }
A B C D E F G 动态查找表-平衡二叉树 • 起因:提高查找速度,避免最坏情况出现。如下图情况的出现。虽然完全二叉树的树型最好,但构造困难。常使用平衡树。
动态查找表-平衡二叉树 • 平衡二叉树 • 平衡二叉树又称 AVL树(Adelson-Velskii & Landis于 1962年发明),它具有如下性质: • 或者为空树, • 或者根结点的左、右子树也均为平衡二叉树,且左、右子树的树高之差的绝对值不超过1。 • 平衡因子 • 结点的左子树高度减去右子树高度的值称为该结点的平衡因子。 • 平衡二叉树也可以这样定义:平衡二叉树是所有结点的平衡因子的绝对值均小于2的二叉树。结点的平衡因子为 +1、-1、0
-1 -1 14 14 +2 -1 +1 -1 9 28 9 28 +1 +1 -1 0 0 +1 -1 5 18 50 5 12 18 50 0 0 0 0 0 0 0 30 60 30 60 3 17 3 7 17 0 0 0 0 53 63 53 63 不是平衡树 是平衡树 不是完全二叉树 动态查找表-平衡二叉树 注意:完全二叉树必为平衡树,平衡树不一定是完全二叉树。
危机结点 -1 14 -1 +1 -1 14 9 28 +2 +1 -1 0 0 +1 -1 9 28 5 12 18 50 +1 0 0 +1 -1 原平衡因子为 0 0 5 12 18 50 0 0 0 30 60 3 7 17 0 +1 0 0 0 0 0 30 60 3 7 17 53 63 0 0 53 63 2 平衡树 平衡二叉树的插入 • 要求:插入之后仍保持平衡二叉树的性质不变 在平衡树中插入数据域为 2 的结点
危机结点 -1 14 +2 +1 -1 9 28 +1 0 0 +1 -1 原平衡因子为 0 5 12 18 50 0 +1 0 0 0 30 60 3 7 17 0 0 53 63 2 平衡二叉树的插入 插入操作解决方案: 1. 新结点插入后,找到平衡因子越轨的结点,调整以此节点为根的子树,调整后,子树高度一定要保持不变(能做到吗?) 2. 不涉及到危机结点的父亲结点 3. 仍保持平衡二叉树的性质不变。
危机结点 -1 14 2 3 5 7 9 12 +2 +1 -1 9 28 5 +1 0 0 +1 -1 原平衡因子为 0 5 12 18 50 3 9 0 +1 0 0 0 30 60 3 7 17 2 7 12 0 0 53 63 2 平衡二叉树的插入 中序序列 右旋转处理
危机结点 -1 -1 14 14 +2 +1 -1 -1 9 28 0 28 5 +1 0 0 +1 -1 原平衡因子为 0 +1 -1 5 12 18 50 +1 0 18 50 3 9 0 +1 0 0 0 0 30 60 0 3 7 17 0 0 0 30 60 0 0 17 2 7 12 0 0 53 63 2 53 63 平衡二叉树的插入
平衡二叉树的插入LL • 左处理(新插入结点出现在危机结点的左子树上进行的调整)的情况分析(设插入前树高h) • 1、LL 情况:(LL:表示新插入结点在危机结点的左子树的左子树上) 危机结点 +1 0 +2 A B 0 0 +1 B A LL 处理 AR h-2 BL h-1 BR AR h-2 BL BR h-2 右旋转A h-2 h-1 处理前:高度为 h + 1 中序序列: 处理后:高度为 h 中序序列: B A B A BL BR AR BL BR AR B A 注意:处理后 平衡因子为 0
危机结点 +1 +2 0 A C 0 -1 0 -1 B LR 处理 A B AR h-2 0 +1 C h-2 BL h-3 h-2 BL h-2 CL CR AR h-2 h-3 h-2 CL CR 处理后: 高度为 h 中序序列: 处理前: 高度为 h + 1 中序序列: B C CR A BL CL AR B C CR A BL CL AR 注意:处理后 平衡因子为 0,0,-1 B C A 平衡二叉树的插入LR(a) 2、LR 情况:(LR:表示新插入结点在危机结点的左子树的右子树上) 情况A: 左旋转B然后右旋转A
危机结点 +1 +2 0 A C 0 -1 0 +1 B LR 处理 A B AR h-2 0 -1 C h-2 BL h-2 h-3 h-2 BL CL CR AR h-2 h-3 CL CR h-2 处理后: 高度为 h 中序序列: 处理前: 高度为 h + 1 中序序列: CL B C A CR BL AR CL B C A CR BL AR B C A 注意:处理后 平衡因子为 +1,0,0 平衡二叉树的插入LR(b) • LR :新插入结点在危机结点的左子树的右子树上 • 情况B: 左旋转B然后右旋转A
危机结点 +1 0 +2 A C 0 0 0 -1 B B A LR 处理 0 C 新插入结点 注意:处理后 平衡因子为 0,0,0 处理前: 高度为 2 中序序列: 处理后: 高度为 1 中序序列: B C A C A C A B B • 四种情况的区分: • 如果 的平衡因子为+1 则为 LL型处理;否则为 LR型处理: • 若 的平衡因子为+1、-1 、0 ;则分别为 LRA、LRB、LRC型处理 B C 平衡二叉树的插入LR(c) • LR:表示新插入结点在危机结点的左子树的右子树上 • 情况C:
平衡二叉树的插入-右处理 右处理(新插入结点出现在右子树上需进行的调整): 1、RR 情况: (RR:表示新插入结点在危机结点的右子树的右子树上) 处理图形和 LL 镜象相似 2、RL 情况: (RL:表示新插入结点在危机结点的右子树的左子树上) A、处理图形和 LRA 镜象相似 B、处理图形和 LRB 镜象相似 C、处理图形和 LRC 镜象相似
2 0 -1 0 1 -2 AUG MAR JUN JUN JUL MAR FEB APR APR MAY JAN FEB MAY JUL MAY AUG SEP OCT AUG APR FEB OCT SEP JAN LR 平衡二叉树的插入举例 • 例:有一组关键字序列{JAN、FEB、MAR、APR、MAY、JUN、JUL、AUG、SEP、OCT、NOV、DEC},以此建立 AVL 树。
-2 -1 1 JAN FEB MAR APR OCT OCT MAY JUN JUL AUG NOV DEC NOV SEP JAN FEB MAR APR MAY JUN JUL AUG OCT MAR JAN SEP RL RR 平衡二叉树的插入举例(续)
平衡二叉树的查找分析 具有 N 个结点的平衡树,高度 h 满足: log2(N+1) ≤ h ≤ loga(sqrt(5)*(N+1)) - 2 其中:a= (1+sqrt(5))/2 查找的时间复杂度O(logn)
平衡二叉树的查找分析(续) 构造一系列结点个数最少的平衡二叉树, T1,T2 ,T3 ,……Th;这种树的高度分别为 1、2、3、……h。 T1 高度 h = 1 结点个 数最少 T3 高度 h = 3 结点个 数最少 T2高度 h = 2 结点个 数最少 • 有th = th-2 + th-1+ 1 • 该数的序列为 1、2、4、7、12、20、33、54、88 ...… • 而 Fibonacci 数列为:0、1、1、2、3、5、8、13、21、34、55、89…… • 所以: t(h) = f(h+2) - 1;于是转化为求 Fibonacci 数的问题。 • 由于: f(h) ≈αh/ sqrt(5); α= (1+sqrt(5))/2 • ......
AVL插入算法:数据结构 struct Node { ElemType data; int bf; struct Node *lchild, *rchild; }; #define LH +1 /* 左子树高 */ #define EH 0 /* 等高 */ #define RH -1 /* 右子树高 */
AVL插入算法:右旋/左旋 void R_Rotate(struct Node **pp) { p = *pp; lc = p->lchild; p->lchild = lc->rchild; lc->rchild = p; *pp = lc; } void L_Rotate(struct Node **pp) { p = *pp; rc = p->rchild; p->rchild = rc->lchild; rc->lchild = p; *pp = rc; }
AVL插入算法:简单情况 int InsertAVL(struct Node **pp, ElemType e) //返回值:-1未插入,0高度不变,1增高 { T = *pp; if (T == NULL) { // 递归出口 *pp = T = malloc(sizeof(struct Node)); T->data = e; T->lchild = T->rchild = NULL; T->bf = EH; return 1; } else if (e.key == T->Data.key) return -1; } else if (e.key < T->Data.key) { ...
AVL插入算法:左/右子树插入 }else if (e.key < T->Data.key) { result = InsertAVL(&T->lchild, e); if (result != 1) return result; switch (T->bf) { case LH: LeftBalance(pp); return 0; case EH: T->bf = LH; return 1; case RH: T->bf = EH; return 0; } } else if (e.key > T->Data.key) { result =InsertAVL(&T->rchild, e); if (result != 1) return result; switch (T->bf) { case RH: RightBalance(T); return 0; case EH: T->bf = RH; return 1; case LH: T->bf = EH; return 0; } } }
AVL插入算法:平衡处理 void LeftBalance(struct Node **pp) { a = *pp; b = a->lchild; switch (b->bf) { case LH: a->bf = b->bf = EH; R_Rotate(pp); break; case RH: c = b->rchild; switch (c->bf) { case LH: a->bf = RH; b->bf = EH; break; case EH: a->bf = b->bf = EH; break; case RH: a->bf = EH; b->bf = LH; break; } c->bf = EH; L_Rotate(&a->lchild); R_Rotate(pp); } }
平衡二叉树的删除算法 参照二叉排序树中的算法,删除后修正平衡因子,若平衡性被破坏,利用单一/双重旋转恢复。
红黑树 树的每个结点都被着上了红色或者黑色,结点所着的颜色被用来检测树的平衡性 。1972年由Rudolf Bayer发明的。它的平衡性要求比AVL树梢宽松,实践证明该算法效率很高
动态查找表: B 树和 B+树 为什么采用B_ 树和 B+ 树? 海量数据存放在外存中,不可能一次全部调入内存。因此,要多次 访问外存。但硬盘的驱动受机械运动的制约,速度慢。所以,主要矛盾变为减少访外次数。 在 1970 年由 R bayer 和 E macreight 提出用B_ 树作为索引组织文件。提高访问速度、减少时间。 例如: 用二叉树组织文件,当文件的记录个数为 100,000时,要找到给定关键字的记录,需访问外存17次(log100,000),太长了! 文件头,可常驻内存 索引文件 50 75 25 15 35 60 90 数据文件 95 10 20 30 40 55 70 80 内存 文件访问示意图:索引文件、数据文件存在盘上
B树 • B树是一种平衡的多路查找树,主要应用在文件系统中。 • 一棵 m 阶的B树,或为空树,或为满足下列特性的 m 叉树: • 树中每个结点最多有 m 棵子树; • 若根结点不是叶子结点,则最少有两棵子树; • 除根之外的所有非终端结点最少有m/2棵子树; • 所有非终端结点包含(n,A0,K1,A1,K2,…,Kn,An)信息数据; • 其中:n为结点中关键字个数,Ai为指向子树的指针,Ki为关键字。 • A0:<K1 的结点的地址(指在该 B_ 树中) • K1:关键字 • A2:> K1 且 < K2 的结点的地址(指在该 B_ 树中) • 余类推 ……… • An:> Kn 的结点的地址(指在该 B_ 树中) • 注意:K1 <K2 < …... < Kn • 所有叶子结点在同一层次上,不带信息。
F F F F F F F F F F F F B树 • 例如:m = 4 阶B树。除根结点和叶子结点之外,每个结点的孩子个数 • 至少为 m/2 = 2 个;结点的关键字个数至少为 1 。 • 该B树的深度为 4。 叶子结点都在第 4 层上。 1,35 第 1 层 1,18 第 2 层 2,43,78 第 3 层(L层) 1,11 1,27 1,39 3,47,58,64 1,99 第 4 层(L+1层)