6.1 查找算法概述

一、基本概念

查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算。

被查找的对象是由一组记录组成的表或文件,而每个记录则由若干个数据项组成,并假设每个记录都有一个能唯一标识该记录的关键字。在这种条件下,查找的定义是:给定一个值k,在含有n个记录的表中找出关键字等于k的记录。若找到,则查找成功,返回该记录的信息或该记录在表中的位置,否则查找失败,返回相关的指示信息。

采用何种查找方法首先取决于使用哪种数据结构来表示“表”,即表中记录的组织方式。为了提高查找速度,常常用某些特殊的数据结构来组织表,或对表事先进行诸如排序这样的运算。

若在查找的同时对表做修改运算(如插入和删除),则相应的表称为动态查找表,否则称之为静态查找表

查找也分内查找外查找之分。若整个查找过程都在内存进行,则称之为内查找;若查找过程中需要访问外存,则称之为外查找。

查找的效率:

平均查找长度(Average Search Length,ASL):需和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度。

  对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL = Pi*Ci的和。
  Pi:查找表中第i个数据元素的概率。
  Ci:找到第i个数据元素时已经比较过的次数。

二、查找算法的分类

先介绍查找算法的分类,建立查找算法的理论框架,再学习起来思路会比较清晰。

参考博客:七大查找算法

但是,该博客没有建立清晰的查找算法分类,但是对主要的查找算法的基本思想和复杂度进行了介绍,并有C++源码,可以学习一个。

如下是教科书上对查找算法的分类及其代表性算法,本章及后文再介绍各个算法时,相信大家会有比较明确的概念了。查找算法的大体框架如下,下文是对树表之外的算法进行介绍~

查找算法分类
算法分类 主要算法 说明
线性表查找/静态查找表 顺序查找、二分查找和分块查找 下面会对其中的算法进行介绍,算法思想都很简单。
树表查找/动态查找表 二叉搜索树、平衡二叉树、B-树、B+树 详情请关注数据结构之树中对树表查找的介绍。
哈希表查找/散列查找 就是哈希算法 下面也会介绍哈希的基本原理。


三、线性表查找

线性表有顺序和链式两种存储结构,此处只讨论顺序表的查找,链表的相关问题可参考第一章。

1. 顺序查找

顺序查找也称为线形查找,从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。

复杂度分析: 

查找成功时的平均查找长度为:(假设每个数据元素的概率相等) ASL = 1/n(1+2+3+…+n) = (n+1)/2 ;
当查找不成功时,需要n+1次比较,时间复杂度为O(n);

所以,顺序查找的时间复杂度为O(n)

傻瓜式的查找,n较大时不建议使用,效率低。其优点是对表的结构无任何要求。

2. 二分查找

这是这一节的重点,后面会有相关的数道题目对这一方法进行全面解读。

元素必须是有序的,如果是无序的则要先进行排序操作。

基本思想:也称为是折半查找,属于有序查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。二分查找的过程可以用二叉树来描述。

复杂度分析:最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n)

注:折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。——《大话数据结构》

3. 插值查找以及斐波那契查找

二分查找、插值查找以及斐波那契查找都可以归为一类——插值查找。插值查找和斐波那契查找是在二分查找的基础上的优化查找算法。

插值查找

基本思想:基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,差值查找也属于有序查找。

折半查找这种查找方式,不是自适应的(也就是说是傻瓜式的)。二分查找中查找点计算如下:

  mid=(low+high)/2, 即mid=low+1/2*(high-low);

通过类比,我们可以将查找的点改进为如下:

  mid=low+(key-a[low])/(a[high]-a[low])*(high-low),

  也就是将上述的比例参数1/2改进为自适应的,根据关键字在整个有序表中所处的位置,让mid值的变化更靠近关键字key,这样也就间接地减少了比较次数。

注:对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。

复杂度分析:查找成功或者失败的时间复杂度均为O(log2(log2n))。

斐波那契查找

基本思想:也是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。同样地,斐波那契查找也属于一种有序查找算法。

斐波那契序列的特点对有序表进行分割的,要求开始表中记录的个数为某个斐波那契数小1,即n=F(k)-1。(注意这是对元素总数的强制性要求)

