如何提升自己写代码的能力

如何提高代码能力?
写代码指的是将算法实现为程序代码,前提是你已经决定好了你要实现的算法,对代码能力的讨论应当在这一前提下。

代码能力包含写代码与调试代码两方面的能力。

写代码的能力
评价一个人写代码的能力自然是以其写代码的速度为标准,但这里的写代码的速度指的是一个人在写全新代码时的速度,而不是指他写旧有代码的速度。

譬如写线段树这个数据结构,基本上所有 OIers 都能炼到炉火纯青的地步,写一个线段树板子也只是手到拈来,5 分钟就能上百行,那么这就意味着他的写代码能力强吗?

未必,这只能说明他对线段树的记忆好。倘若是在写一道思维含量高的题,程序的每一步都是全新的,需要自己一步一步想,那么这就是在创造新代码,这时候他的写代码速度才能反映他的写代码的真实能力。

而不管是赛场上,还是以后工作中,社会所看重的肯定是创造新代码的能力,所以 OIers 在平时训练时就要注意锻炼自己创造新代码的速度。

当然 OIers 毕竟也要上赛场的,一些基本的板子,如线段树、树状数组、倍增等板子,肯定也要练到滚瓜烂熟,最起码的要求是在赛场上不需要编译运行也能保证完全正确。

在草稿纸上写代码
不过管是写哪种类型的代码,写代码之前都必须要在脑子里先过一遍,边写边想很可能写着写着发现算法有错误,之前写的那么多行代码就白写了,而且就算算法没有错误,也会大大增加打错字符的概率,为后续的代码调试增添负担。

在创造新代码时,大脑的思维负担是很高的,为了减少记忆负担,可以在草稿纸上写代码。著名的计算机科学家高德纳就一直坚持这个习惯,他在写任何代码前都要先用铅笔在草稿纸上写一遍,然后再搬到笔记本上。

高给其它人的解释是,人打字的速度远远快于思考的速度,边打边想会导致思维出现太多停顿,用笔写字则可以让输入与思考的速度保持一致,所以在纸上写代码的过程是最流畅的,也更容易从中发现算法上的一些小错误并及时改正。

当然,对于 OIers 来说,那些经过千锤百炼的板子代码就不必在草稿纸上过一遍了,在纸上写代码还是挺消耗时间的。

随着你的阅历增多,经验增长,对你来说,需要在纸上写的代码会越来越少,也越来越抽象和关键。这就意味着在纸上写下的代码不必严格遵循编程语言的语法规范或者伪代码的语法规范,能够简写一些步骤,只要自己看得懂就行,就像你在英语听说考试中不可能把听到的每个单词都完整地写在草稿纸上,你只可能记录下一些关键词,甚至连关键词都只记下首字母和尾字母。

先主后次
创造新的代码时,肯定有很多部分要实现,而实现的顺序应是从实现算法关键部分的代码开始,然后再是次要部分。至于哪一部分关键哪一部分次要就凭借你自个灵活判断啦。

多用 assert 语句

在程序的开头加上

#include <assert.h>
//#include 也行
然后就可以使用 assert 函数了。

它的作用是接受一个 bool 数据类型的参数,运行程序时,若该参数为 true 则什么事也不会发生,若该参数为 false 则程序会直接报错并告诉你触发了哪个 assert 语句。

比如现在要计算一个概率数组 {p_i}(1\le i\le n) ,其中 p_i 是第 i 个事件发生的概率,显然有 \sum_{i=0}^{n}p_i=1 ,所以若计算完了数组,可以用 assert 判断一下是否满足这个条件。

#include //fabs 函数需要通过这个库导入
double sum = 0;
for(int i = 1; i <= n; ++i){
sum += p[i];
}
assert(fabs(sum-1.0) <= 1e-5)
再比如调用某个函数时需要传入两个整数 L 和 R ,作为区间的左端点和右端点,因为肯定有 L\le R ,所以可以 assert。

void function(int L, int R){
assert(L <= R);
}
如果你的程序中已经出现了很多个 assert 语句,但又嫌手动删除它们太麻烦可以在 #include <assert.h> 前加入一个宏定义来使 assert 失效。

#define NDEBUG
#define <assert.h>
然后在 main 函数里加入 assert(false) 来判断 assert 是否确实失效。

养成随处写 assert 的习惯可以帮助你快速定位程序出错的地方,当然前提是若程序无误则 assert 一定不会报错。

