C++程序设计:原理与实践(进阶篇)15.4 链表

15.4 链表


下面让我们再回顾一下序列概念的图形表示:

 

将它与我们描绘vector内存结构的示意图相比较:

 

下标0本质上与迭代器v.begin()一样都指向同一个元素,并且下标v.size()与v.end()一样都指向最后一个元素之后的位置。

vector的元素在内存中是连续存储的。这并非STL序列概念所要求的特性,因此在STL中,很多算法在将一个元素插入两个已有元素的中间时都不需要移动已有的元素。上面序列抽象概念的图示意味着,在不移动其他元素的前提下进行元素插入(或元素删除)操作是可能的。STL迭代器概念支持上述操作。

直接体现上述STL序列概念的数据结构是链表(linked list)。在抽象模型中的箭头通常由指针实现。链表中的一个元素是“链接”的一部分,一个“链接”由这一元素以及一个或多个指针组成。如果链表的一个链接只包含一个指针(指向下一个链接),我们称这样的链表为单向链表,如果一个链接包含一个指向前驱链接的指针以及一个指向后继链接的指针,则这样的链表为双向链表。在后续小节中,我们将勾勒一个双向链表的实现,且该实现与C++标准库list的实现相同。双向链表的概念可图示如下:

 

上述概念可由代码实现:

 

一个Link的内存布局如下所示:

 

链表的实现方法和呈现给用户的方法有多种。附录C中给出了标准库所采用的一种方法。在本节中,我们将只概述链表的关键属性——能够在不影响其他已有元素的前提下插入和删除元素,展示如何遍历一个链表,以及给出链表使用的一个示例。

当你考虑使用链表时,我们强烈建议你将自己所考虑的链表操作绘图表示。图形是描绘链表操作的一种十分有效的方法。

15.4.1 链表操作

对于链表,我们需要使用哪些操作呢?

对vector所实现的操作(构造函数、大小等),除了下标。

插入(添加一个元素)和删除(移除一个元素)。

访问元素以及遍历链表:迭代器。

在STL中,上述的迭代器类型是list类的一个成员,因此我们也会这样设计:

 

就像“我们的”vector并没有完全实现标准库vector一样,上述list定义与标准库list定义也不完全相同。但上述list中没有任何错误,它仅仅是不完全而已。“我们的”list的目的在于加深你对链表的理解:链表是什么,list应该如何实现,以及如何使用list的关键特性。如果读者想获得更多的信息,请参考附录C或其他专家级别的C++书籍。

迭代器是STL list定义中的核心部分。迭代器被用于标示元素插入的位置以及待删除(擦除)的元素。它们也可被用于在链表中进行“导航”。迭代器的这一用途与我们在15.1节和15.3.1节中使用指针遍历数组和向量十分相似。迭代器的这一风格对于标准库算法而言十分关键(见16.1~16.3节)。

为什么不在list中使用下标操作呢?我们可以为list实现下标操作,但它会是一种极为缓慢的操作:list[1000]操作将会从第一个元素开始访问每个元素,直到访问到第1000个元素为止。如果我们希望这么做,那么可以自己实现这一操作(或使用advance(),参见15.6.2节)。因此,标准库list并没有提供下标语法。

我们将迭代器的类型作为list的成员(一个嵌套类)的原因在于,我们没有任何理由将迭代器的类型实现为全局类。这一迭代器的类型将只会由list类使用。另外,这也使得我们能够将每一容器的迭代器都命名为iterator。在标准库中存在着list<T>::iterator、vector<T>::iterator、map<K,V>::iterator等迭代器类型。

15.4.2 遍历

list迭代器必须提供*、++、==和!=操作。因为标准库中的链表为双向链表,因此该链表还提供了--操作,以实现链表的“从后”向前的遍历操作:

 

