1. 程式人生 > >演算法---二分法

演算法---二分法

二分法可以歸為兩大類:
* 二分查詢演算法
* 二分排序演算法
* 二分合並演算法

二分查詢演算法

演算法中經常用到二分查詢演算法,比如最常規的應用就是在一個有序陣列中找特定的數,但是如何寫出一個完整準確的二分法呢,邊界條件如何判斷,到底是等於還是不等?可能會困惱大家,比如說查詢第一個等於5的數,那又在如何查詢呢?查詢最後一個等於5的數,又該如何改變?
這裡有一個公式,可以解決所有這類的問題,它分為四步走:
1. 判定條件為start+1小於end,start=0, end=size-1
2. mid=start+((end-start)>>1)
3. 如果data[mid]滿足條件直接返回,如果滿足條件的資料在mid的右邊則將start=mid,如果滿足條件的資料在mid左邊則將end=mid
4. 根據條件再次判定start,end兩個元素是否滿足條件
其實整個過程就不斷縮小範圍的過程,最後一步就是將整個陣列的範圍縮小到start end兩個元素而已。
這裡第3步有一個小的竅門就是不管什麼情況下都應該單獨判data[mid]大於等於和小於的情況,而不應該合併,因為會比較容易出錯。
下面以求在有序陣列中求最後一個等於特定元素的位置為例

class Solution {
public:
    /**
     * @param A an integer array sorted in ascending order
     * @param target an integer
     * @return an integer
     */
    int lastPosition( vector< int>& A, int target) {
        if( A.empty() ){
            return -1;
        }

        int Start = 0
; int End = A.size() - 1; while(Start+1 < End) { auto Mid = Start + ( (End-Start) >> 1 ); if( A[Mid] == target){相等的情況只可能出現的Mid的右邊 Start = Mid; } else if( A[Mid] < target){ Start = Mid; } else
{ End = Mid; } } if( A[End] == target){因為是求最後一個等於target的元素位置,所以對於一個兩個元素的陣列,當然是先判斷end了 return End; } else if( A[Start] == target){ return Start; } else{ return -1; } } };

非常簡單吧。
典型題目,求sqrt

class Solution {
public:
    /**
     * @param x: An integer
     * @return: The sqrt of x
     */
    int sqrt(int x) {
        if(0 == x){
            return 0;
        }

        long Start = 0;
        long End = x;
        while(Start+1 < End)
        {
            auto Mid = Start + ( (End-Start) >> 1);
            long MidSquare = Mid * Mid;
            if(MidSquare == x){
                return Mid;
            } else if(MidSquare < x){
                Start = Mid;
            } else{
                End = Mid;
            }
        }

        if(End*End < x){
            return End;
        } else{
            return Start;
        }
    }
};

還有一種典型的題目就是在一個sorted array中尋找一個數字,但是這個sorted array可能旋轉過的

二分排序演算法

二分排序演算法思想是分治,將大的問題簡化成為小的問題,所有小的問題全部解決了,整個問題就解決了。
二分排序的期望複雜度是O(nlgn),最差情況是O(n^2)
二分排序中最重要的是partition函式,直接寫不容易寫對,下面直接給出模板

               int pivot = nums[begin];
               int left = begin;
               int right = end;
               while (left < right)
              {
                      while (left < right && nums[right] > pivot) {
                           right--;
                     }
                      nums[left] = nums[ right] ;

                      while (left < right && nums[left] <= pivot) {
                           left++;
                     }
                      nums[right] = nums[ left] ;
              }
               nums[left] = pivot;
               return left;最後leftright會相遇,相遇位置存放的就是pivot,接下來就是繼續處理左右兩個子集

典型應用有,求第k大數
從大到小的採用partition方法,如果返回的pivot位置正好是k-1,則直接返回,如果比k-1小,則第k-1個數肯定在pivot的右邊,也就是找k-1-pivot大的數,如果比k-1大,則在pivot的左邊找第k-1大的數。
這種演算法叫做quickselect,期望的複雜度是O(n),最差是O(n^2)

另外一種場景就是在一個數據流中求中位數或者第k大數
資料流的意思就是不能一下子load出所有的資料,只能一個一個處理,這種場景就需要建立兩個PQ,一個是最小堆,一個是最大堆,前面k個數為最小堆,後面len-k個數為最大堆,當有新的數進來的時候先判斷這個數是應該放在左邊還是右邊,然後直接插入,再重新調整,保證左邊的資料個數為k個就可以,所以每次壓入一個新數,左邊的堆頂元素則為所求。這也是採用partition的思想來解決問題。
這裡有一種變形就是如果是window的第k大數,則需要用hashheap,因為window移動的時候需要增加一個元素同時再刪除一個元素,hash就可以最快的找到元素從heap中刪除。
ps:在求前k個數或者第k大數時優先考慮quickselect,然後就是PQ是一個比較好的策略,演算法的複雜度為nlgk

二分合並

二分排序是將大問題分解成為小問題,當所有小問題全部解決了,那最後的結果也就出來了,二分合並則是先將問題分割成為小問題,解決每個小問題,然後將小問題的結果合併得到最後的結果。
最典型的問題就是合併n個連結串列,n個有序連結串列,如何合併成為一個有序連結串列?這道題目有兩種解法:
1. 將n個有序序列的第一個元素壓入到PQ中,然後從PQ每次取出最大值,同時將最大值的下一個節點再次壓入PQ中,依次將所有節點合併。這個演算法的複雜度為mlgn,總共有m個節點
2. 每次兩兩合併,最終得到一個結果,這是典型的合併排序,這個演算法複雜度為mlgn

小結

二分法在實際的專案中運用還是非常多的,算是基本演算法,其中心思想是分治,它除了在陣列中使用較多,另外一個應用的場景就是二叉樹的問題。