1 二分法學習筆記總結
參考
https://leetcode-cn.com/problems/search-insert-position/solution/te-bie-hao-yong-de-er-fen-cha-fa-fa-mo-ban-python-/
https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/solution/er-fen-cha-zhao-suan-fa-xi-jie-xiang-jie-by-labula/
1.1 取中位數索引的方法
- 傳統的方法
int mid = (left + right) /2
,在 left 和 right 比較大的時候, 兩者相加很可能超過 int 類的最大值,即發現整型溢出; - 改進
int mid = left + (right - left) /2
- 最好的寫法
int mid = (left + right) >>> 1
>>
, 右移時,丟棄右邊指定位數,左邊補上符號位;>>>, 無符號右移運算符
,丟棄右邊指定的位數,左邊補上 0 。所以對負數右移,可以變成正數;
1.2 循環條件
while (left <= right)
,退出循環時,要考慮返回 left 和 right;
2 二分查找模板思想
2.1 循環條件
while (left < right)
, 在退出循環的時候,一定有left == right
,這時返回 left 或者 right 都可以;- 退出循環的時候還有一個數沒有考慮,即退出循環之前索引 left 或者 索引 right 上的值?這點可以等到退出循環以後考慮,甚至有時不用考慮,就確定它是目標數;
2.2 左、右邊界值
- 如果左、右邊界值不包括目標數,會出現錯誤;
- 如果 left 和 right 表示的是數組的索引,要考慮“索引有效性的問題”,即 索引是否越界;
2.3 中位數索引
- 中位數先寫
int mid = (left + right) >>> 1
,然後根據分支的情況,再做調整;
當數組個數是偶數的時候:
int mid = left + (right -left) / 2
, 得到左中位數的索引int mid = left + (right -left + 1) / 2
, 得到右中位數的索引
當數組個數是奇數的時候,二者都能得到最中間的元素
int mid = left + (right -left) / 2
等價於int mid = (left + right) >>> 1
int mid = left + (right -left + 1) / 2
等價於int mid = (left + right + 1) >>> 1
2.4 邏輯分支
- 先寫邏輯上最容易想到的分支,這個分支邏輯通常是排除中位數的邏輯;
- 例如,求 x 的平方根時,如果一個數的平方小於或者等於 x, 那麼這個數可能是也可能不是 x 的平方根;但是可以肯定的是,如果一個數的平方大於 x ,這個數肯定不是 x 的平方根;
- 所以先寫 “容易想到”的分支,排除中位數之後,通常另一個分支就不排除中位數,不必具體考慮另一個分支的邏輯的具體含義;
- 在循環內只寫 2 個分支,一個分支排除中位數,另一個分支不排除中位數,循環中不單獨對中位數做判斷;
- “夾逼法”,沒有必要在每一輪循環開始前單獨判斷當前中位數是否是目標元素,因此分支數少了一支,代碼執行效率高
- 可能出現以下 2 種模板
2.5 左、右中位數的選取
- 根據分支邏輯選擇中位數的類型,選擇的標準是避免出現死循環
當候選區間只剩下兩個元素的時候,進入中位數是左邊界的邏輯,由於左邊界不收縮,下一次循環還選左中位數,左邊界還不收縮,如此下去,進入了死循環。解決方案:使用右中位數;
當候選區間只剩下 2 個元素的時候,一旦進入中位數是右邊界的邏輯,由於右邊界不收縮,下一次循環還選右中位數,右邊界還不收縮,如此下去,進入了死循環;解決方法:換成左中位數
2.6 退出循環的處理
- 退出循環的時候,可能需要對“夾逼法”剩下的那個數單獨做一次判斷;
- 如果能確定候選區間裏一定有目標元素,則不需要這一步。例如:x 的平方根一定在 [0,x] 內,所以退出
while ( left < right)
後,不必單獨判斷 left 或者 right 是否符合; - 如果不確定候選區間裏是否存在目標元素,則需要單獨做一次判斷。因爲目標數可能不在數組中,當候選區間變成一個數的時候,需要單獨判斷這個數是否爲目標數。
3 死循環代碼測試
public static int testSqrt(int x) throws InterruptedException {
if (x == 1 || x == 0) {
return x;
}
int left = 1;
int right = x / 2;
while (left < right) {
Thread.sleep(1000);
System.out.println("left = " + left + ",right= " + right);
// 在分支左區間不發生收縮時,中位數要選右中位數
int mid = (left + right) >>> 1;
System.out.println("mid = " + mid);
int square = mid * mid;
if (square > x) {
System.out.println("進入 right = mid -1 分支");
right = mid - 1;
} else {
System.out.println("進入 left=mid 分支");
left = mid;
}
System.out.println();
}
return left;
}
public static void main(String[] args) throws InterruptedException {
int res = testSqrt(9);
System.out.println(res);
}