翻译:疯狂的技术宅
作者:Valinda Chan
英文标题:10 Steps to Solving a Programming Problem
英文链接:https://codeburst.io/10-steps...
本文首发微信公众号:充实的脑洞
我总是听到刚入行不久的程序员这样说:知道自己要实现什么功能,同时处理逻辑和基本语法也都明白,但是就不知道该怎么写代码。如果把别人的的代码给你看,或者有人给你你一些指导,或许你能明白其中的思路。但是,在实际开发时仍然障碍重重。即使语法或逻辑都明白,也很难自己的想法转化为代码。在本文中我将会告诉大家我自己是怎么做的,还有一些解决典型问题的方法,希望能够对大家有所帮助。
1. 把给你的需求反复阅读三遍以上(或者直到看吐了为止)
如果不能理解给你的需求,也就没有办法实现它。 实际的需求和你认为的需求有很大的区别。假设有一个需求,当你阅读前几行时非常容易,但是接下来你就会假设其余部分与你曾经看到过的东西类似。比如你要做一个像“刽子手”一样的游戏,一定要通读它所有的规则,即便你曾经玩过这个游戏。我就曾经接受了一个开发任务,就是做一个像“刽子手”一样的游戏,但是当我看完需求中所有的规则时,才意识到要做的应该是“邪恶的刽子手”(这是一个深坑!)。
有时我会试着向一个朋友解释某个需求,看她对我解释的理解是否和我的需求一致。如果你不想在开发了一半的时候才发现自己误解了这个需求,那么在开始的时候多花点时间是值得的。你对问题越了解,就越容易解决它。
假设我们要创建一个简单的函数selectEvenNumbers
,这个函数的参数一个存放整数的数组,返回值evenNumbers
是一个只存在偶数的数组。如果没有偶数,那么久返回一个空数组。
function selectEvenNumbers() {
// your code here
}
以下是我思考的问题:
计算机怎样去判断是不是偶数? 检查该数是否能被2整除
我传给这个函数的参数是什么? 一个数组
数组中保存的内容是什么? 一个或多个整数
数组中元素的数据类型是什么? 整数
这个函数的目的是什么?之行结束后要返回什么? 目标是得到所有偶数,并把它们保存到数组中返回。如果没有偶数,就返回一个空数组。
2.至少使用三组模拟数据进行手动模拟
找一张草稿纸,人工解决这个问题。至少考虑三组模拟数据,注意要考虑到极端情况和边界问题。
极端情况:在正常操作参数范围之外产生的问题或情况。或者是多个变量或条件都在其指定范围内,但是都同时处于极端的水平的情况。
边界问题:仅在极端(最大或最小值)参数的情况下发生的问题或状况。
举个例子,下面是一些要使用的样本数据集:
[1]
[1, 2]
[1, 2, 3, 4, 5, 6]
[-200.25]
[-800.1, 2000, 3.1, -1000.25, 42, 600]
在刚开始的时候,很容易忽略这些步骤。
因为你的大脑对于偶数的概念十分清楚,所以只要看到一组数据,就可以从中找到2,4,6这样的数字,几乎意识不到自己的大脑是怎么思考的。可以尝试更多的数据,它会改变你大脑通过观察来解决问题的习惯。这有利于帮你实现真正有效的算法。
我们来看第一个数组:[1]
查看数组
[1]
中唯一的元素判断是否为偶数:嗯,并不是
确定这个数组中没有其他的元素了
确定在这个数组中没有偶数
返回一个空数组
接下来看第二个数组:[1, 2]
先看数组
[1, 2]
中的第一个元素数字是1
判断是否为偶数:不是
看数组中的下一个元素
数字是2
判断是否为偶数:是的
创建一个数组
evenNumbers
,并把数字2
添加到其中确定数组中没有其他元素了
返回的数组
evennumbers
是[ 2 ]
再多看几遍。请注意处理[1]
的步骤和[ 1, 2 ]
略有不同。这就是为什么我要尝试多种不同的组合。在这些数据中,有的只存在一个元素;有些是浮点数,而不是整数;有些是一个元素中有多个数字,有些是负数。
3.简化并优化你的步骤
寻找模式,找到概括问题的方法,看看能不能减少无用或重复的步骤。
创建一个函数
selectEvenNumbers
创建一个保存数据的空数组
evenNumbers
检查数组
[1, 2]
中的每个元素找到第一个元素
判断它是否可以被
2
整除。如果是,就加到evennumbers
中找到下一个元素
重复步骤4
重复步骤5和步骤4,一直到数组中没有任何其他元素
返回数组
evenNumbers
,不管它是不是空数组
这个方法可能会让你想起数学归纳法:
证明当
n = 1
,n = 2
,...
的情况下成立假设当
n = k
时成立证明当
n = k + 1
时成立
4. 写出伪代码
伪代码
我们已经有了处理步骤,接下来就要编写出伪代码了,伪代码可以转换成真实的代码,这有助于定义代码的结构,并使编码变得更加容易。您可以在纸上写伪代码,也可以在代码编辑器中用注释的形式来写。如果你在电脑上做会分心,我建议你用纸和笔来完成。
通常伪代码并没有什么特定的规则,不过有的时候我可能会使用自己熟悉的某种语言的语法。所以不要被语法所纠缠。把精力放在逻辑和步骤上。
对于我们所面对的问题,可以有很多不同的方法。 例如,您可以使用filter
,但是为了尽可能简单地说明前面的例子,我们现在将使用一个基本的for
循环(但是当我们重构代码时,将会使用filter
)。
下面是一个伪代码的例子,它有比较多的语言描述:
function selectEvenNumbers
create an array evenNumbers and set that equal to an empty array
for each element in that array
see if that element is even
if element is even (if there is a remainder when divided by 2)
add to that to the array evenNumbers
return evenNumbers
下面这段伪代码比较简洁:
function selectEvenNumbers
evenNumbers = []
for i = 0 to i = length of evenNumbers
if (element % 2 === 0)
add to that to the array evenNumbers
return evenNumbers
只要你能把它逐行地写出来,并且理解每一行的逻辑,用哪种方式并不重要。
最后还要回顾一下,确保自己没有走偏。
5. 把伪代码翻译成真正的代码并进行调试
当伪代码被准备好之后,就可以把每一行伪代码用自己正在使用的语言实现了。在这个例子中我们将使用JavaScript。
如果你把伪代码写在了纸上,那么就把它作为注释输入到自己的代码编辑器中,之后再替换为代码中的每一行。
然后我调用这个函数,并给它一些我们之前使用过的样本数据集。可以用它们来检查代码执行的结果是否和预期一致。还可以编写测试用例来检查实际的输出是否符合预期。
selectEvenNumbers([1])
selectEvenNumbers([1, 2])
selectEvenNumbers([1, 2, 3, 4, 5, 6])
selectEvenNumbers([-200.25])
selectEvenNumbers([-800.1, 2000, 3.1, -1000.25, 42, 600])
我通常在每个变量或者每一行后面都使用console.log()
。这将会帮助我检查变量值和代码是否符合预期。通过这种方法,可以很容易的发现代码中的问题。下面的例子是我在运行时会检查哪东西。在我所有的代码中都会这样做。
function selectEvenNumbers(arrayofNumbers) {
let evenNumbers = []
console.log(evenNumbers) // I remove this after checking output
console.log(arrayofNumbers) // I remove this after checking output
}
最后使每一行伪代码都有对应的真实代码。//
后面是伪代码,其它部分是用JavaScript实现的真实代码。
// function selectEvenNumbers
function selectEvenNumbers(arrayofNumbers) {
// evenNumbers = []
let evenNumbers = []
// for i = 0 to i = length of evenNumbers
for (var i = 0; i < arrayofNumbers.length; i++) {
// if (element % 2 === 0)
if (arrayofNumbers[i] % 2 === 0) {
// add to that to the array evenNumbers
evenNumbers.push(arrayofNumbers[i])
}
}
// return evenNumbers
return evenNumbers
}
为了避免混淆,我去掉了伪代码。
function selectEvenNumbers(arrayofNumbers) {
let evenNumbers = []
for (var i = 0; i < arrayofNumbers.length; i++) {
if (arrayofNumbers[i] % 2 === 0) {
evenNumbers.push(arrayofNumbers[i])
}
}
return evenNumbers
}
有时候,初级开发人员会被语法所困扰,导致难以继续前进。记住:语法会随着时间的推移而逐渐熟练起来。在编码的时候因为语法问题去翻参考材料并不丢人。
6. 简化并优化你的代码
你可能已经注意到,简化和优化是经常性的话题。
“简单性是可靠性的先决条件。”
——荷兰计算机科学家Edsger W. Dijkstra,计算科学研究领域的先驱
在这个例子中,优化的方法之一就是通过使用filter
返回一个新数组来过滤原来数组中的项。这样我们就不用再去定义另外一个变量evenNumbers
,因为filter
将返回一个新的数组,其中包含与过滤器匹配的元素并复制一个新的数组。 这样就不会改变原来的数组。我们也不用使用for
循环来进行遍历。过滤器将会遍历每个项,如果在数组中的元素符合条件就返回true,否则就返回false
将其忽略。
function selectEvenNumbers(arrayofNumbers) {
let evenNumbers = arrayofNumbers.filter(n => n % 2 === 0)
return evenNumbers
}
简化和优化代码可能需要迭代多次,以确定进一步简化和优化代码的方法。
这里有一些需要牢记的问题:
简化和优化的目标是什么?目标会被你的团队风格或个人喜好所左右。是尽可能地压缩代码还是使代码更易阅读? 如果是后者,你可能会用单独的代码行来定义变量或计算某些变量,而不是试图在一行中做这些事。
怎样做才能使代码容易阅读?
还有没有多余的步骤可以去掉?
有没有变量或函数始终没有被用到过?
是不是存在重复的步骤?看能不能在另外一个函数中定义它们。
有没有更好的处理边界问题的办法?
编写程序的本意是为了供人阅读,只是顺便让计算机能够执行它。
——“计算机程序的结构与解释”作者Gerald Jay Sussman和Hal Abelson
7.调试
这一步应该贯穿始终。在调试的过程中,您会很容易发现逻辑上的错误或漏洞。要充分利用集成开发环境(IDE)和调试器。当我遇到bug时,会逐行跟踪代码,来检查是否存在不符合预期地方。以下是我使用的一些技巧:
实用控制台可以查看错误信息,有时候它会告诉我需要检查哪一行,这就给了我一个大概的思路:从哪里开始。尽管有时候问题并不在提示给出的那一行。
注释掉某些代码块或者行,并输出调试信息,来检查剩余的代码是否能正常运行。可以根据实际情况对代码进行注释。
使用不同的测试数据,看看代码是否仍然可以工作。以此来检查是否存在我没有想到的情况。
如果想要尝试另外一种完全不同的方法,可以保存不同版本的文件。我可不想在恢复原来代码的时候后悔莫及!
最有效的调试工具是仔细的思考,再加上输出清晰的调试信息。
——普林斯顿大学计算机科学教授Brian W. Kernighan
8.添加有效的注释
很有可能在一个月之后你会忘记自己的代码都是什么意思,使用你代码的其他人可能也不知道。这就是为什么要添加有效的注释的原因:为了让你在回头看这些代码时节省时间。
不要这样去注释:
// 这是一个数组,并且遍历它
// 这是一个变量
我试着做一些简要、高级的注释,在出问题的时候可以帮我搞明白这段代码到底是起到什么作用。尤其是在处理更复杂的问题时非常有用。它有助于理解某个特定功能在做什么以及为什么这样做。通过使用清晰的变量名、函数名和注释,你(和其他人)应该能够理解:
这段代码是做什么用的?
它是怎样工作的?
9.通过代码评审获得反馈
从你的团队成员、教授和其他开发者那里得到反馈。检查堆栈是否会溢出。看别人如何解决这个问题并从中吸取教训。有时解决问题的方法有好几种。把它们都找出来,这样你进步会很快。
别在意你写出良好风格的代码会花费多少时间,因为一旦你写出了糟糕的代码,那将会更慢。
——Bob Martin,软件工程师,敏捷宣言的合著者之一
10.实践,不停的实践
哪怕是经验再丰富的开发人员也总是在不停的实践与学习。如果你得到了有用的建议,那么就要去照着做。重复做相同或类似的事情,不停的鞭策自己。随着一个又一个的问题的解决,最终你会成长起来。在每一次成功之后,一定要对问题进行回顾。记住,编程和任何事一样,会随时间的推移变得更加简单、更加自然而然。
欢迎扫描二维码关注微信公众号:充实的脑洞,第一时间推送我翻译的国外最新技术文章。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。