查找1-查找算法知识图谱

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013904227/article/details/90140188

本文算是查找算法的序章,主要对接下来会涉及到的查找算法类型做一个铺叙,不涉及到具体的算法实现。查找算法算是数据结构与算法并重的一类知识点了,像之前的排序,其实大多数时候只涉及到简单的顺序线性表结构,但是查找可能会涉及到二叉树、哈希表等更稍微复杂一点的数据结构。

还是按照我之前自己订阅的「数据结构与算法之美」专栏的顺序来写,这算是我个人的一个学习路程总结,还是想说下,这个专栏的特点是把一些基础数据结构与算法给你串起来,并且涉及到一些实际的应用场景和思考方式,个人觉得还是挺值的哈。

二分查找

基础二分查找的对象主要是数组,通常有以下几个需求:

  1. 查找第一个与给定值相等的值的位置。
  2. 查找第一个大于给定值的位置。
  3. 查找最后一个等于给定值的位置。
  4. 查找最后一个小于给定值的位置。

可能会好奇为什么没有查找第一个小于给定值的位置,因为上面默认原始数组数据都是从小到大有序排列好了的,这样的话第一个小于就没有意义,很明显,要不就是第 0 项,要不就是没有。如果数组数据是从大到小排列的,那么上面第 2、3、4 项就可以换一种说法了。

如果是数组数据不重复,并且只要查找与给定值相等的元素的位置,那么这个就很好办了,属于最简单的一类二分查找算法。

之前排序里面的「插入排序」其实就能满足这个需求,不过那个二分查找也是改动过的,它是查找最后一个等于给定值的元素的位置,其实跟上面说的最简单二分查找算法是差不多的,因为数组项元素限定了不重复嘛。

二分查找从名字上来看,很容易知道它的查找算法复杂度基本就是 O(logN) 了,因为二分嘛。当一个数据序列已经是有序的话,那它们的查找就会非常快速。也就是这个前提,二分查找的数据如果是变化的,那就不能非常单纯的用一个线性数组来组织了。

数据的变化包含基本的:增加、删除。外加一个查找的操作,对于数组来讲,它的插入操作与删除操作复杂度都不低,如果是数组这种线性表类型的结构的话,可以参见之前的插入排序算法,重点是删除,数组的删除操作相对于链表来讲还是挺麻烦的。

那么线性链表呢,它的删除操作非常快,插入操作也非常快,但是查找就很慢了,因为对于基础线性表结构来讲,你只能顺序遍历,这个就无法达到二分查找的效率。所以就有了跳表。

跳表

跳表是由链表作为基础结构的,它在基本线性链表的基础上又增加了几层链表索引结构,图形化的结构网上就有很多了,我这里就不画了,后续单篇的时候再去介绍。

跳表使用链表的形式组织,前面知道,如果是基本线性链表的话,它的查找操作是比较耗时的,那么跳表就解决了这个问题,它通过建立多级索引的形式来加快查找的速度。但是多级索引又会带来另外的问题。

插入的时候如果达到了索引的临界值,那就需要增加一节、一级或者是多级索引,这个比基本线性链表更耗时、更麻烦一点。删除的时候如果也达到了临界值,那就可能需要删除多级索引,也会更加耗时一点。再者多级索引增加了存储空间,这又会带来存储空间的消耗,也就是常说的空间换时间。

纵然有以上那么多的新引入问题,但是为了查找的速度,这些都是可以忍受的,因为大多数情况下,这种场景都是查找的多,插入删除的少。况且插入、删除的操作复杂度是可控的,时间消耗其实并没有增加非常多,唯一增加比较多的就是存储空间了。

跳表跟红黑树是一个应用场景稍稍比较接近的两个数据结构,但是跳表有一个,那就是它支持按照区间进行查找,这个是一个独特的优势,当然红黑树也不是不可以,但是它的这方面效率要比跳表低。

散列表

散列表的思想就是「对号入座」,比如你有一个 0~100 的数,分别分布在数组项第 0~100 项,那么你找起来就非常方便呐,直接索引对应数值的下标即可。散列表的基本思想就是这样子,不过肯定不会跟这个例子一样这么简单。

散列表涉及到的几个概念:键;散列函数;装载因子;散列冲突。

键就是每个元素的标识符,比如对于按照年龄排序、查找的数据集来讲,它的键(key)就可以是年龄,基本就是这个意思。

散列函数就是根据键来求散列值的一个工具,比如我们要根据某一个人的身份证号来找到某个人,但是身份证号 11 位,数值显得太大了,那么我们就可以把 11 位身份证号每一个位转化为字符,然后相加,最后对某个数求一个模,这样就是一个简单的散列函数了。

装载因子就是已经使用的数组项占整个散列表的比例,装载因子越大,造成散列冲突的概率就会越高。通常散列表要维持装载因子在某一个固定的值,比如 0.75,这个值与泊松分布有关系,就不细说了。超过这个值之后,散列表最好就需要进行动态扩容了,以此来降低装载因子。

散列函数并不能完美的保证所有的键值经过散列之后都能够全局唯一,所以必然会有一些键值经过散列之后得到同样的散列值,这个时候就需要通过「开放寻址法」与「链表法」来解决散列冲突,这两个放在后续单篇里面介绍。通常情况下来讲,更常用链表法来解决散列冲突。

二叉树

理想情况下,二叉树的查找效率接近或者等于二分查找,但是二叉树是通过链表形式来组织的,二叉树的动态性能比数组的二分查找要好很多。

之前接触到的堆排序,其中就有一点二叉树的影子,堆是一种完全二叉树。二叉树要想达到比较好的查找性能,就要要求其尽量平衡,满二叉树就是一种非常平衡的二叉树,理想的平衡二叉树的查找效率就可以等于二分查找。

二叉树涉及到访问、插入、删除操作。访问有前序遍历(根-左-右)、中序遍历(左-根-右)、后续遍历(左-右-根)三种。作为一种动态数据结构,二叉树在实际应用的时候,插入与删除操作是一定会破坏整个树的平衡性的,这就需要通过某些手段来维持其平衡性。

所以就有了诸多的变体二叉树,比如 AVL 树、红黑树等等,它们的目的只有一个,那就是在支持动态数据增减的同时维持树的整体平衡性,避免平衡失效导致查找效率的退化。

树可以支持按照一定的顺序来遍历输出元素,而散列表就不太行,散列表里面存的都是无序的数据,很难按照一定的顺序来遍历输出,除非事先进行排序。

End

其实还有一些其它的算法,但是我没有归到这一类里面,其实这个归类也是我自己瞎搞的,觉得这样归类也还算合理,就先这样弄了。接下来就会基本上按照上面的顺序来对这一类算法进行学习、介绍。


想做的事情就去做吧

猜你喜欢

转载自blog.csdn.net/u013904227/article/details/90140188