二分查找:劍指offer 53:在排序數組中查找數字
如何利用“數組是排序的”這一特點設計更快的算法是這一題最好的解決辦法?本節我們討論的本題都是基於這一特點展開的。
通常,我們需要在一個長度爲n的數組中查找一個數,需要次,所以順序掃描/查找的時間複雜度爲。顯然這不是最好的方法。
接下來,我們思考如何更好地利用二分查找算法。二分查找無非就是從數組的中間位置開始,然後討論三種情況(針對升序數組)。
- 如果中間位置的數滿足/等於設定條件,找到結束;
- 如果中間位置的數小於設定條件,滿足條件的數在後半段;
- 如果中間位置的數大與設定條件,滿足條件的數在前半段;
程序實現的僞代碼框架可以有以下參考:
int Binary_Search(int* numbers, int length)
{
if(numbers == nullptr || length <= 0)
return -1;
int left = 0;
int right = length - 1;
while(legt <= right)
{
int middle = left + (right-left)>>1;
if(numbers[middle] == middle) //設定條件
return middle;
if(numbers[middle] > middle)
right = middle - 1;
else
left = middle + 1;
}
return -1;
}
分析至此,我們清楚地知道,二分查找的思想就是在於找到這個設定條件。那麼,我們就來以劍指offer中面試題53的三種情況做分別介紹。
題目一:數字在排序數組中出現的次數。
統計一個數字在排序數組中出現的次數。例如,輸入排序數組{1,2,3,3,3,3,4,5}和數字3,由於3在數組中出現了4次,因此輸出爲4。
假設我們要統計數字k在排序數組中出現的次數。如何採用二分查找的方法找到第一個k和最後一個k是本題的核心問題。
我們先分析如何找到第一個k。二分查找算法總是先拿數組中間的數字和k做比較。如果中間的數字比k大,那麼k只有可能出現在數組的前半段,下一輪我們只在數組的前半段查找就可以了。如果中間的數字比k小,那麼k只有可能出現在數組的後半段,下一輪我們只在數組的後半段查找就可以了。
如果中間的數字和k相等呢?我們先判斷這個數字是不是第一個k。如果中間數字的前面一個數字不是k,那麼此時中間的數字剛好就是第一個k;如果中間數字的前面一個數字也是k,那麼第一個k肯定實在數組的前半段,下一輪我們仍然需要在數組的前半段查找。
基於這種思路,我們可以很容易地寫出遞歸的代碼找到排序數組中的第一個k。下面是一段參考代碼:
int GetFirstK(int* data, int length, int k, int start, int end)
{
if(start > end)
return -1;
int middleIndex = (start + end)/2;
int middleData = data[middleIndex];
if(middle == k)
{
if((middleIndex > 0 && data[middleIndex - 1] != k)
|| middleIndex == 0)
return middleIndex;
else
end = middleIndex - 1;
}
else if(middleData > k)
end = middleIndex - 1;
else
start = middleIndex + 1;
return GetFirstK(data,length,k,start, end);
}
我們可以用同樣的思路在排序數組中找到最後一個k。基於遞歸寫出如下代碼:
int GetLastK(int* data, int length, int k, int start, int end)
{
if(start > end)
return -1;
int middleIndex = (start + end)/2;
int middleData = data[middleIndex];
if(middle == k)
{
if((middleIndex < length -1 && data[middleIndex - 1] != k)
|| middleIndex == length -1 )
return middleIndex;
else
start = middleIndex + 1;
}
else if(middleData < k)
start = middleIndex + 1;
else
end = middleIndex - 1;
return GetFirstK(data,length,k,start, end);
}
在分別找到第一個k和最後一個k的下標之後,我們就能計算出k在數組中出現的次數。相應的代碼如下:
int GetNumberOfK(int* data, int length, int k)
{
int number = 0;
if(data != nullptr && length > 0)
{
int first = GetFirstK(data,length,k,0,length-1);
int last = GetLastK(data,length,k,0,length-1);
if(first > -1 && last > -1)
number = last - first + 1;
}
return number;
}
題目二:0~n中缺失的數字
一個長度爲n-1的遞增排序數組中的所有數字都是唯一的,並且每個數字都在範圍之內。在範圍內的n個數字中有且只有一個數字不在該數組中,請找出這個數。
因爲這些數字在數組中是排序的,因此數組中開始的一些數字與它們下標相同。也就是說,0在下標爲0的位置,1在下標爲1的位置,以此類推。如果不在數組中的那個數字記爲m,那麼所有比m小的數字的下標都與它們的值相同。
由於m不在數組中,那麼m+1處在下表爲m的位置,m+2處在下標爲m+1的位置,以此類推。我們發現m正好是數組中第一個數值和下標不相等的下標,因此這個問題轉換成在排序數組中找出第一個值和下標不相等的元素。
int GetMissingNumber(int* numbers, int length)
{
if(numbers == nullptr || length <= 0)
return -1;
int left = 0;
int right = length -1;
while(left < right)
{
int middle = (right + left)>>1;
if(numbers[middle] != middle)
{
if(middle == 0 || numbers[middle-1] == middle -1)
return middle;
right = middle -1;
}
else
left = middle + 1;
}
if(left == length)
return length;
}
題目三:數組中數值和下標相等的元素
假設一個單調遞增的數組裏的每個元素都輸證書並且是唯一的。請編寫程序實現一個函數,找出數組中任意一個數值等於其下標的元素。例如,在數組{-3,-1,1,3,5}中,數字3和它的下標相等。
採用二分查找方法,假設我們某一步抵達數組中的第i個數字。如果我們很幸運,該數字的值剛好也是i,那麼我們就找到了一個數字和其下標相等。
那麼當數字的值和下標不相等的時候該怎麼辦呢?
假設數字的值爲m,我們先考慮m大與i的情形,即數字的值大與它的下標。由於數組中的所有數字都唯一併且單調遞增,那麼對於任意大於0的k,位於下標i+k的數字的值大與或等於m+k。另外,因爲m>i,所以m+k>i+k。因此,位於下標i+k的數字的值一定大與它的下標。這意味着如果第i個數字的值大與i,那麼他右邊的數字都大於對應的下標,我們都可以忽略。下一輪查找我們只需要從他左邊的數字中查找即可。
數字的值m小於它的下標i的情形和上面類似。下面是基於二分查找的參考代碼:
int GetNumberSameAsIndex(int* numbers, int length)
{
if(numbers == nullptr || length <= 0)
return -1;
int left = 0;
int right = length - 1;
while(legt <= right)
{
int middle = left + (right-left)>>1;
if(numbers[middle] == middle)
return middle;
if(numbers[middle] > middle)
right = middle - 1;
else
left = middle + 1;
}
return -1;
}
二分查找算法屬於分治的思想,它的應用非常廣,之後我再給大家總結其他類型的題目。例如,二進制解析、藥品有效性查找、進階可採用三分法等。
推薦閱讀:
[1] 數據結構與算法 | 你知道快速排序,那你知道它的衍生應用嗎?Partition函數
[2] 數據結構與算法 | 數據結構中到底有多少種“樹”?一文告訴你
[3] 數據結構與算法 | 數據結構與算法之美 | 別怕,有我!KMP 算法詳解
關注微信公衆號:邁微電子研發社,回覆獲取更多精彩內容。
知識星球:社羣旨在分享AI算法崗的秋招/春招準備攻略(含刷題)、面經和內推機會、學習路線、知識題庫等。