今天这篇博客就聊聊几种常见的查找算法,当然本篇博客只是涉及了部分查找算法,接下来的几篇博客中都将会介绍关于查找的相关内容。本篇博客主要介绍查找表的顺序查找、折半查找、插值查找以及Fibonacci查找。本篇博客会给出相应查找算法的示意图以及相关代码,并且给出相应的测试用例。当然本篇博客依然会使用面向对象语言Swift来实现相应的Demo,并且会在github上进行相关Demo的分享。

查找在生活中是比较常见的,本篇博客所涉及的这几种查找都是基于线性结构的查找。也就是说我们的查找表是一个线性表,我们要查找某个元素在线性表中的位置。顺序查找就是从头到尾一个个进行比较,直到找到为止,此方法适用于无序的查找表。而折半查找、插值查找以及Fibonacci查找的查找表都是有序的,下方的内容会详细的介绍到。进入今天博客的主题。

一、查找协议的定义

因为本篇博客我们涉及查找表的多种查找方式,而且查找表的数据结构都是线性结构。基于Swift面向对象语言的特征以及面向接口编程的原则,我们先给我们所有的查找方式定义一个协议。本篇博客中所有的查找方式都会遵循这个查找类型,这样便于外部统一调用,也方便我们测试和扩展。

下方这个SearchType协议就是我们所定义的查找协议。下方这个协议虽然比较简单,但是还是比较重要的,协议中定义了本篇博客所涉及的查找方式对外的调用方式。协议中的search()方法就是外部要调用的方法。该函数第一个参数就是要查找的查找表,第二个参数就是要查找的关键字。该函数的返回值就是关键字在查找表中的位置。如果没有找到就会返回0。

  

二、顺序查找

上面也简单的提了一下,顺序查找表是从头到尾以此进行对比,直到找到我们要查找的元素位置。如果未找到,就返回0。当然从顺序查找的这个过程中我们就可以看出来顺序查找适用于无序的查找表。也就是说,当我们使用顺序查找作用于查找表时,我们是不用关心查找表的顺序的。

为了更直观的理解顺序查找,我们可以看一下下方的示意图。在查找表中存储着A~H的元素,我们要查找G元素在该查找表中的位置,我们需要从A开始以此匹配,当找到G时,就返回G在查找表中的位置。

  

根据上面我们不难给出代码实现,下方代码这个SequentialSearch这个类就是我们创建的赋值顺序查找的类。当然该类要遵循SearchType,并且给出search()方法的实现。search()方法中的实现内容比较简单,就是一个for循环,依次从头到尾进行匹配。匹配成功后就返回该关键字在线性表中的位置。代码比较简单在此就不做过多赘述了。

  

对于顺序查找,我们可以将其进行优化。在的search实现中,i是从范围中取的,所以每次得判断i是否在特定范围中。在我们优化后的代码中就不用做此判断。优化的手段就是将我们要匹配的关键字item追加到查找表的尾部,我们称之为哨兵,如果查找的结果是哨兵的位置,那么说明查找失败,search()函数就返回零。当然你也可以将哨兵放在第一个位置,从后往前的进行查找,不过如果你的查找表是顺序存储的话,不建议将哨兵插入到第一个位置,因为顺序表的插入操作是比较费时的。

  

根据上面这个示意图,我们不难给出相应的代码实现。下方这个代码片段就是设置了哨兵的顺序查找方法。因为代码比较简单,在此就不做过多赘述了。

  

 

三、折半查找

折半查找又称为二分查找,折半查找的作用对象是有序的查找表,也就是说,我们的查找表是已经排好序的。之所以称为折半查找,是因为在每次关键字比较时,如果不匹配,则根据匹配结果将查找表一份为二,排除没有关键子的那一半,然后在含有关键字的那一半中继续折半查找。

