跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

链表基础概念及常用算法题解析

链表是一种物理存储结构上不连续的线性表,数据元素顺序通过指针连接。文章回顾了链表的结构定义、遍历、节点申请、尾插、头插及删除操作,对比了 C 与 C++ 的实现差异及 nullptr 与 NULL 的区别。随后通过反转链表、区间反转、K 组翻转、合并两个排序链表及合并 K 个排序链表等经典算法题,演示了迭代与递归在链表处理中的具体应用。

SecGuard发布于 2026/3/16更新于 2026/9/867 浏览
链表的概念以及结构

概念:链表是一种物理存储结构上不连续的、非顺序的存储结构,数据元素的顺序是通过节点中的指针来实现的。

结构:(此处指单向非循环链表)链表中每个节点的存储元素一般包含两部分,数据和下一个节点的地址。

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

![链表结构示意图]

遍历打印链表

在链表中访问元素需要通过每个节点中存放的下一个元素地址,才能找到下一个节点的位置。具体代码实现如下:

// 遍历链表
void SListPrint(SListNode* phead) {
    SListNode* cur = phead; // 创建一个指针来指向头结点的位置
    while(cur) {
        printf("%d->", cur->val); // 打印当前节点的数据
        cur = cur->next; // 将指针指向下一个节点
    }
}
申请节点

链表中每一个节点都是动态开辟出来的,所以要新增一个节点之前要先申请一个节点,每个节点的大小为该结构体的大小,开辟成功之后,该节点数据部分存放输入的值,地址(next)置为 NULL。

C 语言代码展示如下(这部分主要是为了了解具体实现步骤):

// 申请节点
SListNode* SListBuyNode(SListNodeDataType x) {
    SListNode* newnode = (SListNode*)malloc(sizeof(SListNode)); // 申请地址空间
    if (newnode == NULL) { // 判断是否成功
        perror("malloc:");
        exit(-1);
    }
    newnode->data = x;
    newnode->next = NULL;
    return newnode;
}

用 C++ 则更为简洁且更加安全,代码如下:

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {};
};

ListNode* newNode = new ListNode(newValue);
尾插

因为链表无法通过元素下标访问,所以我们要想知道下一个节点的位置,只能通过遍历整个链表。

这里分为两种情况:

  • 链表为空,直接插入
  • 链表不为空,遍历找到尾节点,再插入
// 尾插
void SListPushBack(ListNode** head, int x) {
    ListNode* newNode = new ListNode(x); // 创建一个新节点
    if(head == nullptr) {
        *head = newNode; // 如果要插入的链表为空,则直接将新建的节点作为头节点即可
    } else {
        ListNode* current = *head;
        while(current != nullptr) {
            current = current->next;
        }
        current->next = newNode;
    }
}
头插

在头部插入的话,只需要将当前节点置为头节点,再将当前节点的 next 改为头节点。

// 头插
void SListPushFront(ListNode** head, int x) {
    // 申请新增节点
    ListNode* newNode = new ListNode(x);
    // 将当前节点的 next 改为头节点
    newNode->next = *head;
    // 将头节点设为当前节点
    *head = newNode;
}

函数参数 ListNode* head 是值传递,这意味着函数内部对 head 的修改不会影响到函数外部的实参。如果要修改外部的头指针,应该传递头指针的指针(ListNode** head)或者头指针的引用(ListNode*& head)。

删除
  1. 链表为空,不能删除
  2. 删除头节点:如果要删除的是头节点,先保存头节点,再将下一个节点更新为头节点
  3. 删除其余位置节点

具体代码展示如下:

// 删除链表中值为 target 的节点
ListNode* deleteNode(ListNode* head, int target) {
    // 链表为空
    if(head == nullptr) {
        return nullptr;
    }
    // 删除头节点
    if(head->val == target) {
        // 先记下头节点位置
        ListNode* temp = head;
        head = head->next;
        delete temp;
        return head;
    }
    // 删除其余位置
    ListNode* cur = head;
    // 找到目标位置的前一个节点
    while(cur->next && cur->next->val != target) {
        cur = cur->next;
    }
    if(cur->next) {
        ListNode* temp = cur->next; // 记录下要删除节点的下一个节点
        cur->next = cur->next->next; // 跳过要删除的节点
        delete temp; // 删除节点
    }
    return head;
}

最后再加一个判断 if(cur->next !=nullptr) 的原因:

