二分查找
二分查找针对的是一个有序的数据集合,查找思想有点类似分治思想。 每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为0。
时间复杂度
二分查找的时间复杂度是O(logn),这是一种极其高效的时间复杂度,有的时候甚至比时间复杂度是常量级O(1)的算法还要高效。
因为logn是一个非常“恐怖”的数量级,即便n非常非常大,对应的logn也很小。比如n等于2的32次方,大约是42亿。也就是说,如果我们在42亿个数据中用二分查找一个数据,最多需要比较32次。
用大O标记法表示时间复杂度的时候,会省略掉常数、系数和低阶。对于常量级时间复杂度的算法来说,O(1)有可能表示的是一个非常大的常量值,比如O(1000)、O(10000)。所以,常量级时间复杂度的算法有时候可能还没有O(logn)的算法执行效率高。
局限性
依赖数组
二分查找依赖的是顺序表结构,简单点说就是数组。主要原因是二分查找算法需要按照下标随机访问元素。数组按照下标随机访问数据的时间复杂度是O(1),而链表随机访问的时间复杂度是O(n)。所以,如果数据使用链表存储,二分查找的时间复杂就会变得很高。
由此可能会引发第二个问题,数据量过大也不适合二分查找。数组需要内存空间的连续,对内存的要求比较苛刻。
有序数据
数据必须是有序的。如果数据没有序,我们需要先排序。
递归实现示例
1 | public static int bSearchInternally(int[] nums, int left, int right, int value) { |
循环实现示例
1 | public static int bSearch(int[] nums, int value) { |
跳表
跳表使用空间换时间的设计思路,通过构建多级索引来提高查询的效率,实现了基于链表的“二分查找”。跳表是一种动态数据结构,支持快速的插入、删除、查找操作,时间复杂度都是O(logn)。
跳表的空间复杂度是O(n)。不过,跳表的实现非常灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗。虽然跳表的代码实现并不简单,但是作为一种动态数据结构,比起红黑树来说,实现要简单多了。所以很多时候,我们为了代码的简单、易读,比起红黑树,我们更倾向用跳表。
为什么Redis会使用跳表实现有序集合而不是红黑树
Redis中的有序集合是通过跳表来实现的,严格点讲,其实还用到了散列表。我们现在暂且忽略这部分。Redis的开发手册中的有序集合支持的核心操作主要有下面这几个:
- 插入一个数据;
- 删除一个数据;
- 查找一个数据;
- 按照区间查找数据(比如查找值在[100, 356]之间的数据);
- 迭代输出有序序列。
其中,插入、删除、查找以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度跟跳表是一样的。
- 按照区间来查找数据这个操作,红黑树的效率没有跳表高。对于按照区间查找数据这个操作,跳表可以做到O(logn)的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了。这样做非常高效。
- 跳表更容易代码实现。虽然跳表的实现也不简单,但比起红黑树来说还是好懂、好写多了,而简单就意味着可读性好,不容易出错。
- 跳表更加灵活,它可以通过改变索引构建策略,有效平衡执行效率和内存消耗。