0

0

c++如何实现一个链表_c++数据结构之链表实现全过程

下次还敢

下次还敢

发布时间:2025-09-24 15:28:01

|

562人浏览过

|

来源于php中文网

原创

C++链表通过节点和指针实现动态数据结构,核心优势在于动态大小、高效插入删除、内存利用率高,适用于数据量变化大或频繁增删的场景;相比数组,链表无需连续内存,但随机访问效率低且有额外指针开销;实现时需警惕空指针解引用、内存泄漏、指针更新错误等陷阱,调试可通过打印、画图、断点等方式;可扩展为双向链表以支持前后遍历,或增加反转、排序、合并等复杂操作。

c++如何实现一个链表_c++数据结构之链表实现全过程

在C++中实现一个链表,核心在于定义节点结构体,然后通过指针将这些节点串联起来,形成一个动态的数据序列。它不像数组那样在内存中连续存储,而是通过每个节点内部的一个指针,指向序列中的下一个(或上一个)元素。说白了,就是自己手动搭积木,每一块积木(节点)都知道下一块积木在哪里,而你只需要记住第一块积木的位置就行了。

解决方案

实现一个单向链表,我们通常需要一个Node结构体和一个LinkedList类。

1. Node结构体: 这是链表的基本单元,它至少包含两部分:存储的数据和指向下一个节点的指针。

template 
struct Node {
    T data;
    Node* next;

    Node(T val) : data(val), next(nullptr) {}
};

2. LinkedList类: 这个类会管理链表的整体结构,包括头节点(head)和一些基本操作,比如添加、删除、查找、打印等。

#include  // 用于输出
#include  // 用于异常处理

template 
class LinkedList {
private:
    Node* head; // 链表的头节点

public:
    // 构造函数
    LinkedList() : head(nullptr) {}

    // 析构函数:释放所有节点内存,防止内存泄漏
    ~LinkedList() {
        Node* current = head;
        while (current != nullptr) {
            Node* nextNode = current->next;
            delete current;
            current = nextNode;
        }
        head = nullptr; // 确保头指针为空
    }

    // 在链表头部添加元素
    void addHead(T val) {
        Node* newNode = new Node(val);
        newNode->next = head;
        head = newNode;
    }

    // 在链表尾部添加元素
    void addTail(T val) {
        Node* newNode = new Node(val);
        if (head == nullptr) { // 如果链表为空,新节点就是头节点
            head = newNode;
            return;
        }
        Node* current = head;
        while (current->next != nullptr) { // 遍历到链表尾部
            current = current->next;
        }
        current->next = newNode;
    }

    // 在指定位置插入元素(位置从0开始)
    void insert(int index, T val) {
        if (index < 0) {
            throw std::out_of_range("Index cannot be negative.");
        }
        if (index == 0) {
            addHead(val);
            return;
        }

        Node* newNode = new Node(val);
        Node* current = head;
        Node* prev = nullptr;
        int count = 0;

        while (current != nullptr && count < index) {
            prev = current;
            current = current->next;
            count++;
        }

        if (count < index) { // 如果index超出了链表长度
            throw std::out_of_range("Index out of bounds.");
        }

        prev->next = newNode;
        newNode->next = current;
    }

    // 删除指定值的第一个元素
    bool remove(T val) {
        if (head == nullptr) {
            return false; // 链表为空
        }

        if (head->data == val) { // 如果要删除的是头节点
            Node* temp = head;
            head = head->next;
            delete temp;
            return true;
        }

        Node* current = head;
        Node* prev = nullptr;
        while (current != nullptr && current->data != val) {
            prev = current;
            current = current->next;
        }

        if (current == nullptr) { // 没找到
            return false;
        }

        prev->next = current->next; // 跳过当前节点
        delete current;
        return true;
    }

    // 查找元素是否存在
    bool find(T val) const {
        Node* current = head;
        while (current != nullptr) {
            if (current->data == val) {
                return true;
            }
            current = current->next;
        }
        return false;
    }