下方就是折半查找的示意图,在下方示意图中,我们查找A--H这个查找表中关键字G的位置。下方就是每个步骤的具体说明

  • (1)标记查找表的范围,查找表的初识范围就是整张表,所以查找表的下边界low=1,查找表的上边界high=8。查找表的中间位置mid=low+(high-low)/2=(high+low)/2 = 4。所以我们将G与mid所对应的D比较大小。比较结果为G>D。

  • (2)由上一步的比较结果,我们得知上面一轮中,前一半的数据是没有我们要查找的关键字G的。所以将前一半查找表中的数据进行丢弃,重新定义查找表的范围,因为mid处的元素以及匹配完毕了,要想丢弃前半部分的的数据,我们只需更新查找表的下边界移动到mid后方即可。也就是将查找表的范围缩小到上一步查找表范围的后半部分。此刻查找表的下边界low=mid + 1 = 4+1 = 5。查找表的下边界更新后,mid的位置也会变化,所以我们要对mid进行更新,mid的位置仍然是low和high的中心,mid = (high + low)/2 = (8+5)/2=6。此刻mid处的元素为F, 将G与F比较,可知G > F。

  • (3)由G>F这个结果,我们得出,上一轮查找表的前半部分的数据需要丢弃,所以要还需要更新low的值,low= mid + 1 = 6+1 = 7。 mid = (8+7)/2=7。此刻的mid处的元素是G, 所以找到的我们要找的值,返回mid = 7。

  

上面是一个完整的二分查找的实例,不过在上述实例中,只对low和mid的值进行了更新,因为都是抛弃了前半部分。当item<items[mid]时,我们就需要丢弃查找表的后半部分,更新上边距high的值。不难得出,上边边界high的值更新为high=mid-1。将查找表的范围缩小到前半部分继续查找。根据这些叙述,我们不难给出代码实现,下方代码段就是折半查找的Swift语言的实现。如下所示:

    

四、插值查找

插值查找其实说白了就是上面二分查找的优化,因为从中间对查找表进行拆分并不是最优的解决方案。因为我们的查找表是有序的,当我们感觉一个值比较大时,会直接从后边来查找。比如举个现实生活中的例子,当你在翻字典是,查找“zhi”相关的字,如果让你直接翻内容的话,你肯定从奔着字典的后边几页去了,而不是从中间进行二分对吧。

插值查找就是让mid更趋近于我们要查找的值,将查找表缩小到更小的范围中,这样查找的效率肯定会提升的。至于如何将mid更趋近于我们要查找的值呢,那么这就是我们“插值查找”要做的事情了。在折半查找中我们知道mid = low + 1/2(high-low)。因为high-low前面的权值是1/2,所以会将查找表进行折半。插值查找就是将这个1/2权值修改成一个更为合理的一个值。

我们将上述的表达式进行修改mid = low + weight*(high-low),从这个表达式中我们可以看出weight的值越大,mid的值也就也靠后,这符合我们想要的规则。因为我们的查找表是有序的,查找的关键字越大,有越往后,我们就可以根据要查找的关键字来求出weight的值。我们不难求出weight=(key - low)/(high-low)。上面这个表达式就可以求出在当前查找表范围中,我们要查找的这个key值在查找表中的权值。

说这么多,其实插值查找与折半查找的区别就在于mid的计算方法上。下方就是插值查找的一个完整实例。我们要查找82在相应查找表中的位置。具体步骤如下所示:

  • (1)、首先初始化我们查找表的范围low=1, high=8。计算我们关键字82在当前查找表范围内的权值weight=(key - low)/(high-low)=(82-10)/(98-10)=0.82。由权值,我们就可以容易的求出mid的值mid = low + weight*(high-low) = 1 + 0.82*(8-1)=6。所以我们将82与items[mid]=79进行比较,可知82>79。

  • (2)、由上面82>79的比较结果可知,mid之前的查找表可以被抛弃,所以我们可以查找表的下边界更新为low=mid+1=7。在更新后的查找表中,82对应的权值weight=(82-82)/(98-82)=0。由此刻的weight我们可以求出mid=7+0*(8-7) = 7。此刻我们将82于mid对应的值进行比较,发现匹配成功,将mid进行返回。

  

