对于查找,我们最容易想到的就是遍历了,但是当数组很大的时候,遍历查找的开销是很大的,时间复杂度是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
可以发现,迭代比递归查找快了不少。二分查找和顺序查找差距非常明显。看得出来,二分查找对于查找来说是一个巨大的进步。