    // 打印链表所有元素
    void print() const {
        Node* current = head;
        if (current == nullptr) {
            std::cout << "List is empty." << std::endl;
            return;
        }
        while (current != nullptr) {
            std::cout << current->data << " -> ";
            current = current->next;
        }
        std::cout << "nullptr" << std::endl;
    }

    // 获取链表长度
    int size() const {
        int count = 0;
        Node* current = head;
        while (current != nullptr) {
            count++;
            current = current->next;
        }
        return count;
    }
};

// 示例使用
int main() {
    LinkedList myList;
    myList.addHead(10);
    myList.addTail(20);
    myList.addHead(5);
    myList.addTail(30);
    myList.print(); // Output: 5 -> 10 -> 20 -> 30 -> nullptr

    myList.insert(2, 15);
    myList.print(); // Output: 5 -> 10 -> 15 -> 20 -> 30 -> nullptr

    std::cout << "Find 20: " << (myList.find(20) ? "Yes" : "No") << std::endl; // Output: Yes
    std::cout << "Find 100: " << (myList.find(100) ? "Yes" : "No") << std::endl; // Output: No

    myList.remove(15);
    myList.print(); // Output: 5 -> 10 -> 20 -> 30 -> nullptr

    myList.remove(5); // 删除头节点
    myList.print(); // Output: 10 -> 20 -> 30 -> nullptr

    myList.remove(30); // 删除尾节点
    myList.print(); // Output: 10 -> 20 -> nullptr

    std::cout << "List size: " << myList.size() << std::endl; // Output: 2

    return 0;
}

C++链表与数组相比,有哪些核心优势和适用场景?

链表和数组,这哥俩在数据结构里算是老对手了,各有各的脾气和用武之地。从我的经验来看,链表最大的魅力在于它的动态性插入/删除的效率。你想想看,数组一旦声明了大小,就板上钉钉了,想扩容?那就得重新找一块更大的地方,然后把所有数据搬过去,这效率可想而知。但链表不一样,它天生就是弹性的,需要多大的空间就申请多少个节点,每个节点独立存在,通过指针连接。

具体来说,链表的优势体现在:

立即学习C++免费学习笔记(深入)”;

  • 动态大小: 链表可以根据需要动态增长或缩小,无需预先确定大小。这在处理数据量不确定,或者数据量变化很大的场景下,简直是救命稻草。
  • 高效的插入和删除: 如果你已经知道要操作的位置(比如找到了某个节点),那么在链表的中间插入或删除一个元素,只需要修改几个指针的指向,操作复杂度是O(1)。而数组呢?插入或删除一个元素,后面的所有元素都得跟着挪位置,那可是O(N)的开销。
  • 内存利用率: 链表节点可以分散在内存的任何地方,只要有空闲内存就能分配,不像数组需要一大块连续的内存空间。这对于内存碎片化比较严重的系统,或者需要存储大量小对象的场景来说,是个不小的优势。

当然,链表也不是万能的。它的缺点也很明显:

  • 随机访问效率低: 数组可以通过索引直接访问任何元素,O(1)。但链表想访问第N个元素?对不起,你得从头开始一个一个地遍历过去,效率是O(N)。
  • 额外的内存开销: 每个节点除了存储数据,还得额外存储一个或多个指针,这无疑增加了内存的消耗。
  • 缓存不友好: 由于节点在内存中可能不连续,CPU的缓存机制就很难发挥作用,这可能会影响程序的整体性能。

所以,什么时候用链表呢?

  • 当你的数据量是动态变化的,且你无法预估其最大值时。
  • 当你需要频繁地在数据集合的中间进行插入或删除操作时(比如实现一个任务队列,或者编辑器的撤销/重做功能)。
  • 当内存碎片化是你的主要顾虑时。
  • 实现一些其他数据结构,比如、队列、图的邻接表等。

反之,如果你的数据量相对固定,或者你需要频繁地通过索引进行随机访问,那么数组或者std::vector会是更好的选择。

在C++中实现单向链表时,最常见的陷阱和调试技巧是什么?

