链表逆序是常见的数据结构操作,主要涉及如何反转单向链表。下面介绍几种常见的单向链表逆序方法,包括详解及每种方法的优缺点:
方法一:迭代法
这是最常见和直接的方法,使用迭代的方式来反转链表。
步骤
- 初始化三个指针:
prev
(初始为 NULL)current
(初始为链表的头节点head
)next
(初始为 NULL)
- 在
current
不为 NULL 时,重复以下操作:
- 将
next
指向current->next
- 将
current->next
指向prev
- 将
prev
移动到current
- 将
current
移动到next
- 最后,将
head
指向prev
。
以下是 C++ 的实现代码:
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
ListNode* reverseList(ListNode* head) {
ListNode* prev = NULL;
ListNode* current = head;
ListNode* next = NULL;
while (current != NULL) {
next = current->next; // 保存当前节点的下一个节点
current->next = prev; // 当前节点的 next 指向前一个节点
prev = current; // 移动 prev 到当前节点
current = next; // 移动当前到下一个节点
}
head = prev; // 新的头节点是原链表的尾节点
return head;
}
优点
- 时间复杂度:O(n),遍历链表一次。
- 空间复杂度:O(1),只使用了常数级别的额外空间。
缺点
- 逻辑较为简单,但可能对于新手而言有一点不容易理解指针操作。
方法二:递归法
递归方法利用系统调用栈来反转链表,本质上也是一种迭代,不过递归更加优雅。
步骤
- 递归基线条件:当节点为空或只剩一个节点(
head == NULL
或head->next == NULL
),返回头节点。 - 否则,递归反转剩余的链表,最后调转当前节点和下一个节点的指向。
以下是 C++ 的实现代码:
ListNode* reverseListRecursive(ListNode* head) {
// 基线条件
if (head == NULL || head->next == NULL) {
return head;
}
// 递归反转其余链表
ListNode* rest = reverseListRecursive(head->next);
// 调转当前节点和下一个节点的指向
head->next->next = head;
head->next = NULL;
return rest;
}
优点
- 代码更简洁优雅。
- 对于理解递归的编程者来说,这种方法更加直观。
缺点
- 空间复杂度:O(n),由于递归函数调用栈会占用线性空间。
- 对于非常长的链表,递归可能导致栈溢出问题(stack overflow)。
方法三:栈法
利用栈的数据结构特性(LIFO)来辅助反转链表。
步骤
- 遍历整个链表,将节点依次推入栈中。
- 从栈中依次弹出节点,重新调整指针指向。
以下是 C++ 的实现代码:
#include <stack>
ListNode* reverseListUsingStack(ListNode* head) {
if (head == NULL) {
return NULL;
}
std::stack<ListNode*> stack;
ListNode* current = head;
// 将所有节点推入栈中
while (current != NULL) {
stack.push(current);
current = current->next;
}
// 弹出第一个节点作为新的头节点
head = ();
stack.pop();
current = head;
// 重新调整弹出节点的指向
while (!stack.empty()) {
current->next = ();
stack.pop();
current = current->next;
}
current->next = NULL; // 尾节点指向 NULL
return head;
}
优点
- 思路简单,容易理解。
- 无需修改递归函数,可以很好地进行栈内存管理。
缺点
- 空间复杂度:O(n),需要额外使用栈来存储链表节点。
- 由于需要遍历和结构双重操作,实际运行效率较低。
方法四:头插法(尾插法)
通过头插法(或尾插法)重新构建链表。
步骤
- 初始化一个新的链表的头指针,并设置为 NULL。
- 遍历原链表,将每个节点依次插入到新链表的头部(或尾部)。
- 更新头指针为新构建的头指针。
以下是 C++ 的实现代码示例(头插法):
ListNode* reverseListHeadInsert(ListNode* head) {
ListNode* newHead = NULL; // 新链表的头指针
ListNode* current = head;
while (current != NULL) {
ListNode* next = current->next; // 保存当前节点的下一个节点
current->next = newHead; // 将当前节点插入新链表的头部
newHead = current; // 更新新的头指针
current = next; // 移动当前节点
}
return newHead;
}
优点
- 时间复杂度:O(n),只需一次扫描。
- 空间复杂度:O(1),只需常数额外空间。
缺点
- 操作增加了新链表的创建,对原链表进行了更改。
总结
- 迭代法:常用,简单高效,适合一般情况。
- 递归法:优雅简洁,适合理解递归思维的场景,但在链表长度过长时需谨慎。
- 栈法:思路清晰,但需额外空间,不适合高效要求场景。
- 头插法:直接且高效,但操作上新增链表。
不同方法适用于不同需求和场景,选用时需要综合考虑效率、内存消耗和代码简洁性。
测试
package main
import "fmt"
// ListNode 定义链表节点结构体
type ListNode struct {
Data int
Next *ListNode
}
// LinkedList 定义链表结构体
type LinkedList struct {
Head *ListNode
}
// NewLinkedList 初始化空链表
func NewLinkedList() *LinkedList {
return &LinkedList{Head: nil}
}
// InsertAtHead 在头部插入节点
func (list *LinkedList) InsertAtHead(data int) {
newNode := &ListNode{Data: data, Next: list.Head}
list.Head = newNode
}
// InsertAtTail 在尾部插入节点
func (list *LinkedList) InsertAtTail(data int) {
newNode := &ListNode{Data: data}
if list.Head == nil {
list.Head = newNode
return
}
current := list.Head
for current.Next != nil {
current = current.Next
}
current.Next = newNode
}
// DeleteNode 删除节点
func (list *LinkedList) DeleteNode(data int) {
if list.Head == nil {
return
}
if list.Head.Data == data {
list.Head = list.Head.Next
return
}
current := list.Head
for current.Next != nil && current.Next.Data != data {
current = current.Next
}
if current.Next != nil {
current.Next = current.Next.Next
}
}
// PrintList 打印链表
func (list *LinkedList) PrintList() {
current := list.Head
for current != nil {
fmt.Printf("%d -> ", current.Data)
current = current.Next
}
fmt.Println("nil")
}
func main() {
list := NewLinkedList()
list.InsertAtHead(1)
list.InsertAtHead(2)
list.InsertAtHead(3)
list.InsertAtHead(4)
fmt.Println("链表内容:")
list.PrintList()
list.ThreeReverse()
list.PrintList()
}
// ThreeReverse 三指针法
func (list *LinkedList) ThreeReverse() {
var curr = list.Head
var pre *ListNode
var next *ListNode
for curr != nil {
next = curr.Next
curr.Next = pre
pre = curr
curr = next
}
list.Head = pre
}