ARTS - No.2
A
删除链表的节点
这是一个简单的需求,遍历链表,找到需要删除的结点,把它从链表中删除即可。(题目保证链表中节点的值互不相同;若使用 C 或 C 语言,你不需要 free 或 delete 被删除的节点)
于是我不假思索地写出如下代码:
struct ListNode* deleteNode(struct ListNode* head, int val){
if(!head){
return head;
}
if(head->val == val){
return head->next;
}
struct ListNode* pre_node = head;
while(pre_node && pre_node->next){
if(pre_node->next->val == val){
pre_node->next = pre_node->next->next;
break;
}
pre_node = pre_node->next;
}
return head;
}
貌似没有什么问题了。但是我需要单独判断删除的结点是不是头结点,搞出来一个if,挺不爽。其实给head前面加一个虚假的结点dummy,就相当于整个链表从第二个结点开始操作,可以把流程一般化。
struct ListNode* deleteNode(struct ListNode* head, int val){
struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
dummy->next = head;
struct ListNode* pre_node = dummy;
while(pre_node && pre_node->next){
if(pre_node->next->val == val){
pre_node->next = pre_node->next->next;
break;
}
pre_node = pre_node->next;
}
head = dummy->next;
free(dummy);
return head;
}
直到有一天,我看见了一篇文章——Two star programming,然后参考文中的代码写出了下面的程序:
struct ListNode* deleteNode(struct ListNode* head, int val){
struct ListNode** indirect = &head;
for(; *indirect; indirect = &((*indirect)->next)){
if((*indirect)->val == val){
*indirect = (*indirect)->next;
break;
}
}
return head;
}
非常神奇的程序,用二级指针简化了操作。之前操作链表的方式都是拿着一个结点去找下一个结点,而二级指针相当于拿着一个指向结点的箭头(也就是next指针)去找下一个结点,修改(*indirect)就相当于修改next指针。
R
Two star programming & Three Star Programmer
《Two star programming》中这个操作链表结点的说法来自这里: Linus Torvalds Answers Your Questions ,在大佬问答里面的 favorite hack 这个问题的回答里,Linus表达了自己的观点:
So there's lots of pride in doing the small details right. It may not be big and important code, but I do like seeing code where people really thought about the details, and clearly also were thinking about the compiler being able to generate efficient code (rather than hoping that the compiler is so smart that it can make efficient code despite the state of the original source code).
划重点:
- doing the small details right;
- thought about the details;
- thinking about the compiler being able to generate efficient code
所以这就扯出了有关“优秀”的标准问题。Linus赞赏注重代码细节和执行效率的程序(员),就像上面那个链表删除节点的程序,能用类似*pp = entry->next一句话解决的问题就不要两句。
Linus Torvalds Answers Your Questions 里这些问答还是很有意思的,推荐大家看一下,Linus的答复到处都散发着他的张扬个性。
而在《Two star programming》的末尾有一个链接,就是《Three Star Programmer》,这是一篇程序员们对三级指针的批判,各种观点都有,这下齐了;-)。有的发言也很值得深思,比如 PaulHudson 和 TomStambaugh 的发言提到了程序员应该重视的点,不应该只是掌握一种方法的使用,还要多思考代码之外的事情:为什么可以这么用、这种方法背后的需求是什么?
T
状态翻转检测
我第一次见到这个代码是在一个单片机按键检测程序里,实现非常巧妙。
核心代码就是这句:(last_state ^ state) & state
last_state和state取值0或1,异或操作的机制是“相同为0,相异为1”。
假设有一个二进制序列001100,初始的last_state和state分别为第0,1个元素,值均为0,然后右移,看看last_state ^ state和(last_state ^ state) & state会输出什么结果。

| 代码 | 状态1 | 状态2 | 状态3 | 状态4 | 状态5 |
|---|---|---|---|---|---|
| last_state ^ state | 0 | 1 | 0 | 1 | 0 |
| (last_state ^ state) & state | 0 | 1 | 0 | 0 | 0 |
效果就是last_state ^ state检测出了所有跳变,而& state或者& (!state)可以提取其中的上升沿或者下降沿,这在按键检测程序里是非常实用的技巧。
然而它只应用在按键检测里吗?那可不一定,比如,用这个思路还可以解决 LeetCode No.817 ,这是我的解答。
S
取舍
这次的观点是“取舍”,这是由《Two star programming》和《Three Star Programmer》引申出来的一点思考——程序可读性和效率的取舍。
在 Algorithm 里,介绍了 Linus 大佬赞赏的"pointer to the entry pointer"的实现方法,这种实现注重细节并且高效,但是比较一下我前面写的两种实现,这种实现最难以理解。我去看别人的用dummy结点的代码,可以很快理解,但是看到struct ListNode** indirect = &head;,不由得开始颤抖,我内心是不喜欢阅读这样的代码的。我第一次看到它时,花了好久去理解,还尝试画个图出来,最后发现它仍然难以言传,只可意会。
当 Linus 提到激动人心的代码时,他是这么说的:
That said, we do have lots of pretty cool code in the kernel. I'm particularly proud of our filename lookup cache, but hey, I'm biased. That code is not for the weak of heart, though, because the whole lockless lookup (with fallbacks to more traditional locked code) is hairy and subtle, and mortals are not supposed to really look at it. —— Linus Torvalds Answers Your Questions
所以他本人也承认了这种"cool code"是我等"mortals"难以理解和阅读的。之后,我还在《Three Star Programmer》里看到了 TomStambaugh 的这段话:
The ThreeStarProgrammers I've known have been devastating to my teams and the code they produce. The attitude that, in my experience, they seem to bring with them is "I'm smarter than you and tougher than you, and I'm going to prove it in my code. Watch *THIS*!" ... and out pops some inscrutable hieroglyphic mess. I cite the C++ reference manual as the canonical example. This attitude has, in my experience, been so destructive that it has totally overwhelmed any advantage brought by their technical expertise. ——Three Star Programmer
TomStambaugh 极其反感程序员为了证明自己的能力而搞出来一些别人看不懂的东西。当然这是有前提的——这些复杂的别人看不懂的东西有更好的实现方式。而反观这个链表删除的代码,可读性和高效性处于相反的方向,这是"取舍(Tradeoff)",不属于单纯的炫技。
所以,要如何评价这样的代码呢?我想,如果要给出答案,就需要把应用场景作为前提。如果对效率要求极高,那么当然是把它封装成库函数,尽管源代码很晦涩,但是它作为轮子就是去解决需求的,毕竟大部分应用场景是调用接口,少有人去研究源码。而在其他一些常见的业务里,在那些需要长期维护,而且不断易主的代码里面,使用类似的代码就显得"不近人情"了,在满足需求的前提下,我想我会放弃一些效率方面的考量,而偏向于写出更加易读、清晰的代码。

浙公网安备 33010602011771号