第3章 高质量的代码
3.1 面试官谈代码质量
- 考查代码的容错能力,不能容忍代码只针对假想的“正常值”进行处理,不考虑异常状况,也不考虑资源的回收等问题。
- 该掌握的知识点没掌握
- 功能性错误,忽略边界
- 变量名、函数命名规范,解决问题知道用什么数据结构
- 程序的正确性和鲁棒性。对输入参数的检查、处理错误和异常的方法、命名方式等。
3.2 代码的规范性
清晰的书写+清晰的布局+合理的命名---->规范的代码
写代码时,最好用完整的英文单词组合命名变量和函数,以便读懂代码。
函数,变量命名规范。函数一般由大写字母开头,中间以大写字母分隔。变量一般由小写字母开头,中间用大写字母隔开。
3.3 代码的完整性
通常需要检查代码是否完成了基本功能、输入边界值是否能得到正确的输出、是否对各种不合规范的非法输入作出了合理的错误处理。
- 从三个方面保证代码的完整性:功能测试、边界测试和负面测试。写代码前,将可能的输入都想清楚,避免在程序中出现各种漏洞,也就是说在编程前要考虑单元测试。边界测试,循环结束的边界条件,递归终止的边界值。负面测试就是考虑各种可能的错误输入
- 3种错误处理方法:第一种方法是函数用返回值来告知调用者是否出错;第二种方法是当发生错误时设置一个全局变量,可以再返回值中传递计算结果;第三种方法是异常,当函数运行出错,抛出一个异常,可以根据不同的出错原因定义不同的异常类型。
优点 | 缺点 | |
返回值 | 和系统API一致 | 不能方便地使用计算结果 |
全局变量 | 能够方便的使用计算结果 | 用户可能会忘记检查全局变量 |
异常 | 可以为不同出错原因定义不同异常类型,逻辑清晰明了 | 有些语言不支持异常,抛出异常时对性能有负面影响 |
面试题11:数值的整数次方,实现函数double Power(double base,int exponent),求base的exponent次方,不能使用库函数,不需要考虑大数问题。
-
- 考点:思维的全面性,虽然题目不难,但是需要考虑的周全一些。对效率要求高的话,考查快速乘方
- 测试用例:把底数和指数分别设为负数,正数和零
- 位运算的效率比乘除法及求余运算的效率要高很多,可以用右移代替除以2,用位&运算代替求余运算符%
面试题12:打印1到最大的n位数,输入数字n,按顺序打印出从1到最大的n位十进制数。
-
- 考点:能否想到是大数问题,判断是否n位最大数的方法,会不会打印出前面的0
- 测试用例:功能测试(1,2,3……),负面测试(输入-1,0)
- 扩展问题:用char表示十进制0~9浪费内存,有更高效方法否? 定义一个函数,实现任意两个整数的相加。
- 如果题目关于n位整数并没有限定n的取值,或者输入任意大小的整数,需要考虑大数问题。字符串是一个简单有效的表示大数的方法
面试题13:在O(1)时间删除链表节点,给定单向链表的头指针和一个结点指针,定义一个函数在O(1)时间节点删除该节点
-
- 思路:删除结点指针所指内容,一定要顺序遍历链表吗?把结点的下一个结点内容复制到该结点,删除后一个结点。需要考虑边界情况,如果就是最后一个结点,或者链表只有一个结点时的情况。
- 测试用例:功能测试(删除一个结点,尾结点,头结点,只有一个结点的链表),负面测试(指向头结点的指针null,指向要删除结点的指针为null)
- 考虑 的全面性,对链表的删除/添加操作一定要考虑边界条件,头尾结点,是否只有一个元素,空链表等情况
面试题14:调整数组顺序使奇数位于偶数前面,输入一个整数数组,实现一个函数来调整该数组中数字的顺序,是所有奇数位于前半部分,偶数位于后半部分。
-
- 思路:两个指针,分别向中间移动,判断数字应该属于那半部分,交换
- 测试用例:功能测试(输入数字中奇偶交替出现,偶数都在奇数前面,奇数都在偶数前面),特殊情况测试(null指针,只有一个数字)
3.4 代码的鲁棒性robust
鲁棒性是指程序能够判断输入是否合乎规范要求,并对不合要求的输入予以合理的处理。提高代码的鲁棒性的有效途径是进行防御性编程,即预见在什么地方可能会出现问题,并为这些可能出现的问题制定处理方式。最简单也是最实用的防御性编程就是在函数入口处添加代码以验证用户输入是否符合要求。还可以 多问个“如果不……那么……”这样的问题。
问题15:输入一个链表,输出该链表中倒数第k个结点,从1开始计时。
-
- 思路:简单粗暴的方法,遍历两次,第一次获得链表长度n,第二次获取n-k+1的结点。只遍历一次,用两个指针,第一个指针比第二个指针领先k-1步,当它指向链表尾部时,第二个指针刚好指向倒数k结点。(编写时需要注意细节问题)
- 测试用例:功能测试(k结点在链表的中间,头结点,尾结点),特殊测试(链表头结点为null,链表结点数少于k,k等于0)
- 当用一个指针遍历链表不能解决问题时,可以尝试用两个指针,其中一个遍历的速度快一些(如一次走两步),或者让它先在链表上走若干步。
问题16:反转链表,定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。
-
- 思路:遍历的时候,把pNext指针指向前一个结点。
- 测试用例:功能测试(输入链表含有多个结点,链表中只有一个结点),特殊输入测试(链表头结点为null指针)
- 在写代码之前,一定要仔细分析和设计,提前想好测试用例,保证代码的鲁棒性和完整性
问题17:合并两个排序的链表,输入两个递归的链表,合并这两个链表并使新链表中的结点仍然是递增排序。
-
- 思路:两个指针分别指向两个链表的头结点,依次遍历并比较
- 测试用例:功能测试(输入的两个链表中有多个结点,结点值互不相同或者存在值相等的多个结点),特殊测试(两个链表的一个或者两个头结点为null指针,两个链表中只有一个一个结点)
- 在写代码之前全面分析哪些情况会引入空指针,并考虑清楚怎么处理这些空指针。
问题18:树的子结构,输入两颗二叉树A和B,判断B是不是A的子结构。
-
- 思路:利用递归判断逐次判断根结点,左右子树是否相等,需要注意空指针情况
- 测试用例:功能测试(A和B都是普通二叉树,树B是或者不是树A的子结构),特殊情况(两棵二叉树的一个或者两个根结点为null指针,二叉树的所有节点都没有左子树或者右子树)
- 二叉树相关的代码有大量的指针操作,第一次使用指针的时候,都要问自己这个指针有没有可能是null,如果是,如何处理。
- 重要的事情说三遍:写代码前设计好测试用例,写好代码用测试用例检验自己的代码。
3.5 本章小结
如何写出高质量的代码:规范性,完整性和鲁棒性三个方面。
- 规范性:书写清晰,布局清晰,命名合理
- 完整性:完成基本功能,考虑边界条件,做好错误处理
- 鲁棒性:采取防御式编程,处理无效的输入。