实现链表,尤其是在C++这种需要手动管理内存的语言里,说实话,是个“坑”不少的活。我刚开始学的时候,没少在这里面栽跟头。最常见的几个陷阱,几乎每个初学者都会遇到:

  1. 空指针解引用 (Null Pointer Dereferencing): 这是头号杀手。当你试图访问一个nullptr指向的内存时,程序会直接崩溃。比如,遍历链表时,忘记检查current != nullptr就去访问current->next。或者删除头节点时,忘记处理链表为空的情况。

    • 应对策略: 永远在访问指针指向的成员之前,检查它是否为nullptr。特别是处理headcurrentprev这些关键指针时。
  2. 内存泄漏 (Memory Leaks): 在C++中,new出来的内存,用完了一定要delete。链表操作中,如果你删除了一个节点,但忘记delete它,或者改变了指针指向导致旧节点“失联”,那么这块内存就永远无法被回收了。尤其是在remove函数和析构函数中,这是高发区。

    • 应对策略: 养成好习惯,newdelete成对出现。在remove操作中,确保被删除的节点在指针断开连接后立即delete。析构函数必须遍历所有节点并delete。使用智能指针(如std::unique_ptr)可以大大缓解这个问题,但对于初学手动实现,还是得老老实实地delete
  3. 指针更新错误: 这是最微妙也最难发现的错误之一。在插入、删除节点时,需要修改prev->nextcurrent->next等指针。如果修改顺序不对,或者漏掉了某个指针的更新,链表结构就可能被破坏,导致部分节点丢失,或者形成循环链表(非预期)。特别是处理头节点或尾节点的特殊情况时,很容易出错。

    • 应对策略: 慢下来,画图!在纸上画出链表的节点和指针,然后一步一步地模拟你的代码如何改变这些指针。这比在脑子里想清楚要有效得多。
  4. 边界条件处理不当: 链表为空、只有一个节点、在头/尾插入/删除、索引越界等,这些都是边界条件。很多bug都发生在这些边缘地带。

    • 应对策略: 编写测试用例时,一定要覆盖所有边界条件。在实现函数时,优先考虑这些特殊情况。

调试技巧:

  • 打印大法 (Print Statements): 这是最直接有效的方式。在关键位置(如每次指针更新后、进入/退出循环时)打印出headcurrentprev的地址和它们指向的数据。这样可以清晰地看到链表结构的变化。
  • 使用调试器 (Debugger): 学习使用GDB(或VS/Xcode自带的调试器)是C++开发的必备技能。设置断点,单步执行,观察变量(尤其是指针)的值,可以帮你追踪程序执行路径和内存状态。
  • 画图模拟: 我前面提到了,这是我个人觉得最有效的“预调试”方法。在编码之前,或者遇到复杂逻辑时,先在纸上画出链表的状态,然后模拟每一步操作,确保指针的走向是正确的。
  • 小步快跑: 不要一次性写完所有功能。先实现addHeadprint,确保它们工作正常;再实现addTail,然后是remove等等。每次增加一个功能,都进行充分测试。

链表操作,本质上就是对指针的精细管理。只要你对指针的指向、内存的分配与释放有清晰的认识,并且足够细心,就能避开大部分陷阱。

如何扩展C++链表以支持更复杂的操作或实现双向链表?

当我们对单向链表的基本操作驾轻就熟之后,自然会想到如何让它更强大,适应更多场景。扩展链表主要有两个方向:一是增强现有功能,二是改变其基本结构。

1. 实现双向链表 (Doubly Linked List):

单向链表只能从前往后遍历,如果想从后往前找,或者在已知当前节点的情况下删除它(而不需要它的前一个节点),那就麻烦了。双向链表就是为了解决这个问题而生的。它在每个节点中额外增加一个指向前一个节点的指针。

节点结构体变化:

template 
struct DoublyNode {
    T data;
    DoublyNode* next;
    DoublyNode* prev; // 新增:指向前一个节点的指针

    DoublyNode(T val) : data(val), next(nullptr), prev(nullptr) {}
};

