深入分析二分查找及其變體


1—一般二分查找

一般的二分查找代碼如下:

int search(int A[], int n, int target)
{
int low = 0, high = n-1;
while(low <= high)
{
// 注意:若使用(low+high)/2求中間位置容易溢出
int mid = low+((high-low)>>1);
if(A[mid] == target)
return mid;
else if(A[mid] < target)
low = mid+1;
else // A[mid] > target
high = mid-1;
}
return -1;
}

上面的二分查找非常的朴實,上述二分查找的作用當然就是:找到數組A[]中等於target的元素。但是這個查找元素隱含了一個條件:這個數組的元素是不包含重復元素的。這個限制可以說是非常的大。我們來看一下,假設存在重復元素,按照上述找法,找的是誰

7 7 7 7 8 10;7

即假設我們找7,顯然第一次就找到了,這個“7”在A[2]的位置,也就是我們按照上述思路找,能找到。但並不是最開始的7或者結尾的7.

2—找到有重復元素數組第一個索引元素

假設我們要找到最開始的7,應該如何修改代碼

呢?

先上代碼:

int searchFirstPos(int A[], int n, int target)
{
if(n <= 0) return -1;
int low = 0, high = n-1;
while(low < high)
{
int mid = low+((high-low)>>1);
if(A[mid] < target)
low = mid+1;
else // A[mid] >= target
high = mid;
}
if(A[low] != target)
return -1;
else
return low;
}

這個代碼,為何會找到最開始的7呢?我們看看發生了什么

還是:

7 7 7 7 8 10;7

第一次后,high ->A[2],循環沒結束

第二次后,high->A[1],循環沒結束

第三次后,high->A[0],循環結束

循環結束條件喂 low == high!

我們再看一個例子:

5 7 7 7 7 8 10;7

第一次后,high ->A[3],循環沒結束

第一次后,high ->A[1],循環沒結束

第三次后,low->A[1],循環結束。

再看一個例子

2 5 7 8 9 10 12 13 13 14;13

第一次后,low ->A[5],循環沒結束

第二次后,high ->A[7],循環沒結束

第三次后,low ->A[6],循環沒結束

第三次后,low ->A[7],循環結束

總結:

也就說:找出現的第一個值,其必然結果是low == high的時候,但是為何是第一個,而不是最后一個呢?

這個主要取決於下面這行代碼:

else // A[mid] >= target
high = mid;

也就是說即使A[mid] == target,我們也會使得high == target,換句話而言,即使A[high] == target;

我們也會讓high向第一個出現查找值的索引位置靠攏!!!

if(A[mid] < target)
low = mid+1;

不過仍然是借鑒了傳統二分查找的思想,mid的值小了,就讓low=mid+1;

真正要指向第一個,要做的就是:讓high向第一個靠攏!!!!

3—找到重復元素數組最后一個元素

上述中說到找第一個元素,要讓high向第一個靠攏,而這里要找最后一個元素,則要low向最后一個元素靠攏。

先上代碼:

int searchLastPos(int A[], int n, int target)
{
if(n <= 0) return -1;
int low = 0, high = n-1;
while(low < high)
{
/*
這里中間位置的計算就不能用low+((high-low)>>1)了,因為當low+1等於high
且A[low] <= target時,會死循環;所以這里要使用low+((high-low+1)>>1),
這樣能夠保證循環會正常結束。
*/
int mid = low+((high-low+1)>>1);
if(A[mid] > target)
high = mid-1;
else // A[mid] <= target
low = mid;
}
if(A[high] != target)
return -1;
else
return high;
}

這里需要注意的是下面這行代碼:

int mid = low+((high-low+1)>>1);

假設仍然是:

int mid = low+((high-low)>>1);

我們看會發生什么?

以下述序列為例;

1 2 7 7 7 8 9 13;7

第一次:mid->A[3],low-->A[3];

第二次:mid->A[5],high-->A[4];

第三次:mid->A[3],low->A[3].....而此時low < high,出現死循環;

可以再舉出其他例子,但是結果表明,問題總是出現在最后一步,也就是最后一步總有higg-low =1; 且mid一直等於low,這使得循環一直為死循環。

究其原因,是因為:/2導致的向下取整。而high-low+1可以保證向上取整!!!

那我們為何要這么做呢?主要原因在於:

if(A[mid] > target)
high = mid-1;

