九章算法系列(#2 Binary Search)-課堂筆記

前言面試

先說一些題外的東西吧。受到春躍大神的影響和啓發,推薦了這個算法公開課給我,晚上睡覺前點開一看發現課還有兩天要開始,本着要好好系統地學習一下算法,因而就爬起來拉上兩個小夥伴組團報名了。今天聽了第一節課,說真的很實用,特別是對於我這種算法不紮實,而且又想找工做,提升本身的狀況。 那就很少說廢話了,之後每週都寫個總結吧,就趁着這一個月好好把算法提升一下。具體就從:課堂筆記、leetcode和lintcode相關習題、hdu和poj相關習題三個方面來寫吧。但願本身可以堅持下來,給你們分享一些好的東西。算法

 

 outline:數組

  • 第一境界:會寫程序
    • Find First Position of Target
    • Find Last Position of Target
  • 第二境界:找到第一個/最後一個知足某個條件的位置/值
    • Search a 2D Matrix
    • Find Minimum in Rotated Sorted Array
  • 第三境界:保留有解的那一半
    • Find Peak Element

 

 

課堂筆記學習

二分查找這類題之前接觸的也算是比較多的了,因此還算相對熟悉,但今天聽老師講過之後,仍是以爲有了不少新的認識,最有印象的就是令狐老師講的三個境界:優化

 


 

1. 第一境界:會寫程序spa

這個境界我自認爲在刷了那麼多leetcode以後算是沒有問題的了,套了很多模版,雖然還有一些邊界問題考慮不周全,可是通過調試,應該沒有什麼問題,通過幾回面試,也面到過Binary Seach。想必你們也有很好的基礎。 正如老師說的,這個境界仍是存在一些問題,好比解決二分程序的三大痛點、權衡遞歸與非遞歸。 對於第一個問題,其實就是start和end的位置選取,好比容易進入死循環,或者容易分不清楚到底應該是start = mid仍是start = mid+ 1等。如下給出一個代碼模版,這個也是我以前寫二分問題常常會寫的樣子:調試

int start = 0, end = nums.size() - 1;
while (start < end){
    int mid = (start + end)/2;
    if (...) {...}
    else if (...) {...}
    else {...}
}

 想必你們都會把循環條件寫成start < end或者start <= end這樣的,這樣在一些狀況下也確實沒有問題(這裏直接上一個題):code

  Find First Position of Targetblog

  http://www.lintcode.com/zh-cn/problem/first-position-of-target/
排序

給定一個排序的整數數組(升序)和一個要查找的整數target,用O(logn)的時間查找到target第一次出現的下標(從0開始),若是target不存在於數組中,返回-1

樣例

在數組 [1, 2, 3, 3, 4, 5, 10] 中二分查找3,返回2

這個題應該是Binary Search最基礎的題,直接套用模版就能夠,如下是這個題的代碼(Bug Free):

    int binarySearch(vector<int> &array, int target) {
        if (!array.size()) return -1;
        int start = 0, end = array.size() - 1;
        while (start < end) {
            int mid = (start + end) >> 1;
            if (array[mid] < target) {
                start = mid + 1;
            } else if (array[mid] > target) {
                end = mid -1;
            } else {
                end = mid;
            }
        }
        if (array[start] == target) return start;
        return -1;
    }

 

由於比較簡單,就再也不多說了,這裏須要注意的幾個點是,int mid = (start + end) >>1;其實就是int mid = (start + end)/2;由於在面試中若是會位運算的話,仍是可以給面試官留下很好的印象。有的人說直接加起來除以2會溢出,其實start和end不會大到超過int的最大值的,由於一個vector也不會去開闢那麼大的空間,可是寫成`int mid = (end - start)/2 + start;`也能顯得你比較不錯。綜上,兩種方法均可以。 在這個題中由於是找第一個與target相等的值,因此用這種方法不會出問題,可是在考慮下面的題,就會出現問題:

  Find Last Position of Target

  http://www.lintcode.com/zh-cn/problem/last-position-of-target/

給一個升序數組,找到target最後一次出現的位置,若是沒出現過返回-1

樣例

給出 [1, 2, 2, 4, 5, 5].

target = 2, 返回 2.

target = 5, 返回 5.

target = 6, 返回 -1.

 錯誤代碼以下:

while (start < end) {
        int mid = ( start + end ) >>1;
        if (A[mid] < target) {
       start = mid + 1;
     }
else if ( A[mid] > target) {
       end = mid -1;
     }
else {
       start = mid;
     } }

這裏若是這樣寫的話,代碼就會進入死循環,由於在求mid的時候是向左邊取整的。考慮這樣的一個狀況[...,5,5],假設target爲5,那麼start就會一直向右靠近,最後到n-2的位置,而end此時爲n-1,再次進入循環mid等於n-2,因此就進入了死循環。 根據課上老師所說的,建議你們寫成start + 1 < end,最後再判斷start和end(按照所需前後判斷)便可,這種寫法適用於全部的狀況,不容易出現問題。 ps. 這裏把條件寫成以下也可行:

while (start + 1 < end) {
    int mid = (start + end)>>1;
    if (A[mid] > target) {
    end = mid; } else {
    start = mid;
  } }

由於start和end不論是否包括mid值都不影響最後的結果。 這個境界須要理解一個重點: 二分法實際上就是把區間變小的問題,把一個長度爲n的區間變爲n/2,而後再變小,即:

T(n) = T(n/2) + O(1) = O(logn)

經過O(1)的時間,把規模爲n的問題變爲n/2 當面試的時候,有O(n)的解,若是面試官須要你進一步優化,那麼很大可能就是須要用二分O(logn)的方法來作。 實際上的步驟:

**區間縮小-> 剩下兩個下標->判斷兩個下標**

**注:不要把縮小區間和獲得答案放在一個循環裏面,容易出問題,增長難度**

 


 

 2. 第二境界:找到第一個/最後一個知足某個條件的位置/值

這個境界就是第一個境界的進階版本,就是可以把一些實際的應用問題轉換爲二分的核心問題:把具體的問題轉變爲找到數組中的第一個/最後一個知足某個條件的位置/值。 
就很少說了,直接上題吧:
  Seach a 2D Matrix

寫出一個高效的算法來搜索 m × n矩陣中的值。

這個矩陣具備如下特性:

  • 每行中的整數從左到右是排序的。
  • 每行的第一個數大於上一行的最後一個整數。

樣例

考慮下列矩陣:

[
  [1, 3, 5, 7],
  [10, 11, 16, 20],
  [23, 30, 34, 50]
]

給出 target = 3,返回 true

這道題最簡單的方式就是對每一行進行一次二分查找,第一行沒有找到就找第二行,以此類推,那麼時間複雜度爲0(nlogn)。 若是須要再進行優化,那麼能夠這樣考慮:由於條件中有每行的第一個數大於上一行的最後一個整數。因此咱們能夠先對每行的第一個數來進行一個二分查找,找到最後一個不大於target的數(注意這裏是二分查找的核心思想)而後再對這一行進行二分查找便可,這樣首先對每行的第一個數查找複雜度爲0(logn),再對某一行進行查找,複雜度爲O(logn),因此爲O(logn)。代碼以下(Bug Free):
bool searchMatrix(vector<vector<int> > &matrix, int target) {
    if (!matrix.size()||!matrix[0].size()) return false;
    int start = 0, end = matrix.size() - 1;
    while (start + 1 < end) {
        int mid = (end - start)/2 + start;
        if (matrix[mid][0] < target) start = mid;
        else end = mid;
    }
    int new_start = 0,new_end = matrix[0].size()-1;
    int index = matrix[end][0] <= target ?end:start;
    while (new_start + 1 < new_end) {
        int mid = (new_end - new_start)/2 + new_start;
        if (matrix[index][mid] > target) new_end = mid;
        else if (matrix[index][mid] < target)
            new_start = mid;
        else return true;
    }
    if (matrix[index][new_end] == target) return true;
    if (matrix[index][new_start] == target) return true;
    return false;
}
這個題關鍵在於要對兩個端點的把握,仍是二分查找的基本流程:先縮小區間,而後對兩個剩餘的端點進行判斷。
固然這個題也有不須要進行兩次計算的方法,由於當前行的全部元素嚴格大於第一行,因此能夠把矩陣考慮爲一維的數組,只須要在切換的時候進行一個行和列的轉換便可,具體代碼以下(Bug Free):
    bool searchMatrix(vector<vector<int> > &matrix, int target) {
        if (!matrix.size()||!matrix[0].size()) return false;
        int m = matrix.size();
        int n = matrix[0].size();
        int start = 0, end = n * m - 1;
        while (start + 1 < end) {
            int mid = (end - start)/2 + start;
            int x = mid / n;
            int y = mid % n;
            if (matrix[x][y] > target) {
                end = mid;
            } else {
                start = mid;
            }
        }
        int x = start / n;
        int y = start % n;
        if (matrix[x][y] == target) {
            return true;
        }
        x = end / n;
        y = end % n;
        if (matrix[x][y] == target) {
            return true;
        }
        return false;
    }

 

再來一個題吧:
  Find Minimum in Rotated Sorted Array

假設一個旋轉排序的數組其起始位置是未知的(好比0 1 2 4 5 6 7 可能變成是4 5 6 7 0 1 2)。

你須要找到其中最小的元素。

你能夠假設數組中不存在重複的元素。

樣例

給出[4,5,6,7,0,1,2]  返回 0

這個題若是按照我之前的想法,就是最直觀的辦法:直接從頭遍歷,發現某一個值小於前一個值,而且小於後一個值,那麼這個值就是最小的。這樣的複雜度就是O(n),我記得有一次面試的時候就是這麼回答了,而後遭到了面試官無情的鄙視。 這裏使用二分的話,是很是有技巧的,這個技巧也是對於以後難一些的題來講須要掌握的,咱們要時刻不能忘記二分的宗旨:把具體的問題轉變爲找到數組中的額第一個/最後一個知足某個條件的位置/值。這題其實能夠這麼考慮:由最小值爲中心把兩邊分開,兩邊都是遞增的,然後一部分的最大值也嚴格小於前一部分的全部值,顯然,最後一部分的最大值就是num[n-1]那麼咱們只須要找到比這個值小的第一個值便可。這裏是否是又回到了最原始的問題。代碼以下(Bug Free):
    int findMin(vector<int> &num) {
        if (!num.size()) return 0;
        int start = 0, end = num.size() - 1;
        int target = num[end];
        while (start + 1 < end) {
            int mid = (end - start)/2 + start;
            if (num[mid] <= target) {
                end = mid;
            }
            else {
                start = mid;
            }
        }
        if (num[start] <= target) {
            return num[start];
        }
        else {
            return num[end];
        }
    }
這題稍微有一些和以前不同的地方,就是在兩個部分之間進行了一個權衡,在start和end的變換的地方,須要你們注意。
 

 
3. 第三境界:保留有解的那一半
 
有了前面兩個階段的鋪墊,達到必定的訓練之後,應該也差很少可以熟練掌握中等和簡單的題了,其實足夠深入地理解了二分的精髓之後,能夠把幾個習題都作一遍,儘可能仍是達到Bug Free的級別吧(這裏所說的Bug Free是指可以不用編譯器的狀況下,直接空手寫代碼,用眼睛和筆來調試,最後提交後Accepted)。
第三個階段呢,其實就是回到了二分自己的定義,個人理解就是:二分法其實就是把問題不斷縮小爲原來的n/2,而後再找到相應的位置進行處理。那麼二分法的最高境界就是學會去 保留有答案的那一半,也許你內心會想:這個我原本就知道啊,可是真正到了實際操做的時候,仍是會有搞不清楚的時候。貼出一道題:
   Find Peak Element

給出一個整數數組(size爲n),其具備如下特色:

  • 相鄰位置的數字是不一樣的
  • A[0] < A[1] 而且 A[n - 2] > A[n - 1]

假定P是峯值的位置則知足A[P] > A[P-1]A[P] > A[P+1],返回數組中任意一個峯值的位置。

樣例

給出數組[1, 2, 1, 3, 4, 5, 7, 6]返回1, 即數值 2 所在位置, 或者6, 即數值 7 所在位置.

這個題算是比較簡單的題,可是重要的是理解其中的思想,仍是回到二分法第三個階段的核心: 保留有答案的那一半。這道題只是要求找到其中一個峯值便可。峯值知足的條件就是左邊的部分是單調遞增的,而右邊的部分是單調遞減的(若是該點可導,而且導數爲0,那麼這個點就是峯值),咱們很容易在紙上畫出來某個點的四種狀況(以下圖所示):

 

第一種狀況:當前點就是峯值,直接返回當前值。

第二種狀況:當前點是谷點,不論往那邊走均可以找到峯值。

第三種狀況:當前點處於降低的中間,往左邊走能夠到達峯值。

第四種狀況:當前點處於上升的中間,往右邊走能夠達到峯值。

分析了四種狀況,那麼就容易把有答案的一半保留下來了,接下來就判斷是否可以找到峯值便可。代碼以下(Bug Free):

  int findPeak(vector<int> A) {
      if (!A.size()) return 0;
      int start = 0;
      int end = A.size() -1;
      while (start + 1 < end) {
          int mid = (end - start)/2 + start;
          if (A[mid] > A[mid - 1] && A[mid] > A[mid + 1]) {
              return mid;
          } else if (A[mid] <= A[mid+1] && A[mid] >= A[mid -1]) {
              start = mid;
          } else if (A[mid] >= A[mid+1] && A[mid] <= A[mid -1]) {
              end = mid;
          } else {
              start = mid;
          }
      }
      if (start >= 1 && A[start] > A[start - 1] && A[start] > A[start + 1]) return start;
if (end <= A.size()-2 && A[end] > A[end-1] && A[end] > A[end+1]) return end; }

這道題的難點其實就是把各類狀況考慮一下,而後把有答案的部分保留下來,基本上就沒有問題了。

 


 

總結

本文只是挑選了一些比較好的課上的題進行了講解,還有部分題沒有寫出來,也會在後續的博客中。

對於我我的而言,二分法算是比較熟悉的一個方法,以前在作微軟校招第一題的時候用的就是二分的方法。在面試中也是比較經常使用到的一種方法,由於總有那麼一種說法嘛:比0(n)還要快的算法複雜度,那必須就是0(logn)了(這裏說的是在通常的面試狀況下)那麼O(logn)就必然要考慮二分的方法來作了。通常都會與一些排序的序列、在一段有規則的序列等狀況中找到符合某個條件的位置/值。這個模塊仍是須要多練習,而後就可以很好上手了,若是想要可以在算法面試中有更好的突破,仍是須要去解決一些難一點的題,諸如poj或者hdu這樣的應用場景的題。

 

這也是本人第一次認真寫一個技術長文,雖然也沒有什麼特別深奧的東西,讀到這裏說明你也是很給我面子的了,以後還會繼續更新一些本身的想法和一些好的題目,但願你們多多支持!

相關文章
相關標籤/搜索