目标节点不存在于链表中:如果链表中不存在值为 target 的节点,while 循环会一直执行到链表末尾,此时 cur->next 为 nullptr。当 while 循环条件 cur->next != nullptr && cur->next->val != target 中的 cur->next == nullptr 时,循环终止。在这种情况下,就不应该执行删除操作,因为没有找到要删除的节点。所以 if (cur->next != nullptr) 这个判断可以防止在链表中没有目标节点时误删节点或引发空指针异常。

nullptr 和 NULL 的区别

在 C++ 中,nullptr 和 NULL 都用于表示空指针,但它们之间存在一些重要区别:

  • nullptr:nullptr 是 C++ 11 引入的关键字,它的类型是 std::nullptr_t。这是一种特殊的类型,专门用于表示空指针。它可以隐式转换为任何指针类型,使得代码在处理空指针时更加类型安全。例如:
int* ptr1 = nullptr;
double* ptr2 = nullptr;
  • NULL:NULL 是一个宏,通常在 C 标准库头文件(如 <stdio.h>)或 C++ 兼容头文件中定义。在 C 语言中,它通常被定义为 ((void*)0),在 C++ 中,它通常被定义为 0 或 0L(长整型 0)。由于它本质上是整数类型(在 C++ 中),当将 NULL 赋值给指针时,可能会导致一些潜在的类型混淆问题。例如,假设有一个函数重载,同时接受指针和整数类型参数:
void func(int num);
void func(int* ptr);

func(NULL); // 在 C++ 中,这可能会导致编译错误或调用错误的函数,因为 NULL 可能被解释为整数 0
func(nullptr); // 明确调用 func(int* ptr)
经典算法题示例
题目一:反转链表

![反转链表示意图]

/**
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 *     ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param head ListNode 类
     * @return ListNode 类
     */
    ListNode* ReverseList(ListNode* head) {
        ListNode* per = nullptr;  // 定义前序节点
        while(head) { // 判断是否到结尾
            ListNode* temp = head->next; // 记录下一个节点
            head->next = per;  // 反转当前节点
            per = head; // 前序节点向后移
            head = temp; // 当前节点后移
        }
        return per;
    }
};

总结:逐个翻转,定义了前序节点和当前节点,逐步翻转,逐步后移,直到链表尾部。

题目二:链表内指定区间反转

![链表内指定区间反转示意图]

/**
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 *     ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param head ListNode 类
     * @param m int 整型
     * @param n int 整型
     * @return ListNode 类
     */
    ListNode* reverseBetween(ListNode* head, int m, int n) {
        // 定义一个表头
        ListNode* res = new ListNode(-1);
        res->next = head;
        // 定义前序节点
        ListNode* per = res;
        // 定义当前节点
        ListNode* cur = head;
        // 找到 m 位置
        for(int i = 1; i < m; i++) {
            per = cur;
            cur = cur->next;
        }
        // 从 m 反转到 n
        for(int i = m; i < n; i++) {
            ListNode* temp = cur->next; // 记录当前节点下一个节点
            cur->next = temp->next; // 越过当前节点
            temp->next = per->next; // 当前节点反转
            per->next = temp; // 前序节点连到最后
        }
        // 返回去掉表头
        return res->next;
    }
};

理解起来比较抽象,建议多写几遍加深记忆。

![手绘理解图]

题目三:链表中的节点每 k 个一组翻转

![链表 k 组翻转示意图]

/**
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 *     ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param head ListNode 类
     * @param k int 整型
     * @return ListNode 类
     */
    ListNode* reverseKGroup(ListNode* head, int k) {
        // 找到每次反转的尾部
        ListNode* tail = head;
        // 遍历 k 次找到当前组的尾部
        for(int i = 0; i < k; i++) {
            if(tail == nullptr) return head;
            tail = tail->next;
        }
        // 定义所需的前序节点和当前节点
        ListNode* per = nullptr;
        ListNode* cur = head;
        while(cur != tail) {
            ListNode* temp = cur->next;
            cur->next = per;
            per = cur;
            cur = temp;
        }
        head->next = reverseKGroup(tail, k);
        return per;
    }
};

总结:

  1. 定义一个尾部,遍历 k 找到当前组的队尾(如果遍历时剩下的不足 k 个,则直接返回最后一组的 head)
  2. 定义前序、当前节点,对当前小组进行翻转
  3. 记下当前组的 head(翻转前为头,现在是尾),给 head 赋值为递归函数的返回值(即下一组的头节点),递归传入当前 tail(指向当前组的下一个位置,即下一组的头部)
  4. 返回当前的首部