调试代码的能力
调试代码的能力简直比写代码的能力还要重要,估计有不少 OIers 都有这样的经历,写出一份代码所花的时间可能只是十几分钟到一个小时,但调试代码的时间却数倍于前者,往往调试到最后,发现错误竟然只是写代码时粗心打错了几个字符。

调代码的过程主要是发现程序中的两种错误。

第一种是写代码时不可避免犯下的一些低级错误,如敲错字符,漏写语句,这种低级错误最让人头疼,也浪费了大量时间。

对付这种低级错误,最有效的方法是静态排错,即不运行代码,只通过看代码、分析代码来调试。

比如写完一道题目的代码后,不是立刻点编译运行的按钮,而是先浏览一遍自己代码,把程序的每一行语句都仔细检查一遍(即便是你写过千百遍的代码也可能出现小错误),像写完语文作文后检查自己有没有错别字一样。

OI 中的题目通常只需要 100~200 行的程序就能实现,所以浏览一遍整个程序所花费的精力和时间是可以接受的,基本上浏览完一遍后就能找出 70% 的低级错误,浏览两遍就能找出 95%。

第二种就是思维错误这类高级错误,这类错误的来源不是因为粗心所导致的打错符号,而是来源于算法上的错误,这类错误往往只是一些细枝末节的小问题,只要发现了,对算法稍加修改就能解决。

这类错误,以我的经验,比较难通过静态排错发现,因为你在静态排错中的大部分时候,只是在检查你写下的程序与你脑子里想的程序是否一致,而高级错误,是你脑子里想的程序的错误。

所以我在调试这些高级错误的时候,只会使用输出中间数据这一古老而低效的方法。

当然,如果不断提升静态排错的能力,使得能在检查程序的时候对一些细节进行演绎,那么也是可以发现高级错误的,这就需要凭借经验和积累了。

分模块架构
对于一些较复杂的程序,进行分模块肯定是能减轻思维负担的,要善于进行函数封装和 struct 封装,这不会使你的程序运行效率有多大降低,编译器往往会帮你完成绝大多数数优化,但这会使你架构代码和写代码的速度显著提高。

\text{Debug} 流程
最后给一份我的 \text{Debug} 流程作为参考,希望能帮助到你。

(1)重新计算一遍程序的空间复杂度,确定你的程序所用到的各种数组的数据范围在,然后检查你的程序当中设置的常量、宏定义常量、声明数组时的大小是否能应对极限数据。

(2)静态排错:写完程序后先扫视一遍每一行代码、每一个字符,处理敲错、敲漏字符等低级错误,确保敲出来的程序与脑子里想的程序一致。

(3)运行程序,检查能否过大样例、对拍。如果没问题则万事大吉,如果有问题,再次进行静态排错,并增加注意事项(4)、(5),因此第二次静态排错会比第一次静态排错要耗时。

(4)浏览每个函数(包括全局函数、结构体内的函数)时,思考运行程序时参数的数据范围,然后对你的代码在所有可能的参数组合下进行演绎,确保你的代码都能正确且符合预期效率地完成任务。

(5)思考每个函数运行时的抽象意义,即它完成的任务是什么,如果这个函数有返回值,思考这个返回值的意义,并审视一遍这个计算这个返回值的每一个细节,检查自己有没有出现思维错误。

比方说某一个函数,给定 w ,要求统计 x<w 和 x=w 的 x 的数目,通过 pair<int,int> 返回,但因为做大部分题时计算的贡献都是 x\le w 以及在这个任务下同时存在符号 < 与符号 = 带来的干扰,潜意识里认为第一项计算的是 x\le w ,认为这个函数该返回的是 x\le w 和 x=w 的 x 的数目。

此外还要检查对于数据的计算过程有没有爆 int 或爆 long long。

若某一函数过于复杂——包含不同阶段的任务(比如 main),则每一阶段的任务应当视作独立的一个函数,并演绎不同阶段任务之间的衔接。

(6)如果上述流程都没成功帮助你通过大样例和对拍,那你所犯下的错误一定是算法上的错误,对于这种不幸,只有最古老而传统的方法——输出中间值是你最后的救命稻草了(本质上断点调试也是输出中间值),即通过小样例或对拍找一组让你的程序出错的小规模数据(如 n\le 10,w\le10 ),人工模拟程序的运行并发现错误所在。

输出中间值能发现 \text{100%} 的错误,但你可能会(尤其对于数据结构题)花费数倍于想算法和写代码的时间来调试。不到万不得已就不要用输出中间值,如果通过输出中间值调试,最后调出的 \text{Bug} 还是一些敲错字符、小思维错误的话既浪费生命(不会从调试中收获什么),也打击自信心。

  • 10
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值