1. 程式人生 > >Binary Search(一)

Binary Search(一)

提起二分查詢,許多人都會想:這是一個簡單問題,我只用一個while或者遞迴就能解決,三行程式碼最多。再把上下限背過,就沒有什麼能難得住的二分查詢問題了。但事實究竟是這樣嗎? 我們一起從頭看一遍二分查詢,其中穿插各個OJ的二分查詢問題,同時時不時的我們會加入演算法導論中對於這個問題的引申。 ##什麼是二分查詢? 二進位制搜尋是電腦科學中最基本和最有用的演算法之一。 它描述了在有序集合中搜索特定值的過程。我們需要對二進位制搜尋中的名詞賦予一個簡單的定義:

target - 要搜尋的值 index - 搜尋的當前位置 left ,right - 我們用來維持搜尋空間的指標 mid- 用來應用條件來確定我們是應該向左還是向右搜尋的索引 ##二分查詢的過程   在二分查詢的基本形式中,二分搜尋在具有指定左右索引的連續序列(搜尋空間)上執行。維護搜尋空間的左,右和中間標記,並比較搜尋目標; 如果條件不滿足或者值不相等,則消除目標不可能存在的一半(有序序列),並繼續搜尋剩下的一半,直到成功為止。 如果搜尋以空的數字集合部分結束,則無法滿足條件並且未找到目標。 給定n個元素的排序(按升序排列)整數陣列nums和目標值,編寫一個函式來搜尋nums中的目標。 如果target存在,則返回其索引,否則返回-1

Leetcode Binary Search 此程式碼只有 34.4% 為AC,甚至低於許多高階演算法!

 class Solution {
 public:
 int search(vector<int>& nums, int target) {
     if(nums.size() == 0)return -1;
        int left =0;
        int right = nums.size()-1;
        while(left<=right){
           int mid = (left+right) >>1
; if(nums[mid]<target)left = mid+1; else if(nums[mid]>target)right =mid-1; else return mid; }return -1; } };

演算法的應用

由於二進位制搜尋是一種演算法,在每次比較後將搜尋空間劃分為2。 所以每次需要搜尋集合中的索引或元素時,都應考慮二進位制搜尋。 如果集合是無序的,我們可以在應用二進位制搜尋之前對其進行排序1

總的來說,我們可以將此部分分為:

Created with Raphaël 2.2.0
開始若集合未排序則排序在兩部分搜尋空間中確定其一在搜尋空間中查詢target結束

Markdown畫圖,冒號後面必須空一格,不然報錯。我的天吶。對萌新一點都不友好。

我們總希望能夠研究的二分查詢問題越多越好,雖然我們擁有的思路相同,但每次我們檢視不同大佬程式碼時,它的實現似乎都略有不同。 儘管每個實現在每個步驟中將問題空間劃分為1/2,但其中一個有許多問題:

  1. 為什麼它的實現略有不同?
  2. 大佬在想什麼?
  3. 哪種方式更容易?
  4. 哪種方式更好?

###經過多次失敗的嘗試,和資料的閱讀查詢,附上幾個二分查詢的主要模板,並且附上幾個相似的例子。

1、同上面的基本演算法相同的寫法,我們的譚浩強、嚴蔚敏老師就是這麼教我們的

int binarySearch(vector<int>& nums, int target){
  if(nums.size() == 0)
    return -1;
	int left = 0;
	int right = nums.size() - 1;
  while(left <= right){
    int mid = left + (right - left) / 2;
    if(nums[mid] == target){ return mid; }
    else if(nums[mid] < target) { left = mid + 1; }
    else { right = mid - 1; }
  }
  return -1;
}
  1. 初始條件: left = 0, right = length-1(陣列終點下標)
  2. 終止條件: left > right
  3. 查詢左搜尋區間:right = mid -1
  4. 查詢右搜尋區間:left = mid+1

Ex1:計算並返回x的平方根,其中x保證為非負整數。

由於返回型別是整數,因此將截斷十進位制數字,並僅返回結果的整數部分。

迴圈體中的判斷條件 if(x/mid >= mid) 若為乘法if(mid*mid <=x)時會**TLE**,感興趣的話可以查閱相關資料。

class Solution {
public:
    int mySqrt(int x) {
        if (x <2)return x;
        int left = 0;
        int right = x ;
        while(left< right){
            int mid = (right + left) /2;
            if (x/mid >= mid) left = mid+1;
            else right = mid;
        }return right-1;
        
    }           
};