上述过程的代码实现并不复杂,只需要将折半查找中的mid的计算方式进行替换即可。下方的InterpolationSearch类就是我们插值查找的类,当然该类也要遵循SearchType协议。在下方代码段中,除了红框部分中的代码,其余的与折半查找的代码完全一致。代码比较简单,在此就不做过多赘述了。

  

五、Fibonacci查找

接下来我们来聊聊斐波那契(Fibonacci)查找。其实就是按照Fibonacci数列来分隔查找表。如果你之前了解过Fibonacci数列的话,那么Fibonacci查找应该好理解。下方我们生成Fibonacci数列,然后使用该数列对我们的查找表进行分割。

1.生成Fibonacci数列

首先我们要生成Fibonacci数列以供我们Fibonacci查找使用。在Fibonacci数列中下一项的值等于前两项的值的和,如果用数学公式来表示的话即为F(n)=F(n-1)+F(n-2)(n>1), F(0)=0, F(1)=1, 根据此规则就可以生成我们的Fibonacci数列。在Fibonacci数列中,n越大,F(n-1)/F(n)的zh值就越接近于0.618,我们知道0.618是黄金分割比,所以斐波那契数列又叫做黄金分割数列。所以我们要实现的Fibonacci查找也可以被称为黄金分割查找。

首先我们先根据Fibonacci数列的规则,来生成Fibonacci数列备用。下方这个就是我们生成Fibonacci数列的方法。下方的FibonacciSearch类就是我们Fibonacci查找的类,其中的fibonacciSequence中存储的就是我们的fibonacci数列。下方的createFibonacciSequence()方法就是创建Fibonacci数列的方法。如下所示:

  

2.Fibonacci查找示意图

Fibonacci查找其实就是利用Fibonacci数列将查找表进行拆分,拆分成F(n-1)和F(n-2)两部分。也就是说如果我们的查找表元素的个数为F(n),那么low到mid(查找表的前半部分)的元素的个数为F(n-1), 而后半部分(min---high)的元素个数就是F(n-2)。有上述的分割关系,我们可知mid = low + F(n-1) - 1。

说白了,Fibonacci查找其实就是使用Fibonacci数列将查找表进行分割,然后求出mid的位置,将关键字与mid进行比较,然后决定是抛弃后半部分还是前半部分。下方是使用Fibonacci数列查找82在相应查找表的具体步骤。

  • (1)、首先准备好Fibonacci数列备用,然后计算查找表元素的个数n在Fibonacci数列中的范围。下方实例中的查找表的个数为9,由F(6)=8 < 9 < F(7)=13这个关系,我们可知查找表从9个元素扩展到13个元素就可以使用斐波那契数列进行分割了,因为F(7)=13, 我们将对7进行标记,也就是key=7。

  • (2)、为了可以使用Fibonacci数列进行分割,我们将查找表扩充到13个元素(F(7) = 13)。查找表后边扩充的元素的值与原查找表最后一个元素的保持一致即可。

  • (3)、将扩充后的查找表使用Fibonacci数列进行第一轮的分割。因为F(7)=13=F(6) + F(5) = 8 + 5, 所以我们将查找表分为两部分,前半部分的元素个数为F(6)=8个,而后半部分的个数为F(5)=5个,此刻我们的mid的值为mid=low + F(6) -1 = 1+8-1=8。我们将82于mid出的元素进行比较(82<98)。

  • (4)、由82<98这个结果我们可以将查找表的范围缩小到上面分割的前半部分。所以我们将high的值进行更新high = mid - 1 = 7。我们继续将前半部分使用Fibonacci数列进行分割,前半部分的个数为F(6)=8, 因为F(6)=F(5)+F(4) = 5+3, 所以我们可以将新的查找表在此分为F(5)=5和F(4)=3两部分。此刻的mid=low+F(5)-1=1+5-1 = 5。82与items[5]=58比较,可以得出82>58,此刻key=6。

  • (5)、由82>58这个结果我们可以知道,上一轮的查找表的前半部分应该被丢弃掉。我们将查找表缩小到后半部分(F(4)对应的部分)。后半部分的元素个数为F(4)=3个,我们可以继续将查找表进行拆分,此刻的key=4。我们先更新low的位置,low=mid+1=5+1 = 6。那么mid = low+F(3)-1 = 6+2-1=7。此刻82=items[mid]=items[7]=82, 查找成功将mid返回。

  