理解了也不难,总之还是要多写两遍。

题目四:合并两个排序的链表

![合并两个排序链表示意图]

/**
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 *     ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param pHead1 ListNode 类
     * @param pHead2 ListNode 类
     * @return ListNode 类
     */
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
        ListNode* res = new ListNode(-1);  // 定义一个表头
        ListNode* tail = res;  // 用来遍历的指针
        while(pHead1 != nullptr && pHead2 != nullptr) {
            if(pHead1->val < pHead2->val) {
                tail->next = pHead1;
                pHead1 = pHead1->next;
            } else {
                tail->next = pHead2;
                pHead2 = pHead2->next;
            }
            tail = tail->next;
        }
        // 处理剩余的
        if(pHead1 != nullptr) {
            tail->next = pHead1;
        } else {
            tail->next = pHead2;
        }
        return res->next;
    }
};

逻辑清晰,阅读代码即可理解。

题目五:合并 k 个已排序的链表

主要是递归和分治的一个思想,将链表数组不断等分,直到只剩最后两个数组,然后将这两个数组排序合并,在逐步回归合并。

![合并 k 个排序链表示意图]

主要难点就是递归合并,排序部分跟上一道题一模一样。

/**
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 *     ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param lists ListNode 类 vector
     * @return ListNode 类
     */
    
    // 用来合并两个链表
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
        ListNode* res = new ListNode(-1);  // 定义一个表头
        ListNode* tail = res;  // 用来遍历的指针
        while(pHead1 != nullptr && pHead2 != nullptr) {
            if(pHead1->val < pHead2->val) {
                tail->next = pHead1;
                pHead1 = pHead1->next;
            } else {
                tail->next = pHead2;
                pHead2 = pHead2->next;
            }
            tail = tail->next;
        }
        if(pHead1 != nullptr) {
            tail->next = pHead1;
        } else {
            tail->next = pHead2;
        }
        return res->next;
    }
    
    // 划分合并区间
    ListNode* divideMerge(vector<ListNode*>& lists, int left, int right) {
        if(left > right) return NULL;
        else if(left == right) { // 正好剩下最后一个链表
            return lists[left];
        }
        // 从中间分成两段
        int mid = (left + right)/2;
        return Merge(divideMerge(lists, left, mid), divideMerge(lists, mid + 1, right));
    }
    
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        // k 个列表归并排序
        return divideMerge(lists, 0, lists.size() - 1);
    }
};

能看懂,但是自己完整写下来还是有困难,建议多加练习。

目录

  1. 链表的概念以及结构
  2. 遍历打印链表
  3. 申请节点
  4. 尾插
  5. 头插
  6. 删除
  7. nullptr 和 NULL 的区别
  8. 经典算法题示例
  9. 题目一:反转链表
  10. 题目二:链表内指定区间反转
  11. 题目三:链表中的节点每 k 个一组翻转
  12. 题目四:合并两个排序的链表
  13. 题目五:合并 k 个已排序的链表

更多推荐文章

查看全部
  • C++ 面向对象编程核心:继承机制深度解析
  • Docker 部署 SpringBoot 项目拉取 JDK 镜像报错解决方案
  • AI 创作实战指南:从提示词工程到商业变现逻辑
  • 基于 Obsidian 与 OpenClaw 的 AI 知识管理方案
  • Cursor 实战:Web 版背单词应用开发
  • B-树模拟实现详解
  • Python 数据可视化实战:Matplotlib 基础与进阶
  • C++ 智能指针:示例、原理与适用场景详解
  • Python 操作 Word 文档入门与实战指南
  • Claude Code 本地化接入魔搭社区指南
  • npm 安装 OpenClaw 遇到 Git 报错的处理方案
  • JVMS工具在Windows平台管理JDK版本实践
  • JavaScript 中 Document 对象常见属性分析
  • PRIDE-PPPAR 安装与配置完整指南
  • Python 数据容器详解:列表、元组、字符串、集合与字典
  • Java 重构实战:GitHub Copilot 上下文感知应用
  • llama.cpp 大模型部署指南:CPU/GPU 兼容与 Docker 快速启动
  • Neo4j 图数据库安装与基础使用指南
  • 基于 YOLO26 深度学习的无人机视角河道水面垃圾检测系统
  • 新机型 Copilot 键替代右 Ctrl 键的解决方案

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online