C++手写链表、反转链表、删除链表节点、遍历、为链表增加迭代器

news2024/9/20 18:43:56

  本篇博客介绍如何使用C++实现链表,首先编写一个简单的链表,然后增加模板,再增加迭代器。

简单链表的实现

  链表的结构如下:
在这里插入图片描述

  首先需要定义链表的节点:

struct ListNode
{
    int data;
    ListNode* pNext;

    ListNode(int value = 0):
        data(value), pNext(nullptr){}
};

  再定义链表类

class LinkedList
{
public:
    LinkedList():m_pHead(nullptr)
    {

    }

    ~LinkedList()
    {
        Clear();
    }

    void Add(int value)
    {
        if(!m_pHead)
        {
            m_pHead = new ListNode(value);
        }
        else
        {
            ListNode* current = m_pHead;

            while(current->pNext)
            {
                current = current->pNext;
            }

            current->pNext = new ListNode(value);
        }
    }

    void PrintList()
    {
        if(!m_pHead)
        {
            cout << "LinkedList is empty" << endl;
            return;
        }

        ListNode* current = m_pHead;

        while (current)
        {
            cout << current->data << " ";
            current = current->pNext;
        }
        
        cout << endl;
    }

    // 反转链表
    void Reverse() {
        ListNode* prev = nullptr;
        ListNode* current = m_pHead;
        ListNode* next = nullptr;

        // 从头节点开始反转,让头节点指向空,让下一个节点指向头节点
        while (current != nullptr) {
            next = current->pNext;   // 保存下一个节点
            current->pNext = prev;   // 反转当前节点的指针
            prev = current;         // 更新prev到当前节点
            current = next;         // 移动到下一个节点
        }

        m_pHead = prev; // 更新头节点
    }

    // 删除第一个具有指定值的元素
    void Remove(int val) {
        if (!m_pHead)
            return;

        // 如果要删除的是头节点
        if (m_pHead->data == val) {
            ListNode* toDelete = m_pHead;
            m_pHead = m_pHead->pNext;
            delete toDelete;
            return;
        }

        ListNode* current = m_pHead;
        while (current->pNext && current->pNext->data != val) {
            current = current->pNext;
        }

        if (current->pNext) {
            ListNode* toDelete = current->pNext;
            current->pNext = current->pNext->pNext;
            delete toDelete;
        }
    }

    void Clear()
    {
        while (m_pHead)
        {
            ListNode* pDelete = m_pHead;
            m_pHead = m_pHead->pNext;
            delete pDelete;
            pDelete = nullptr;
        }
    }

private:
    ListNode* m_pHead;
};

  main代码测试:

#include <iostream>
#include "LinkedList0.h"

using namespace std;

int main()
{   
    LinkedList* pList = new LinkedList();
    pList->Add(1);
    pList->Add(4);
    pList->Add(7);
    pList->Add(10);

    pList->PrintList();

    pList->Reverse();

    pList->PrintList();

    pList->Remove(7);

    pList->PrintList();

    delete pList;
    pList = nullptr;

    return 0;
}

运行结果如下:
1 4 7 10
10 7 4 1
10 4 1

如何添加链表元素

  首先根据头指针的位置,将头指针移到末尾,当然这里使用的临时指针current 来移动,然后将头结点的next只想新的结点, 头指针m_pHead的地址并没有变化。

void Add(int value)
{
    if(!m_pHead)
    {
        m_pHead = new ListNode(value);
    }
    else
    {
        ListNode* current = m_pHead;

        while(current->pNext)
        {
            current = current->pNext;
        }

        current->pNext = new ListNode(value);
    }
}

如何清空链表

  从链表头开始依次清空

void Clear()
 {
     while (m_pHead)
     {
         ListNode* pDelete = m_pHead;
         m_pHead = m_pHead->pNext;
         delete pDelete;
         pDelete = nullptr;
     }
 }

  这里也是移动头指针,知道达到尾结点的next.

如何删除链表节点

  删除链表节点会改变当前节点的指向,如果删除的是头结点,则比较简单,删除其它结点相对复杂

