930 likes | 1.08k Views
数 据 结 构 第 6 章 查找. 第 6 章 查找. 查找表是由同一类型的数据元素 ( 或记录 ) 构成的集合。 由于“集合”中的数据元素之间存在着松散的关系,因此查找表是一种应用灵便的结构。 对查找表经常进行的操作 : 查询某个“特定的”数据元素是否在查找表中; 检索某个“特定的”数据元素的各种属性; 在查找表中插入一个数据元素; 从查找表中删去某个数据元素。. 查找表类别. 静态查找表 仅作查询和检索操作的查找表。 动态查找表
E N D
第6章 查找 • 查找表是由同一类型的数据元素(或记录)构成的集合。 • 由于“集合”中的数据元素之间存在着松散的关系,因此查找表是一种应用灵便的结构。 • 对查找表经常进行的操作: • 查询某个“特定的”数据元素是否在查找表中; • 检索某个“特定的”数据元素的各种属性; • 在查找表中插入一个数据元素; • 从查找表中删去某个数据元素。
查找表类别 • 静态查找表 • 仅作查询和检索操作的查找表。 • 动态查找表 • 有时在查询之后,还需要将“查询”结果为“不在查找表中”的数据元素插入到查找表中;或者,从查找表中删除其“查询”结果为“在查找表中”的数据元素。
定义 • 关键字 • 是数据元素(或记录)中某个数据项的值,用以标识(识别)一个数据元素(或记录)。 • 若此关键字可以识别唯一的一个记录,则称为“主关键字”。 • 若此关键字能识别若干记录,则称为“次关键字” • 查找 • 根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素或(记录)。 • 若查找表中存在这样一个记录,则称“查找成功”。查找结果给出整个记录的信息,或指示该记录在查找表中的位置; • 否则称“查找不成功”。查找结果给出“空记录”或“空指针”。
定义 • 查找算法的平均查找长度(Average Search Length) • 为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值 其中:n 为表长, Pi为查找表中第i个记录的概率, Ci为找到该记录时,曾和给定值比较过的关键字的个数。
如何进行查找? • 查找的方法取决于查找表的结构。 • 由于查找表中的数据元素之间不存在明显的组织规律,因此不便于查找。为了提高查找的效率, 需要在查找表中的元素之间人为地 附加某种确定的关系,换句话说, 用另外一种结构来表示查找表。
6.2 静态查找表 • 静态查找表 template <class ElemType, class KeyType> struct StaticTable { ElemType * m_data; int m_size; //查找表的长度 };
6.2.1 顺序查找 • 顺序查找(Sequence Search) 从表的一端开始,顺序扫描整个查找表。比较数据元素的关键字和给定值key, • 若相等,则查找成功,返回该数据元素在查找表中的下标; • 若一直查找到表的另一端都未能找到关键字等于key的元素,则查找失败,返回值为-1。
6.2.1 顺序查找 • 顺序表的查找算法(1) // 无监视哨情况 template <class ElemType,class KeyType> int SeqSearch(StaticTable <ElemType,KeyType> & R, const KeyType key) { int i = 0; while( i < R.m_size && R.m_data[i]!= key) i++; if ( i < R.m_size) return i; //查找成功 return -1; //查找失败 } 问题: 每次都要进行越界检查,效率低,如何改进?
i i i i i ST.elem 18 21 37 29 43 92 05 64 56 75 02 0 1 2 3 4 5 6 7 8 9 10 11 i i i i i i i i i i i i ST.elem 18 21 37 29 43 92 05 64 56 75 02 0 1 2 3 4 5 6 7 8 9 10 11 6.2.1 顺序查找 例如: 查找43 43 47 比较次数: 查找第1个元素: 1 查找第i个元素: i 查找失败: n+1 查找47
6.2.1 顺序查找 本算法通过设置“监视哨”,使查找过程中的每一步免去了检测整个表是否查找完毕,提高了查找的效率。 • 顺序表的查找算法(2) // 有监视哨情况 template <class ElemType,class KeyType > int SeqSearchWithMonitor(StaticTable<ElemType, KeyType>&R, const KeyType key) { int i = 0; R.m_data [R.m_size] = key; // 给定值key作为监视哨 while (R.m_data [i] != key) i++; if ( i < R.m_size) return i; return -1; }
6.2.1 顺序表的查找 平均时间性能: 对顺序表而言, Ci = i ASL = P1 + 2P2 + … + (n-1)Pn-1+nPn 在等概率查找的情况下 顺序表查找的平均查找长度为:
6.2.2 折半查找 • 上述顺序查找表的查找算法简单, 但平均查找长度较大,特别不适用于表长较大的查找表。 • 若以有序表表示静态查找表,则查找过程可以基于“折半”进行。
6.2.2 折半查找 • 折半查找 • 查找过程:每次将待查记录所在区间缩小一半 • 适用条件:采用顺序存储结构的有序表 • 算法实现: • 设表长为n,low、high和mid分别指向待查元素所在区间的上界、下界和中点,k为给定值 • 初始时,令low = 0, high = n-1, mid = (low + high) / 2让k与mid指向的记录比较 • 若k==r[mid].key,查找成功 • 若k<r[mid].key,则high=mid-1 • 若k>r[mid].key,则low=mid+1 • 重复上述操作,直至low>high时,查找失败
折半查找示例 查找25 low mid high low mid high low mid high
折半查找示例 查找72 low mid high low mid high low mid high 失败 high low
6.2.2 折半查找 • 折半查找算法 //折半查找 template <class ElemType,class KeyType > int BinarySearch(StaticTable<ElemType, KeyType > &R, const KeyType key) { // low表示所查区间的下界,high表示所查区间的上界 int low = 0,high = R.m_size - 1; while (low <= high) { int mid = (low + high) / 2; if (R.m_datas[mid] == key) return mid; // 查找成功 else if (R.m_datas[mid] > key) high = mid - 1; // 在前半区间继续查找 else low = mid + 1; // 在后半区间继续查找 } return -1; // 查找失败 }
6.2.2 折半查找 • 折半查找的判定树 上例中长度为11的有序表,每个元素查找成功的比较次数 3 4 2 3 4 1 3 4 2 3 4 5 ASL= 2 8 0 3 6 9 1 4 7 10
6.2.2 折半查找 • 折半查找的效率 • n>50时,查找成功的折半查找的平均查找长度大约为 • 由此可见,折半查找的效率比顺序查找的高,但折半查找只适用于有序表,且限于顺序存储结构,对线性链表无法有效的进行。
6.2.3 分块查找 • 分块查找(Block Search)又称为索引顺序查找。 • 索引顺序查找表由分块有序的顺序表和索引表组成。 • 块内无序,块间有序。 • 查找过程: • 由索引确定数据元素所在的块;(折半查找或者顺序查找) • 在相应的块中进行查找。(顺序查找) 可见,索引顺序查找的过程也是一个“缩小区间”的查找过程。 • 注意:索引可以根据查找表的特点来构造。
6.2.3分块查找 索引表 查45
6.2.3分块查找 • 分块查找的效率 • 分块查找的平均查找长度 =查找“索引”的平均查找长度+ 查找“顺序表”的平均查找长度
6.3动态查找表 1)从查找性能看,最好情况能达O(logn),此时要求表有序; 2)从插入和删除的性能看,最好情况能达O(1),此时要求存 储结构是链表。
6.3.1 二叉排序树 • 二叉排序树(Binary Sort Tree) • 二叉排序树或者是一棵空树;或者是具有如下特性的二叉树: • 若根结点的左子树不空,则左子树上所有结点的值均小于根结点的值; • 若根结点的右子树不空,则右子树上所有结点的值均大于根结点的值; • 它的左、右子树也都分别是二叉 排序树。
6.3.1 二叉排序树 • 二叉排序树示例 25 36 10 7 18 31 45 9 29 不是 二叉排序树
6.3.1 二叉排序树 • 二叉排序树结点结构 // 二叉排序树结点结构 template <class ElemType,class KeyType> struct BSTNode{ ElemType data; //结点数据域 BSTNode<ElemType,KeyType> *lchild; //结点左孩子 BSTNode<ElemType,KeyType> *rchild; //结点右孩子 };
6.3.1 二叉排序树 • 二叉排序树类 template< class ElemType,class KeyType> class BST{ public: BST (); ~BST ( ); bool Search(const KeyType & key) const; bool Insert(const ElemType & e,const KeyType & key); bool Delete(const KeyType & key); void InorderTraverse() const; private: BSTNode<ElemType,KeyType> * m_root; //二叉排序树根指针 void _Destroy(BSTNode<ElemType,KeyType> * &root); //销毁二叉排序树 bool _Search(KeyType key,BSTNode<ElemType,KeyType> *&f, BSTNode<ElemType,KeyType> *&p) const; void _InorderTraverse(BSTNode<ElemType,KeyType>* root) const; };
二叉排序树的查找 • 二叉排序树的查找算法 • 若根指针为空,则查找失败; • 否则, • 若给定值等于根结点的关键字,则查找成功; • 若给定值小于根结点的关键字,则继续在左子树上进行查找; • 若给定值大于根结点的关键字,则继续在右子树上进行查找。
二叉排序树的查找算法例 • 查找关键字50,35,90,95 二叉排序树 50 50 50 50 50 50 30 30 80 80 20 40 40 90 90 35 35 85 32 88
二叉排序树的查找算法 //非递归方式实现查找 //在二叉排序树中查找关键字等于key的数据元素,若查找成功,则指针p指向该数据 //元素结点,f指向p的双亲,并返回true, //否则指针f指向查找路径上访问的最后一个结点并返回false; template< class ElemType,class KeyType> bool BST<ElemType,KeyType>::_Search(KeyType key,BSTNode<ElemType, KeyType> * &f,BSTNode<ElemType, KeyType> * &p) const { f = NULL; p = m_root; while (p && p->data != key){ f = p; if (key < p->data) //到左子树继续查找 p = p ->lchild; else //到右子树继续查找 p = p->rchild; } return p != NULL; }
二叉排序树的插入 • 二叉排序树的插入算法 • 根据动态查找表的定义,“插入”操作在查找不成功时才进行; • 若二叉排序树为空树,则新插入的结点为新的根结点;否则,新插入的结点必为一个新的叶子结点,其插入位置由查找过程得到。
二叉排序树的插入算法 // 当二叉排序树m_root中不存在关键字等于key的数据元素时,插入e并返回true, //否则返回false。 template< class ElemType,class KeyType> bool BST<ElemType, KeyType>::Insert(const ElemType & e, const KeyType & key) { BSTNode<ElemType,KeyType> *p,*s, *f; if (!_Search(key,f,p)){ //查找不成功,f是查找路径上的最后结点 s = new BSTNode<ElemType,KeyType>; s->data = e; s->lchild = s->rchild = NULL; if (!f) m_root = s; //被插结点s为新的根结点 else if (e < f->data) f->lchild = s; //被插结点s为左孩子 else f->rchild = s; //被插结点s为右孩子 return true; } return false; //树中已有关键字相同的结点,不再插入 }
二叉排序树的删除算法 • 二叉排序树的删除算法 • 和插入相反,删除在查找成功之后进行,并且要求在删除二叉排序树上某个结点之后,仍然保持二叉排序树的特性。 • 被删除的结点是叶子;其双亲结点中相应指针域的值改为“空”。 • 被删除的结点只有左子树或者只有右子树;其双亲结点的相应指针域的值改为指向被删除结点的左子树或右子树”。 • 被删除的结点p既有左子树,也有右子树。将p的中序遍历直接前驱替代p结点,再删除p的直接前驱结点。
S Q P 中序遍历:Q S PL P P 中序遍历:Q S PL (2) S S S Q Q P Q 中序遍历:PL P S Q Q 中序遍历:PL S Q (1) 二叉排序树的删除算法 PL PL PL PL
S P Q 中序遍历:P PR S Q Q 中序遍历:PR S Q S S S Q P Q Q 中序遍历:Q S P PRPR 中序遍历:Q S PR 二叉排序树的删除算法 PR PR (3) PR PR (4)
F F P S C C Q Q S 中序遍历:CL C ……QL Q SL S PR F (5) F F P C C 中序遍历:CL C PR F 中序遍历:CL C P PR F (6) 二叉排序树的删除算法 PR PR CL CL QL QL SL SL 中序遍历:CL C……QL Q SL S P PR F PR CL PR CL
例 15 15 15 15 15 8 1 13 8 1 22 22 21 22 22 删除27 删除1 删除13 删除22 13 13 5 5 5 14 14 14 21 21 21 21 66 66 66 66 66 14 14 27 5 5 二叉排序树的删除算法 8 8 8 7 7 7 7 7
二叉排序树的删除算法 // 当二叉排序树m_root中存在关键字等于key的数据元素时,删除并重接它的左 //或右子树,返回true,否则返回false。 template< class ElemType,class KeyType> bool BST<ElemType,KeyType>::Delete(const KeyType & key) { BSTNode<ElemType,KeyType> *p,*f; if (!_Search(key,f,p)) return false; //查找成功,p指向被删除结点,f指向p的双亲结点 if (!p->rchild) { //p右子树为空,重接它的左子树 if (f == NULL) //p的双亲为空,表明p是根结点 m_root = p->lchild; else { if (f->lchild == p) f->lchild = p->lchild; else f->rchild = p->lchild; } delete p; }
二叉排序树的删除算法(续) else if (!p->lchild) { //p左子树为空,重接它的右子树 if (f == NULL) //p的双亲为空,表明p是根结点 m_root = p->rchild; else { if (f->lchild == p) f->lchild = p->rchild; else f->rchild = p->rchild; } delete p; }
二叉排序树的删除算法(续) else { //p左右子树均不空 BSTNode<ElemType,KeyType> *s; f = p; s = p->lchild; //s为p左子树的根结点 while (s->rchild){ //沿着s右子树分支向右走到尽头, //即为p结点的前驱结点 f = s; s = s->rchild; } p->data = s->data; if (f != p) f->rchild = s->lchild; // 重接*f的右子树 else f->lchild = s->lchild; // 重接*f的左子树 delete s; } return true; }
50 (b) 30 (a) 40 40 60 50 55 30 55 65 60 65 二叉排序树的查找性能 • 二叉排序树的查找分析: • 含有n个结点的二叉树的平均查找长度和树的形状有关 ASL(a)=1/6(1+2+2+3+3+3)=14/6 ASL(b)=1/6(1+2+3+4+5+6)=21/6
二叉排序树的查找性能 • 一般的: • 二叉排序树上查找某关键字等于结点值的过程,其实就是走了一条从根到该结点的路径,与给定值的比较次数不超过树的高度。 • 最坏情况,如上页(b)图所示的单支树,相当于顺序查找, • 最好情况,二叉排序树的形态同折半查找的判定树, ASL=树的深度为:log 2(n+1) -1 • 随机情况,ASL=O(logn)
6.3.2平衡二叉排序树 • 思考: • 如何提高二叉排序树的查找效率? • 解决方法 • 尽量避免出现单支树,让二叉树的形状均衡! 平衡二叉树
6.3.2平衡二叉排序树 • 平衡二叉排序树(AVL) • 定义: • 平衡因子(BF):结点的左子树深度减去右子树深度。 • 平衡二叉排序树(AVL): 或是一棵空树,或者是具有如下性质的二叉树: • 它的左子树和右子树深度之差的绝对值不超过1 • 左子树和右子树都是AVL树。
6.3.2 平衡二叉排序树 • 平衡二叉排序树中每个结点的平衡因子只能为0,-1,1。 5 5 4 8 4 8 2 2 不是平衡二叉排序树 是平衡二叉排序树 1
平衡二叉排序树的调整 • 在一棵AVL树中插入一个新结点,可能仍然是平衡的,也有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡。 • 平衡调整 • LL型平衡调整 • RR型平衡调整 • LR型平衡调整 • RL型平衡调整
B A A B AR 平衡二叉排序树的调整 • LL型平衡调整(单向右旋) 1. BL 单向右旋 BL BR BR AR h Height( AR)=Height(BL)=Height(BR)=h
B A A B AL 平衡二叉排序树的调整 • RR型平衡调整(单向左旋) 1. BR 单向左旋 AL BL BL BR h Height( AL)=Height(BL)=Height(BR)=h
C A A A C B AR AR 平衡二叉排序树的调整 • LR型平衡调整(先单向左旋再单向右旋) 1. B BL CL CR h AR BL BL C CR B CL CL CR 单向左旋 单向右旋 Height( BL)=Height(AR)=h Height( CL)=Height(CR)=h-1