数据结构与算法之查找

基本概念

仅存储数据而不获取数据是不可能的,这就是查找。查找的定义如下:

查找(Searching)就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。

查找表(Search Table)是由同一类型的数据元素(或记录)构成的集合。

简单来说就是,在我们定义的数据结构中,查找位于某个位置的数据。查找又根据操作方式不同分为静态查找动态查找两种,前者是仅获取数据不进行其他操作,后者则需要动态改变数据,比如在查找过程中插入新数据,或者删除某个已存在的数据。

接下来,对应之前介绍的几种数据结构,来说说它们各自的查找方式。

顺序表查找

顺序表,通常就是一个数组,数据在内存中按顺序排列,但并不是有序的,所以查找需要遍历,一般写法如下:

private int searchLinear1(int[] table, int data){
    // len一定要先计算出来
    int len = table.length;

    for (int i = 0; i < len; i++) {
        if(table[i] == data){
            return i;
        }    
    }
    return -1;
}

我们想当然就会写出上述代码,而且都会把数组的长度先计算出来,这算是一点小优化吧。在数据量很小的时候以上做法并没有什么问题,但是当数据量增多时,循环体每次执行i<len的判断会对时间产生非常大的影响。我们可以通过增设一个“哨兵”来去除判断,代码如下:

private int searchLinear2(int[] table, int data){
    int len = table.length;

    table[0] = data;

    int i = len-1;
    while (table[i]!=data) {
        i--;
    }

    return i;
}

这和方法1有何区别呢?可以看到,原本for循环体每次要执行三条操作,使用while时只需要两条就可以了。当数据量较大时差异会十分明显。

顺序表这种结构,天然不适合大规模数据,但它结构简单,算法简单,在很多场景下还是十分适用的。

有序表查找

顺序表的特点我们已经掌握了,很明显它的查找时间复杂度是O(n),如果要在一亿条数据中查找,效率相对来说就太低了。如果数据是有序的,我们就可以采取一些手段大幅提升查找效率。

1. 折半查找

折半查找就和我们猜一个1-100之间的数字一样,不会有人傻傻地从1猜到100,而是先猜一个中间的数比如50,然后根据大小再猜50-100之间的数字,这样一下减少了一半,只需要几次就能猜出结果了。代码实现如下:

private int searchOrdered(int[] table, int data){
    int low, high, mid;

    low = 0;
    high = table.length-1;
    
    while(low<=high){
        mid = (low+high)/2;
        if(data<table[mid]){
            high = mid-1;
        }else if(data>table[mid]){
            low = mid+1;
        }else{
            return mid;
        }
    }

    return -1;
}

代码很好理解,折半查找的最坏时间复杂度是O(logn)。

2. 插值查找

折半查找是一种十分优秀的查找方式,但是它限定了每次都从中间取值,在一些场景下这是没有必要的,比如还是猜1-100之间的数,但是我们知道它是一个比较小的数,这时候我们还会从50开始猜吗?再举一个例子,读一本大约500页的书,第一次读了100页,下一次翻开时,我们肯定不是先翻到一半,而是翻到靠前的位置,再找到100页续读。插值查找就是针对类似这种情况的优化。

在折半查找中,我们的关键代码是mid=(low+high)/2,我们可以把它变换成如下形式:

折半查找

我们可以这样理解这个公式,low表示起始位置,(high-low)代表长度,也就是从起始位置加上长度的一半,这就是中点。所以这里的1/2相当于偏移量,如果我们改变它的值,就可以偏移到别的位置。插值查找的做法如下:

插值查找

这里的key就是目标值,分母表示的是值的范围,分子表示目标值相对最小值的差值,这样便得到了key相对于数据的偏移比例,比如最小值是50,最大值是100,目标值是60,那它所在位置相对的比例应该是1/5处,如下所示:

偏移值

原理搞清楚了,它的实现也就很清楚了,只要把折半查找里的公式换掉就可以。但是插值查找对数据要求也是很苛刻的,数据必须较为均匀地分布,只有均匀的数据才可以这样使用比例计算偏移量。因为这个偏移量最终要映射到表中,比如下面的数据:

非均匀分布

我们要从中获取10,按照插值查找的方式,第一次获取的mid值在位置0处,但实际上10却处于中点处,这就是因为表中的数据增加不均匀造成的。

3. 斐波那契查找

裴波那契查找和插值查找目的一样,都是对折半查找的优化,它主要是依据裴波那契数列的黄金分割原理。现在,我们通过一个示例演示它的原理。首先有如下数组,我们要查找的值为72:

初始值

数组下标最大值为9,在裴波那契数值8和13之间,我们把13的下标7记为k,作为分割时的初始值。然后把数组扩展到长度为13,剩余元素用最大值填充,然后建立两个指针分别指向原数组的开始和结尾,如下所示:

扩充

接下来就是确定mid指针位置,它的计算方式为:mid = low + F(k-1) - 1; 其中F(n)表示位置n的裴波那契值。所以mid的位置为7,如下所示:

mid值

现在,目标值72比mid值小,所以要把high值前移,前移方式和折半查找一样。这时查找范围从0-F(k)变为了0-F(k-1),所以k值也要减小1,变为6,然后进行下一次比较。如下图所示:

缩小范围

现在目标值72比mid值大,所以low需要后移,这时中间的元素有多少个呢?根据裴波那契的定义F(n) = F(n-1) + F(n-2),可以知道F(n) - F(n-1) = F(n-2) ,所以再下一次的查找相当于在F(k-2)上进行,所以k值应该减小2,变为4,如下所示:

