算法——查找之二分查找

对于查找,我们最容易想到的就是遍历了,但是当数组很大的时候,遍历查找的开销是很大的,时间复杂度是O(n)。

而二分查找的开销就小了很多,时间复杂度是O(logn)。但是它是有前提条件的,数组必须是有序的。

我们知道排序的时间复杂度是O(nlogn)。乍一看下去,似乎排序+二分查找的时间复杂度比顺序查找要大。但是我们排序是一次的,查找是多次的。

例如查找n次的话

顺序查找需要花费开销:O(n^2)。

二分查找需要花费开销:排序O(nlogn) + n次查找O(nlogn) = O(nlogn)。

这样一看就知道二分查找的优势了,极大的减小了查找所需要的开销。

原理:二分查找也成为折半查找,需要数组有序。将查找目标target和中间结点元素mid进行比较,mid元素将数组分成两个部分。因为数组是有序的。假如target>mid,说明查找的目标应该在右半边数组,反之target<mid,则说明应该在左半边查找目标。这样递归进行,直至target被找到为止。

原理通俗易懂。

递归实现如下:

public static int recurSearch(Comparable[] orderArray, int low, int high, Comparable target) {
	if (low > high) return -1;
	int mid = (low + high) / 2;
	int compared = orderArray[mid].compareTo(target);
	if (compared > 0) { // 中值比目标大
		return recurSearch(orderArray, low, mid - 1, target);
	} else if (compared < 0) { // 中值比目标小
		return recurSearch(orderArray, mid + 1, high, target);
	} else {
		return mid;
	}
}

如果找不到元素,则返回-1。

当然,我们知道,递归总是没有迭代这么快。我们将这个尾递归转化成迭代算法。

迭代实现如下:

public static int iteratorSearch(Comparable[] orderArray, int low, int high, Comparable target) {
	while (low <= high) {
		int mid = (low + high) / 2;
		int compared = orderArray[mid].compareTo(target);
		if (compared > 0) {
			high = mid - 1;
		} else if (compared < 0) {
			low = mid + 1;
		} else {
			return mid;
		}
	}
	return -1;
}

这两个实现,在特定场合会出现点问题,这个主要看需求。

有些用例下,我们需要找到第一次出现的那个元素的位置,而二分查找找到的元素的位置可能并不是第一次出现的。

在这种用例下,我们进行一点小小的修改就可以达到目标:我们只需要在返回结果之前,往前遍历一次,找到和这个元素相等的最前面的那个元素就可以了。

最后来比较一下顺序查找,递归二分查找,迭代二分查找的效率:

他们都进行n次查找。

public static void main(String[] args) {
	final int NUM = 100000;
	Integer[] a1 = new Integer[NUM];
	Integer[] a2 = new Integer[NUM];
	Integer[] a3 = new Integer[NUM];
	Integer[] a4 = new Integer[NUM];
	a1[0] = a2[0] = a3[0] = a4[0] = -1;
	for (int i = 1; i < NUM; i++) {
		a1[i] = (int) (Math.random() * NUM);
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
	}
	
	long startTime;
	long endTime;
	
	startTime = System.currentTimeMillis(); // 获取开始时间
	for (int i = 0; i < NUM; i++) {
		int position = OrderSearch.search(a1, 0, a1.length - 1, a1[i]);
		if (!a1[position].equals(a1[i])) {
			System.exit(-1);
		}
	}
	endTime = System.currentTimeMillis();
	System.out.println("顺序查找cost: " + (endTime - startTime) + " ms");
	
	startTime = System.currentTimeMillis(); // 获取开始时间
	QuickSort.sort2(a1);
	assert isSorted(a1);
	for (int i = 0; i < NUM; i++) {
		int position = BinarySearch.recurSearch(a1, 0, a1.length - 1, a1[i]);
		if (!a1[position].equals(a1[i])) {
			System.exit(-1);
		}
	}
	endTime = System.currentTimeMillis();
	System.out.println("二分递归查找cost: " + (endTime - startTime) + " ms");
	
	startTime = System.currentTimeMillis(); // 获取开始时间
	QuickSort.sort2(a2);
	assert isSorted(a2);
	for (int i = 0; i < NUM; i++) {
		int position = BinarySearch.iteratorSearch(a2, 0, a2.length - 1, a2[i]);
		if (!a2[position].equals(a2[i])) {
			System.exit(-1);
		}
	}
	endTime = System.currentTimeMillis();
	System.out.println("二分迭代查找cost: " + (endTime - startTime) + " ms");
}

结果如下:

顺序查找cost: 3078 ms
二分递归查找cost: 99 ms
二分迭代查找cost: 43 ms

可以发现,迭代比递归查找快了不少。二分查找和顺序查找差距非常明显。看得出来,二分查找对于查找来说是一个巨大的进步。

点赞