如何才能写出正确的程序?

闹闹吃鱼 · · 525 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

平时我们写程序的时候,总是莫名其妙的出现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所在的位置,不可能是我们要找的位置,所以我们就需要再往左侧移动一位

说到现在,我想你现在已经彻底弄懂了leftright所代表的含义,那么当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

如果当leftright数值足够大的时候,我们再这样求和时,int类型就越界啦~

那越界了咋搞呢?还记得我们之前有说过如何进行大整数求和吧?嗯,记得的话,那你还挺不错的,如果你用那种方式去解决它们求中间值时造成的越界…我只能说…你是不是傻啊…

其实我们稍微改一改middle 的求值方式就好了,从加法改成了减法,那么自然也就不会越界啦~

就是改成这样子middle = left + (right - left) / 2就好了~

我这几十年之后的马后炮,真酸爽

写在最后

之前说的每周算法题,说实话,停更有好长一段时间了

今天这篇继上一篇,间隔时间估摸着有好几周了…emmmm

不是我不想在这个系列上保持持续更新,而是这货你们连个在看都不点,让我没有欲望继续写下去

你仔细看看我之前的每周算法题系列,就会发现,没有一篇是只说算法的,都是夹杂着其它的思想,或者是开发实践,又或者是一些别的经验

如果只是拿着LeetCode上的题目实现一遍,然后跟你们讲解一遍,我觉得这样做是没用任何意义,因为你们完全可以去Github上看人家的solution,一次性看几十个题都没问题,也可以直接去LeetCode一天刷个几十题,何苦还需要在我这看我BB叨呢…

最后为了证明你彻底掌握循环不变量的关键思想,给你们留个问题吧

我们在之前声明时left = 0right = len(arr) - 1,现在将right改成right = len(arr),我们后面在处理区间时,当target < arr[middle]时,leftright又该怎么赋值呢?

给你们一个提示,我们在写right = len(arr)-1时的查询范围是[left,right],而right = len(arr)时,查询的范围是[left,right),注意查询区间的改变~

之所以给你们留个问题,是因为只看不做还不思考,是很难彻底弄明白的~

在底下留言给出你的思考吧!

微信扫码关注公众号「闹闹吃鱼」,每周都有好分享,还可领取学习资源哦~不仅仅只是技术!


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:闹闹吃鱼

查看原文:如何才能写出正确的程序?

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

525 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传