3、Fibonacci查找的代码实现

原理分析完毕后,给出代码实现不是什么难事呢。大体结构与二分查找依然类似。就是根据Fibonacci数列来计算mid的值,然后不断的缩小查找表的范围。首先我们需要查找当前查找表需要扩展到几个元素可以被Fibonacci数列进行分割。下方这个函数就是计算查找表扩展后的元素的个数。findNumberInFibonacci()方法有一个参数,这个参数就是当前查找表的元素的个数,该方法的返回值就是扩充后查找表的个数。

  

求出要扩充的个数,接下来我们就需呀给查找表进行扩充了。下方这个方法就是对查找表进行扩充。扩充时使用的元素是原查找表最后一个值。

  

对查找表扩充完毕后,接下来就该进行查找了。下方是Fibonacci查找的核心代码。代码的具体步骤与上述的示例图是一一对应的。需要注意的一点是key值的更新。下方代码中的key其实就是Fibonacci数列的下标,当前范围内查找表的个数==F[key]。因为我们查找表的范围是不断缩小的,所以key值也是会变化的。我们将查找表(查找表的元素个数为F[key])分割为F[key-1](前半部分)与F[key-2](后半部分)两部分,如果将后半部分进行抛弃,那么key值就为key-1, 如果将前半部分抛弃,那么key=key-2,这一点需要注意。

  

六、测试用例

至此、我们顺序查找、折半查找、插值查找、斐波那契查找聊完了,并且给出了相应的代码实现。接下来就到了我们测试的时间了。因为上面所有的查找类都遵循了一个SearchType协议,所有我们的测试用例可以共用一份,这也是面向接口编程的好处之一。下方就是我们本篇博客的测试用例。

  

上方的测试用例我们使用的是一个,只要传入不同的查找类的对象,我们就可以使用相应的查找方法进行查找。下方就是我们本篇博客测试用例的输出结果。

  

本篇博客的篇幅也够长的了,就先到这儿吧,上述实例的完整Demo会在github上进行分享, 下篇博客我们将要介绍其他几种查找方式。

github链接地址:https://github.com/lizelu/DataStruct-Swift/tree/master/SearchDemo

