我将在这篇文章中带你了解我从头至尾解决编程问题的策略。我既在谷歌的日常工作中使用这一策略,也在和各种水平的编程人员(训练营、大学生和实习生等)合作时使用它(帮助他们学习和成长)。应用这一结构化的流程能尽可能地缩减令人沮丧的调试过程,在更少的时间里得到更清晰更正确的代码。
一步步介绍
我将使用一个示例练习题来进行说明。
问题:「给定两个字符串 sourceString 和 searchString,返回 searchString 出现在 sourceString 中的第一个索引。如果 searchString 未在 sourceString 中出现,则返回 -1。」
第一步:画出来
直接从写代码开始实际上是一个荒谬又懒惰的思路。在写一篇文章之前,你首先会搞清楚你的假设和证据,以确保你的论证是合理的。如果你不这样做,后面当你发现你写的内容整体无法合适地组合起来时,你又要重新开始,这会浪费很多时间。写代码也是这样,甚至可能更糟,就像把洗发水揉进眼睛里那么糟。
通常而言,一个问题的解决方案并不是浅显直观的,即使可能乍看起来很简单。在纸上琢磨一下能让你找到解决方案并验证该方案在一些不同场景中的效果。所有这些工作都应在写下任何一行代码之前完成。
所以先不要写代码。甚至不要去想代码。后面你会有足够的时间去添加分号和括号。现在你只需要搞清楚你作为一台人体计算机,会如何解决这个问题。
画图。使用箭头。在小方框里填写数字。只要能让你可视化这个问题,就去做吧。你的目标是解决问题,你能用纸张和铅笔尽情发挥,而不受限于键盘。
首先找一些简单的输入。比如,假设你的函数是「取一个字符串」:「abc」。搞清楚正确的结果是什么。然后去想「如何」解决这个问题以及所涉及到的步骤。
我们假设我们的字符串有以下值:
sourceString: "abcdyesefgh"
searchString: "yes"
我的想法就会像下面这样展开:
好吧,我看到 sourceString 包含 searchString。但我是怎么看出来的?嗯,我首先从 sourceString 的开头开始阅读,检查每 3 个字符构成的片段是否与词「yes」匹配,直到结束。比如,我看了 abc、bcd、cdy 等组合。当我看到索引 4 时,我找到了 yes,所以我确定找到了匹配并且是从索引 4 开始。
当我们写下我们的算法时,我们需要确保我们表达了一切并能够处理所有可能的情况。当我们确实能找到匹配时,返回正确答案当然很好,但我们也需要在没有匹配时也能返回正确答案。
我们再以另一对字符串为例:
sourceString: "abcdyefg"
searchString: "yes"
这里,我们同样首先从 sourceString 的开头开始阅读,检查每 3 个字符构成的片段是否与词「yes」匹配,直到结束。当我们看到索引 4 时,找到了「yef」,差点就匹配了,但却并没有完全匹配,因为第三个字符不同。所以我们继续检查,直到字符串末尾,最后确定其中没有匹配,因此返回 -1。
我们已经确定了解决这个问题的一系列步骤(在编程领域,我们称之为「算法」),我们也已经尝试了一些不同场景,每一次都得到了正确的结果。现在,我们已经很相信我们的算法有效了,现在是时候形式化这个算法了,这就是下一个步骤。
第二步:写成普通话
我们想想在第一步确定的算法,然后用平实易懂的语言把它写出来。这能使这些步骤显得具体,让我们在写代码时能够回顾参考。
从字符串开头开始检查
检查每 3 个字符构成的片段(具体字符数量视 searchString 而定)
如果其中有任何片段等于 searchString,就返回当前索引
如果直到该字符串结束也没找到任何匹配,就返回 -1
看起来不错哟!
第三步:写伪代码
伪代码并不是真正的代码,但却模仿了代码的结构。下面是我为上面的算法写的伪代码:
for each index in sourceString,
there are N characters in searchString
let N chars from index onward be called POSSIBLE_MATCH
if POSSIBLE_MATCH is equal to searchString, return index
at the end, if we haven't found a match yet, return -1.
我还可以让伪代码更接近代码一点,比如这样写:
for each index in sourceString,
N = searchString.length
POSSIBLE_MATCH = sourceString[index to index+N]
if POSSIBLE_MATCH === searchString:
return index
return -1
伪代码与代码的接近程度完全由你决定,随着时间的推移,你会发现最适合自己的风格!
第四步:将你能做到的部分翻译成代码
注:对于更简单的问题,这个步骤可以与上一步一起完成。
这是整个流程中我们第一次必须考虑句法、函数参数和语言规则。你也许没法写出所有一切,但没有关系,把你知道的部分写出来!
function findFirstMatch(searchString, sourceString) {
let length = searchString.length;
for (let index = 0; index < sourceString.length; index++) {
let possibleMatch = <the LENGTH chars starting at index i>
if (possibleMatch === searchString) {
return index;
}
}
return -1;
}
注意,我在这段代码中留了一些空白。这是故意的!我不确定 JavaScript 中切分字符串的句法是什么,所以我要在下一步查一下。
第五步:不要猜测
我看到代码新人常会犯一个错误:在互联网上找一些写着「可能有效」的东西,然后不加测试地将其插入到自己的程序中。你的程序中你不理解的片段越多,你就越不可能最后得到正确的解决方案。
每增加一个你不确定的东西,你的程序可能出错的方式都会翻一倍。对某事不确定?没有问题——如果你的代码无效,那么这里可能就是问题所在。
附注:你的程序可能出错的方式遵循梅森序列:a(n) = (2^n) — 1
首先测试你的新代码。在网上找东西是可以的,但你在将其插入你的程序之前应该在另一个单独的小空间里测试一下它,以确保其工作方式和你所想的一样。
在前一步中,我并不确定在 JavaScript 中该如何选择一个字符串的特定部分。所以我谷歌了一下:
https://www.google.com/search?q=how+to+select+part+of+a+string+in+javascript
第一个结果来自 w3schools,有点旧了,但通常比较可靠。
https://www.w3schools.com/jsref/jsref_substr.asp
基于这个结果,我猜想我应该使用
substr(index, searchString.length)
来提取 sourceString 中的各部分。但这只是一个假设,仅此而已。所以,我首先创建了一个小例子来测试其行为。
>> let testStr = "abcdefghi"
>> let subStr = testStr.substr(3, 4); // simple, easy usage
>> console.log(subStr);
"defg"
>> subStr = testStr.substr(8, 5); // ask for more chars than exist
"i"
现在,我已经确定这个函数的效果了。所以当我将其插入到我的程序中时,我知道如果我的程序没有效果,那么原因不会是我加入的这段新代码。
有了这样的保证,我就可以完成程序的最后一部分了。
function findFirstMatch(searchString, sourceString) {
let length = searchString.length;
for (let index = 0; index < sourceString.length; index++) {
let possibleMatch = (
sourceString.substr(index, length));
if (possibleMatch === searchString) {
return index;
}
}
return -1;
}
总结
如果你读到了这里,我现在只想说:试试这种策略。回到你上周因为受挫而搁置一旁的编程问题。我保证你能立马看到效果。祝你好运,编程快乐!
原文链接:https://blog.usejournal.com/how-a-googler-solves-coding-problems-ec5d59e73ec5