演算法---二分法
二分法可以歸為兩大類:
* 二分查詢演算法
* 二分排序演算法
* 二分合並演算法
二分查詢演算法
演算法中經常用到二分查詢演算法,比如最常規的應用就是在一個有序陣列中找特定的數,但是如何寫出一個完整準確的二分法呢,邊界條件如何判斷,到底是等於還是不等?可能會困惱大家,比如說查詢第一個等於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;最後left和right會相遇,相遇位置存放的就是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
小結
二分法在實際的專案中運用還是非常多的,算是基本演算法,其中心思想是分治,它除了在陣列中使用較多,另外一個應用的場景就是二叉樹的問題。