一、基本概念
查找就是在指定的数据集合(查找表)中,找到某个特定的数据元素。
查找表:同一类型的数据元素构成的集合。
关键字:数据元素中某个数据项的值,又称为键值。如果关键字可以唯一的对应一条记录,则称为主关键字。对于可以对应多个数据的关键字,被称之为次关键字。
静态查找表:查找表中的数据在查找过程中相对稳定,查找过程中不增加或删除表中的元素。
动态查找表:查找过程中,根据结果动态增删表中的元素。
为了提高查找效率,我们需要相应的查找数据结构。对于静态查找表,可以使用线性表结构。如果数据元素具备主关键字,通过对关键字排序后,可以使用折半查找法提高查找效率。
对于动态查找表,可以考虑二叉排序树。另外,Hash表可以以有效来提升查找效率。
二、静态查找
1、顺序查找法:数据元素保存在线性表结构中,从前向后依次遍历,找到满足查找条件的数据元素。顺序查找算法实现起来最简单,平均查找次数为(n+1)/2,时间复杂度为O(n)。
2、有序查找:如果查找表中的数据元素是有序排列的,则可以从表中的中值开始比较,这样有两个好处:1、需要查找的数据落在中间的概率比较高。2、一旦发现中值不是需要查找的数据,根据它们的大小关系,可以直接排除中值左边或右边的全部数据,迅速缩小查找范围。由此可知,有序查找的一个关键问题就是如何选取中值。
2.1、折半查找法:
中值选取:mid=(low+high)/2
算法思路:
- 判断target ?= a[mid];
- 修改low或high后,重新计算mid,并再次判断target ?= a[mid];
- 直到low>=high,或找到target == a[mid]
时间复杂度:O(logn)
2.2、插值查找法:
中值选取:基本思路就是先计算需要查找的目标key值在有序表中的权重,再根据权重得到中值。
mid=low+key−a[low]a[high]−a[low](high−low)
算法思路:同上
时间复杂度:同上
2.3、斐波那契查找:斐波那契数列的特征是某个位置的数值等于前两个位置数值之和,该数列越往后相邻的两个数的比值越趋向于黄金比例值(0.618),所以又称黄金分割数列。斐波那契查找与折半查找很相似,它是根据斐波那契序列的特点对有序表进行分割的。
算法思路:
1、在斐波那契数列找一个数F[n],使得F[n]等于或略大于查找表中元素个数的,将原查找表扩展为长度为F[n]个(如果要补充,使用最后一个最大的元素补充,直到满足F[n]个元素)。
2、我们知道斐波拉契数列的特征就是F(n)= F(n-1)+ F(n-2),这里经过第1步的数据补充,待查找数据的数量为F(n),然后,将整个数据分为两部分,前半部分F[n-1]个元素,后半部分F[n-2]个元素(即完成后进行斐波那契分割),确定要查找的元素在哪一部分并递归,直到找到。
时间复杂度:同上
上面三种查找方式区别只是选取mid点的策略不同,时间复杂度都是一样的。
这三种查找方式都需要数据保存在数组中,否则每次选取中值都是个大问题。
数组结构只适合静态查找表,对于需要频繁增、删数据元素的动态表,存在无法解决的效率问题
2.4、线性索引查找
前面谈论的查找算法,查找目标基本上被简化成了一个数值。哪怕是一个巨大无比或巨小无比的数值,终究也只是一个数字,经过排序以后的数字,查找起来都不难。但是现实中的数据信息可能更复杂而无法排序,比如,学生的信息(包括:姓名、性别、籍贯、出生日期等等),虽然在录入计算机的时候,学生信息被数字化,但这些被数字化的信息并不是一个简单的数字。为了实现对这种信息的快速处理,通常的方法就是给每一条数据建立索引。就好像学校给每个学生分配一个学号,要查某个学生的信息,只要通过学号就可实现快速查询。一个索引至少应包含两个信息:关键字和其对应的记录在存储器中的位置信息。所以,线性索引查找原理是:对于复杂的数据通过抽象出简单的key,并对key值排序,实现数据的快速查询,它是顺序查找的一种改进方法。
稠密索引:将数据集中的每个记录对应一个索引项。对于稠密索引这个索引表来说,索引一定是按照关键码有序排列的。
分块索引:稠密索引因为索引项与数据集的记录个数相同,所以空间代价很大,如果稠密索引的大小也超出了内存的容量,那么查找索引也会带来IO。为了减少索引的个数,我们可以对数据集进行分块,然后再对每一块建立一个索引项,最终减少索引项的个数。(分块索引也需要有序排列后才能被快速处理,即块内无须,块间有序)
分块索引的数据结构至少包含三个部分:
- 最大关键码–存储每一块中的最大关键字。
- 存储每一块中记录的个数以便于循环时使用。
- 用于指向块首数据元素的指针,便于开始对这一块中记录进行遍历。
三、动态查找
1、哈希表
如果数据元素形为<key, value>
结构,且key值为整数,最好的存储方式是采用数组结构,其中key作为数组下标,value作为数组元素。这样在查找数据的时候,通过key值可以直接找到value,中间没有比较的过程,时间复杂度为O(1)。
如果key值非常大,且不连续;或者数据元素需要频繁的增加或删除,甚至无法预测最大的key值(例如,<IP地址,主机名>
这样的信息结构)。对于这些情况,数组结构就显得非常的不合适,通过哈希表可以比较好的解决这些问题。
哈希表(Hash table)也叫散列表,它提供了一种方法,通过key值映射,可直接访问到表中的一个固定位置,以加快查找的速度。这个映射函数叫做哈希(散列)函数,存放记录的数组叫做哈希(散列)表。
从上面的示意图,我们可以理解哈希表的大致原理。
- 分散不连续的key值经过hash函数处理以后,可以映射到一个相对紧凑的数组空间。这个数组就是哈希表。这就解决了key值太大,数组无法定义的问题。
- 不同的key值经过hash函数处理可能得到相同的散列地址,即k1≠k2,而f(k1)=f(k2),这种现象称为碰撞(英语:Collision)。哈希函数的设计目标就是要让哈希表每个结点的冲突概率接近一致。大概的方式有:直接定址、数字分析、平方取中、折叠、除留余数法。
- 对于无法避免的冲突,对应右固定的冲突处理方法,包括:开发定址法、再哈希法、链地址法、建立公共溢出区。一般链地址法比较容易理解。
经过哈希函数和冲突处理,我们可以发现,哈希表的优势包括:
- 查找的第一步通过key可以直接找到哈希表内的一个固定地址,不需要比较,这个过程时间复杂度最低(是一个常数和数据规模无关)。
- 如果存在冲突,通过冲突处理,一个长的线性表被分割成几个短的线性表,查找范围大大缩小,查找效率也会明显提升。
- 冲突处理还可以有效满足动态增、删数据的操作。所以,哈希表也属于动态查找表。
2、二叉排序树:
哈希表可以满足动态查找表的要求,但是,查找效率似乎明显比不上折半查找法。所以,接下来介绍的二叉排序树就是一种高效的动态查找数据结构。
二叉排序树又称为二叉查找树,二叉树排序的特征:左子树<父节点<右子树
- 如果左子树不为空,则左子树上所有结点的值均小于它的根节点的值
- 如果右子树不为空,则右子树上所有结点的值均大于它的根节点的值
- 它的左子树和右子树也分别是二叉排序树
二叉排序树的基本操作
查找操作:先找父节点比较,小的话就比较左子树,大的话就比较右子树。
插入操作:不需要拆除已有数据的连接关系,只需要找到合适的结点后,根据插入数值的大小关系,将数据放在结点的左子树或右子树。
删除操作:相对比较复杂,删除之前,首先是查找要删除的结点。
如果发现待删除结点本身就是叶子结点,则直接删除。
如果待删除结点只有左子树或只有右子树,将左或右子树整个移到删除结点的位置。
如果待删除结点同时有左、右子树,需要用它的前驱或后继结点来替换待删除结点。
二叉排序树存在的问题:如图所示,频繁增删一个二叉排序树,可能出现一个右斜树,导致查找效率退化为顺序查找O(n)。
3、平衡二叉树(又称AVL树)——二叉排序树的改进措施:
保证二叉树尽量平衡是保证二叉树查找效率的关键。平衡二叉树也是一种二叉排序树,它是一个引入了平衡概念的二叉树,同时具有以下性质:
- 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
平衡二叉树实现原理:面向查找时间最优建立起来的二叉排序树进过多次删除操作后,如果我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。最终造成树的平衡性受到破坏,导致查找操作的时间复杂度增加。所以,建立平衡二叉树就是要建立一个同时满足:左子树<父节点<右子树,以及它的所有结点的左、右子树的高度差的绝对值不超过1。建立平衡二叉树的大部分过程和二叉查找树是一样的,区别就在于插入和删除之后要写一个旋转算法去维持平衡,另外每个结点都需要一个平衡因子用于判断是否平衡。
平衡因子:二叉树结点的左子树深度减去右子树深度的值称为平衡因子BF。平衡二叉树上所有结点的平衡因子只可能是-1,0,1。
平衡二叉树实现算法:
typedef struct Node
{
DataType data; // 结点数据
int bf; // 结点的平衡因子
struct Node *lchild, *rchild; // 如果有必要,还可以增加一个指向父节点的指针
}Node, *Node;
建立平衡二叉树基本思路:
1. 首先要保证满足二叉排序树的特征:左子树<父节点<右子树
2. 在插入结点的过程中,随时计算所有结点的平衡因子,发现平衡因子绝对值大于1的情况立刻调整。
3. 调整的原则是,首先确定最小不平衡子树的根节点和它的平衡因子,平衡因子大于1的,表示左边多了,需要右旋。平衡因子小于-1的,表示右边多了,需要左旋。
4. 在旋转前,还需要增加一个判断,保证准备旋转的最小不平衡子树上根结点和它的子节点的平衡因子数值的符号相同。
5. 整个旋转过程中还要避免一种情况就是,某个结点旋转以后,它以前的左孩子变成了右孩子,或者是右孩子变成左孩子。
另外,还有一种红黑树也是一种平衡二叉树,但是和AVL相比,它不追求完全平衡,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。当前,C++ STL模板库中关联容器map/set/multimap/multiset内部的数据结构基本是用红黑树实现的,它的时间复杂度和AVL相同,但统计性能比AVL树更高。