1.62k likes | 1.7k Views
第八章 查找. 所谓查找 (Search) 又称检索 , 就是在一个数据元素集合中寻找满足某种条件的数据元素。 查找在计算机数据处理中是经常使用的操作。查找算法的效率高低直接关系到应用系统的性能。 查找的方法很多 , 本章将介绍一些常用的查找算法 , 主要有 : 线性表的查找、树表的查找和散列表的查找。 并对有关的算法进行 性能分析和对比。. 8.1 基本概念. 1 . 数据表 是指数据元素的有限集合 。 例如 , 为了查询统计职工人事档案情况 , 可以建立如下一个表格 , 包括 : 编号、姓名、性别、出生日期、基本工资 等信息。
E N D
第八章 查找 所谓查找(Search)又称检索,就是在一个数据元素集合中寻找满足某种条件的数据元素。 查找在计算机数据处理中是经常使用的操作。查找算法的效率高低直接关系到应用系统的性能。 查找的方法很多,本章将介绍一些常用的查找算法,主要有:线性表的查找、树表的查找和散列表的查找。 并对有关的算法进行性能分析和对比。
8.1 基本概念 1.数据表 是指数据元素的有限集合。 例如,为了查询统计职工人事档案情况,可以建立如下一个表格,包括: 编号、姓名、性别、出生日期、基本工资等信息。 该表格中的每一个职工的信息就是一个数据元素。 对该表格可以根据编号查找职工的基本工资等信息; 也可以根据职工的姓名查找职工的基本工资等信息。
8.1基本概念 2.关键字 数据表中数据元素一般有多个属性域(字段),也就是由多个数据成员组成,其中有一个属性域可用来区分元素,作为查找或排序的依据,该域即为关键字。 每个数据表用哪个属性域作为关键字,要根据具体的应用需要而定。即使是同一个表,在解决不同问题的场合也可能取不同的域做关键字。 如果在数据表中各个元素的关键字互不相同,就称这种关键字主关键字。比如编号,学号(假设不重号)
8.1 基本概念 3.查找 最常见的一种查找(Search)方式是事先给定一个值,在数据表中找到其关键字等于给定值的数据元素。查找结果通常有两种可能:
8.1 基本概念 3.查找:(续) 查找结果通常有两种: 一种可能是查找成功: 即找到关键字等于给定值的数据元素,这时作为查找结果,可报告该数据元素在数据表中的位置,还可进一步给出该数据元素的具体信息,后者在数据库技术中叫做检索; 另一种可能是查找不成功(查找失败): 即数据表中找不到其关键字等于给定值的数据元素,此时查找的结果可给出一个"空"记录或"空"指针。
8.1 基本概念 4.静态查找表和动态查找表: 数据表的组织有两种不同方式: 其一,数据表的结构固定不变,当查找失败时,作为查找结果只报告一些信息,如失败标志、失败位置等,这类数据表称为静态查找表; 其二,数据表的结构在插入或删除数据元素过程中会得到调整,当查找失败时,则把给定值的数据元素插入到数据表中,这类组织方式称为动态查找表。
8.1 基本概念 5.查找的效率: 查找的时间复杂度一般用平均查找长度(ASL)来衡量。 平均查找长度是指在数据表中查找各数据元素所需进行的关键字比较次数的期望值,其数学定义为: 其中: Pi表示待查找数据元素在数据表中出现的概率 Ci表示查找此数据元素所需进行关键字的比较次数 查找效率的另一个指标是最大查找长度: MAX()=max{Ci|i=1,2,……,n} 8.2 线性表上的查找
8.2 线性表上的查找 8.2.1 顺序查找 线性表上的顺序查找的基本思想是: 设有n个数据元素的顺序表,从表的一端开始,用给定的值依次和表中各数据元素的关键字进行比较, 若在表中找到某个数据元素的关键字和给定值相等,则查找成功,给出该数据元素在表中的位置; 若查遍整个表,不存在关键字等于给定值的数据元素,则查找失败,给出失败信息。 线性表上的顺序查找可以采用顺序存储结构和链式存储结构。 采用顺序存储结构的数据表称为顺序表。顺序表适合作静态查找。
数据表类的定义: const int MaxSize=20//数据表中元素的最大个数 template <class Type> class dataList; //数据表类的前视定义 template <class Type> classNode { //数据表中的结点类定义 friendclass dataList<Type>; public: Node ( const Type & value ) : key ( value ) { }//构造函数 Type getKey ( ) const { return key; }//取关键字 void setKey ( Type k ) { key = k; }//关键字的赋值 private: Type key; //关键字 other; //其它信息 };
数据表类的定义: template <class Type> classdataList { //数据表类的定义 public: dataList ( ) : CurrentSize (0), Table (new Node <Type > [MaxSize]) { } virtual ~dataList ( ) { delete [ ] Table; } protected: Type *Table; int CurrentSize; }; 保护成员:是和下一页所示的顺序表searchList类继承dataList这个基类有关, searchList类是dataList这个基类的派生类
如此公有继承后,派生类searchList的对象可以访问基类dataList中的公有成员,派生类searchList的成员函数可以访问基类dataList中的公有成员和保护成员如此公有继承后,派生类searchList的对象可以访问基类dataList中的公有成员,派生类searchList的成员函数可以访问基类dataList中的公有成员和保护成员 数据表类的定义: template <class Type> class searchList : public dataList<Type> { //作为dataList派生类的顺序表类的定义 public: searchList ( ) : dataList<Type> ( ) { } virtual ~searchList ( ) { } virtualint Search ( const Type & k ) const; };
i i i i i 找64 0 1 2 3 4 5 6 7 8 9 10 11 例 5 13 19 21 37 56 64 75 80 88 92 在顺序存储结构下的顺序查找算法如下: template <class Type> int searchList<Type> :: Search ( const Type & k ) const {//在顺序表中查找关键字为k的数据元素 int i = CurrentSize; while ( Table[i].getKey ( ) != k ) i --; return i; } 查找成功 比较次数=5 一般而言 比较次数: 查找第n个元素: 1 查找第n-1个元素:2 ………. 查找第1个元素: n 查找第i个元素: n-i+1 查找失败: n+1 算法还存在 什么问题?
找64 0 1 2 3 4 5 6 7 8 9 10 11 例 5 13 19 21 37 56 64 75 80 88 92 在顺序存储结构下的顺序查找算法如下: template <class Type> int searchList<Type> :: Search ( const Type & k ) const {//在顺序表中查找关键字为k的数据元素 int i = CurrentSize; while ( Table[i].getKey ( ) != k ) i --; return i; } 所起作用:检测整个表有没有查找结束 带来什么问题? i>=1&& 如何避免? 在查找失败时会下标越界或无意义 设置监视哨 如何设置? 一般而言 比较次数: 查找第n个元素: 1 查找第n-1个元素:2 ………. 查找第1个元素: n 查找第i个元素: n-i+1 查找失败: n+1 算法还存在 什么问题? 数组的低端 或高端
i i i i i i i i i i i i 找94 0 1 2 3 4 5 6 7 8 9 10 11 例 94 5 13 19 21 37 56 64 75 80 88 92 监视哨 查找失败 在顺序存储结构下的顺序查找算法如下: template <class Type> int searchList<Type> :: Search ( const Type & k ) const {//在顺序表中查找关键字为k的数据元素 int i = CurrentSize; Table[0].setKey ( k ); //在0号位置设置监视哨 while (Table[i].getKey ( ) != k ) i --; return i; }
i i i i i 找64 0 1 2 3 4 5 6 7 8 9 10 11 例 64 5 13 19 21 37 56 64 75 80 88 92 监视哨 查找成功 在顺序存储结构下的顺序查找算法如下: template <class Type> int searchList<Type> :: Search ( const Type & k ) const {//在顺序表中查找关键字为k的数据元素 int i = CurrentSize; Table[0].setKey ( k ); //在0号位置设置监视哨 while (Table[i].getKey ( ) != k ) i --; return i; }
在等概率情形下查找成功的平均查找长度为: 查找失败的平均查找长度为n+1。 思考以下问题: 在无序表和有序表上进行顺序查找时,各自在查找成功和查找不成功情况下的平均查找长度有何区别?(假设等概率) 又,对无序表而言,若查找概率P1>=P2>=……>=Pn,则为了使得平均查找长度最小,应该"从前往后"还是"从后往前"进行查找?
8.2.2 有序表的折半查找 对有序表通常可用折半查找的方法来进行查找。设有n个数据元素按其关键字从小到大的顺序存放在一个顺序表中(开始时,查找区间的下限low=0,上限high=n-1),折半查找的算法思想为: (1)如果查找区间长度小于1(low>high),则表示查找失败,返回-1;否则继续以下步骤。 (2)求出查找区间中间位置的数据元素下标mid(mid=(low+high)/2); (3)区间中间位置的数据元素的关键字Table[mid].getKey()与给定值x进行比较,比较的结果有三种可能:
1)若Table[mid].getKey()=x,则查找成功,报告成功信息并返回其下标mid;1)若Table[mid].getKey()=x,则查找成功,报告成功信息并返回其下标mid; 2)若Table[mid].getKey()<x,则说明如果数据表中存在要找的数据元素,该数据元素一定在mid的右侧,可把查找区间缩小到数据表的后半部分(low=mid+1),再继续进行折半查找(转步骤1); 3)若Table[mid].getKey()>x,则说明如果数据表中存在要找的数据元素,该数据元素一定在mid的左侧。可把查找区间缩小到数据表的前半部分(high=mid-1),再继续进行折半查找(转步骤1)。
设有序表为{8,11,23,34,46,68,71,86},下图(a)给出了查找关键字为23的数据元素时的查找过程,找到所查数据元素一共做了3次关键字比较。图(b)给出了查找关键字为52的数据元素时的查找过程,直到确认查找失败也执行了3次关键字比较。设有序表为{8,11,23,34,46,68,71,86},下图(a)给出了查找关键字为23的数据元素时的查找过程,找到所查数据元素一共做了3次关键字比较。图(b)给出了查找关键字为52的数据元素时的查找过程,直到确认查找失败也执行了3次关键字比较。
折半查找的算法可以用递归的形式和迭代的形式实现:折半查找的算法可以用递归的形式和迭代的形式实现: template <class Type> class orderedList : public dataList<Type> {//作为dataList派生类的有序表类的定义 public: orderedList ( ) : dataList<Type> ( ) { } virtual ~orderedList ( ) { } virtual int BinarySearch ( const Type & k, const int low, const int high ) const ; //折半查找的递归算法 virtual int BinarySearch ( const Type & k ) const ; //折半查找的迭代算法 };
template <class Type> in orderedList <Type>:: BinarySearch ( const Type & k ) const { //折半查找的迭代算法 int high = CurrentSize-1, low = 0, mid; while ( low <= high ) { mid = ( low + high ) / 2; if ( Table[mid].getKey ( ) < k ) low = mid + 1; elseif ( Table[mid].getKey ( ) > k ) high = mid - 1; elsereturn mid; } //while return -1; }
template <class Type> int orderedList<Type> :: BinarySearch(const Type &k,const int low, const int high) const {//折半查找的递归算法 int mid ; if ( low > high ) mid = -1; else { mid = ( low + high ) / 2; if ( Table[mid].getKey( ) < k ) mid = BinarySearch ( k, mid +1, high ); elseif ( Table[mid].getKey( ) > k ) mid = BinarySearch ( k, low, mid -1 ); } return mid; }
对有序表的折半查找过程可以用下图来表示,但要分析折半查找的性能,则不直观。对有序表的折半查找过程可以用下图来表示,但要分析折半查找的性能,则不直观。 为了便于分析折半查找算法的性能,我们可以通过构造一个二叉查找树来表示折半查找过程.
在二叉查找树上,每个结点表示有序表中的一个数据元素。在二叉查找树上,每个结点表示有序表中的一个数据元素。 设二叉查找树有n个结点,可用如下的方法来构造它: (1)当n=0时,二叉查找树为空树; (2)当n<>0时, 二叉查找树的根结点是有序表中序号mid为(n-1)/2的数据元素, 根结点的左子树是与有序表Table[0]Table[mid-1]相对应的二叉查找树, 根结点的右子树是与有序表Table[mid+1]Table[n-1]相对应的二叉查找树。
对二叉查找树进行扩充,让树中所有结点的空指针都指向一个外部结点(用方框表示,相应原来的数据元素结点称为内结点),它们代表了那些进行关键字比较不成功的结点。对二叉查找树进行扩充,让树中所有结点的空指针都指向一个外部结点(用方框表示,相应原来的数据元素结点称为内结点),它们代表了那些进行关键字比较不成功的结点。 这样的二叉查找树称为扩充二叉树。
为了讨论方便,假设有序表的长度n=2h-1,则描述折半查找的二叉查找树的高度为h,h=log2(n+1)。为了讨论方便,假设有序表的长度n=2h-1,则描述折半查找的二叉查找树的高度为h,h=log2(n+1)。 第1层结点有1个,查找第1层结点要比较1次,共比较1×1=1次; 第2层结点有2个,查找第2层结点要比较2次,共比较2×2=4次; ……, 第i(1ih)层结点有2i-1个,查找第i层结点要比较i次,共比较i×2i-1次; ……; 第h层结点有2h-1个,查找第h层结点要比较h次,共比较h×2h-1次. 假定每个结点的查找概率相等,即Pi=l/n,则查找成功的平均查找长度为:
8.3 索引顺序表 当数据对象个数n很大时,如果用无序表形式的顺序存储结构存储,用顺序查找法查找,效率是很低的,因为平均来说要查找几乎一半的元素。 如果采用有序表形式的顺序存储结构存储,即使用折半查找能提高一点效率,但插入新的记录又需要排序,时间开销也很可观。 因此很纠结。 怎么办? 我们还有另外的办法,就是建立索引顺序表的方法实现存储和查找。 例如:查英语词典
8.3 索引顺序表 索引顺序表一般由主表和索引表两个部分组成,两者均采用顺序存储结构。如下图所示是一个学生信息的数据表: 主表中存放数据元素的全部信息,索引表中存放数据元素的主关键字和索引信息。 有时候还可以对索引表再建立索引,形成二级索引。
每块的最大关键字 该块在完全索引表中的起始位置 这里的完全索引表的关键字“值”有什么规律? 二级索引图示: "分块有序" 地址:1~4的最大关键字是1100 地址:5~8的最大关键字是2100 地址:9~11的最大关键字是3100 地址:12~15的最大关键字是4100
查找关键字为1003的记录 对二级索引结构进行查找时,一般分为二次查找。 先在二级索引表中按给定值K进行查找,以确定接下来要查完全索引表中的第几个子表(设为第i子表), 然后再在第i个子表中顺序查找关键字等于K的索引项。 如果找到关键字等于K的索引项,则可以根据索引项内的地址直接从主表中读取相应的数据元素; 否则,表示查找失败。
查找关键字为1003的记录 二级索引查找成功时的平均查找长度为: ASLIndexSeq=ASLIndex + ASLSubList 其中, ASLIndex 是在二级索引表中查找成功的平均查找长度, ASLSubList是在完全索引表的某个子表内查找成功的平均查找长度。
设完全索引表的长度为n ,分成均等的m个子表,每个子表k个对象,则m=n/k 。 在等概率的情况下,每个子表的查找概率为1/m,子表内各对象的查找概率为1/k。 (1)若对二级索引表采用折半查找;对子表用顺序查找,则二级索引查找成功时的平均查找长度为: ASLIndexSeq=ASLIndex +ASLSubList log2(m+1)-1+(k+1)/2 log2(1+n/k)+k/2
设完全索引表的长度为n ,分成均等的m个子表,每个子表k个对象,则m=n/k 。 在等概率的情况下,每个完全索引表的查找概率为1/m,子表内各对象的查找概率为1/k。 (2)若二级索引表和子表都用顺序查找,则二级索引查找成功时的平均查找长度为: ASLIndexSeq =(m+1)/2+(k+1)/2 =(m+k)/2+1=(n/k+k)/2+1 =(n+k2)/(2k)+1 请问:此时每个子表的长度k是多少时,ASLIndexSeq取最小值??? 以上所介绍的是表上的查找,下面介绍树上的查找
8.4 二叉排序树 8.4.1 二叉树的定义 针对右图的二叉树,如果要查找关键字是23的结点,怎么查找? 我们是否可以通过二叉树的遍历(先、中、后以及层次遍历)的方法实现? 可以。 但是找到23以后,照理说算法就不必再运行下去了,然而遍历算法则是要对所有的结点进行访问的,如此效率是否太低? 请大家看看现在的这棵二叉树按照关键字有什么特点?
8.4 二叉排序树 8.4.1 二叉树的定义 发现右图的二叉树具有这样的特点: (1)左子树上所有结点的关键字都小于根结点的关键字。 ( 2)右子树上所有结点的关键字都大于根结点的关键字。 这样的特点对于提高查找效率来说,有什么好处? 可以减少下一次的待查找记录的个数,直观地告诉我们是可以减少平均查找长度的。 我们称这样的二叉树为“二叉排序树”
8.4 二叉排序树 8.4.1 二叉树的定义 二叉排序树或者是一棵空树,或者是具有下列性质的二叉树: (1)左子树(如果存在)上所有结点的关键字都小(大)于根结点的关键字。 ( 2)右子树(如果存在)上所有结点的关键字都大(小)于根结点的关键字。 (3)左子树和右子树也是二叉排序树。
二叉排序树的抽象数据类型的描述见教材P260-261二叉排序树的抽象数据类型的描述见教材P260-261 二叉排序树类的类声明:
二叉排序树类的类声明: #include <iostream.h> template <class Type> class BSTree; //二叉排序树类的前视定义 template <class Type> classBSTreeNode : public BinTreeNode<Type> { //二叉排序树结点类的定义,继承二叉树结点类 friendclass BSTree <Type>; public: BSTreeNode ( ) : leftChild (NULL), rightChild (NULL) { }//构造函数 BSTreeNode (const Type d) : data (d),leftChild (NULL), rightChild (NULL) { } //构造函数 ~BSTreeNode ( ) { }//析构函数 protected: Type data ;//数据域(在此为关键字) BSTreeNode <Type> *leftChild, *rightChild; };
template <class Type> class BSTree : public BinaryTree<Type> {//二叉排序树的类定义,继承二叉树类 public: BSTree ( ) : root (NULL) { }//构造函数 ~BSTree ( ); //析构函数 int Find ( const Type & k ) const { return Find ( k, root ) != NULL; }//查找 Type Min ( ) ; //求最小 Type Max ( ) ; //求最大 void Insert ( const Type & x ) { Insert ( x, root ); } void Delete ( const Type & k ) { Delete ( k, root ); } private: BSTreeNode<Type> *root; //二叉排序树的根指针 BSTreeNode<Type> *Find (const Type & k,BSTreeNode<Type>*ptr ) const; void Insert ( const Type & x, BSTreeNode<Type> *& p ); void Delete ( const Type & k, BSTreeNode<Type> *& p ); } 8.4.2 二叉排序树上的查找
8.4.2 二叉排序树上的查找 45 12 53 3 37 100 24 61 90 二叉排序树 78 查找过程: 从根结点出发,结点的值与key进行比较: (1)相等时查找成功; (2) key较小时,沿左子树继续查找(无左子树表明查找失败); (3) key较大时,沿右子树继续查找(无右子树表明查找失败)。 算法思想见下页:
算法思想: 45 12 53 3 37 100 24 61 90 二叉排序树 78 • 根据二叉排序树的定义,查找其值为给定值K的结点的过程为: • 若二叉排序树为空,则表明查找失败,应返回空指针。否则, • 若给定值K等于根结点的值,则表明查找成功,应返回根结点的指针。否则, • 若给定值K小于根结点的值,则继续在根的左子树中查找。否则, • 若给定值K大于根结点的值,则继续在根的右子树中查找。 • 显然,这是一个递归过程。
二叉排序树上的查找 二叉排序树的查找算法可以用递归和迭代两种方法实现。
template <class Type> BSTreeNode<Type> * BSTree<Type> :: Find (const Type & k, BSTreeNode<Type> * p) const{ //在p为根的二叉排序树上进行查找的递归算法 if ( p == NULL ) returnNULL; //查找失败 elseif ( k < p->data ) //在左子树上递归查找 return Find ( k, p->leftChild ); elseif ( k > p->data ) //在右子树上递归查找 return Find ( k, p->rightChild ); elsereturn p; //相等,查找成功 }
template <class Type> BSTreeNode<Type> * BSTree<Type> :: Find(const Type & k, BSTreeNode<Type>* p)const { //在p为根的二叉排序树上进行查找的迭代算法 BSTreeNode<Type> * temp = p; if ( p != NULL ) { while ( temp != NULL ) { if (k == temp->data) return temp; //查找成功 if (k >temp->data) temp = temp->rightChild; //查找右子树 else temp = temp->leftChild; //查找左子树 } }// while }//if(p!=NULL) return temp; //查找失败 } 8.4.2 二叉排序树的插入
8.4.2 二叉排序树的插入 为了向二叉排序树中插入一个新元素,必须先检查这个元素在二叉排序树中是否已经存在。 因此,在插入之前,首先在二叉排序树中检查待插入的数据元素。 如果查找成功,说明树中已经存在这个数据元素,就不再插入; 如果查找不成功,说明树中不存在关键字等于给定值的数据元素,就把新元素插到查找操作失败的地方。
二叉排序树中插入结点 示例 下面先介绍二叉排序树的生成
二叉排序树的生成 • 令二叉排序树为空二叉树; • 就i=1,2,…,n,重复将ki插入到二叉排序树中去,使得二叉树仍然是二叉排序树(实际上被插入的新结点都是作为叶子结点插入的) • 二叉排序树的生成过程举例说明(见下页)
10 10 10 10 10 10 10 10 18 18 18 18 18 18 18 18 3 3 3 3 3 3 3 8 8 8 8 8 12 12 12 2 2 7 例 {10, 18, 3, 8, 12, 2, 7} 8 18 3 12 10 • 二叉排序树的生成过程 12 8 2 7 • 二叉排序树的生成 • 令二叉排序树为空二叉树; • 就i=1,2,…,n,重复将ki插入到二叉排序树中去,使得二叉树仍然是二叉排序树(实际上被插入的新结点都是作为叶子结点插入的) • 二叉排序树的插入算法 见下页
10 18 3 8 12 2 7 在二叉排序树中插入一个新元素的算法描述: template <class Type> void BSTree<Type>:: Insert (const Type & x, BSTreeNode<Type> * & p) { //在p为根的二叉排序树插入结点x的递归算法 if ( p == NULL ) //空二叉树 p = new BSTreeNode<Type> (x); //创建数据元素x的结点 elseif ( x < p->data ) //在左子树插入 Insert ( x, p->leftChild ); elseif ( x > p->data ) //在右子树插入 Insert ( x, p->rightChild ); else //结点x已存在 { cout << "There has node x" << endl; exit (1); } }
在二叉排序树中插入一个新元素的算法描述: template <class Type> void BSTree<Type>:: Insert (const Type & x, BSTreeNode<Type> * & p) { //在p为根的二叉排序树插入结点x的递归算法 if ( p == NULL ) //空二叉树 p = new BSTreeNode<Type> (x); //创建数据元素x的结点 elseif ( x < p->data ) //在左子树插入 Insert ( x, p->leftChild ); elseif ( x > p->data ) //在右子树插入 Insert ( x, p->rightChild ); else //结点x已存在 { cout << "There has node x" << endl; exit (1); } } 请比较 二叉排序树的查找算法 template <class Type> BSTreeNode<Type> * BSTree<Type> :: Find (const Type & k, BSTreeNode<Type> * p) const{ //在p为根的二叉排序树上进行查找的递归算法 if ( p == NULL ) returnNULL; //查找失败 elseif ( k < p->data ) //在左子树上递归查找 return Find ( k, p->leftChild ); elseif ( k > p->data ) //在右子树上递归查找 return Find ( k, p->rightChild ); elsereturn p; //相等,查找成功 }