如何反转链表

  反转链表也是面试中的经常考察的点,需要非常熟练,主要是思路,从头结点开始原地反转,只是代码的写法相对复杂点,一旦操作顺序不对,很容易出错,代码如下:

// 反转链表
void Reverse() {
    ListNode* prev = nullptr;
    ListNode* current = m_pHead;
    ListNode* next = nullptr;

    // 从头节点开始反转,让头节点指向空,让下一个节点指向头节点
    while (current != nullptr) {
        next = current->pNext;   // 保存下一个节点
        current->pNext = prev;   // 反转当前节点的指针
        prev = current;         // 更新prev到当前节点
        current = next;         // 移动到下一个节点
    }

    m_pHead = prev; // 更新头节点
}

链表模板的实现

  代码如下:

/*

自定义链表 
添加迭代器

*/


#pragma once
#include <iostream>

using namespace std;

template<class T>
struct ListNode
{
    T data;
    ListNode<T>* pNext;

    ListNode(T value = 0):
        data(value), pNext(nullptr){}
};

// 迭代器类
template<typename T>
class LinkedListIterator {
public:
    explicit LinkedListIterator(ListNode<T>* node) : m_node(node) {}

    T& operator*() const { 
        cout << "run T& operator*() const " << endl;
        return m_node->data; 
    }

    T* operator->() { 
        cout << "run T* operator->()" << endl;
        return &m_node->data; 
    }

    // 前缀递增
    LinkedListIterator& operator++() {
        cout << "run LinkedListIterator 前缀递增" << endl;
        m_node = m_node->pNext;
        return *this;
    }

    // 后缀递增
    LinkedListIterator operator++(int) {
        cout << "run LinkedListIterator 后缀递增" << endl;
        LinkedListIterator temp = *this;
        m_node = m_node->pNext;
        return temp;
    }

    friend bool operator==(const LinkedListIterator& a, const LinkedListIterator& b) {
        return a.m_node == b.m_node;
    };

    friend bool operator!=(const LinkedListIterator& a, const LinkedListIterator& b) {
        return a.m_node != b.m_node;
    };

private:
    ListNode<T>* m_node;
};

template<class T>
class LinkedList
{
public:
    using iterator = LinkedListIterator<T>;   // 先声明,并且是public, 不然在外部无法使用
 
public:
    LinkedList():m_pHead(nullptr)
    {

    }

    ~LinkedList()
    {
        Clear();
    }

    void Add(T value)
    {
        if(!m_pHead)
        {
            m_pHead = new ListNode<T>(value);
        }
        else
        {
            ListNode<T>* current = m_pHead;

            while(current->pNext)
            {
                current = current->pNext;
            }

            current->pNext = new ListNode<T>(value);
        }
    }

    void PrintList()
    {
        if(!m_pHead)
        {
            cout << "LinkedList is empty" << endl;
            return;
        }

        ListNode<T>* current = m_pHead;

        while (current)
        {
            cout << current->data << " ";
            current = current->pNext;
        }
        
        cout << endl;
    }

    // 反转链表
    void Reverse() {
        ListNode<T>* prev = nullptr;
        ListNode<T>* current = m_pHead;
        ListNode<T>* next = nullptr;

        // 从头节点开始反转,让头节点指向空,让下一个节点指向头节点
        while (current != nullptr) {
            next = current->pNext;   // 保存下一个节点
            current->pNext = prev;   // 反转当前节点的指针
            prev = current;         // 更新prev到当前节点
            current = next;         // 移动到下一个节点
        }

        m_pHead = prev; // 更新头节点
    }

    // 删除第一个具有指定值的元素
    void Remove(T val) {
        if (!m_pHead)
            return;

        // 如果要删除的是头节点
        if (m_pHead->data == val) {
            ListNode<T>* toDelete = m_pHead;
            m_pHead = m_pHead->pNext;
            delete toDelete;
            return;
        }

        ListNode<T>* current = m_pHead;
        while (current->pNext && current->pNext->data != val) {
            current = current->pNext;
        }

        if (current->pNext) {
            ListNode<T>* toDelete = current->pNext;
            current->pNext = current->pNext->pNext;
            delete toDelete;
        }
    }