即,只要A[mid]>target,high的的值總會減小。也就是說,即使我們向上取整,最終也會使得high指向正確的位置,low也會因為向上取整的原因,最終使得low和high收斂到同一個位置(比如low->A[3]=7,high->A[4]=7.),而low則不同,low刷新成mid,但最后一步有可能不收斂,mid的值不再刷新時候,low的值也不刷新,從而導致low和high不會收斂到同一個位置。

4—給定一個有序(非降序)數組A,若target在數組中出現,返回其第一個位置,若不存在,返回它應該插入的位置

我們稍做分析就知道,對於代碼:

上述問題1、2、3無論是哪個問題,當找不到target時候,low==high等於target應該處於的位置是恆成立的。因此,這道題的代碼:

int searchPos(int A[], int n, int target)
{
if(n <= 0) return -1;
int low = 0, high = n-1;
while(low < high)
{
int mid = low+((high-low)>>1);
if(A[mid] < target)
low = mid+1;
else // A[mid] >= target
high = mid;
}
   return low;
}

5—給定一個有序(非降序)數組A,可含有重復元素,求絕對值最小的元素的位置

這個問題也很簡單,僅僅給出思路:

絕對值最小的數當然是0,這個問題轉化為:找數組中0的位置,若沒找到0,那么最終low==high指向的位置的數或者low-1(或者high-1)

指向的數就是最小的。

6—一個有序(升序)數組,沒有重復元素,在某一個位置發生了旋轉后,求target在變化后的數組中出現的位置,不存在則返回-1

0 1 2 4 5 6 7 可能變成 2 4 5 6 7 0 1

很明顯的特征在於:有序數組旋轉后,存在着兩個有序部分。可能我們會想到對這兩部分分別進行二分查找,這個思路總體上是沒有問題的。但是問題在於我們如何知道這個數據轉折點在哪?又或許我們是否有必要知道呢?

當然了,我們可以按照下面這個思路去處理問題:

第一步:尋找那個數據轉折點(比如上述序列中就是7)

第二步,判斷target所屬區間(轉折點前還是后)

第三步:二分查找

當然了,這個思路是完全ok的,也可以按照這個思路去處理,事實上,我開始也是這么做的。但是實際情況是,我們根本沒有必要這么做,沒必要去找那個數據轉折點的位置。

因為這個數組旋轉一次后,我們只需要關注旋轉后的數組的中間元素,一個很重要的特點是:中間元素兩邊的子數組至少有一個是有序的。因此我們可以判斷target是否在這個有序子數組中。從而決定target的搜索區間。

先上代碼:

int searchInRotatedArray(int A[], int n, int target) 
{
int low = 0, high = n-1;
while(low <= high)
{
int mid = low+((high-low)>>1);
if(A[mid] == target)
return mid;
if(A[mid] >= A[low])
{
// low ~ mid 是升序的
if(target >= A[low] && target < A[mid])
high = mid-1;
else
low = mid+1;
}
else
{
// mid ~ high 是升序的
if(target > A[mid] && target <= A[high])
low = mid+1;
else
high = mid-1;
}
}
return -1;
}

這段代碼可以說是相當完美!

來分析一下:

整體而言,這段代碼仍然采用二分查找法。也許我們會心有余悸,但是仔細分析發現,非常的巧妙。

if(A[mid] >= A[low]) 

這個判斷是用來表明:前半段是否是是升序有序子序列。如果是的話:

if(target >= A[low] && target < A[mid])
high = mid-1;

如果同時要找的數大於首個數,而小於中間元素,那么要找的數就位於有序序列之間。自然也就執行了:

high = mid-1;

該算法的精華在於:

else

這個else是指不滿足於上述if條件的所有可能。自然也包括了二分查找的另一半target > A[mid]。但是其作用不僅僅是這個。那他的作用是什么呢?

我們看,要想所查找target位於有序數組中,他需要滿足:

A[mid] >= A[low] && target >= A[low] && target < A[mid]

或者:

A[mid] < A[low] && target > A[mid] && target <= A[high]

不滿足上述條件的時候,發生了:

low = mid+1;

或者:

high = mid-1;

這樣的意義何在呢?沒錯就是通過改變low和high的索引,改變了mid的位置,最終也就是隨着迭代的進行,使得target總可以處於一個有序子數組中,並找到它。也就說,最重要的代碼:就是那個

else

請再深入推敲上上述代碼。尤其是else的作用。對這個問題進一步思考:如果這個數組存在重復元素,那么還能進行二分查找嗎?顯然不能,


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM