平时我们写程序的时候,总是莫名其妙的出现Bug,一鼓作气的写完一个接口后,经常被队友喷「你怎么一回事?结果与预期不一致啊」
老脸一红,然后一个人藏在角落调试半天,才发现原来是某一个边界条件没处理好,导致Bug重重,今天我们从二分查找算法,来聊聊如何写出正确的程序
像我们熟知的二分查找算法,在1946年就被提出来了,但是到了1962年才出现了完全没有Bug的二分查找法
二分查找法的思想并不复杂,我们很多人张口就能来,但是为什么间隔了辣么多年,才诞生完全没有Bug的二分查找算法呢?
二分查找算法,难就难在边界条件的处理上,我们经常写出Bug,往往也是出现在边界条件没处理好而导致
因为学习一个算法的思想是很简单的,但是让思想落地,写出完全没有Bug的二分查找,却并不是一件容易的事情
所以我们在写程序时,首先就需要明确的边界的意义,在程序内部的实现时,就是要不断的维护这个边界的意义
二分查找法的思想在这简单复述一下
1.在一个有序的数组中,查询一个目标值target,
2.如果目标值target 比数组的中间值要大,那么我们就往数组中间值后面的范围内去查询
3.如果目标值target 比数组的中间值要小,我们就往数组中间值前面的范围内查询
4.如此往复的执行2,3步,直到查询到目标值target 等于其数组范围内的某一个值,然后返回其值对应的索引位置,又或者数组中压根就没有我们想要找的目标值target
好了,说完了算法的思想,我们就来动手实践一下把。在这里我就用Go语言来实现了,给你们宣传了一个多月让你们学Go语言,还给你们找了那么多学习资料,有从小白到中高级的Go语言视频,也有从小白到大牛的书籍资料。你们要现在一点都没看,那可就枉费我一片苦心了
想学Golang,但还没有头绪的童鞋,看完文章后直接在公众号后台回复「Go」,即可领取全部学习资源,别忘了回来点个在看啊
把算法思想落地
我们先定义一个二分查找的函数MyBinarySearch
,之所以叫这名字-,-是因为Go有个binary
包,里面有叫BinarySearch
的函数,咱们需要跟它区分开来,否则编译器会提示我们在瞎搞(仿佛在跟我说:你是不是傻,有现成的不用,偏要自己搞)
func MyBinarySearch(arr []int, target int) (middle int) {
}
我们传入一个数组,以及一个要查询的目标值target,如果target 存在于数组中,我们则返回它对应的索引位置
二分查找,容易出问题的地方就是边界问题,那咱们先定义先设定一个边界,我们需要在边界范围内去查询
// 明确边界的意义,在 [left,right] 中寻找target
left, right := 0, len(arr)-1
我们要铭记,我们在程序中,每一个变量都是有意义的,我们需要明确每一个变量的意义,我们的查询范围,就是在[left,right]
中去查询目标值target ,看清楚了,我在这定义的是一个闭区间,也就包含了left 和right 本身所在的位置
明确边界变量的意义后,我们在后面的查找过程中,还需要不断的维护这个意义
我们来循环判断,边界是否有意义,如果这个边界的范围区间包含了有效的整数,则代表这个边界是有意义的,所以当边界存在意义时,我们循环判断此时的中间值是否等于目标值target
for left <= right {
// 在 [left,right] 中寻找target
middle = (left + right) / 2 // 中间值的索引位置
}
在这可能有人不解,为什么要用<=
呢?,比如闭区间[8,8] ,那么这个区间依然是有意义的,因为有一个整数8 ,如果去掉=
,那就变成了[8,8),而此时这个区间范围内,是没任何整数的
现在我们就需要来判断,目标值target 于现在查询范围内中间值的关系,看它是等于,还是小于又或者是大于中间值
如果相等,那就好办了,这不就是我们要找的嘛~直接返回中间值的索引位置就好了
if arr[middle] == target {
return
}
那如果不满足,我们就只好再来判断,目标值是小于中间值还是大于中间值了
如果目标值target 小于中间值,那么我们就需要缩小查询的区间范围了
这个时候,我们查询范围就发生了改变,右侧要范围要缩短到中间值的位置,用Code来表达,就是right = middle - 1
if target < arr[middle] {
// 如果target < 中间值 则代表我们要在左边区间查找
right = middle - 1
}
你可能会问,为什么要-1
呢?因为我们已经明确知道target < arr[middle]
,也就是middle
所在的位置,不可能是我们要找的位置,所以我们就需要再往左侧移动一位
说到现在,我想你现在已经彻底弄懂了left
与right
所代表的含义,那么当target > arr[middle]
时,我们的right
自然也要在middle
的位置上往右移动一位
if target > arr[middle] {
// 如果target > 中间值,则代表我们要在右边区间查找
left = middle + 1
}
上面的Code全部拼凑起来,就是如下所示,如果当目标值不在数组中时,我们就返回 -1
// MyBinarySearch 二分查找法,在有序数组中查询目标元素target,并返回元素对应的索引值
func MyBinarySearch(arr []int, target int) (middle int) {
left, right := 0, len(arr)-1 // 明确边界的意义,在[left,right]中寻找target
for left <= right {
middle = (left + right) / 2
// 当left > right 时,意味着边界不存在,则代表数组中没有目标值target
// 所以当left <= right 时,我们就遍历数组
if arr[middle] == target {
return
}
if target < arr[middle] {
// 如果target < 中间值 则代表我们要在左边区间查找
right = middle - 1
}
if target > arr[middle] {
// 如果target > 中间值,则代表我们要在右边区间查找
left = middle + 1
}
}
return -1
}
我还是简单说下Go语言的函数语法吧,因为我在定义函数的时候,就写明了返回值的变量名middle
,给返回值声明了变量名后,执行函数时会给返回值的变量初始化为0值,return
返回值也会自动指定对应的变量名
所以我在函数中没有定义middle
以及把return middle
直接写成了return
循环不变量
我猜很多人都是第一次听说这个专业术语,其实我上面已经多次强调这个术语所代表的含义
现在再刻意解释一下,仔细看完上面的内容的你,现在我一说,你肯定就能懂
我们在上面的二分查找中,一直在循环left <= right
,这就是循环,当left <= right
时,我们的循环不会终止
而不变量是什么呢?left
以及right
不都是变量么?你怎么说它的不变量呢?
left
以及right
的值虽然一直都是在改变的,但是它们所代表的含义却是一直都没有改变过,因为我们寻找的永远都是在[left,right]
这个闭区间中寻找我们的目标值
程序中left
以及right
的变化,也只是在不断缩小这个闭区间的范围,并没有改变其声明时所代表的含义,注意我说的,是没有改变声明时的含义
所以要想写出正确的程序,在声明每一个变量时,我们都需要明确其含义,变量在改变时,我们只能改变其数值,而不能改变其变量所代表的意义
一旦声明其变量的意义,后面的程序都是在维护其意义。就像我们每一个人一生下来就有意义,而我们人生的经历,都是在为了完成人生意义而必须所拥有的铺垫
之所以我们的Bug越写越多,多半是因为对变量的含义理解的不透彻,并且经常声明一些无意义的变量所导致(可能你认为有些变量并不是无意义的变量,但事实多半如此)
你以为到这就完了?
二分查找从提出到最后无Bug实现,期间经历了16年,要这么简单的就完结了。。。怕是没办法瞒天过海16年了
我们在上面实现的二分查找还是有个Bug隐藏着,那就是middle = (left +right) / 2
如果当left
和right
数值足够大的时候,我们再这样求和时,int
类型就越界啦~
那越界了咋搞呢?还记得我们之前有说过如何进行大整数求和吧?嗯,记得的话,那你还挺不错的,如果你用那种方式去解决它们求中间值时造成的越界…我只能说…你是不是傻啊…
其实我们稍微改一改middle 的求值方式就好了,从加法改成了减法,那么自然也就不会越界啦~
就是改成这样子middle = left + (right - left) / 2
就好了~
我这几十年之后的马后炮,真酸爽
写在最后
之前说的每周算法题,说实话,停更有好长一段时间了
今天这篇继上一篇,间隔时间估摸着有好几周了…emmmm
不是我不想在这个系列上保持持续更新,而是这货你们连个在看都不点,让我没有欲望继续写下去
你仔细看看我之前的每周算法题系列,就会发现,没有一篇是只说算法的,都是夹杂着其它的思想,或者是开发实践,又或者是一些别的经验
如果只是拿着LeetCode上的题目实现一遍,然后跟你们讲解一遍,我觉得这样做是没用任何意义,因为你们完全可以去Github上看人家的solution,一次性看几十个题都没问题,也可以直接去LeetCode一天刷个几十题,何苦还需要在我这看我BB叨呢…
最后为了证明你彻底掌握循环不变量的关键思想,给你们留个问题吧
我们在之前声明时left = 0
,right = len(arr) - 1
,现在将right
改成right = len(arr)
,我们后面在处理区间时,当target < arr[middle]
时,left
和right
又该怎么赋值呢?
给你们一个提示,我们在写right = len(arr)-1
时的查询范围是[left,right]
,而right = len(arr)
时,查询的范围是[left,right)
,注意查询区间的改变~
之所以给你们留个问题,是因为只看不做还不思考,是很难彻底弄明白的~
在底下留言给出你的思考吧!
微信扫码关注公众号「闹闹吃鱼」,每周都有好分享,还可领取学习资源哦~不仅仅只是技术!
今天的文章从二分查找算法来看如何写出正确的程序分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/19726.html