本篇核心知识:链表基础、链表常用操作(清空、交换)、单链表经典题(求长度、反转、查找倒数第 K 个节点、快慢指针、递归逆序打印、链表判环 / 求环长、链表相交、指定节点 O (1) 删除)、栈结构简介
一、链表基础
概念
链表是链式线性结构,内存不连续,依靠指针将多个节点串联。节点分为两大组成部分:
-
数据域:存储业务数据;
-
指针域 :存储相邻节点地址,单链表仅有后继指针
next,双向链表包含前驱prev+ 后继next。
特性
-
链表无固定容量,可动态增删节点,无需提前开辟连续内存;
-
不支持下标随机访问,只能从头部开始逐一遍历;
-
分为
(1)带头结点(哨兵节点)
(2)不带头结点
两种设计:
带头结点:头结点不存有效数据,统一空表、头部增删逻辑,代码更简洁;
不带头结点:首节点即为有效节点,头部操作需要特殊判断。
代码示例(单链表基础节点定义)
#include <iostream>
using namespace std;
// 单链表节点
struct Node
{
int data; // 数据域
Node* next; // 指针域:指向后继节点
Node(int val) : data(val), next(nullptr) {}
};
二、链表基础操作(清空、交换两个链表)
2.1 链表清空
概念
释放链表中所有堆节点内存,防止内存泄漏,最终将链表置为空表。
特性
-
逐个遍历有效节点,调用
delete释放; -
带头链表仅清空有效节点,保留哨兵头结点;
-
清空后将尾指针、后继指针置空,避免野指针。
代码示例
// 清空带头单链表
void clear(Node* head)
{
Node* p = head->next;
while (p != nullptr)
{
Node* temp = p;
p = p->next;
delete temp; // 逐个释放节点
}
head->next = nullptr; // 头结点后继置空,链表为空
}
2.2 交换两个链表
概念
交换两个链表的所有数据节点,仅交换头尾指针,不挪动节点、不拷贝数据。
特性
-
带头链表:只需交换两个链表的头结点后继、尾指针即可,效率极高;
-
单链表仅操作
next指针,双向链表需同步处理prev和next; -
本质修改指针指向,节点本身内存位置不变。
代码示例(交换两个带头单链表)
// 交换两个链表的有效节点
void swapList(Node* h1, Node* Node* h2)
{
// 临时指针中转
Node* temp = h1->next;
h1->next = h2->next;
h2->next = temp;
}
三、单链表经典算法(面试高频)
3.1 求链表长度
概念
遍历链表所有有效节点,统计节点总个数。
特性
-
时间复杂度:(O(n)),需要完整遍历一次链表;
-
遍历起点:带头链表从
head->next开始,不带头链表直接从头指针开始; -
空链表长度为 0。
代码示例
int getLength(Node* head)
{
int count = 0;
Node* p = head->next;
while (p != nullptr)
{
count++;
p = p->next;
}
return count;
}
3.2 单链表反转
概念
颠倒节点遍历顺序,原尾节点变为首节点,原首节点变为尾节点,仅修改指针指向,不修改数据。
特性
-
单链表仅能单向遍历,常用两种实现方案:
-
头插法(推荐,逻辑简单);
-
三指针迭代法;
-
-
时间复杂度:(O(n));
-
反转后原链表失效。
代码示例(头插法反转)
void reverseList(Node* head)
{
Node* cur = head->next;
Node* newHead = nullptr;
while (cur != nullptr)
{
Node* next = cur->next; // 暂存后继节点
cur->next = newHead; // 当前节点头插
newHead = cur;
cur = next;
}
head->next = newHead; // 重新挂载到哨兵头
}
3.3 查找倒数第 K 个节点
概念
在单链表中定位倒数第 K 个节点,快慢指针法是最优解法。
特性
-
基础解法:先求总长度,再正向遍历
len-K步,遍历两次链表; -
快慢指针法(双指针):仅遍历一次,效率更高;
快指针先走 K 步;
快慢指针同步向后移动,快指针到末尾时,慢指针即为倒数第 K 节点;
-
边界判断:K 非法(K≤0 / K > 链表长度)直接返回空。
代码示例(快慢指针版)
Node* findLastK(Node* head, int k)
{
if (k <= 0) return nullptr;
Node* fast = head->next;
Node* slow = head->next;
// 快指针先走k步
for (int i = 0; i < k && fast != nullptr; i++)
{
fast = fast->next;
}
// 步数不足,K超过链表长度
if (fast == nullptr && i < k)
return nullptr;
// 快慢同步后移
while (fast != nullptr)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
3.4 查找链表中间节点
概念
利用快慢指针查找链表中间位置节点。
特性
-
慢指针每次走 1 步,快指针每次走 2 步;
-
快指针走到末尾时,慢指针指向中间节点;
-
链表长度为偶数:得到靠右的中间节点。
代码示例
Node* findMid(Node* head)
{
Node* slow = head->next;
Node* fast = head->next;
while (fast != nullptr && fast->next != nullptr)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
3.5 从尾到头打印链表
概念
单链表只能正向遍历,实现逆序打印常用递归法 和栈结构。
特性
-
递归法:利用函数调用栈,先递归走到链表尾部,回溯时打印(无需额外空间);
-
栈法:节点依次入栈,再出栈打印(先进后出特性);
-
递归深度过大易造成栈溢出。
代码示例(递归实现)
// 递归逆序打印
void printReverse(Node* p)
{
if (p == nullptr)
return;
printReverse(p->next); // 先递归到尾部
cout << p->data << " "; // 回溯时打印
}
// 调用方式
printReverse(head->next);
3.6 单链表判环
概念
判断链表是否存在环形结构(尾节点不再置空,指向链表内部节点)。
特性
-
核心解法:快慢指针(追击法);
-
逻辑:无环时快指针必然先走到
nullptr;有环时快慢指针最终一定会相遇; -
快指针步长 2,慢指针步长 1。
代码示例
bool hasCycle(Node* head)
{
Node* slow = head->next;
Node* fast = head->next;
while (fast != nullptr && fast->next != nullptr)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast) // 指针相遇,存在环
return true;
}
return false; // 快指针走到末尾,无环
}
3.7 求环的长度
概念
已知链表有环,计算环内节点总个数。
特性
-
先利用快慢指针找到环内相遇点;
-
固定一个指针,另一个指针继续遍历,再次相遇时统计步数,即为环长。
代码示例
int getCycleLen(Node* head)
{
if (!hasCycle(head)) return 0;
Node* slow = head->next;
Node* fast = head->next;
// 找到相遇点
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast) break;
}
// 统计环长
int len = 0;
Node* p = slow;
do
{
p = p->next;
len++;
} while (p != slow);
return len;
}
3.8 判断两个单链表是否相交
概念
两个链表尾部节点指向同一个节点,即为相交链表。
特性
-
核心结论:两个相交单链表,尾节点一定相同;尾节点不同则必然不相交;
-
实现思路:分别遍历到两个链表尾部,对比尾指针地址。
代码示例
bool isIntersect(Node* h1, Node* h2)
{
Node* p1 = h1->next;
Node* p2 = h2->next;
// 遍历到尾节点
while (p1->next != nullptr) p1 = p1->next;
while (p2->next != nullptr) p2 = p2->next;
return p1 == p2; // 尾节点地址相等则相交
}
3.9 查找相交的第一个节点
概念
找到两个相交链表的首个公共节点,两种主流解法。
解法 1:长度差值法(推荐,不修改节点结构)
-
分别求出两个链表长度;
-
长链表先走「长度差」步;
-
两个指针同步后,第一个相等节点即为交点。
代码示例
Node* findInterNode(Node* h1, Node* h2)
{
int len1 = getLength(h1);
int len2 = getLength(h2);
Node* p1 = h1->next;
Node* p2 = h2->next;
// 长链表先走差值
if (len1 > len2)
{
for (int i = 0; i < len1 - len2; i++)
p1 = p1;
}
else
{
for (int i = 0; i < len2 - len1; i++)
p2 = p2;
}
// 同步遍历找交点
while (p1 != nullptr && p2 != nullptr)
{
if (p1 == p2) return p1;
p1 = p1->next;
p2 = p2;
}
return nullptr;
}
解法 2:标记计数法(修改节点)
给每个节点增加计数标记,遍历两个链表,标记重复的节点即为交点;缺点:需要修改节点结构。
3.10 O (1) 时间删除指定节点
概念
已知指向待删除节点的指针,要求不遍历链表、以 (O(1)) 时间复杂度完成删除。
特性
-
单链表无法直接获取前驱节点,不能常规摘链;
-
核心思路:偷梁换柱
将后继节点的数据拷贝到当前节点;
跳过后继节点,释放原后继内存;
-
边界:待删除节点为尾节点时,仍需要遍历找前驱。
代码示例
// 传入待删除节点p(非尾节点)
void delNode(Node* p)
{
if (p == nullptr || p->next == nullptr)
return;
// 拷贝后继数据
p->data = p->next->data;
// 释放后继节点
Node* temp = p->next;
p->next = p->next->next;
delete temp;
}
拓展
该算法仅适用于非尾节点;若为尾节点,必须正向遍历找到前驱节点才能删除。
四、栈结构简介
概念
栈是受限线性表 ,遵循 先进后出(FILO) 规则,仅允许在栈顶进行插入(入栈)、删除(出栈)操作。
特性
-
操作端口唯一:所有操作仅限栈顶;
-
实现方式:顺序表(数组)、链表均可实现栈;
-
典型应用:递归调用、表达式求值、逆序操作(链表逆序打印)。
代码示例(链式栈简易实现)
struct StackNode
{
int data;
StackNode* next;
StackNode(int val):data(val),next(nullptr){}
};
// 入栈
void push(StackNode*& top, int val)
{
StackNode* newNode = new StackNode(val);
newNode->next = top;
top = newNode;
}
// 出栈
void pop(StackNode*& top)
{
if (top == nullptr) return;
StackNode* temp = top;
top = top->next;
delete temp;
}
五、拓展总结 & 考点梳理
-
快慢指针:链表高频考点,用于求中间节点、倒数 K 节点、链表判环,核心是步长差异;
-
递归:适合单链表逆序打印,注意递归深度限制;
-
O (1) 删除节点:单链表经典巧解,利用数据拷贝规避找前驱的问题;
-
链表相交 / 判环:笔试、面试必考,优先使用长度差、快慢指针标准解法;
-
链表操作核心原则:操作指针前先暂存后继节点,防止断链 ,使用完毕及时
delete避免内存泄漏。