二分查詢變形記:從迴圈不變數說起
二分查詢
首選我們介紹標題中提到的兩個名詞:
二分查詢
二分查詢的搜尋過程從陣列的中間元素開始,如果中間元素正好是要查詢的元素,則查詢成功;如果某一特定元素大於或者小於中間元素,則在陣列大於或小於中間元素的那一半中查詢,而且跟開始一樣從中間元素開始比較。如果在某一步驟陣列為空,則代表找不到。這種搜尋演算法每一次比較都使搜尋範圍縮小一半。
迴圈不變數
其主要用來幫助理解演算法的正確性。形式上很類似與數學歸納法,它是一個需要保證正確斷言。對於迴圈不變式,必須證明它的三個性質:
- 初始化:它在迴圈的第一輪迭代開始之前,應該是正確的。
- 保持:如果在迴圈的某一次迭代開始之前它是正確的,那麼,在下一次迭代開始之前,它也應該保持正確。
- 終止:迴圈能夠終止,並且可以得到期望的結果,不變式給了我們一個有用的性質,那就是獲得正確的結果。
那這兩個東西又有什麼關聯呢?上面已經提到迴圈不變數可以證明演算法的正確性!這樣的話,我們下面就運用迴圈不變數證明一下二分查詢各種應用演算法的正確性。
-
給定一個有序(升序,我們後面所有提到的資料均為升序,後面不再贅述)陣列A,求任意一個i使得A[i]等於target,不存在則返回-1
這是最原始的二分查詢,利用陣列有序性進行折半查詢,時間複雜度為O(nlogn)。設原始資料為A[0 ~ n-1]- 初始化:low = 0, high = n - 1,mid = low + (high - low)/2。如果targer存在原始陣列中,其對應index一定處於[low, high], 即 A[low] <= target <= A[high]
- 保持: 對於 A[mid] <target,則index只能存在[mid + 1, high]中,且low = mid + 1;對於 A[mid] > target, 則index只能存在[low, mid - 1], 且high = mid;對於A[mid] = target,因為 low <= mid <= high, index = mid,直接返回。
- 終止:low >high(即high + 1)。待處理陣列為空,表示tartget不存在此陣列中。
int search(int A[], int n, int target) { int low = 0
後面的案例,也都可以按照上述方法證明演算法的正確性,但我們不一一贅述,但在描述演算法的時候,我們會指出,迴圈過程中不變的性質(雖然這個性質可能在初始化的時候不一定成立),這對我們理解演算法為何這麼寫有很大幫助!
-
給定一個有序陣列A,可含有重複元素,求最小的i使得A[i]等於target,不存在則返回-1
// 兩種寫法 int lowerBound(int A[], int n, int target) { int low = 0, right = n - 1; while(low <= high) { /* 為什麼上面一定是 low <= high? low < high 行不行? */ int mid = low + (high - low)/2; if(A[mid] < target) low = mid + 1; else high = mid - 1; } /* 迴圈過程中,當low大於0時,因為A[mid] < target時,low=mid+1, 所以A[low-1] < target; 當high小於n-1時,因為A[mid] >= target時,high = mid - 1, A[high + 1 >= target; 迴圈結束時,low 等於 high + 1,所以,如果A[low](A[high + 1])存在就等於target, 那麼low(high)就是target出現的最小位置,否則target在陣列中不存在。 綜上所示,迴圈不變式為: A[low - 1] < target <= A[high + 1] */ if(low < n && A[low] == target) return low; return -1; } 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; } /* 迴圈過程中,當low大於0時,因為A[mid] < target時,low=mid+1, 所以A[low-1] < target; 當high小於等於n-1時,因為A[mid] >= target時,high = mid, A[high] >= target; 迴圈結束時,low 等於 high,所以,如果A[low](A[high])就等於target, 那麼low(high)就是target出現的最小位置,否則target在陣列中不存在。 綜上所示,迴圈不變式為: A[low - 1] < target <= A[high] */ if(A[low] != target) return -1; else return low; }
-
給定一個有序陣列A,可含有重複元素,求最大的i使得A[i]等於target,不存在則返回-1
// 兩種寫法 int upperBound(int A[], int n, int target) { int low = 0, hihg = n - 1; while (low <= high) { /* */ int mid = low + (high - low) / 2; if (A[mid] > target) high = mid - 1; else low = mid + 1; } /* 迴圈過程中,當high小於n-1時,因為A[mid] > target時,high=mid-1, A[high+1] > target的; 當low大於0時,因為A[mid] <= target時,low = mid + 1, A[low - 1] <= target; 迴圈結束時,low 等於 high + 1,所以,如果A[high](A[low - 1])等於target, 那麼high(low - 1)就是target出現的最大位置,否則target在陣列中不存在。 綜上所示,迴圈不變式為: A[low - 1] <= target < A[high + 1] */ if (high >= 0 && A[high] == target) return high; return -1; } 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), 這樣能夠保證迴圈會正常結束。 ? 如果high-low+1溢位了怎麼辦 */ int mid = low+((high-low+1)>>1); if(A[mid] > target) high = mid-1; else // A[mid] <= target low = mid; } /* 迴圈過程中,當high小於n-1時,因為A[mid] > target時,high=mid-1, A[high+1] > target的; 當low大於0時,因為A[mid] <= target時,low = mid, A[low] <= target; 迴圈結束時,low 等於 high,所以,如果A[high](A[low])等於target, 那麼high(low)就是target出現的最大位置,否則target在陣列中不存在。 綜上所示,迴圈不變式為: A[low] <= target < A[high + 1] */ if(A[high] != target) return -1; else return high; }
-
給定一個有序陣列A,可含有重複元素,求最大的i使得A[i]小於target,不存在則返回-1
int LastBoundLessThan(int A[], int n, int target) { int low = 0, high = n - 1; while (low <= high) { /* */ int mid = low + (high - low) / 2; if (A[mid] >= target) high = mid - 1; else low = mid + 1; } /* 迴圈過程中,當high小於n-1時,因為A[mid] > target時,high=mid-1, A[high+1] > target的; 當low大於0時,因為A[mid] <= target時,low = mid + 1, A[low - 1] <= target; 迴圈結束時,low 等於 high + 1,所以,如果A[high](A[low - 1])等於target, 那麼high(low - 1)就是target出現的最大位置,否則target在陣列中不存在。 綜上所示,迴圈不變式為: A[low - 1] < target <= A[high + 1] */ if (high >= 0 && A[high] < target) return high; return -1; } int searchLastPosLessThan(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), 這樣能夠保證迴圈會正常結束。 ? 如果high-low+1溢位了怎麼辦 */ int mid = low+((high-low+1)>>1); // 注意,不要導致死迴圈 if(A[mid] < target) low = mid; else // A[mid] >= target high = mid-1; } /* 迴圈過程中,當low大於0時,因為A[mid] < target時, low=mid,A[low] < target; 當high小於n-1時,因為A[mid] >= target時, high = mid-1,A[high+1] >= target的; 迴圈結束時,low 等於 high,所以,如果A[low](A[high])小於target, 那麼low(high)就是要找的位置,否則不存在這樣的位置(A[0] >= target時)。 迴圈不變式: A[low] < target <= A[high+1] */ return A[low] < target ? low : -1; }
-
給定一個有序陣列A,可含有重複元素,求最小的i使得A[i]大於target,不存在則返回-1
int FiststBoundGreaterThan(int A[], int n, int target) { int low = 0, high = n - 1; while(low <= high) { int mid = low + (high - low)/2; if(A[mid] > target) high = mid - 1; else low = mid + 1; } /* 迴圈過程中,當low大於0時,因為A[mid] <= target時,low=mid+1, 所以A[low-1] <= target; 當high小於等於n-1時,因為A[mid] > target時,high = mid - 1, A[high + 1] > target; 迴圈結束時,low 等於 high + 1,所以,如果A[low](A[high + 1])大於target, 那麼low(high)就是target出現的最小位置,否則target在陣列中不存在。 綜上所示,迴圈不變式為: A[low - 1] <= target < A[high + 1] */ if(low < n && A[low] > target) return low; return -1; } int searchFirstPosGreaterThan(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) high = mid; else // A[mid] <= target low = mid+1; } /* 迴圈過程中,當low大於0時,因為A[mid] <= target時,low=mid+1, 所以A[low-1] <= target; 當high小於等於n-1時,因為A[mid] > target時,high = mid, A[high] > target; 迴圈結束時,low 等於 high,所以,如果A[low](A[high])大於target, 那麼low(high)就是target出現的最小位置,否則target在陣列中不存在。 綜上所示,迴圈不變式為: A[low - 1] <= target < A[high] */ return A[high] > target ? high : -1; }
下面的一些問題是二分查詢拓展性的問題,均可重用或改進上述 程式碼解決:
-
給定一個有序陣列A,可含有重複元素,求target在陣列中出現的次數
求出第一次出現位置和最後一次出現位置,相減即可獲得int count(int A[], int n, int target) { int firstPos = searchFirstPos(A, n, target); // 第一次出現位置 if(firstPos == -1) return 0; int lastPos = searchLastPos(A, n, target); // 最後一次出現位置 return lastPos-firstPos+1; // 出現次數 }
-
給定一個有序陣列A,若target在陣列中出現,返回位置,若不存在,返回它應該插入的位置
leetcode原題:Search Insert Position
題目分析,簡單來說就是尋找第一個大於等於target的位置,如果target大於所有數,則在vector最後插入該數。int searchInsert0(int A[], int n, int target) { if(target > A[n-1]) return n; int low = 0, high = n-1; while(low <= high) { int mid = low + (high - low)/2; if(A[mid] < target) low = mid + 1; else high = mid - 1; } return low; }
8. 給定一個有序陣列A,可含有重複元素,求絕對值最小的元素的位置
找第一個大於等於0的位置,然後和前一個元素的絕對值比較,返回絕對值較小的元素的位置
```c++
int searchMinAbs(int A[], int n)
{
int low = 0, high = n-1;
while(low < high)
{
int mid = low+((high-low)>>1);
if(A[mid] < 0)
low = mid+1;
else // A[mid] >= 0
high = mid;
}
/* 迴圈結束時,如果low != n-1,A[low] >= 0,如果low>0,A[low-1] < 0 */
if(low > 0 && abs(A[low-1]) < abs(A[low]))
return low-1;
else
return low;
}
```
9. 給定一個有序陣列A和一個有序(非降序)陣列B,可含有重複元素,求兩個數組合並結果中的第k(k>=0)個數字
這個題目出現了兩個陣列,有序的,不管怎樣我們就應該首先考慮二分查詢是否可行。若使用順序查詢,時間複雜度最低為O(k),就是類似歸併排序中的歸併過程。使用用二分查詢時間複雜度為O(logM+logN)
```c++
int findKthIn2SortedArrays(int A[], int m, int B[], int n, int k)
{
if(m <= 0) // 陣列A中沒有元素,直接在B中找第k個元素
return B[k];
if(n <= 0) // 陣列B中沒有元素,直接在A中找第k個元素
return A[k];
int i = (m-1)>>1; // 陣列A的中間位置
int j = (n-1)>>1; // 陣列B的中間位置
if(A[i] <= B[j]) // 陣列A的中間元素小於等於陣列B的中間元素
{
/*
設x為陣列A和陣列B中小於B[j]的元素數目,則i+1+j+1小於等於x,
因為A[i+1]到A[m-1]中還可能存在小於等於B[j]的元素;
如果k小於i+1+j+1,那麼要查詢的第k個元素肯定小於等於B[j],
因為x大於等於i+1+j+1;既然第k個元素小於等於B[j],那麼只
需要在A[0]~A[m-1]和B[0]~B[j]中查詢第k個元素即可,遞迴呼叫下去。
*/
if(k < i+1+j+1)
{
if(j > 0)
return findKthIn2SortedArrays(A, m, B, j+1, k);
else // j == 0時特殊處理,防止死迴圈
{
if(k == 0)
return min(A[0], B[0]);
if(k == m)
return max(A[m-1], B[0]);
return A[k] < B[0] ? A[k] : max(A[k-1], B[0]);
}
}
/*
設y為陣列A和陣列B中小於於等於A[i]的元素數目,則i+1+j+1大於等於y;
如果k大於等於i+1+j+1,那麼要查詢到第k個元素肯定大於A[i],因為
i+1+j+1大於等於y;既然第k個元素大於A[i],那麼只需要在A[i+1]~A[m-1]
和B[0]~B[n-1]中查詢第k-i-1個元素,遞迴呼叫下去。
*/
else
return findKthIn2SortedArrays(A+i+1, m-i-1, B, n, k-i-1);
}
// 如果陣列A的中間元素大於陣列B的中間元素,那麼交換陣列A和B,重新呼叫即可
else
return findKthIn2SortedArrays(B, n, A, m, k);
}
```
10. 一個有序陣列,可以含有重複元素,在某一個位置發生了旋轉後,求target在變化後的陣列中出現的位置,不存在則返回-1
如 0 1 2 4 5 6 7 可能變成 4 5 6 7 0 1 2
我們先比較中間元素是否是目標值,如果是返回位置。如果不是,我們就應該想辦法將搜尋區間減少一半。因為存在旋轉變化,所以我們要多做一些判斷。我們知道因為只有一次旋轉變化,所以中間元素兩邊的子陣列肯定有一個是有序的,那麼我們可以判斷target是不是在這個有序的子陣列中,從而決定是搜尋這個子陣列還是搜尋另一個子陣列。
```c++
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;
}
```
11. 一個有序陣列,可以含有重複元素,在某一個位置發生了旋轉後,求target在變化後的陣列中出現的位置,不存在則返回-1
直接上程式碼:
```c++
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] && A[mid] == A[high]) {
for( int i = low + 1; i < high; i++) {
if (target == A[i]) return i;
}
}
if(A[mid] >= A[low])
{
// low ~ mid 是升序的
if(target >= A[low] && target < A[mid])
high = mid-1;
else
low = mid+1;
}
else if(A[mid] <= A[high])
{
// mid ~ high 是升序的
if(target > A[mid] && target <= A[high])
low = mid+1;
else
high = mid-1;
}
}
return -1;
}
```
12. 一個有序陣列,沒有重複元素,在某一個位置發生了旋轉後,求最小值所在位置
我們用兩個指標分別指向陣列第一個元素和最後一個元素,然後尋找中間元素。如果中間元素位於前面遞增子陣列彙總(A[mid] > A[low]), 則最小元素應該位於中間元素的後面。 我們可以把第一個指標指向中間元素位置,在縮小尋找空間的同時,第一個指標仍然位於前面的遞增子陣列中。
如果中間元素位於後面的遞增子陣列中,那麼中間元素應該小於最後一個元素即A[low] < A[high]。則最小元素應該位於中間元素的前面(或者中間元素本身),我們可以把第二個指標指向中間元素。
這樣在縮小尋找範圍的同時,第一個指標一定指向前方的遞增子陣列,第二個指標一定指向後面的遞增子陣列。最終他們會指向相鄰的兩個元素,第二個指標指向的恰好是最小元素,迴圈終止。
```c++
int searchMinInRotatedArray(int A[], int n)
{
if(n == 1)
return 0;
int low = 0, high = n-1;
int mid = low;
while(A[low] > A[high])
{
cout << low << " " << high << endl;
if(high - low == 1) {
mid = high;
break;
}
mid = low + (high - low)/2;
if(A[mid] > A[low]) low = mid;
else if(A[mid] < A[high]) high = mid;
}
return mid;
}
```
我們用兩個指標分別指向第一個和最後一個元素。然後尋找中間元素,如果A[mid] < A[low],則最小值在low ~ mid範圍內,我們將第二個指標指向中間元素位置;否則最小元素在mid ~ high範圍內,我們將第一個元素指向中間位置。當兩個指標兩個相鄰位置時,則迴圈終止。
```c++
int searchMinInRotatedArray1(int A[], int n)
{
if(n == 1)
return 0;
int low = 0, high = n-1;
while(low < high - 1) // 保證mid != low且mid != high
{
int mid = low+((high-low)>>1);
if(A[mid] < A[low]) // 最小值在low~mid
high = mid;
else // A[mid] > A[low], // 最小值在mid和high之間
low = mid;
}
return A[low] < A[low+1] ? low : low+1;
}
```
13.一個有序陣列,可能含有重複元素,在某一個位置發生了旋轉後,求最小值所在位置
[1, 2, 2, 2, 2], [2, 1, 2, 2, 2], [2, 2, 1, 2, 2], [2, 2, 2, 1, 2], [2, 2, 2, 2, 1]這些都是有第一個陣列旋轉一次變化來的,我們不能通過11提到的兩種方法確定是否存在元素1
對11中提到的第一個方法,我們進行改善:
```c++
int searchMinInRotatedArray_duplicate(int A[], int n)
{
if(n == 1)
return 0;
int low = 0, high = n-1;
int mid = low;
while(A[low >= A[high]])
{
if(high - low == 1) {
mid = high;
}
mid = low + (high - low)/2;
// 如果下表 low, index, high 元素相等,此時只能順序查詢
if(A[mid] == A[low] && A[mid] == A[high])
return minInorder(a, low, high);
if(A[mid >= A[low]]) low = mid;
else if(A[mid] <= A[high]) high = mid;
}
return mid;
}
int minInorder(int A[], int low, int high) {
int res = A[low];
int index;
for(int i = low + 1; i < high; i++) {
if (res > A[i]) res = A[i], index = i;
}
return index;
}
```
對第二個方法進行修改,我還沒有想到,有想法的同學可以評論或聯絡我
14. 一個有序陣列,可能含有重複元素,在某一個位置發生了旋轉後,求第k(k > 0)小元素
利用上一題的解答,求出最小值所在位置後,便可以求出第k小元素
```c++
int searchKthInRotatedArray(int A[], int n, int k)
{
int posMin = searchMinInRotatedArray_duplicate(A, n);
return A[(posMin+k-1)%n];
}
```