算法与数据结构(九) 查找表的顺序查找、折半查找、插值查找以及Fibonacci查找的更多相关文章

  1. 算法与数据结构(一) 线性表的顺序存储与链式存储(Swift版)

    温故而知新,在接下来的几篇博客中,将会系统的对数据结构的相关内容进行回顾并总结.数据结构乃编程的基础呢,还是要不时拿出来翻一翻回顾一下.当然数据结构相关博客中我们以Swift语言来实现.因为Swift ...

  2. [C++]数据结构:线性表之顺序表

    1 顺序表 ADT + Status InitList(SeqList &L) 初始化顺序表 + void printList(SeqList L) 遍历顺序表 + int ListLengt ...

  3. 【PHP数据结构】线性表?顺序表?链表?别再傻傻分不清楚

    遵从所有教材以及各类数据结构相关的书书籍,我们先从线性表开始入门.今天这篇文章更偏概念,是关于有线性表的一个知识点的汇总. 上文说过,物理结构是用于确定数据以何种方式存储的.其他的数据结构(树.图). ...

  4. 浅谈算法和数据结构: 七 二叉查找树 八 平衡查找树之2-3树 九 平衡查找树之红黑树 十 平衡查找树之B树

    http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html 前文介绍了符号表的两种实现,无序链表和有序数组,无序链表在插入的 ...

  5. 数据结构算法C语言实现(三十二)--- 9.1静态查找表

    一.简述 静态查找表又分为顺序表.有序表.静态树表和索引表.以下只是算法的简单实现及测试,不涉及性能分析. 二.头文件 /** author:zhaoyu date:2016-7-12 */ #inc ...

  6. java数据结构之有序表查找

    这篇文章是关于有序表的查找,主要包括了顺序查找的优化用法.折半查找.插值查找.斐波那契查找: 顺序优化查找:效率极为底下,但是算法简单,适用于小型数据查找: 折半查找:又称为二分查找,它是从查找表的中 ...

  7. 【C/C++】查找(一):静态查找表

    {静态查找表 + 动态查找表} 所谓动态,就是,找的时候没有则添加,或者能删除 关键字:primary key:用来表示查找表中的一条记录 {主关键字 + 次关键字} 主关键字是唯一的,用来唯一的标识 ...

  8. 查找->静态查找表->折半查找(有序表)

    文字描述 以有序表表示静态查找表时,可用折半查找算法查找指定元素. 折半查找过程是以处于区间中间位置记录的关键字和给定值比较,若相等,则查找成功,若不等,则缩小范围,直至新的区间中间位置记录的关键字等 ...

  9. cb16a_c++_顺序容器的选用_排序_二分查找

    /*cb16a_c++_顺序容器的选用_排序_二分查找顺序容器: 1.vector的优点与缺点 vector优点:排序利用下标,快速排序,做二分查找非常快 2.list的优点与缺点 list优点:插入 ...

随机推荐

  1. Python高手之路【六】python基础之字符串格式化

    Python的字符串格式化有两种方式: 百分号方式.format方式 百分号的方式相对来说比较老,而format方式则是比较先进的方式,企图替换古老的方式,目前两者并存.[PEP-3101] This ...

  2. 线性判别分析LDA原理总结

    在主成分分析(PCA)原理总结中,我们对降维算法PCA做了总结.这里我们就对另外一种经典的降维方法线性判别分析(Linear Discriminant Analysis, 以下简称LDA)做一个总结. ...

  3. MVVM模式解析和在WPF中的实现(三)命令绑定

    MVVM模式解析和在WPF中的实现(三) 命令绑定 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二)数据绑定 MVVM模式解析和在WPF中 ...

  4. ABP文档 - SignalR 集成

    文档目录 本节内容: 简介 安装 服务端 客户端 连接确立 内置功能 通知 在线客户端 帕斯卡 vs 骆峰式 你的SignalR代码 简介 使用Abp.Web.SignalR nuget包,使基于应用 ...

  5. Linux虚拟机的安装(使用Centos6.3)

    1.什么是虚拟机? 虚拟机指通过软件模拟的具有完整硬件系统功能的.运行在一个完全隔离环境中的完整计算机系统 2.安装Linux虚拟机前要做的准备 2.1:一台windows环境的pc 2.2:下载VM ...

  6. .net 分布式架构之配置中心

    开源QQ群: .net 开源基础服务  238543768 开源地址: http://git.oschina.net/chejiangyi/Dyd.BaseService.ConfigManager ...

  7. spring注解源码分析--how does autowired works?

    1. 背景 注解可以减少代码的开发量,spring提供了丰富的注解功能.我们可能会被问到,spring的注解到底是什么触发的呢?今天以spring最常使用的一个注解autowired来跟踪代码,进行d ...

  8. 来吧,HTML5之基础标签(下)

    <dialog> 标签 定义对话框或窗口. <dialog> 标签是 HTML 5 的新标签.目前只有 Chrome 和 Safari 6 支持 <dialog>  ...

  9. Java消息队列--JMS概述

    1.什么是JMS JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送 ...

  10. HIVE教程

    完整PDF下载:<HIVE简明教程> 前言 Hive是对于数据仓库进行管理和分析的工具.但是不要被“数据仓库”这个词所吓倒,数据仓库是很复杂的东西,但是如果你会SQL,就会发现Hive是那 ...