二分搜索
折半搜索,也稱二分查找算法、二分搜索,是一種在有序數組中查找某一特定元素的搜索算法。搜素過程從數組的中間元素開始,如果中間元素正好是要查找的元素,則搜素過程結束;如果某一特定元素大於或者小於中間元素,則在數組大於或小於中間元素的那一半中查找,而且跟開始一樣從中間元素開始比較。如果在某一步驟數組爲空,則代表找不到。這種搜索算法每一次比較都使搜索範圍縮小一半。
時間複雜度:二分搜索每次把搜索區域減少一半,很明顯時間複雜度爲O(logN)。
空間複雜度:O(1),雖以遞歸形式定義,但是尾遞歸,可改寫爲循環。
二分搜索的基本實現
二分查找法在算法家族大類中屬於“分治法”,分治法基本都可以用遞歸來實現的,二分查找法的遞歸實現如下:
int binary_search(int array[], int low, int high, int target)
{
if (low > high) return -1;
int mid = (low + high)/2;
if (array[mid]> target)
return binary_search(array, low, mid -1, target);
if (array[mid]< target)
return binary_search(array, mid+1, high, target);
return mid;
}
非遞歸實現:
int binary_search(int array[], int low, int high, int target)
{
while(low <= high)
{
int mid = (low + high)/2;
if (array[mid] > target)
high = mid - 1;
else if (array[mid] < target)
low = mid + 1;
else //find the target
return mid;
}
//the array does not contain the target
return -1;
}
在輪轉後的有序數組上應用二分查找法
二分法是要應用在有序的數組上,如果是無序的,那麼比較和二分就沒有意義了。不過還有一種特殊的數組上也同樣可以應用,那就是“輪轉後的有序數組(Rotated Sorted Array)”。它是有序數組,取期中某一個數爲軸,將其之前的所有數都輪轉到數組的末尾所得。比如{7, 11, 13, 17, 2, 3, 5}就是一個輪轉後的有序數組。非嚴格意義上講,有序數組也屬於輪轉後的有序數組——取首元素作爲軸進行輪轉。
下邊就是二分查找法在輪轉後的有序數組上的實現(假設數組中不存在相同的元素)
int SearchInRotatedSortedArray(int array[], int low, int high, int target)
{
while(low <= high)
{
int mid = (low + high) / 2;
if (target < array[mid])
if (array[mid] < array[high])//the higher part is sorted
high = mid - 1; //the target would only be in lower part
else //the lower part is sorted
if(target < array[low])//the target is less than all elements in low part
low = mid + 1;
else
high = mid - 1;
else if(array[mid] < target)
if (array[low] < array[mid]) // the lower part is sorted
low = mid + 1; //the target would only be in higher part
else //the higher part is sorted
if (array[high] < target) //the target is larger than all elements in higher part
high = mid - 1;
else
low = mid + 1;
else //if(array[mid] == target)
return mid;
}
return -1;
}
對比普通的二分查找法,爲了確定目標數會落在二分後的那個部分,需要更多的判定條件。但還是實現了O(log n)的目標。
找到輪轉後的有序數組中第K小的數
對於普通的有序數組來說,這個問題是非常簡單的,因爲數組中的第K-1個數(即A[K-1])就是所要找的數,時間複雜度是O(1)常量。但是對於輪轉後的有序數組,在不知道輪轉的偏移位置,我們就沒有辦法快速定位第K個數了。
不過我們還是可以通過二分查找法,在log(n)的時間內找到最小數的在數組中的位置,然後通過偏移來快速定位任意第K個數。當然此處還是假設數組中沒有相同的數,原排列順序是遞增排列。
在輪轉後的有序數組中查找最小數的算法如下:
//return the index of the min value in the Rotated Sorted Array, whose range is [low, high]
int findIndexOfMinVaule(int A[], int low, int high)
{
if (low > high) return -1;
while (low < high)
{
int mid = (low + high)/2;
if (A[mid] > A[high])
low = mid +1;
else
high = mid;
}
//at this point, low is equal to high
return low;
}
接着基於此結果進行偏移,再基於數組長度對偏移後的值取模,就可以找到第K個數在數組中的位置了:
//return the index of the kth element in the Rotated Sorted Array
int findKthElement(int A[], int m, int k)
{
if (k > m) return -1;
int base = findIndexOfMinVaule(A, 0, m-1);
int index = (base+k-1) % m;
return index;
}
題目:有一類數組,例如數組[1,2,3,4,6,8,9,4,8,11,18,19,100] 前半部分是是一個遞增數組,後面一個還是遞增數組,但整個數組不是遞增數組,那麼怎麼最快的找出其中一個數?
分析:此題數組不是嚴格遞增的數據,因爲有重複的元素。對數組的前半部分和後半部分分別進行二分查找。
#include <iostream>
using namespace std;
//二分查找
int binary_search(int* a, int low, int high, int goal)
{
while(low <= high)
{
int middle = low + ((high-low)>>1);
if(a[middle] == goal)
return middle;
else if(a[middle] < goal)
low = middle + 1;
else
high = middle - 1;
}
return -1;
}
void getNum(int *a, int len, int goal)
{
int i, index;
for(i = 0; i < len-1; i++)
{
if(a[i] > a[i+1]) //找到前、後兩個數組的分界點
break;
}
if(a[i] >= goal) //對前面數組進行二分查找
{
index = binary_search(a, 0, i, goal);
printf("%d\n",index);
}
if(a[i+1] <= goal) //對後面數組進行二分查找
{
index = binary_search(a, i+1, len-1, goal);
printf("%d\n",index);
}
}
int main(void)
{
int a[]={1,2,3,4,6,8,9,4,8,11,18,19,100};
int len = 13, goal = 8;
getNum(a,len,goal);
return 0;
}
整數的求平方根函數
這個其實也是畢竟常見的面試問題,要求不調用math庫,實現對整數的sqrt方法,返回值只需要是整數。
其實這個問題用數學的表達方式就是:對於非負整數x,找出另一個非負整數n,其中n滿足 n^2 <= x < (n+1)2。
所以最直接的方法就是從0到x遍歷過去直到找到滿足上述條件的n。這個算法的複雜度自然是O(n)。
仔細想想,其實要找的數是在0和x之間,而他正巧可以視爲一個有序的數組。似乎有可以運用二分查找法的可能。再回想二分查找法是要找到滿足“與目標數相等”這一條件的數,而這裏同樣也是要找滿足一定條件的數。所以就可以用二分法來解這個問題了,讓複雜度降爲O(logn)。
爲方便起見,假設傳入的參數是非負的整數,因此使用unsigned int。
unsigned int sqrt(unsigned int x)
{
//no value should larger than max*max, otherwise it would be overflow
unsigned int max = (1 << (sizeof(x)/2*8))-1; //65535
if (max*max < x) return max;
unsigned int low = 0;
unsigned int high = max-1;
unsigned int mid = 0;
while (1)
{
mid = (low + high)/2;
if (x < mid * mid)
high = mid-1;
else if((mid+1)*(mid+1) <= x)
low = mid+1;
else //if(mid * mid <= x && x < (mid+1)*(mid+1))
break;
}
return mid;
}
參考:
Find the k-th Smallest Element in the Union of Two Sorted Arrays – LeetCode
http://www.leetcode.com/2011/01/find-k-th-smallest-element-in-union-of.html
Median of Two Sorted Arrays –LeetCode
http://www.leetcode.com/2011/03/median-of-two-sorted-arrays.html