PS:由于微信的更新将所有文章字体缩小了,所以本文是按照新版微信的字体大小进行的排版,尽量让所有代码块、解释、图片同时容纳在屏幕内,方便阅读。 链表是一种很简单数据结构,但是在面试题中常常出现。链表的每个节点包含一个指向下一个节点的指针,跟串糖葫芦似得穿起来。 struct Node { int val; Node* next; }
链表结构简单,但是却很漂亮,因为它具有天生的递归结构,所以几乎所有链表的操作都有一个递归的实现方式,这也是本篇文章的主要目的:教大家递归地处理链表。 这篇文章主要写两个链表最常考察的两个操作,包括链表翻转和两个有序链表的合并,并且给出迭代和递归两种解法。 首先来简单热下身,给一个链表头,求这个链表的长度。 迭代版本: int size(Node* head) { int len = 0; while(head != NULL) { head = head->next; len++; } return len; }
递归版本: int size(Node* head) { if (head == NULL) return 0; return size(head->next) + 1; }
现在开始正题,先来讲解一下翻转一个链表。解释一下: 比如有个链表是 1->2->3->4,你要把它变成 4->3->2->1,返回头结点(这里就是 4 那个节点)。 首先,看一下迭代算法: Node* reverse(Node* head) { if (head == NULL) return NULL; Node *curr = head, *prev = NULL; while (curr != NULL){ Node* next = curr->next; curr->next = prev; prve = curr; curr = next; } return prev; }
画个中间过程的图就好理解了,注意 prev 指向的节点和 curr 是不相连的,因为它们在算法开始的时候就不相连。prev 走过之后的链表已经翻转完成了:

这三个指针的操作过程莫名让我想起科普片里细胞中的酶组装氨基酸的过程。。。 下面上递归算法,很漂亮,但绝对得不好理解(很适合拿去装逼): Node* reverse(Node* head) { if (head == NULL || head->next == NULL) return head; Node* newHead = reverse(head->next); head->next->next = head; head->next = NULL; return newHead; }
这个算法很精妙,我画个图就容易理解了(明白递归函数是干什么的,并相信它一定能做好): 
理解之后就一目了然了,如果对递归还不熟悉,请参看旧文的详细解说:浅析递归。
接下来讲解一下合并两个有序链表。其实第一次听到这个问题,我有点误解,所以我在解释一下什么叫合并两个有序链表。 比如两个有序链表分别是: 4->6->9->11 和 1->3->6->8->12 我们需要得到这样一个链表,并返回表头节点: 1->3->4->6->6->8->9->11->12 这个过程有没有很像拉拉链的过程?带着这个直觉就很容易理解迭代解法。 迭代的实现方法很直接,但是需要点常用技巧,如果不想看了就直接看递归版本。 Node* merge(Node* head1, Node* head2) { // 如果有一个头结点是空,就可以直接返回另一个 if (head1 == NULL || head == NULL) return head1 == NULL ? head2 : head1; // 虚拟头结点,方便处理 auto dummy = new Node(0); // 我觉得这个指针很像拉链上的拉锁 Node *p = dummy; // 开始拽着 p 拉拉链 while (head1 != NULL && head2 != NULL) { if (head1->val > head2->val) { p->next = head2; head2 = head2->next; } else { p->next = head1; head1 = head1->next; } p = p->next; } // 一个链表耗尽,剩下的元素都比已合并的链表元素大 // 所以把剩下的直接连到最后就行了 p->next = head1 == NULL ? head2 : head1; return dummy->next; }
这里用到的常用技巧就是虚拟头结点 dummy 的使用。处理链表的时候经常会造一个虚拟头结点连到一个真实头结点的前面。
这样做的好处很多,主要是是方便处理节点为空的特殊情况,减少大量复杂的判定代码。请花时间理解这个算法(这是值得的),然后尝试不要这个虚拟头结点,然后就能理解这样处理的好处了。 画个图理解一下: 
现在开始递归版本。其实递归版本一直比迭代简洁好理解,体验一下: Node* merge(Node* head1, Node* head2) { // 同理,只要有一个空就可以直接返回 if (head1 == NULL || head2 == NULL) return head1 == NULL ? head2 : head1; if (head1->val > head2->val) { head2->next = merge(head1, head2->next); return head2; } else { head1->next = merge(head1->next, head2); return head1; } }
递归解法总是这么精妙。总的逻辑就是:抽出当前两个节点中(head1 和 head2 中)较小的那个,然后把剩下的烂摊子一股脑丢给递归,因为剩下的问题和原问题具有相同结构,且减小了规模。画个图理解下: 
由于之前写了好几篇文章讲解递归,这里就不赘述了,以上解法还可以再写得漂亮些(本质没有变): Node* merge(Node* head1, Node* head2) { if (head1 == NULL || head2 == NULL) return head1 == NULL ? head2 : head1; // 保证 head1 总是值较小的,这样就不用 if else 分支了 if (head1->val > head2->val) swap(head1, head2); head1->next = merge(head1->next, head2); return head1;
|