二分搜索及其擴展

二分搜索

折半搜索,也稱二分查找算法、二分搜索,是一種在有序數組中查找某一特定元素的搜索算法。搜素過程從數組的中間元素開始,如果中間元素正好是要查找的元素,則搜素過程結束;如果某一特定元素大於或者小於中間元素,則在數組大於或小於中間元素的那一半中查找,而且跟開始一樣從中間元素開始比較。如果在某一步驟數組爲空,則代表找不到。這種搜索算法每一次比較都使搜索範圍縮小一半。

時間複雜度:二分搜索每次把搜索區域減少一半,很明顯時間複雜度爲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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章