基本概念
-
二分查找: 思路很简单,细节很重要
- 给mid加1还是减1
- while中是使用 <= 还是 <
- 二分查找的场景:
二分查找框架
int binarySearch(int[] nums, int target) {
int left = 0, right = ...;
while (...) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}
- 不要出现else, 而是将所有情况用else if写清楚,这样可以清楚地展现所有细节
- 计算mid时需要防止溢出:
- 使用
l
e
f
t
+
r
i
g
h
t
−
l
e
f
t
2
left +\frac{right - left}{2}
left+2right−left和
l
e
f
t
+
r
i
g
h
t
2
\frac{left + right}{2}
2left+right的结果相同
- 但是有效防止了left和right值太大相加导致的溢出问题
寻找一个数
-
寻找一个数: 搜索一个数,如果存在,返回索引,否则返回-1
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
}
}
return -1;
}
寻找左侧边界
int left_bound(int[] nums, int target) {
if (nums.length == 0) {
return -1;
}
int left = 0;
int right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
return left;
}
-
问题1: 为什么while是 < 而不是 <= ?
- 因为初始化时right = nums.length, 而不是nums.length - 1
- 因此每次循环的 [搜索区间] 是 [left, right) 左闭右开
-
while (left < right) 的循环终止条件是 left == right, 此时搜索区间为 [left, left), 搜索区间为空,所以可以正确终止
-
问题2: 为什么基本二分查找算法的right是nums.length - 1? 搜索左右边界的right要写成nums.length使得搜索区间编程左闭右开呢?
- 对于搜索左右边界的二分查找算法,通常情况下使用right为nums.length - 1
- 如果使用两端都闭的写法反而更简单
-
问题3: 为什么没有返回 -1的操作? 如果nums中不存在target这个值,怎么处理?
- 对于有序数组nums[2, 3, 5, 7], target = 1, 算法会返回0, 表示 : nums中小于1得元素有0个
- 对于有序数组nums[2, 3, 5, 7], target = 8, 算法会返回4, 表示 : nums中小于8的元素有4个
- 由此可见,函数的返回值即left变量的值的取值区间是闭区间 [0, nums.length]. 增加代码在正确的时候return -1 :
while (left < right) {
...
}
if (left == nums.length) {
return -1;
}
return num[left] == target ? left : -1;
-
问题4: 为什么left = mid + 1, right = mid? 和基本二分查找算法的不一样?
- 因为 [搜索区间] 是 [left, right) 左闭右开区间, 所以当nums[mid] 被检测之后,下一步的搜索区间应该去掉mid分割成的两个区间,即为 [left, mid) 或者 [mid + 1, right)
-
问题5: 为什么该算法能够搜索左侧边界?
- 搜索左侧边界算法的关键在于对于nums[mid] == target情况的处理:
if (nums[mid] == target) {
right = mid;
}
- 找到target时,不是立即返回,而是缩小 [搜索区间] 的上界right, 然后在 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的
-
问题6: 为什么返回left而不是right?
- 返回left和返回right都是一样的
- 因为while的循环终止条件是left == right
-
问题7: 可不可以将right初始值设置为nums.length - 1, 也就是继续使用两边都闭的[搜索区间]? 这样可以将基本二分查找算法和寻找左侧边界的二分查找算法在某种程度上统一起来?
- 可以,只要理解 [搜索区间], 有效避免漏掉元素就可以
- 因为 [搜索区间] 两端都是闭区间,所以right的初始值设置为nums.length - 1,while的终止条件应该是left = right + 1, 所以while中的条件应该使用while(left <= right)
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right -left) / 2;
...
}
}
- 因为 [搜索区间] 是两端都闭的,并且是搜索左侧边界,修改left和right的更新逻辑:
if (nums[mid] == target) {
right = mid -1;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
}
- 由于while的循环终止条件为left == right + 1, 当target比nums中的所有元素都大时,会引起索引越界. 所以最后返回结果的代码应该检查越界情况:
if (left >= nums.length || nums[left] != target) {
return -1;
}
return left;
- 两边都闭的 [搜索区间] :
int left_bound(int[] nums, int target) {
if (nums.length == 0) {
return -1;
}
int left = 0, right = nums.length -1;
while (left < = right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
}
}
if (left >= nums.length || nums[left] != target) {
return -1;
}
return left;
}
- 这种算法都是两端都闭的 [搜索区间], 而且最后返回的也是left变量的值
寻找右侧边界
int right_bound(int[] nums, int target) {
if (nums.length == 0) {
return -1;
}
int left = 0, right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
left = mid + 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] < target) {
right = mid;
}
}
return left - 1;
}
-
问题1: 为什么这个算法能够找到右侧边界?
- 搜索右侧边界算法的关键在于 对于nums[mid] == target情况的处理:
if (nums[mid] == target) {
left = mid + 1;
}
- 当nums[mid] == target时,不是立即返回,而是增大 [搜索区间] 的下界left, 使得区间不断向右搜索,达到锁定右侧边界的目的
-
问题2: 为什么最后返回left-1而不是像左侧边界的函数返回left?既然是搜索右边界,应该返回right才对?
-
while循环的终止条件是left == right, 所以left和right是一样的,可以使用right - 1
- 之所以返回right - 1, 这是搜索右侧边界的一个特殊点,关键在于nums[mid] == target的条件判断:
if (num[mid] == target) {
left = mid + 1;
}
- 因为对left的更新必须是left = mid + 1. 这样while循环结束时 ,nums[left] 一定不等于target, 但是num[left - 1] 可能等于target
-
问题3: 为什么没有返回-1的操作? 如果nums中不存在target这个值,如何处理?
- 因为while循环的终止条件是left == right, 也就是说left的取值范围是 [0, nums.length], 可以添加代码,正确地返回 -1 :
while (left < right) {
...
}
if (left == 0) {
return -1;
}
return nums[left - 1] == target ? (left - 1) : -1;
-
问题4: 是否可以将这个算法的[搜索区间]统一成两端都闭的形式? 保证二分搜索算法的统一?
int right_bound(int[] nums, int target) {
if (nums.length == 0) {
return -1;
}
int left = 0, right = nums.length -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
left = mid + 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
if (right < 0 || nums[right] != target) {
return -1;
}
return right;
}
- 当target比所有元素都小时 ,right会被减到 -1, 所以需要在最后防止越界
统一二分查找算法
基本二分查找算法
- 因为初始化时 : right = nums.length - 1
- 所以决定了 [搜索区间] 是 : [left,right]
- 所以决定了 while(left <= right)
- 同时也决定了left = mid + 1和right = mid - 1
- 因为只需要找到一个target的索引即可
- 所以当nums[mid] == target时立即返回
寻找左侧边界的二分查找算法
- 因为初始化时 : right = nums.length
- 所以决定了 [搜索区间] 是 : [left, right)
- 所以决定了while (left < right)
- 同时也决定了left = mid + 1和right = mid
- 因为需要找到target的最左侧索引
- 所以当nums[mid] == target时不要立即返回
- 而是要收缩右侧边界以锁定左侧边界
寻找右侧边界的二分查找算法
- 因为初始化时 : right = nums.length
- 所以决定了 [搜索区间] 是 : [left,right)
- 所以决定了 while (left < right)
- 同时也决定了left = mid + 1和right = mid
- 因为需要找到target的最右侧索引
- 所以当nums[mid] == target时不要立即返回
- 而是要收缩左侧边界以锁定右侧边界
- 因为收缩左侧边界时 : left = mid + 1
- 所以最后无论返回left还是right, 最终都必须 - 1
统一二分查找算法代码实现
- 对于寻找左右边界的二分查找算法,通常使用左闭右开的 [搜索区间]
- 可以根据逻辑将[搜索区间]全都统一成两端都闭的区间,便于记忆,只要修改两处就可以变化出三种写法:
int binary_search(int[] nums, int target) {
if (nums.length == 0) {
return -1;
}
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
return mid;
}
}
return -1;
}
int left_bound(int[] nums, int target) {
if (nums.length == 0) {
return -1;
}
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
right = mid - 1;
}
}
if (left >= nums.length || nums[left] != target) {
return -1;
}
return left;
}
int right_bound(int[] nums, int target) {
if (nums.length == 0) {
return -1;
}
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
left = mid + 1;
}
}
if (rigth < 0 || nums[right] != target) {
return -1;
}
return right;
}
二分查找算法总结
- 分析二分查找算法时,不要出现else, 全部展开,使用else if更加方便
- 注意 [搜索区间] 和while的终止条件,如果存在漏掉的元素,要在方法最后返回前做检查
- 如果定义左闭右开的 [搜索区间] 搜索左右边界,只要在nums[mid] == target时对代码作修改即可.注意寻找右侧边界时返回值要 - 1
- 如果将 [搜索区间] 全都统一成两端都闭,只要在nums[mid] == target时对代码和返回的逻辑作修改即可