这些函数十分简明且极具效率:函数实现中不存在循环,不存在复杂的表达式,不存在“可疑的”函数调用。如果你还不清楚这些实现的意义,请再快速回顾一下前面的示意图。这一list迭代器只是一个指向链接的指针。注意,即使list<Elem>::iterator的实现(代码)与我们在vector和数组中用作迭代器的简单指针的实现极为不同,两者操作的意义(语义)是相同的。基本上,list迭代器提供了对Link指针的++、--、*、==和!=操作。

现在让我们再次回顾high()的实现:

 

我们可以将其用于list:

 

在上述代码中,Iterator参数的“取值”为list<int>::iterator,并且++、*和!=操作的实现都与数组的代码有很大不同,但操作的意义是相同的。模板函数high()仍然遍历数据(在这里是list)和查找最大取值。我们可以在list的任何位置插入一个元素,因此使用了push_front()在链表首部添加元素,而这一操作的目的仅仅是为了显示我们确实能够这么做。当然,我们也可以像对vector一样对list使用push_back()函数。

试一试

标准库vector不提供push_front()。为什么?为vector实现push_front()并将其与push_back()进行比较。

现在,是时候提出这样的问题了:“如果list为空会怎样?”换句话说,“如果lst.begin() == lst.end()会怎样?”在这种情况下,*p将会试图对最后一个元素ls.end()之后的位置进行解引用,这是一个灾难!或者——可能更糟地——结果可能是一个错误的随机值。

此问题的最后一种描述形式给我们带来了一个提示:可以通过比较begin()和end()测试一个链表是否为空——实际上,可以通过比较序列的开始和结束判断任何STL序列是否为空:

 

这是令序列的end指向最后一个元素之后的位置而不是指向最后一个元素的一个更深层次的原因:空序列不再是一种特殊情况。我们不喜欢特殊情况,因为——根据定义——我们不得不为这些特殊情况编写特殊的代码。

在我们的例子中,可以按如下方式对list进行测试:

 

我们采用这种形式的测试方法系统地测试STL算法。

因为标准库提供了链表,我们在这里不再继续深入探讨它的具体实现。取而代之的是,我们将简要讨论链表适用的场合(如果你对链表的实现细节感兴趣,参考习题12~14)。

时间: 2024-08-06 03:40:36

C++程序设计:原理与实践(进阶篇)15.4 链表的相关文章

c++-关于《C++程序设计原理与实践》第3章例子的一个问题

问题描述 关于<C++程序设计原理与实践>第3章例子的一个问题 本人菜鸟,现正在学习C++.<C++程序设计原理与实践>第3章有一个例子,代码如下: #include #include #include #include #include using namespace std; inline void keep_window_open(){ char ch; cin >> ch; } int main() //C++ Programs start by executi

源代码-C++程序设计原理与实践

问题描述 C++程序设计原理与实践 #include "std_lib_facilities.h" int main() { cout<<"Hello,world!n"; return 0; } 我下了源代码,放到那里才能猜VC98编译时不出错?最好详细点,带有图解 解决方案 ...大哥,都什么年代了还用98

