二分查找
二分查找的代码,总是一看就懂,一写就费。究其原因,二分难就难在其复杂的边界处理上。看了一些博客和总结之后,可以将二分的写法总结为两种方法,根据搜索区间分为两类:左闭右开和左闭右闭。
这里谈谈我自己在学习过程中对这两种写法的理解。首先,这两种思路并不是矛盾的,对于一个查找问题,使用两个中的任何一个都可以。其次,我认为代码在于易读,而不在于精简,在理解了某种思路的基础上一点点对代码精简是没有问题的,但是不能一上来就是王炸级别的几行代码,完全丧失了代码的易读性。最后,我更习惯使用左闭右闭的写法,更符合自己的思维方式。
1. 左闭右闭
不考虑bug-free的一般情况下,查找排序数组中的target的模板如下。
func getFirst(nums []int, k int) int {
left, right := 0, len(nums) - 1
for left <= right {
mid := left + (right - left) / 2
if nums[mid] == k {
return mid
} else if nums[mid] < k {
left = mid + 1
} else {
right = mid - 1
}
}
return -1
}
- 初始条件,
right := len(nums) - 1 - 循环不变量,
left <= right - left和right的 更新逻辑
if nums[mid] == k {
return mid
} else if nums[mid] < k {
left = mid + 1
} else {
right = mid - 1
}
二分查找的难点就在于上面的三个细节的确定。因为经常看到初始条件为right = len(nums), 循环不变量为left < right,更新right时right = mid等情况。其实,完全没必要将这些点混淆,理解模板中左闭右闭的写法后,再针对一些特殊情况(例如有重复元素,答案不存在等)进行一些处理。
事实上,输入的数组并不永远都是完美的,可能出现如 有重复元素,答案不存在,区间为空,搜索上下界 等特殊情况。
// 查找最左侧
func getFirst(nums []int, k int) int {
left, right := 0, len(nums) - 1
for left <= right {
mid := left + (right - left) / 2
if nums[mid] == k {
if mid == 0 || nums[mid-1] != k {
return mid
} else {
right = mid - 1
}
} else if nums[mid] < k {
left = mid + 1
} else {
right = mid - 1
}
}
return -1
}
// 查找最右侧
func getLast(nums []int, k int) int {
left, right := 0, len(nums) - 1
for left <= right {
mid := left + (right - left) / 2
if nums[mid] == k {
if mid == len(nums) - 1 || nums[mid+1] != k {
return mid
} else {
left = mid + 1
}
} else if nums[mid] < k {
left = mid + 1
} else {
right = mid - 1
}
}
return -1
}
很多技术博客中,都建议使用左闭右开 [left, right) 的情况。原因是循环结束时left==right,这样就不用关心是返回left还是返回right了。实际上,只要理解了最终的代码中的过程,通过[left, right]的方式根据需求决定返回left还是right会更加灵活。
本文探讨了二分查找算法的两种常见实现方式——左闭右闭和左闭右开。作者强调了理解这两种方法的重要性,并指出代码的易读性比精简更重要。在实践中,左闭右闭的写法更符合作者的思维方式,而左闭右开则常用于简化循环结束条件。文章还提供了处理特殊情况(如重复元素、不存在答案)的示例代码,强调了在不同场景下选择合适方法的灵活性。
9169

被折叠的 条评论
为什么被折叠?