當然我寫完了我的程式碼後也會出來看神仙:

class Solution {
public:
    int mySqrt(int x) {
    long r = x;
    while (r*r > x)
        r = (r + x/r) / 2;
    return r;

嘖嘖嘖。由於此思路與本文主體無關,但既然看到了神仙,我們剛還在說要揣摩大佬的思路,所以特放在附錄裡2

值得注意的是,上述方法當輸入為 但是一個題目的辦法多種多樣,除了上面的兩個,比如還有Shifting nth root algorithm3

class Solution:
    def mySqrt(self, x):
        res = 0
        bit = 1 << 30
         while bit > x:
            bit >>= 2
                while bit != 0:
            if x >= res + bit:
                x -= res + bit
                res += bit << 1               
            res >>= 1
            bit >>= 2
        return res

言歸正傳,我們能夠看到二分查詢用途廣且方便計算。現在我們來分析一下二分查詢的時間複雜度(討論非遞迴情況,遞迴方式見4)。   (原諒我遞迴方式 C語言版原諒我在wiki上直接粘了一個,就是這個↓)

int binary_search(const int arr[], int start, int end, int khey) {
	if (start > end)
		return -1;
	int mid = start + (end - start) / 2;    //直接平均可能會溢位,所以用此演算法
	if (arr[mid] > khey)
		return binary_search(arr, start, mid - 1, khey);
	else if (arr[mid] < khey)
		return binary_search(arr, mid + 1, end, khey);
	else
	    return mid;        //最後檢測相等是因為多數搜尋狀況不是大於要不就小於
}

易得 T(n)=(n/2)+1T(n) =(n/2) +1 由主方法知 nlog21=1n^{log_21}=1,由於h(n)=Θ(nlog21)=Θ(1)h(n)=\Theta(n^{log_21}) = \Theta(1) 考慮第二種情況:T(n)=Θ(nlogbalgn)T(n)=\Theta(n^{log_ba}lgn),則T(n)=Θ(log2n)T(n) = \Theta(log_2n) 5

Ex2:Guess Number Higher or Lower

我們正在玩猜數字遊戲。 遊戲如下: 我從1到n中選擇一個數字。 你猜測我選擇了哪個號碼。每次你猜錯了,我都會告訴你這個數字是高還是低。可呼叫預定義的API guess(int num),它返回3個可能的結果(-1,1或0)

class Solution {
public:
    int guessNumber(int n) {
        int left = 0;
        int right = n;
        if( n<2) return n;
        while(left<=right){
            int mid = left+(right-left)/2;//否則overflow會導致超時,其他需轉換。API傳入值為int。
            if (guess(mid) == 0){
                return mid;
           }else if (guess(mid)==-1){
                right = mid-1;
            }
            else  left = mid +1;
        }return -1;
    }
};

沒有工作量,多調一個API而已。

  1. 有關排序的演算法見(暫空)。 ↩︎

  2. 此為牛頓法: 設rrf(x)=0f(x) = 0的根,選取x0x_0作為rr初始近似值,過點(x0,f(x0))(x_0,f(x_0))做曲線y=f(x)y = f(x)的切線LLLL的方程為y=f(x0)+f(x0)(xx0)y = f(x_0)+f&#x27;(x_0)(x-x_0),求出LLxx軸交點的橫座標 x1=x0f(x0)f(x0)x_1 = \frac{x_0-f(x_0)}{f&#x27;(x_0)}\qquad,稱x1x_1rr的一次近似值。 過點(x1,f(x1))(x1,f(x1))做曲線y=f(x)y = f(x)的切線,並求該切線與x軸交點的橫座標 x2=x1f(x1)f(x1)x_2 = \frac{x_1-f(x1)}{f&#x27;(x1)}\qquad,稱x2x_2rr的二次近似值。重複以上過程,得rr的近似值序列,其中x(n+1)=x(n)f(x(n))f(x(n))x(n+1)=x(n)-\frac{f(x(n))}{f&#x27;(x(n))}\qquad, 稱為rrn+1n+1次近似值,上式稱為牛頓迭代公式。

    ↩︎
  3. Shifting nth root algorithm: 除了en_wiki,好像沒有太細緻的中文講解,見我的另一篇blog吧。

    ↩︎
  4. 複雜度分析見. ↩︎

  5. 有關於如何求演算法的ΩΘOωo\Omega,\Theta,O,\omega,o請參考另一篇blog。 ↩︎