数据结构与算法之美 - 二分查找与跳表

185 阅读4分钟

二分查找

核心思想

二分查找针对的是一个有序的数据集合,查找思想有点类似分治思想。每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为 0。

O(logn)惊人的查找速度

O(logn)这是一种极其高效的时间复杂度,有的时候甚至比时间复杂度是常量级 O(1) 的算法还要高效。为什么这么说呢?

因为 logn 是一个非常“恐怖”的数量级,即便 n 非常非常大,对应的 logn 也很小。比如 n 等于 2 的 32 次方,这个数很大了吧?大约是 42 亿。也就是说,如果我们在 42 亿个数据中用二分查找一个数据,最多需要比较 32 次。

用大 O 标记法表示时间复杂度的时候,会省略掉常数、系数和低阶。对于常量级时间复杂度的算法来说,O(1) 有可能表示的是一个非常大的常量值,比如 O(1000)、O(10000)。所以,常量级时间复杂度的算法有时候可能还没有 O(logn) 的算法执行效率高。

简单实现

最简单的情况就是有序数组中不存在重复元素,我们在其中用二分查找值等于给定值的数据。

def binary_search(list, item):
	low = 0
	high = len(list) - 1
	while low <= high:
		mid = int((low + high) / 2)
		guess = list[mid]
		if guess == item:
			return mid
		if guess > item:
			high = mid - 1
		else:
			low = mid + 1
	return None

应用场景的局限性

  • 二分查找依赖的是顺序表结构,简单点说就是数组。二分查找算法需要按照下标随机访问元素。
  • 二分查找针对的是有序数据。
  • 二分查找只能用在插入、删除操作不频繁,一次排序多次查找的场景中。更适合处理静态数据。
  • 数据量太小不适合二分查找(没必要,顺序遍历就足够了)
  • 数据量太大也不适合二分查找(需要太大的连续内存)

二分查找变形问题

  • 查找第一个值等于给定值的元素

    public int bsearch(int[] a, int n, int value) {
    	int low = 0;
    	int high = n - 1;
    	while (low <= high) {
    		int mid= low + ((high - low) >> 1);
    		if (a[mid] > value) {
    			high = mid - 1;
    		} else if (a[mid] < value) {
    			low = mid + 1;
    		} else {
    			if ((mid == 0) || (a[mid - 1] != value)) return mid; 
    			else high = mid - 1;
    		}
    	}
    	return -1;
    }
    

    如果 mid 等于 0,那这个元素已经是数组的第一个元素,那它肯定是我们要找的;如果 mid 不等于 0,但 a[mid] 的前一个元素 a[mid-1] 不等于 value,那也说明 a[mid] 就是我们要找的第一个值等于给定值的元素。

    如果经过检查之后发现 a[mid] 前面的一个元素 a[mid-1] 也等于 value,那说明此时的 a[mid] 肯定不是我们要查找的第一个值等于给定值的元素。那我们就更新 high=mid-1,因为要找的元素肯定出现在 [low, mid-1] 之间。

  • 查找最后一个值等于给定值的元素

  • 查找第一个大于等于给定值的元素

  • 查找最后一个小于等于给定值的元素

跳表

  • 跳表实质就是一种可以进行二分查找的有序链表

  • 跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。

  • 跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

  • 跳表的时间复杂度为O(logn),空间复杂度为O(n)。

    我们其实并不需要担心跳表会浪费内存,因为在实际的软件开发中,原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值和几个指针,并不需要存储对象,所以当对象比索引结点大很多时,那索引占用的额外空间就可以忽略了。

  • 跳表支持高效的动态插入和删除,时间复杂度也是O(logn)

  • 跳表索引动态更新:随机插入

    当我们不停地往跳表中插入数据时,如果我们不更新索引,就有可能出现某 2 个索引结点之间数据非常多的情况。极端情况下,跳表还会退化成单链表。