《 C++程序设计:原理与实践(进阶篇.》导读

本节书摘来自华章出版社< C++程序设计:原理与实践(进阶篇)>一书中作者[美] 本贾尼·斯特劳斯特鲁普(Bjarne Stroustrup) 著 刘晓光 李忠伟 王刚 译     前 言 Programming: Principles and Practice Using C++, Second Edition 该死的鱼雷!全速前进. --Admiral Farragut 程序设计是这样一门艺术,它将问题求解方案描述成计算机可以执行的形式.程序设计中很多工作都花费在寻找求解方案以及对其求精上

100分求java语言程序设计进阶篇pdf

问题描述 求java语言程序设计进阶篇pdf 解决方案 解决方案二:同求啊!!!解决方案三:这个网上是没有的,我也在网上找过,我建议你去网上找java核心技术<上下卷>pdf这本书写的也是不错的,,这个网上有电子书的,,这两本书配合着java编程思想,相当的不错的解决方案四:真正的进阶是需要项目练习的,纸上得来终觉浅解决方案五:引用2楼xinzailiulei的回复: 这个网上是没有的,我也在网上找过,我建议你去网上找java核心技术<上下卷>pdf这本书写的也是不错的,,这个网上

学一点 mysql 双机异地热备份----快速理解mysql主从,主主备份原理及实践

原文 学一点 mysql 双机异地热备份----快速理解mysql主从,主主备份原理及实践 感谢大家在上一篇 学一点Git--20分钟git快速上手 里的踊跃发言.这里再次分享干货, 简单介绍mysql双机,多机异地热备简单原理实战. 双机热备的概念简单说一下,就是要保持两个数据库的状态自动同步.对任何一个数据库的操作都自动应用到另外一个数据库,始终保持两个数据库数据一 致. 这样做的好处多. 1. 可以做灾备,其中一个坏了可以切换到另一个. 2. 可以做负载均衡,可以将请求分摊到其中任何一台上

推荐系统——从原理到实践,还有福利赠送!

之前流水账似的介绍过一篇机器学习入门的文章,大致介绍了如何学习以及机器学习的入门方法并提供了一些博主自己整理的比较有用的资源.这篇就尽量以白话解释并介绍机器学习在推荐系统中的实践以及遇到的问题... 也许很多点在行家的眼里都是小菜一碟,但是对于刚刚接触机器学习来说,还有很多未知等待挑战. 所以读者可以把本篇当做是机器学习的玩具即可,如果文中有任何问题,还请不吝指教. 本篇将会以下面的步骤描述机器学习是如何在实践中应用的: 1 什么是推荐系统? 2 机器学习的作用 3 机器学习是如何使用的? 4

Node.js Stream - 进阶篇

上篇(基础篇)主要介绍了Stream的基本概念和用法,本篇将深入剖析背后工作原理,重点是如何实现流式数据处理和 back pressure 机制. 目录 本篇介绍 stream 是如何实现流式数据处理的. 数据生产和消耗的媒介 为什么使用流取数据 下面是一个读取文件内容的例子: const fs = require('fs') fs.readFile(file, function (err, body) { console.log(body) console.log(body.toString(

SQL Server调优系列进阶篇(如何索引调优)

原文:SQL Server调优系列进阶篇(如何索引调优) 前言 上一篇我们分析了数据库中的统计信息的作用,我们已经了解了数据库如何通过统计信息来掌控数据库中各个表的内容分布.不清楚的童鞋可以点击参考. 作为调优系列的文章,数据库的索引肯定是不能少的了,所以本篇我们就开始分析这块内容,关于索引的基础知识就不打算深入分析了,网上一搜一片片的,本篇更侧重的是一些实战项内容展示,希望通过本篇文章各位看官能在真正的场景中找到合适的解决方法足以. 对于索引的使用,我希望的是遇到问题找到合适的解决方法就可以,

WPF 4 DataGrid 控件(进阶篇二)

上一篇<WPF 4 DataGrid 控件(进阶篇一)>中我们通过DataGridTemplateColumn 类自定义编辑了日期列的样式,当然也可以根据个 人需要设置任何样式模板.上例中Pass Exam 列显示学生是否通过考试,但我们并不知道该学生每门学科的成绩是多少.本篇将为 DataGrid 行增加这些详细信息,使得DataGrid 数据更加充实. 首先,我们仍然先更新一下Member 类,增加Math 和History 两门学科: public class Member { publ

ES6 你可能不知道的事 - 进阶篇

前言 这篇文章主要会针对上篇未涉及到的进阶特性展开:而与前一篇文章相同,本文主要介绍这些特性的一些容易忽略的部分,希望能对大家正确认识和使用 ES6 有帮助. 还是那句话,时间和能力有限,针对文章中的问题或不同意见,欢迎随时拍砖.指正! 正文 Module 模块化是个进行了很久的话题,发展历程中出现过很多模式,例如 AMD, CommonJS 等等. Module 是 ES6 的新特性,是语言层面对模块化的支持. 与之前模块加载机制不同,Module 是动态的加载,导入的是变量的 只读引用 ,而