    void Clear()
    {
        while (m_pHead)
        {
            ListNode<T>* pDelete = m_pHead;
            m_pHead = m_pHead->pNext;
            delete pDelete;
            pDelete = nullptr;
        }
    }

    iterator begin() {
        return iterator(m_pHead);
    }

    iterator end() {
        return iterator(nullptr);
    }

private:
    ListNode<T>* m_pHead;
};

  在简单链表的基础上增加模板,这个不难,难得是如何给链表增加迭代器。

  main函数测试:

int main()
{   
    LinkedList<int>* pList = new LinkedList<int>();
    pList->Add(10);
    pList->Add(40);
    pList->Add(70);
    pList->Add(100);

    pList->PrintList();

    pList->Reverse();

    pList->PrintList();

    pList->Remove(70);

    pList->PrintList();

    cout << "使用迭代器" << endl;

    //for(LinkedList<int>::iterator it = pList->begin(); it != pList->end(); it++)
    for(auto it = pList->begin(); it != pList->end(); it++)
    {
        cout << *it << " ";
    }

    cout << endl;

    delete pList;
    pList = nullptr;

    return 0;
}

运行结果如下:
10 40 70 100
100 70 40 10
100 40 10
使用迭代器
run T& operator*() const
100 run LinkedListIterator 后缀递增
run T& operator*() const
40 run LinkedListIterator 后缀递增
run T& operator*() const
10 run LinkedListIterator 后缀递增

如何为链表添加迭代器

  迭代器可以理解为是一个类,在类里封装了对象的++操作之类的方法,声明迭代器类,然后在链表类里声明begin()和end()来获取迭代器,begin()返回指向容器第一个元素的迭代器,而end()返回指向容器末尾的下一个位置的迭代器。比如上面的迭代器声明如下:

// 迭代器类
template<typename T>
class LinkedListIterator {
public:
    explicit LinkedListIterator(ListNode<T>* node) : m_node(node) {}

    T& operator*() const { 
        cout << "run T& operator*() const " << endl;
        return m_node->data; 
    }

    T* operator->() { 
        cout << "run T* operator->()" << endl;
        return &m_node->data; 
    }

    // 前缀递增
    LinkedListIterator& operator++() {
        cout << "run LinkedListIterator 前缀递增" << endl;
        m_node = m_node->pNext;
        return *this;
    }

    // 后缀递增
    LinkedListIterator operator++(int) {
        cout << "run LinkedListIterator 后缀递增" << endl;
        LinkedListIterator temp = *this;
        m_node = m_node->pNext;
        return temp;
    }

    friend bool operator==(const LinkedListIterator& a, const LinkedListIterator& b) {
        return a.m_node == b.m_node;
    };

    friend bool operator!=(const LinkedListIterator& a, const LinkedListIterator& b) {
        return a.m_node != b.m_node;
    };

private:
    ListNode<T>* m_node;
};

  然后再链表类里声明begin和end方法:

using iterator = LinkedListIterator<T>;   // 先声明,并且是public, 不然在外部无法使用

iterator begin() {
    return iterator(m_pHead);
}

iterator end() {
    return iterator(nullptr);
}

  需要注意的是在迭代器类里重写了“前加加”和“后加加”运算符,他们二者有区别

前加加

// 前缀递增
LinkedListIterator& operator++() {
    cout << "run LinkedListIterator 前缀递增" << endl;
    m_node = m_node->pNext;
    return *this;
}

注意这里返回的是引用。

