C/C++ 数据结构(三)链表核心算法

本篇核心知识:链表基础、链表常用操作(清空、交换)、单链表经典题(求长度、反转、查找倒数第 K 个节点、快慢指针、递归逆序打印、链表判环 / 求环长、链表相交、指定节点 O (1) 删除)、栈结构简介


一、链表基础

概念

链表是链式线性结构,内存不连续,依靠指针将多个节点串联。节点分为两大组成部分:

  1. 数据域:存储业务数据;

  2. 指针域 :存储相邻节点地址,单链表仅有后继指针next,双向链表包含前驱prev+ 后继next

特性
  1. 链表无固定容量,可动态增删节点,无需提前开辟连续内存;

  2. 不支持下标随机访问,只能从头部开始逐一遍历;

  3. 分为

    (1)带头结点(哨兵节点)

    (2)不带头结点

    两种设计:

    带头结点:头结点不存有效数据,统一空表、头部增删逻辑,代码更简洁;

    不带头结点:首节点即为有效节点,头部操作需要特殊判断。

代码示例(单链表基础节点定义)

复制代码
#include <iostream>
using namespace std;
​
// 单链表节点
struct Node
{
    int data;       // 数据域
    Node* next;     // 指针域:指向后继节点
    Node(int val) : data(val), next(nullptr) {}
};

二、链表基础操作(清空、交换两个链表)

2.1 链表清空

概念

释放链表中所有堆节点内存,防止内存泄漏,最终将链表置为空表。

特性
  1. 逐个遍历有效节点,调用delete释放;

  2. 带头链表仅清空有效节点,保留哨兵头结点;

  3. 清空后将尾指针、后继指针置空,避免野指针。

代码示例
复制代码
// 清空带头单链表
void clear(Node* head)
{
    Node* p = head->next;
    while (p != nullptr)
    {
        Node* temp = p;
        p = p->next;
        delete temp; // 逐个释放节点
    }
    head->next = nullptr; // 头结点后继置空,链表为空
}

2.2 交换两个链表

概念

交换两个链表的所有数据节点,仅交换头尾指针,不挪动节点、不拷贝数据。

特性
  1. 带头链表:只需交换两个链表的头结点后继、尾指针即可,效率极高;

  2. 单链表仅操作next指针,双向链表需同步处理prevnext

  3. 本质修改指针指向,节点本身内存位置不变。

代码示例(交换两个带头单链表)
复制代码
// 交换两个链表的有效节点
void swapList(Node* h1, Node* Node* h2)
{
    // 临时指针中转
    Node* temp = h1->next;
    h1->next = h2->next;
    h2->next = temp;
}

三、单链表经典算法(面试高频)

3.1 求链表长度

概念

遍历链表所有有效节点,统计节点总个数。

特性
  1. 时间复杂度:(O(n)),需要完整遍历一次链表;

  2. 遍历起点:带头链表从head->next开始,不带头链表直接从头指针开始;

  3. 空链表长度为 0。

代码示例
复制代码
int getLength(Node* head)
{
    int count = 0;
    Node* p = head->next;
    while (p != nullptr)
    {
        count++;
        p = p->next;
    }
    return count;
}

3.2 单链表反转

概念

颠倒节点遍历顺序,原尾节点变为首节点,原首节点变为尾节点,仅修改指针指向,不修改数据

特性
  1. 单链表仅能单向遍历,常用两种实现方案:

    • 头插法(推荐,逻辑简单);

    • 三指针迭代法;

  2. 时间复杂度:(O(n));

  3. 反转后原链表失效。

代码示例(头插法反转)
复制代码
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 个节点,快慢指针法是最优解法。

特性
  1. 基础解法:先求总长度,再正向遍历 len-K 步,遍历两次链表;

  2. 快慢指针法(双指针):仅遍历一次,效率更高;

    快指针先走 K 步;

    快慢指针同步向后移动,快指针到末尾时,慢指针即为倒数第 K 节点;

  3. 边界判断: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. 慢指针每次走 1 步,快指针每次走 2 步;

  2. 快指针走到末尾时,慢指针指向中间节点;

  3. 链表长度为偶数:得到靠右的中间节点。

代码示例
复制代码
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 从尾到头打印链表

概念

单链表只能正向遍历,实现逆序打印常用递归法栈结构