缩小范围

此时目标值还是比mid值小,所以重复第一次操作,low与high和mid均指向了位置5,这时便完成了查找。

在上述示例中,可以发现使用裴波那契查找并没有减少查找次数,这是因为有两次查找过程在mid的左侧。裴波那契查找会把数据分成左侧长,右侧短两部分,也就是说当查找的数据在右侧时,其查询速度会比折半查找快的多,但如果是在左侧,因为每次查询的长度都比折半要长,效率反而会更差。

裴波那契查找的代码请参考文末链接。

4. 总结

在有序表上的查找主要就是以上三种方式,后两者均是在特定情况下对折半查找的优化,具体使用哪种比较合适,还要看实际的数据。

线性索引查找

线性表虽然对数据没有要求,但是查找很慢,而有序表虽然查找很快,却要求数据是有序的。很多时候,我们可能无法对数据进行排序,比如微博的回复有可能有上亿条,进行排序是不现实的。有没有办法能在数据无序情况下,又能有比较好的查找性能呢?这就要用索引。

索引,就是把一个关键字与它对应的记录相关联的过程。

可以类比查字典来简单理解索引,在字典首部的拼音表就是一种索引,每个拼音都对应着一些发音一致的汉字的位置,我们只需要查这个拼音表就可以快速定位到汉字所在的页数。也就是说不需要对汉字进行排序,只需要获取到它的拼音,把这个拼音排好序就可以提高查找效率。

索引按照结构分为线性索引,树形索引和多级索引等,这里介绍的是线性索引,树形索引一般是B树、B+树,会在后续介绍。

定义

线性索引,就是将索引项集合组织为线性结构,也称索引表。

线性索引按照索引方式分为很多类型,主要是以下三种:

1. 稠密索引

稠密索引就是数据集合中的每条数据都对应索引表中的一个索引项,也就是一一对应的关系。原有的数据是无序的,索引表则是有序的,这样就可以按照上方有序表方式进行查找了。稠密索引的缺点也很明显,那就是它的数据量和原数据集合一样大,这通常需要很大的内存空间。假如数据有几亿条,而计算机内存比较小,就需要多次进行IO操作,性能也会大受影响。

2. 分块索引

稠密索引解决了查找的问题,但是数据量太大,而分块索引就折中了这两个问题。就像图书馆摆放图书一样,不同种类的书会摆放在独立的架子上,但是每个架子上的书是随机摆放的,这种块间有序、块内无序的分块方式就是分块索引。显然,在块间查找时是非常快的,而在块内因为无序只能进行遍历。但是因为索引表相对较小,减少了IO操作,所以也有较好的性能。

3. 倒排索引

倒排索引是搜索技术的基础,它是根据关键字索引对应的文章等记录。搜索的处理十分复杂,这里只介绍最基本的原理。

比如有一千个网页,有可能有“朋友”、“咖啡”、“电影”等关键字,我们想筛选出所有包含“朋友”的网页,总不能每查一次,就遍历一次吧?而且每个网页中有数百上千字,这样查找效率十分低下。

遍历肯定是需要的,那我们能不能在第一次遍历之后就把需要的信息记录下来呢?这是可行的,我们只要以关键字建立索引,每个关键字记录下对应的网页地址就可以了。这样我们会得到类似下图的索引表:

倒排索引示例

这样,当我们再次搜索“朋友”时,就能够迅速找到所有的网页地址了。那么倒排查找有什么弊端呢?这就体现在它的插入和删除了,因为相同的网页地址可能被多个关键字记录,也就意味着如果要删除一个网页,就要处理所有相关记录,这给维护带来十分大的困难。当然,真正的搜索技术比这个原理复杂的多,感兴趣的朋友可以自行查阅资料进行研究。

二叉查找树与散列表

以上涉及的线性表,总是无法协调查找和增删之间的矛盾,二叉查找树和散列表则是能实现查找快和增删快的两种方式。这些知识大部分在分析Java集合时做了总结,此处不再赘述,唯有广泛应用于数据库的B树和B+树知识还没有分析,会在随后文章中总结。下面是相关文章链接:

Java集合源码分析之基础(二):哈希表

Java集合源码分析之基础(四):二叉排序树

Java集合源码分析之基础(五):平衡二叉树(AVL Tree)

Java集合源码分析之基础(六):红黑树(RB Tree)

以上涉及代码均已上传至我的github


我是飞机酱,如果您喜欢我的文章,可以关注我~

编程之路,道阻且长。唯,路漫漫其修远兮,吾将上下而求索。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 原文出处:http://www.cnblogs.com/maybe2030/p/4715035.html引文出处:...
    明教de教主阅读 8,929评论 0 7
  • 查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算,例如编译程序中符号表的查找。本文...
    北方蜘蛛阅读 2,756评论 1 4
  • 目录 [1. 顺序查找][2. 二分查找][3. 插值查找][4. 斐波那契查找][5. 树表查找][6. 分块查...
    jiangmo阅读 16,728评论 4 6
  • 本文的整理基于:http://blog.csdn.net/qq_23217629/article/details/...
    阿阿阿阿毛阅读 1,548评论 0 3
  • 一、相关定义 查找——查找就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。所有这些...
    开心糖果的夏天阅读 1,039评论 0 8