二分查詢插入點
二分查詢不僅可用於搜尋目標元素,還可以解決許多變種問題,比如搜尋目標元素的插入位置。
無重複元素情況
Question
給定一個長度為n的有序陣列nums
和一個元素target
,陣列不存在重複元素。現將target
插入陣列nums
中,並保持其有序性。若陣列中已存在元素target
,則插入到其左方。請返回插入後target
在陣列中的索引。
問題一:當陣列中包含target
時,插入點的索引是否就是該元素的索引?
題目要求將target
插入到相等元素的左邊,這意味著插入的target
替換了原來target
的位置。也就是說,當陣列包含target
時,插入點的索引就是該target
的索引。
問題二:當陣列中不存在target
時,插入點是哪個元素的索引?
進一步思考二分查詢過程(m為中點索引):當nums[m] < target
時,這意味著指標i在向大於等於target
的元素靠近。同理,指標j始終在向小於等於target
的元素靠近。
因此二分結束時一定有:i指向首個大於target
的元素,j指向首個小於target
的元素。易得當陣列不包含target
時,插入索引為i。
程式碼示例如下:
/*二分查詢插入點(無重複元素)*/
int binarySearchInsertionSimple(vector<int> &nums, int target){
int i = 0, j = nums.size() - 1;
while (i <= j){
int m = i + (j - i) / 2;
if (nums[m] < target)
i = m + 1;
else if (nums[m] > target)
j = m - 1;
else
return m; // 找到target 返回插入點 m
}
return i; // 未找到 target,返回插入點 i
}
存在重複元素的情況
假設陣列中存在多個target
,則普通二分查詢只能返回其中一個target
的索引,而無法確定該元素的左邊和右邊還有多少個target
。
題目要求將目標元素插入到最左邊,所以我們需要查詢陣列中最左一個target
的索引。
實現步驟:
- 執行二分查詢,得到任意一個
target
索引,記為k。 - 從索引k開始,向左進行線性遍歷,當找到最左邊的
target
時返回。
此方法雖然可用,但其包含線性查詢,因此時間複雜度為O(n)。當陣列中存在很多重複的target
時,該方法效率很低。
現考慮擴充二分查詢程式碼。如圖所示,整體流程保持不變,每輪現計算中點索引m,再判斷target
和nums[m]
的大小關係,分以下幾種情況:
- 當
nums[m] < target
或nums[m] > target
時,說明還沒有找到target
,因此採用普通二分查詢的縮小區間操作,從而使指標i和j向target
靠近。 - 當
nums[m] == target
時,說明小於target
的元素在區間[i,m-1]中,因此採用j = m-1來縮小區間,從而使指標j向小於target
的元素靠近。
迴圈完成後,i指向最左邊的target
,j指向首個小於target
的元素,因此索引i就是插入點。
觀察以下程式碼,其中判斷分支nums[m] > target
和nums[m] == target
的操作相同,因此兩者可以合併。即便如此,我們仍然可以將判斷條件保持展開,因為邏輯更加清晰、可讀性更好。
/*二分查詢插入點(存在重複元素)*/
int binarySearchInsertion(vector<int> &nums, int target){
int i = 0, j = nums.size() - 1;
while (i <= j){
int m = i + (j - 1) / 2;
if (nums[m] < target)
i = m + 1; // target 在區間 [m+1, j] 中
else if (nums[m] > target)
j = m - 1; // target 在區間 [i, m-1] 中
else
j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中
}
// 返回插入點
return i;
}
總的看來,二分查詢無非就是給指標i和j分別設定搜尋目標,目標可能是一個具體元素(例如target
),也可能是一個元素範圍(如小於target
的元素)。
在不斷的迴圈二分中,指標i和j都逐漸逼近預先設定的目標。