特性
  1. 递归法:利用函数调用栈,先递归走到链表尾部,回溯时打印(无需额外空间);

  2. 栈法:节点依次入栈,再出栈打印(先进后出特性);

  3. 递归深度过大易造成栈溢出。

代码示例(递归实现)
复制代码
// 递归逆序打印
void printReverse(Node* p)
{
    if (p == nullptr)
        return;
    printReverse(p->next); // 先递归到尾部
    cout << p->data << " "; // 回溯时打印
}
​
// 调用方式
printReverse(head->next);

3.6 单链表判环

概念

判断链表是否存在环形结构(尾节点不再置空,指向链表内部节点)。

特性
  1. 核心解法:快慢指针(追击法)

  2. 逻辑:无环时快指针必然先走到nullptr;有环时快慢指针最终一定会相遇;

  3. 快指针步长 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 求环的长度

概念

已知链表有环,计算环内节点总个数。

特性
  1. 先利用快慢指针找到环内相遇点;

  2. 固定一个指针,另一个指针继续遍历,再次相遇时统计步数,即为环长。

代码示例
复制代码
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 判断两个单链表是否相交

概念

两个链表尾部节点指向同一个节点,即为相交链表。

特性
  1. 核心结论:两个相交单链表,尾节点一定相同;尾节点不同则必然不相交;

  2. 实现思路:分别遍历到两个链表尾部,对比尾指针地址。

代码示例
复制代码
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:长度差值法(推荐,不修改节点结构)
  1. 分别求出两个链表长度;

  2. 长链表先走「长度差」步;

  3. 两个指针同步后,第一个相等节点即为交点。

代码示例
复制代码
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)) 时间复杂度完成删除。

特性
  1. 单链表无法直接获取前驱节点,不能常规摘链;

  2. 核心思路:偷梁换柱

    将后继节点的数据拷贝到当前节点;

    跳过后继节点,释放原后继内存;

  3. 边界:待删除节点为尾节点时,仍需要遍历找前驱。

代码示例
复制代码
// 传入待删除节点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) 规则,仅允许在栈顶进行插入(入栈)、删除(出栈)操作。

特性

  1. 操作端口唯一:所有操作仅限栈顶;

  2. 实现方式:顺序表(数组)、链表均可实现栈;

  3. 典型应用:递归调用、表达式求值、逆序操作(链表逆序打印)。

代码示例(链式栈简易实现)

复制代码
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;
}

五、拓展总结 & 考点梳理

  1. 快慢指针:链表高频考点,用于求中间节点、倒数 K 节点、链表判环,核心是步长差异;

  2. 递归:适合单链表逆序打印,注意递归深度限制;

  3. O (1) 删除节点:单链表经典巧解,利用数据拷贝规避找前驱的问题;

  4. 链表相交 / 判环:笔试、面试必考,优先使用长度差、快慢指针标准解法;

  5. 链表操作核心原则:操作指针前先暂存后继节点,防止断链 ,使用完毕及时delete避免内存泄漏。

相关推荐
王老师青少年编程1 小时前
2022年CSP-X复赛真题及题解(T2:移动棋子)
c++·真题·csp·信奥赛·复赛·csp-x·移动棋子
变量未定义~2 小时前
摆放小球 、dp求解组合数、求解组合数2
数据结构·算法
Sunsets_Red2 小时前
ABC462D 题解
c++·数学·编程·比赛·atcoder·信息学竞赛·信息学
喵星人工作室2 小时前
C++火影忍者1.1.8
开发语言·c++·游戏
凡人叶枫2 小时前
Effective C++ 条款26:尽可能延后变量定义式的出现时间
linux·开发语言·c++·effective c++
加油码2 小时前
位图 BitMap:用一个 bit 管一个状态,空间直接省到位
c++·算法
四代水门2 小时前
LeetCode刷算法题(C++)
c++·算法·leetcode
一头老黄牛@2 小时前
飞书 × OpenClaw 接入指南:不用服务器,用长连接把机器人跑起来
数据结构·人工智能·程序人生·算法·决策树·自动化·推荐算法
Zhan8611244 小时前
数据接口的序列号机制与丢包检测:西班牙行情数据IBEX指数实时行情接入笔记
大数据·数据结构·笔记·区块链