大家好,我是怒码少年小码。
这一篇咱们就搞一个东西——反转链表。因为这家伙实在是太常见了。
示例:
- 输入:[1,2,3,4,5]
- 输出:[5,4,3,2,1]
你会怎么做?
结点的定义:
struct LinkNode {
int elem;
LinkNode* next;
};
反转链表无非就是链表之间的插入,但我们知道链表插入中头结点和其他结点不一样,为了和普通结点一致,我们只好再建一个“头结点”。
创建一个用于遍历链表的指针,从原链表的第二个结点开始,把遍历指针指向的结点插入到虚拟头结点之后(头插法):
怎么样,是不是十分明朗呀。
LinkNode* reserveList(LinkNode* head) {
//虚拟头结点
LinkNode* virHead = new LinkNode;
//遍历指针
LinkNode* cur = head;
virHead->next = head;
while (cur != nullptr) {
LinkNode* next = cur->next;
cur->next = virHead->next;
virHead->next = cur;
cur = next;
}
//返回真实的头结点
return virHead->next;
}
值得注意的是本例在while循环体中,很多人第一次会写成这样:
//错误写法!!!
while (cur != nullptr) {
cur->next = virHead->next;
virHead->next = cur;
cur = cur->next;
}
这样写的本意是:既然当前结点已经插到新链表中,那我就更新一下当前遍历结点的指向,让它指向下一个结点继续插入。
但是,这是错误的。我们都知道当前遍历结点的下一个结点地址只保存在cur-next
中,当前遍历结点需要插入新链表中,要改变指针域cur->next = virHead->next
,这时cur->next
的值已经变了。
所以在插入前要用一个新的变量保存一下cur->next
的值(例如本例中的next),再当前指针插入完之后,继续更新遍历结点(本例中的cur = next;)
强烈推荐!!这种方法更难,但也会更有水平。先观察一下前后的变化:
我们通常需要三个指针来保存已经反转完的结点、当前结点和下一个要反转的结点。原理如下图:
LinkNode* reserveList2( LinkNode* head ) {
LinkNode* prev = nullptr;
LinkNode* cur = head;
while (cur != nullptr) {
LinkNode* next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}
详细实现过程:
prev
和 cur
并初始化为 nullptr
和 head
。next
。(理由和第一种方法的一样)prev
,实现指针反转。prev
指针为当前结点 cur
。cur
指针为下一个结点 next
。prev
。通过将每个节点的 next
指针指向前一个节点,不断更新 prev
和 cur
指针,最终实现了链表的反转。
LinkNode* reserveList3( LinkNode* head ) {
if (head == NULL || head->next == NULL) {
return head;
}
LinkNode* newHead = reserveList3(head->next);
head->next->next = head;
head->next = nullptr;
return newHead;
}
reserveList3
中,传入链表的头节点 head
。head
的下一个节点作为参数传入递归函数,并将返回值保存在 newHead
中。这一步的作用是将链表的头部节点放到了反转后链表的尾部。head
的下一个节点的下一个节点指向当前节点 head
,即 head->next->next = head
,实现指针的反转操作。head
的下一个节点指向空指针,即 head->next = nullptr
,断开当前节点 head
与下一个节点的连接,避免形成环。newHead
。注意:由于递归使用了系统栈,所以链表过长时可能会导致栈溢出的风险。因此,递归反转链表时应谨慎使用。
对了,前面两种方法我都没写判断链表是否为空或者只有一个结点的条件,主要是为了代码简洁便于理解,毕竟这两种情况就不用反转了(直接返回head)。