链表作为一种基础的数据结构,在实际开发中应用广泛。而链表的逆置操作是链表处理中的经典问题,本文将深入剖析链表原地逆置的算法原理、实现方式以及实际应用场景。
一、链表逆置问题概述
链表逆置是指将一个单向链表的节点顺序反转,使原来的头节点变为尾节点,尾节点变为头节点。根据实现方式的不同,主要分为以下两种类型:
- 非原地逆置:创建新链表,遍历原链表并将节点逐一插入新链表头部,时间复杂度 O (n),空间复杂度 O (n)
- 原地逆置:不创建新链表,仅通过修改节点指针实现逆置,时间复杂度 O (n),空间复杂度 O (1)
显然,原地逆置在空间效率上更具优势,是实际开发中更常用的实现方式。
二、原地逆置算法核心原理
原地逆置的核心思想是通过 "头插法" 重新组织节点连接关系,主要包含三个关键指针:
- 当前节点指针 (p):指向正在处理的节点
- 后继节点指针 (r):保存当前节点的下一个节点,防止指针丢失
- 新头节点指针 (L):原链表头节点,逆置后变为尾节点
算法的核心操作可以概括为:
- 保存当前节点的后继节点
- 将当前节点插入到新链表的头部
- 移动当前节点指针到后继节点
三、代码实现与解析
下面是链表原地逆置的完整实现代码,采用 C 语言风格:
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构
typedef struct LNode {
int data; // 节点数据
struct LNode *next; // 指向下一节点的指针
} LNode, *LinkList;
// 链表原地逆置函数
LinkList Reverse(LinkList L) {
LNode *p, *r; // 定义当前节点指针和后继节点指针
// 初始化:p指向原链表第一个节点
p = L->next;
// 初始化:原链表头节点的next置为NULL,作为新链表的尾节点
L->next = NULL;
// 遍历原链表进行逆置
while (p != NULL) {
// 保存当前节点的后继节点
r = p->next;
// 头插法:将当前节点插入到新链表头部
p->next = L->next;
L->next = p;
// 移动当前节点指针到后继节点
p = r;
}
return L; // 返回逆置后的链表头节点
}
// 创建测试链表
LinkList CreateList(int n) {
LinkList L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;
for (int i = n; i > 0; i--) {
LNode *newNode = (LNode*)malloc(sizeof(LNode));
newNode->data = i;
newNode->next = L->next;
L->next = newNode;
}
return L;
}
// 打印链表
void PrintList(LinkList L) {
LNode *p = L->next;
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
// 释放链表内存
void FreeList(LinkList L) {
LNode *p, *q;
p = L;
while (p != NULL) {
q = p;
p = p->next;
free(q);
}
}
// 主函数:测试链表逆置
int main() {
// 创建一个包含5个节点的链表: 1->2->3->4->5
LinkList L = CreateList(5);
printf("原链表: ");
PrintList(L);
// 逆置链表
L = Reverse(L);
printf("逆置后链表: ");
PrintList(L);
// 释放内存
FreeList(L);
return 0;
}
算法执行过程解析
以链表1->2->3->4->5
为例,逆置过程如下:
-
初始状态:
- L: [头节点] -> 1 -> 2 -> 3 -> 4 -> 5 -> NULL
- p = 1, L->next = NULL
-
第一次循环:
- r = 2 (保存后继节点)
- p->next = L->next = NULL (1 号节点的 next 置为 NULL)
- L->next = p = 1 (头节点指向 1 号节点)
- p = r = 2 (移动到下一个节点)
- 此时链表: [头节点] -> 1 -> NULL
-
第二次循环:
- r = 3
- p->next = L->next = 1 (2 号节点的 next 指向 1)
- L->next = p = 2 (头节点指向 2)
- p = 3
- 此时链表: [头节点] -> 2 -> 1 -> NULL
-
第三次循环:
- r = 4
- p->next = 2
- L->next = 3
- p = 4
- 链表: [头节点] -> 3 -> 2 -> 1 -> NULL
-
第四次循环:
- r = 5
- p->next = 3
- L->next = 4
- p = 5
- 链表: [头节点] -> 4 -> 3 -> 2 -> 1 -> NULL
-
第五次循环:
- r = NULL
- p->next = 4
- L->next = 5
- p = NULL (循环结束)
- 最终链表: [头节点] -> 5 -> 4 -> 3 -> 2 -> 1 -> NULL
四、算法复杂度分析
时间复杂度
- 链表逆置过程需要遍历每个节点一次,时间复杂度为 O (n),其中 n 为链表长度
- 无论链表长度如何,算法执行的基本操作 (指针操作) 次数与节点数量呈线性关系
空间复杂度
- 算法仅使用了固定数量的指针变量 (p, r),没有创建新的节点
- 空间复杂度为 O (1),这是原地逆置的最大优势
五、常见问题与解决方案
1. 空链表或单节点链表处理
- 空链表:直接返回头节点,无需处理
- 单节点链表:逆置后链表不变,算法自动处理
2. 指针丢失问题
- 关键在于先保存后继节点 (r = p->next),再修改当前节点的 next 指针
- 若顺序颠倒,会导致后继节点无法访问
3. 边界条件处理
- 头节点的 next 指针初始化为 NULL,作为逆置后的尾节点
- 循环条件 p != NULL 确保所有节点都被处理
六、拓展实现:递归方式逆置链表
除了迭代实现外,链表逆置也可以通过递归方式实现,代码如下:
// 递归方式逆置链表
LinkList ReverseRecursive(LinkList L) {
// 递归终止条件:空链表或单节点链表
if (L == NULL || L->next == NULL || L->next->next == NULL) {
return L;
}
// 递归逆置后续链表
LinkList newHead = ReverseRecursive(L->next);
// 修改指针实现逆置
L->next->next = L;
L->next = NULL;
return newHead;
}
递归实现的时间复杂度同样为 O (n),但空间复杂度为 O (n)(递归调用栈),因此在实际应用中迭代方式更为常用。