问题:重写"前加加"为什么返回引用?

  在C++中,迭代器的前缀递增(++it)操作符通常返回一个到自身的引用,这样做有几个原因:

  1. 链式操作:返回引用允许连续调用操作符或方法。例如,++(++it)或(*++it).method()。如果不返回自身的引用,这样的操作将无法执行,因为递增操作将不会在原始迭代器上进行。
  2. 性能:通过返回自身的引用,可以避免不必要的对象复制。如果递增操作返回一个新的迭代器实例而不是引用,那么每次递增操作都会产生一个临时迭代器对象,这将增加额外的构造和析构开销。返回引用避免了这些潜在的性能问题。
  3. 一致性和期望:在STL(Standard Template Library,标准模板库)中,迭代器遵循一定的惯例和模式,其中前缀递增返回引用就是这些标准操作之一。遵循这些惯例可以使自定义迭代器的行为与STL中的迭代器保持一致,从而减少使用时的困惑。
  4. 语义清晰:返回自身的引用清楚地表明了前缀递增操作是在修改迭代器本身,而不是创建一个新的迭代器实例。这使得代码的意图更加明确,有助于代码的阅读和维护。

  考虑到这些理由,迭代器设计时通常会使得前缀递增操作符返回迭代器的引用。这是一种约定,也是一种优化设计和实现的方法。而后缀递增(it++)通常返回迭代器的值,这需要创建当前迭代器的一个副本,然后对原迭代器进行递增操作,最后返回副本。这种区别设计允许区分前缀和后缀递增的语义和性能特点。

后加加

代码如下

// 后缀递增
LinkedListIterator operator++(int) {
    cout << "run LinkedListIterator 后缀递增" << endl;
    LinkedListIterator temp = *this;
    m_node = m_node->pNext;
    return temp;
}

问题:为什么“后加加不返回引用”?

  后缀递增操作符(it++)不返回引用的原因是它的语义和行为要求。后缀递增操作的预期行为是先返回迭代器当前的值,然后再对其进行递增。这意味着必须先创建当前状态的一个副本,然后递增原迭代器,最后返回那个副本。这个过程涉及到以下几个关键点:

  1. 值的保留:后缀递增需要保留迭代器递增前的状态。为了实现这一点,它通过创建当前迭代器的一个副本来保存当前状态,然后递增原迭代器,最后返回副本。如果后缀递增返回了引用,那么它将无法满足“先返回当前状态然后递增”的语义要求。
  2. 避免修改返回的迭代器:由于后缀递增返回的是递增前的状态,如果这个返回值是引用,使用者可能会错误地认为对返回的迭代器进行修改会影响到原始迭代器,这与后缀递增的预期行为不符。返回一个副本可以明确表明返回的迭代器是一个全新的、与原迭代器无关的实例。
  3. 语义清晰性:通过返回一个副本而不是引用,后缀递增明确表示了它和前缀递增的不同。前缀递增(++it)表示对原迭代器自身的修改并返回修改后的自身,更适合效率要求较高的场合;而后缀递增(it++)则强调返回原始状态的副本,适用于先使用当前值再递增的场景,即使这意味着可能有额外的性能开销。
  4. 性能考虑:后缀递增的性能通常比前缀递增低,因为它涉及到复制操作。这是设计选择的一部分,开发者在意识到这一点的情况下会倾向于在不需要副本的场景中使用前缀递增,以提高代码的效率。

  综上所述,后缀递增不返回引用是为了满足其特定的语义需求,保证操作的正确性,同时也是为了与前缀递增明确区分,让两者的用途和性能特点更加清晰。

迭代时如何取值

  在使用迭代器中,会有“*it”的取值方法,代码如下:

//for(LinkedList<int>::iterator it = pList->begin(); it != pList->end(); it++)
    for(auto it = pList->begin(); it != pList->end(); it++)
    {
        cout << *it << " ";
    }

  这种“*it”写法,需要重写星号操作符来实现,代码如下:

T& operator*() const { 
    cout << "run T& operator*() const " << endl;
    return m_node->data; 
}

  以上是本篇博客的主要内容,这是链表的基本实现,当然还可以增加锁来实现线程安全,如果在多线程环境担心std容器的安全性,大家可以使用线程安全的容器,比如TBB库提供的容器。TBB链接如下: https://www.intel.com/content/www/us/en/developer/tools/oneapi/onetbb.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1527979.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[Linux]互斥锁(什么是锁,为什么需要锁,怎么使用锁(接口),演示代码)

