链表逆序是常见的数据结构操作,主要涉及如何反转单向链表。下面介绍几种常见的单向链表逆序方法,包括详解及每种方法的优缺点:

方法一:迭代法

这是最常见和直接的方法,使用迭代的方式来反转链表。

步骤
  1. 初始化三个指针:
  • prev(初始为 NULL)
  • current(初始为链表的头节点 head
  • next(初始为 NULL)
  1. current 不为 NULL 时,重复以下操作:
  1. next 指向 current->next
  2. current->next 指向 prev
  3. prev 移动到 current
  4. current 移动到 next
  1. 最后,将 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),只使用了常数级别的额外空间。
缺点
  • 逻辑较为简单,但可能对于新手而言有一点不容易理解指针操作。

方法二:递归法

递归方法利用系统调用栈来反转链表,本质上也是一种迭代,不过递归更加优雅。

步骤
  1. 递归基线条件:当节点为空或只剩一个节点(head == NULLhead->next == NULL),返回头节点。
  2. 否则,递归反转剩余的链表,最后调转当前节点和下一个节点的指向。

以下是 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)来辅助反转链表。

步骤
  1. 遍历整个链表,将节点依次推入栈中。
  2. 从栈中依次弹出节点,重新调整指针指向。

以下是 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.top();
    stack.pop();
    current = head;
    
    // 重新调整弹出节点的指向
    while (!stack.empty()) {
        current->next = stack.top();
        stack.pop();
        current = current->next;
    }
    current->next = NULL;  // 尾节点指向 NULL
    
    return head;
}
优点
  • 思路简单,容易理解。
  • 无需修改递归函数,可以很好地进行栈内存管理。
缺点
  • 空间复杂度:O(n),需要额外使用栈来存储链表节点。
  • 由于需要遍历和结构双重操作,实际运行效率较低。

方法四:头插法(尾插法)

通过头插法(或尾插法)重新构建链表。

步骤
  1. 初始化一个新的链表的头指针,并设置为 NULL。
  2. 遍历原链表,将每个节点依次插入到新链表的头部(或尾部)。
  3. 更新头指针为新构建的头指针。

以下是 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
}

单链表逆序有哪几种方法_Data_02