LinkedList类中的主要变化:

  • headtail指针: 为了方便在两端操作,双向链表通常会同时维护head(头节点)和tail(尾节点)两个指针。
  • 插入操作: 当插入一个新节点时,需要同时更新新节点与前后节点的nextprev指针。比如在中间插入,newNode->prev指向prevNodenewNode->next指向currentNode;同时,prevNode->next指向newNodecurrentNode->prev指向newNode
  • 删除操作: 删除一个节点时,需要将它前后节点的nextprev指针相互连接起来。比如删除targetNode,需要让targetNode->prev->next = targetNode->next,并且targetNode->next->prev = targetNode->prev。同样要处理好头尾节点和空链表的特殊情况。

双向链表虽然增加了内存开销(每个节点多一个指针)和操作的复杂度(每次操作要修改更多指针),但它在某些场景下提供了更高的效率和便利性。

2. 扩展更复杂的操作:

  • 模板化 (Generic Linked List): 我们的示例代码已经使用了模板template ,这使得链表可以存储任何类型的数据(int, double, string, 自定义对象等),大大增加了其通用性。
  • 链表反转 (Reverse a Linked List): 这是一个经典的链表算法题。通过迭代或递归的方式,改变每个节点的next指针指向,使其指向前一个节点。
  • 链表排序 (Sort a Linked List): 链表不像数组那样可以直接通过索引访问,所以一些基于随机访问的排序算法(如快速排序)在链表上效率不高。但像归并排序(Merge Sort)这种,非常适合链表,因为它只需要O(1)的额外空间就可以完成合并操作。
  • 合并两个有序链表: 也是一个常见操作,将两个已经排好序的链表合并成一个仍然有序的链表。
  • 查找链表中间节点: 使用快慢指针(一个指针每次走一步,另一个指针每次走两步),当快指针到达链表末尾时,慢指针正好在中间。
  • 检测链表环 (Cycle Detection): 同样可以使用快慢指针,如果它们最终相遇,则链表存在环。

这些扩展操作,往往需要你对链表的指针操作有更深的理解和更强的抽象能力。每实现一个新功能,都建议先在纸上画图,理清指针的走向,再动手写代码,这样能有效避免很多低级错误。

相关专题

更多
python中print函数的用法
python中print函数的用法

python中print函数的语法是“print(value1, value2, ..., sep=' ', end=' ', file=sys.stdout, flush=False)”。本专题为大家提供print相关的文章、下载、课程内容,供大家免费下载体验。

185

2023.09.27

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

338

2023.08.02

c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

232

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

437

2024.03.01

sort排序函数用法
sort排序函数用法

sort排序函数的用法:1、对列表进行排序,默认情况下,sort函数按升序排序,因此最终输出的结果是按从小到大的顺序排列的;2、对元组进行排序,默认情况下,sort函数按元素的大小进行排序,因此最终输出的结果是按从小到大的顺序排列的;3、对字典进行排序,由于字典是无序的,因此排序后的结果仍然是原来的字典,使用一个lambda表达式作为key参数的值,用于指定排序的依据。

387

2023.09.04

golang结构体相关大全
golang结构体相关大全

本专题整合了golang结构体相关大全,想了解更多内容,请阅读专题下面的文章。

197

2025.06.09

golang结构体方法
golang结构体方法

本专题整合了golang结构体相关内容,请阅读专题下面的文章了解更多。

189

2025.07.04

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

338

2023.08.02

excel表格操作技巧大全 表格制作excel教程
excel表格操作技巧大全 表格制作excel教程

Excel表格操作的核心技巧在于 熟练使用快捷键、数据处理函数及视图工具,如Ctrl+C/V(复制粘贴)、Alt+=(自动求和)、条件格式、数据验证及数据透视表。掌握这些可大幅提升数据分析与办公效率,实现快速录入、查找、筛选和汇总。

0

2026.01.21

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
C# 教程
C# 教程

共94课时 | 7.2万人学习

C 教程
C 教程

共75课时 | 4.1万人学习

C++教程
C++教程

共115课时 | 13.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号