目录 一、锁的概念 一些需要了解的概念 什么是锁&#xff1f;为什么需要锁&#xff1f;什么时候使用锁&#xff1f;怎么定义锁&#xff1f; 二、锁的接口 1.初始化锁 2.加锁 3.申请锁 4.解锁 5.销毁锁 三、实践&#xff08;写代码&#xff09;&#xff1a;黄牛抢票 M…

C#开发中方法使用的问题注意

C#开发中&#xff0c;我们在进行方法内嵌时&#xff0c;需要注意方法回传带值时&#xff0c;我们需要对方法回传的值进行一个赋值传递 如下所示 console.WriteLine("请输入你的爱好&#xff1a;"); string aihao Console.ReadLine(); name ChangeData(name);同时在…

Legacy|电脑Windows系统如何迁移到新安装的硬盘?系统迁移详细教程!

前言 前面讲了很多很多关于安装系统、重装系统的教程。但唯独没有讲到电脑换了新的硬盘之后&#xff0c;怎么把旧系统迁移到新的硬盘上。 今天小白就来跟各位小伙伴详细唠唠&#xff1a; 开始之前需要把系统迁移的条件准备好&#xff0c;意思就是在WinPE系统下&#xff0c;可…

B004-springcloud alibaba 服务容错 Sentinel

目录 高并发带来的问题服务雪崩效应常见容错方案常见的容错思路隔离超时限流熔断降级 常见的容错组件 Sentinel入门什么是Sentinel微服务项目集成Sentinel核心库安装Sentinel控制台实现一个接口的限流 Sentinel的概念和功能基本概念重要功能 Sentinel规则流控规则三种流控模式三…

拒绝云测,热门猫主食冻干对比测评,希喂、SC、VE谁实力更强?

在当今的科学养宠时代&#xff0c;主食冻干已经成为了猫日常饮食不可或缺的一部分。高肉含量的主食冻干不仅易吸收、好消化&#xff0c;更能给猫提供其他猫粮所不能提供的微量物质&#xff0c;更满足猫的全面营养需求。然而&#xff0c;在众多品牌和口味的主食冻干中&#xff0…

抖音找人推广要给多少推广费?CloudNEO:9000+网红资源,助您品牌代言

抖音作为中国最受欢迎的短视频平台之一&#xff0c;吸引了众多用户的关注&#xff0c;也成为了企业推广的热门渠道。然而&#xff0c;很多人对于在抖音上找人推广需要支付多少推广费并不了解。下面让我们来解析一下抖音推广费用的计算方式。 1. 推广形式&#xff1a; 首先&…

ISP技术综述

原文来自技术前沿&#xff1a;ISP芯片终极进化——VP芯片&#xff08;AI视觉处理器&#xff09; 目录 1.计算机视觉的定义 2.与计算机视觉密切相关的概念与计算机视觉密切相关的概念有机器视觉&#xff0c;图像处理与分析&#xff0c;图像和视频理解。 3.计算机视觉的应用 …

[自研开源] MyData v0.7.3 更新日志

开源地址&#xff1a;gitee | github 详细介绍&#xff1a;MyData 基于 Web API 的数据集成平台 部署文档&#xff1a;用 Docker 部署 MyData 使用手册&#xff1a;MyData 使用手册 试用体验&#xff1a;https://demo.mydata.work 交流Q群&#xff1a;430089673 介绍 MyData …

git常见使用

1. 概念 分布式&#xff0c;有远程仓库和本地仓库的概念&#xff0c;因此要注意同步问题git是面向对象的&#xff0c;本质是内容寻址系统。.git目录下有个文件夹objects&#xff0c;存储git库中的对象&#xff0c;git就是根据object建立一种树形结构&#xff0c;将文件和通过h…

数据库应用:Linux 部署 GaussDB

目录 一、实验 1.环境 2.Linux 部署 GaussDB 3.Linux 使用 GaussDB 4.使用 GaussDB 进行表与索引操作 5.使用 GaussDB 进行视图操作 6.使用 GaussDB 进行联表查询 7.使用 GaussDB 进行外键关联 二、问题 1.运行python脚本报错 2. 安装GaussDB 报错 3. install 安装…

