演算法與資料結構——二分查詢插入點

风陵南發表於2024-09-12

二分查詢插入點

二分查詢不僅可用於搜尋目標元素,還可以解決許多變種問題,比如搜尋目標元素的插入位置。

無重複元素情況

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的索引

實現步驟:

  1. 執行二分查詢,得到任意一個target索引,記為k。
  2. 從索引k開始,向左進行線性遍歷,當找到最左邊的target時返回。

此方法雖然可用,但其包含線性查詢,因此時間複雜度為O(n)。當陣列中存在很多重複的target時,該方法效率很低。

現考慮擴充二分查詢程式碼。如圖所示,整體流程保持不變,每輪現計算中點索引m,再判斷targetnums[m]的大小關係,分以下幾種情況:

  • nums[m] < targetnums[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] > targetnums[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都逐漸逼近預先設定的目標。

相關文章