开始将k值与第F(k-1)位置的记录进行比较(即mid=low+F(k-1)-1)而不是二分查找的(mid=low+high)/2),比较结果分为三种

  1)相等,mid位置的元素即为所求

  2)>,low=mid+1,k-=2;

  说明:low=mid+1说明待查找的元素在[mid+1,high]范围内,k-=2 说明范围[mid+1,high]内的元素个数为n-(F(k-1))= Fk-1-F(k-1)=Fk-F(k-1)-1=F(k-2)-1个,所以可以递归的应用斐波那契查找。

  3)<,high=mid-1,k-=1。

  说明:low=mid+1说明待查找的元素在[low,mid-1]范围内,k-=1 说明范围[low,mid-1]内的元素个数为F(k-1)-1个,所以可以递归 的应用斐波那契查找。

复杂度分析:最坏情况下,时间复杂度为O(log2n),且其期望复杂度也为O(log2n)。

4. 分块查找

分块查找又称索引顺序查找,它是顺序查找的一种改进方法。
算法思想:将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……
算法流程:
  step1 先选取各块中的最大关键字构成一个索引表;
  step2 查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;然后,在已确定的块中用顺序法进行查找。

四、哈希表查找

哈希表(Hash table)又称散列表,是除顺序表、链表和索引表之外的又一种存储线性表的存储结构。

算法思想:如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。

基本思路:1)以存储对象的关键字k为自变量,通过给定的哈希函数,把k映射到一个连续的内存单元的地址(或下标),并存储在此;2)根据选择的冲突处理方法解决地址冲突;常见的解决冲突的方法:拉链法和线性探测法。3)在哈希表的基础上执行哈希查找,只需要将待查找的关键字k执行同样的哈希函数得到哈希地址,查找该内存地址的关键字是否为k(需注意冲突问题)。

哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。

在初等水平上,对哈希数据结构的理解到此即可。哈希算法的诸多题目在各个章节都会涉及,本章也不单独列出题目。

如何深入理解哈希算法,需要从以下几个方面继续学习:

1. 哈希函数的选择:

关键字为整数、字符串等情况时函数的选择;

如何构造哈希函数使得到的哈希值更散列(避免地址冲突)。

2. 哈希冲突的解决方法

哈希冲突很难避免,主要与三个因素有关:

一是装填因子α,即哈希表中已存入的记录数n与哈希地址空间大小m的比值。

二是所采用的哈希函数。哈希函数选择适当,可使哈希地址尽可能均匀分布在哈希地址空间上,从而减少冲突的发生。

三是解决冲突的哈希冲突函数。

解决冲突的方法主要分为开放定址法拉链法,可以参见:浅谈算法和数据结构: 十一 哈希表中的介绍,裆燃看教材上的也可。

开放定址法,以发送冲突的哈希地址为自变量,通过某种哈希冲突函数得到一个新的空闲哈希地址,常见方法有线性探索法、平方探索法等,反正就是在其他空位置找一个,在查找时,当前位置没找到的话,还要按照哈希冲突函数继续探索,凭感觉觉得并不靠谱,其实只要到某个地址为空就可。

拉链法,把地址冲突的存储对象,用单链表连接起来,此时哈希表每个单元存储的是单链表的头指针,由于单链表可以插入任意个,理论上都不需要扩容==,一般可把填充因子设定的较高。

相比之下,拉链法处理冲突更简单,平均查找长度较短;填充因子较大,减少浪费,链表空间动态申请,更为灵活。更显著的优点是,哈希表元素的删除,链表中元素的删除很简单,而开放定址法元素如何删除呢?可思考一下。。由于查找时要到某个地址为空,删除时,空地址是查找失败,我们的做法一般是做一个删除的标记(也不可能按哈希冲突函数去向前递补,那样会越来越乱),因此开放定址法在元素频繁添加删除时,并不适合。

3. 哈希表的操作:如查找,插入等,具体是如何实现的

理解Java 的数据结构中HashMap 与HashTable的区别,这是一个经典的问题。深刻理解二者的区别和原理,基本上就对上述三个方面搞明白了。参考博客如下:

HashMap和Hashtable的区别

可以认真学习一下二者的哈希值计算方法,以及在数组加链表的实现原理之上(1.8之后使用了红黑树,有兴趣的再深入了解),元素的插入、查找方法的实现。

还有resize方法,即hashmap的扩容,具体过程为:先创建一个容量为table.length*2的新table,修改临界值,然后把table里面元素计算hash值并使用hash与table.length*2重新计算index放入到新的table里面;注意扩容时用每个元素的hash值全部重新计算index。hashmap与hashtable的初始容量,填充因子和扩容,都是要记住的。


介绍了树表以外的其他查找算法,二分查找的具体案例看下一节;哈希查找的原理也是这一节的关键,具体案例可以看4.4节的几道LeetCode题目~

猜你喜欢

转载自blog.csdn.net/xutiantian1412/article/details/85042207
6.1