广州大彩科技新品发布:大彩科技COF系列2.4寸串口屏发布!

一、产品介绍 此次发布的是S系列平台2.4寸COF超薄结构串口屏&#xff0c;分辨率为240*320&#xff0c;该平台采用了Cortex-M3内核的处理器&#xff0c;内置了2Mbyte PSRAM和64Mbit FLASH&#xff0c;是专为小尺寸串口屏设计的MCU&#xff0c;精简了外围电路。 该平台默认支持大…

【sql】深入理解 mysql的EXISTS 语法

相关文章&#xff1a; 【sql】深入理解 mysql的EXISTS 语法 【sql】初识 where EXISTS 1. 使用格式如下&#xff1a; select * from a where exists ( 任何子查询 ) 代码根据颜色分成两段&#xff0c;前面的是主查询&#xff0c;后面红色的是子查询&#xff0c;先主后子&…

从政府工作报告探计算机行业发展

文章目录 每日一句正能量前言以“数”谋新、加“数”向实人工智能方面人工智能成核心驱动引擎 软件方面通信方面后记 每日一句正能量 该来的始终会来&#xff0c;千万别太着急&#xff0c;如果你失去了耐心&#xff0c;就会失去更多。该走过的路总是要走过的&#xff0c;从来不…

Android14之HIDL报错:Invalid sparse file format at header magic(一百九十六)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

【数据结构与算法】(17):计数排序和基数排序详解

&#x1f921;博客主页&#xff1a;Code_文晓 &#x1f970;本文专栏&#xff1a;数据结构与算法 &#x1f63b;欢迎关注&#xff1a;感谢大家的点赞评论关注&#xff0c;祝您学有所成&#xff01; ✨✨&#x1f49c;&#x1f49b;想要学习更多数据结构与算法点击专栏链接查看&…

智慧城市与数字孪生:科技融合助力城市可持续发展

随着信息技术的迅猛发展&#xff0c;智慧城市和数字孪生作为现代城市发展的重要理念和技术手段&#xff0c;正日益受到广泛关注。智慧城市通过集成应用先进的信息通信技术&#xff0c;实现城市管理、服务、运行的智能化&#xff0c;而数字孪生则是利用数字化手段对物理城市进行…

面试笔记——Redis(使用场景、面临问题、缓存穿透)

Redis的使用场景 Redis&#xff08;Remote Dictionary Server&#xff09;是一个内存数据结构存储系统&#xff0c;它以快速、高效的特性闻名&#xff0c;并且它支持多种数据结构&#xff0c;包括字符串、哈希表、列表、集合、有序集合等。它主要用于以下场景&#xff1a; 缓…

libmodbus编译为64位动态库

通用方法&#xff0c;记录一下&#xff0c;以便后续参考。 Step 1. 下载libmodbus源码 GitHub - stephane/libmodbus: A Modbus library for Linux, Mac OS, FreeBSD and Windows Step 2. 生成配置文件 进入libmodbus-master\src\win32目录&#xff0c;在该目录下打开终端&am…

【火猫TV】DOTA2 BB队员称:队伍非常具有凝聚力

1、近日BB战队队员Nightfall接受采访时表示战队自从去年获得梦幻联赛S20亚军以来&#xff0c;就非常具有凝聚力。 “我认为战队自从去年获得梦幻联赛S20亚军以来&#xff0c;战队就非常具有凝聚力。从那一刻开始&#xff0c;我们这群人不再只是几个选手组建起来的松散组织&…

移动云COCA架构实现算力跃升,探索人工智能新未来

近期&#xff0c;随着OpenAI正式发布首款文生视频模型Sora&#xff0c;标志着人工智能大模型在视频生成领域有了重大飞跃。Sora模型不仅能够生成逼真的视频内容&#xff0c;还能够模拟物理世界中的物体运动与交互&#xff0c;其核心在于其能够处理和生成具有复杂动态与空间关系…