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

  本篇博客介绍如何使用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.mzph.cn/news/747142.shtml

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

相关文章

【C++算法模板】图论-拓扑排序,超详细注释带例题

文章目录 0&#xff09;概述1&#xff09;Kahn算法1&#xff1a;数据结构2&#xff1a;建图3&#xff1a;Kanh算法 2&#xff09;DFS染色1&#xff1a;数据结构2&#xff1a;建图3&#xff1a;DFS 3&#xff09;算法对比【例题】洛谷 B3644 推荐视频链接&#xff1a;D01 拓扑排…

4核8g服务器能支持多少人访问?价格感人,不知道性能如何

腾讯云轻量4核8G12M服务器配置446元一年&#xff0c;646元12个月&#xff0c;腾讯云轻量应用服务器具有100%CPU性能&#xff0c;系统盘为180GB SSD盘&#xff0c;12M带宽下载速度1536KB/秒&#xff0c;月流量2000GB&#xff0c;折合每天66.6GB流量&#xff0c;超出月流量包的流…

Linux下Arthas(阿尔萨斯)的简单使用-接口调用慢排查

使用环境 k8s容器内运行了一个springboot服务&#xff0c;服务的启动方法是main()方法 下载并启动 arthas curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar选择应用 java 进程 就一个进程org.apache.catalina.startup.Bootstrap&#xff0c;输…

支小蜜AI校园防欺凌系统可以使用在宿舍吗?

随着人工智能技术的快速发展&#xff0c;AI校园防欺凌系统已成为维护校园安全的重要手段。然而&#xff0c;关于这一系统是否适用于宿舍环境&#xff0c;仍存在一些争议和讨论。本文将探讨AI校园防欺凌系统在宿舍中的适用性&#xff0c;分析其潜在的优势与挑战&#xff0c;并提…

Vue.js 应用实现监控可观测性最佳实践

前言 Vue 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建&#xff0c;并提供了一套声明式的、组件化的编程模型&#xff0c;帮助你高效地开发用户界面。无论是简单还是复杂的界面&#xff0c;Vue 都可以胜任。 TinyPro 是一套使用 Vue …

提升用户体验,Xinstall智能判定拉起技术来袭

在移动互联网时代&#xff0c;App推广已经成为各大企业的必争之地。然而&#xff0c;随着市场竞争的加剧&#xff0c;如何提升App的转化效率和用户体验成为了推广者们亟待解决的问题。这时&#xff0c;Xinstall的智能判定拉起技术应运而生&#xff0c;为推广者们带来了新的解决…

安卓百度地图API显示隐藏Marker

方法 BaiduMap.Marker.setVisible(boolean) 实现 List<Marker> list_marker new ArrayList<>(); boolean isShowMarker true;Override public boolean onCreateOptionsMenu(Menu menu) {String[] sm { "显隐信息", "显隐照片", "截…

OJ_最大序列和

题干 C实现 #include <stdio.h> #include <algorithm> using namespace std;long long s[1000001]; long long dp[1000002];//dp[i]是前i个元素中必须包含右边缘的最大子序和int main() {int n;scanf("%d",&n);for(int i 0; i< n;i){scanf(&quo…

【Vue3】Vue3中路由规则的 props 配置

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

云计算 3月14号 (TCP三次握手和四次挥手)

1.TCP三次握手和四次挥手 1.TCP的传输过程&#xff1a; Seq 序列号 保障传输过程可靠。 ACK &#xff08;确认消息&#xff09; SYN &#xff08;在建立TCP连接的时候使用&#xff09; FIN &#xff08;在关闭TCP连接的时候使用&#xff09; 3.TCP建立连接的过程&…

让Python自动测试更得心应手——认识一下神奇的pytest测试框架

前言 Python在测试圈的应用非常广泛&#xff0c;特别是在自动化测试以及测试开发的领域&#xff0c;其中在自动化测试中我们常用的测试框架是uniitest和pytest&#xff0c;本文将带领大家搭建以及熟悉pytest的使用。 既然有unittest那么为什么还要用pytest呢&#xff1f; 这…

单目测距+姿态识别+yolov8界面+车辆行人跟踪计数

yolov5单目测距速度测量目标跟踪&#xff08;算法介绍和代码&#xff09; 1.单目测距实现方法 在目标检测的基础上&#xff0c;我们可以通过计算物体在图像中的像素大小来估计其距离。具体方法是&#xff0c;首先确定某个物体的实际尺寸&#xff0c;然后根据该物体在图像中的像…

Vue3--数据和方法

data 组件的 data 选项是一个函数。Vue 在创建新组件实例的过程中会自动调用此函数。   data选项通常返回一个对象&#xff0c;然后 Vue 会通过响应性系统将其包裹起来&#xff0c;并以 $data 的形式存储在组件实例中。 <!DOCTYPE html> <html lang"en"&g…

8分SCI | 揭示随机森林的解释奥秘:探讨LIME技术如何提高模型的可解释性与可信度!

一、引言 Local Interpretable Model-agnostic Explanations (LIME) 技术作为一种局部可解释性方法&#xff0c;能够解释机器学习模型的预测结果&#xff0c;并提供针对单个样本的解释。通过生成局部线性模型来近似原始模型的预测&#xff0c;LIME技术可以帮助用户理解模型在特…

js中的原型(原型对象,对象原型,原型继承,原型链)

js中的原型 一.原型二.constructor 属性三.对象原型四.原型继承五.原型链 一.原型 构造函数通过原型分配的函数是所有实例化对象所共享的。 JavaScript 规定&#xff0c;每一个构造函数都有一个 prototype 属性&#xff0c;指向另一个对象&#xff0c;所以我们也称为原型对象 …

Linux编译器gcc/g++的功能与使用

一、程序的生成 首先&#xff0c;我们知道程序的编译分为四步&#xff1a; 1、预处理 2、编译 3、汇编 4、链接 1.1预处理 预处理功能主要包括头文件展开、宏定义、文件包含、条件编译、去注释等。 所谓的头文件展开就是在预处理时候&#xff0c;将头文件内容拷贝至源文…

C语言从入门到熟悉------第四阶段

指针 地址和指针的概念 要明白什么是指针&#xff0c;必须先要弄清楚数据在内存中是如何存储的&#xff0c;又是如何被读取的。如果在程序中定义了一个变量&#xff0c;在对程序进行编译时&#xff0c;系统就会为这个变量分配内存单元。编译系统根据程序中定义的变量类型分配…

Linux中防火墙相关操作

一、查看防火墙状态 可通过两种方式查看防火墙状态&#xff0c;一种通过systemctl命令&#xff0c;另一种是通过firewall-cmd命令。 1、systemctl status firewalld 2、firewall-cmd --state 二、关闭防火墙 1、暂时关闭&#xff1a;设置暂时关闭防火墙将会在下次重启系统后失…

[LVGL]:MACOS下使用LVGL模拟器

如何在MACOS下使用lvgl模拟器 1.安装必要环境 brew install sdl2查看sdl2安装位置&#xff1a; (base) ➜ ~ brew list sdl2 /opt/homebrew/Cellar/sdl2/2.30.1/bin/sdl2-config /opt/homebrew/Cellar/sdl2/2.30.1/include/SDL2/ (78 files) /opt/homebrew/Cellar/sdl2/2.3…