首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星

数据结构与算法

这个系列的文章也是《一个嵌入式操作系统的实现》系列文章的基础。 操作系统也是一种软件,我们需要设计各种数据结构来管理各种不同的数据,比如说进程描述符、内存表等等。操作系统的效率将直接影响应用程序的效率,我们也需要研究算法来提升系统的运行效率。

正如谭浩强所说,算法是程序的灵魂。它提供了解决问题的方法,是程序运行效率的关键。数据结构与算法是相辅相成的一对兄弟。为了算法方便有效的运行,人们会专门为之设计一些数据结构。 算法的操作对象就是各种各样的数据结构,如果说算法是程序的灵魂,那么数据结构就是程序的血与肉。我曾犹豫了很久是否要写这个系列,因为市面上并不缺少不错的算法和数据结构的书籍, 但在写XiaoTuOS的过程中发现我需要一个基础工具箱,为了方便也为了学习,我们一起来看下这个基础内容吧。这个系列的文章更偏向于介绍数据结构,它将是我们的基础工具箱。

这里所说的数据结构更大程度上是一种容器,就好比说我们要把一些水和沙子运到6楼,就需要一个容器来装着它们。这个容器可以是一个木桶,也可以是一个盆子。 我们的操作是对容器的操作,至于说容器里面具体装得是什么并不太关心。这样实现的基本工具就具有比较好的扩展性和可移植性,这一思想就是所谓的抽象数据类型(abstract data type, ADT)。 另外,我还想说的是,达到一个目的可以很多不同的手段,只是付出的代价不同。用桶来搬运,一次可以多搬一些,但每次都需要很大的力气;而盆子就不需要那么大的力气,但是需要搬运很多次。 算法和数据结构也是如此,我们需要根据具体的问题选择合适的方案。

因为嵌入式系统主要是用C语言开发的,所以这里的也以C语言的实现为主,有时也会实现一些C++的版本。为了更好的体现ADT,C语言中会大量的使用宏定义和指针,C++版本会尽量使用模板(template)。 所以要求读者具有一些基本的C/C++语言基础。当然这里介绍的内容,不仅仅可以用C/C++来实现,其基本概念和原理在其他程序语言中也一样适用。我们可以结合特定的语言特性实现不同的语言版本。

本系列的例程参考小土的数据工具箱。工具箱中的代码将一直更新,文章更新速度比较慢,两者会有比较大差异, 但基本思想是不差的。

第一部分: 线性表

线性表就是一个形如\(\langle a_0, a_1, \cdots, a_{n-1} \rangle\)的具有\(n\)个元素的列表。其中的第\(i\)个元素\(a_i\)的前驱为\(a_{i-1}\), 后继为\(a_{i+1}\)。但是首元素\(a_0\)没有前驱,尾元素\(a_{n-1}\)没有后继。根据存储这些元素的地址空间是否连续,可以有数组和链表两种实现方式。 增加一些特定的插入和删除元素的操作,就可以实现栈的后入先出、堆的先入先出特性。

数组 数组是一种地址空间连续的线性表。得益于连续的地址空间,它的索引效率很高,复杂度为\(O(1)\)。但是其插入和删除的效率就很低,每次都需要大量的数据搬运工作, 复杂度为\(O(n)\)。
链表 链表作为线性表的另一种形态,它具有很高的插入和删除效率,复杂度为\(O(1)\),但是它的索引效率比较低,复杂度为\(O(n)\)。这是因为链表的地址空间是离散的, 各个元素之间通过指针来记录各自的前驱和后继。
本质上栈就是一种线性表,它所保存的数据之间的前驱和后继关系仍然存在。它有专门的入栈和出栈操作,都是在栈顶进行的。最后添加的数据将最先被移除, 也就是所谓的后进先出特性,称之为LIFO。 本文可以看作是对数组链表的扩展, 相关例程也都是上述两个文章例程的扩展。
队列 队列在线性表的一端插入数据,另一端删除数据,对应的操作称为进队和出队。这种操作可以保证其中保存的数据具有先进现出的特性。
双端队列 通过对数据结构栈和队列的分析,我们知道在同一端进行插入和删除操作就可以得到栈,在一端插入另一端删除就可以得到队列。有些时候,我们可能需要组合这些操作, 于是就有了所谓的双端队列,它与栈和队列没什么太大的差别,就是可以在线性表的两端插入和删除数据。

第二部分: 树

树是图的一种特殊形式,它具有如下的特性:如果\(v\)和\(w\)分别是树\(T\)的两个节点,那么在\(v\)和\(w\)之间只有唯一的一条路径。 通常我们都以一个特殊的节点作为根节点,把树描述成有根树的形式,其它所有节点都是根节点的后代。

二叉树 二叉树是一种特殊形式的树,它的每个节点最多只有两个孩子,称之为左子树和右子树。二叉树的一个常用的方式就是所谓的二叉查找树(binary search tree, BST)。 对于二叉查找树中的任何一个节点,其左子树中的所有节点的键值都小于该节点的键值,而右子树中的所有节点的键值都大于该节点的键值。此外,在编译器的领域中,二叉树也是一种使用率很高的数据结构, 它通常用于描述表达式。
红黑树 二叉查找树就是希望通过数据结构实现一种近似的二分查找,从而得到\(O(log(n))\)的平均时间复杂度。但是在极端的情况下,可能生成极端不平衡的二叉树,以致于在最差情况下, 复杂度退化为\(O(n)\)。红黑树就是一种为了解决这一问题而存在的数据结构,它是一种近似平衡的二叉树。
Trie树——字典 Trie树就是一种为了快速查找指定前缀的数据结构,它的形式很像我们翻查英语字典一样,所以也被人们称为前缀树、字典树。 本文中,我们用C++模板提供了一个字典树的实现,并介绍了插入、查找、删除和追溯操作,这些操作都是\(O(n)\)的复杂度,其中\(n\)是序列长度。
KD树 一种二叉树。
  • C++模板形式的KD树

第三部分: 二维线性表

二维数组 本文实现了二维数组,具有很高的索引效率。但是其增加和删除操作的实现需要重新申请内存,因此在需要频繁插入和删除数据的场合效率很低。
二维链表 简介。
  • 源码链接。

第四部分: 图

图\(G\)是由节点集合\(V\)和边集合\(E\)构成的二元组\(G = (V, E)\),其中边集的元素\(e \in E\)也是一个二元组\(e = (u, v)\), 并且\(u \in V, v \in V\)。如果构成边\(e\)的两个节点\(u,v\)不区分先后顺序的话,我们说边\(e\)是无向的,否则是有向的。 根据边是否有方向,可以把图分为有向图和无向图。

我们可以直观的在纸面上表述图,用小点表示点集\(V\),如果节点对\((u, v) \in E\),就在对应的两点之间画一条线来表示边, 对于有向图就用箭头来表示\((u,v)\)的先后顺序。

临接表描述的图 图\(G = (V, E)\)的临接表形式是由\(|V|\)个临界列表构成的。对于每个\(u \in V\),其临接表\(Adj[u]\)中记录了所有与该点有连接的边\((u, v) \in E\),其中\(v\)是\(u\)的临接节点。 临接表的形式适合描述稀疏的图,而且方便增删节点和边。
本文中,我们实现了有向图的临接表,对于无向图只需要无视与边的方向有关的属性即可。
临接矩阵描述的图 图\(G = (V, E)\)的临接矩阵形式使用一个\(|V| \times |V|\)的矩阵来描述节点之间的链接关系。矩阵的元素描述了节点之间的连边。 这种描述方式适用于描述稠密的图,即几乎任意两个节点之间都有一条边相连的图。有时为了提高边或者节点的访问效率,也会考虑使用这种描述方式。

九地第十一

孙子曰:用兵之法:有散地,有轻地,有争地,有交地,有衢地,有重地,有泛地,有围地,有死地。诸侯自战其地者,为散地;入人之地而不深者,为轻地;我得亦利,彼得亦利者,为争地; 我可以往,彼可以来者,为交地;诸侯之地三属,先至而得天下之众者,为衢地;入人之地深,背城邑多者,为重地;山林、险阻、沮泽,凡难行之道者,为泛地;所由入者隘,所从归者迂, 彼寡可以击吾之众者,为围地;疾战则存,不疾战则亡者,为死地。是故散地则无战,轻地则无止,争地则无攻,交地则无绝,衢地则合交,重地测掠,泛地则行,围地则谋,死地则战。

古之善用兵者,能使敌人前后不相及,众寡不相恃,贵贱不相救,上下不相收,卒离而不集,兵合而不齐。合于利而动,不合于利而止。敢问敌众而整将来,待之若何曰: 先夺其所爱则听矣。兵之情主速,乘人之不及。由不虞之道,攻其所不戒也。

凡为客之道,深入则专。主人不克,掠于饶野,三军足食。谨养而勿劳,并气积力,运兵计谋,为不可测。

投之无所往,死且不北。死焉不得,士人尽力。兵士甚陷则不惧,无所往则固,入深则拘,不得已则斗。是故其兵不修而戒,不求而得,不约而亲,不令而信,禁祥去疑,至死无所之。

吾士无余财,非恶货也;无余命,非恶寿也。令发之日,士卒坐者涕沾襟,偃卧者涕交颐,投之无所往,诸、刿之勇也。故善用兵者,譬如率然。率然者,常山之蛇也,击其首则尾至, 击其尾则首至,及其中则首尾俱至。敢问兵可使如率然乎?曰可。夫吴人与越人相恶也,当其同舟济而遇风,其相救也如左右手。是故方马埋轮,未足恃也;齐勇若一,政之道也; 刚柔皆得,地之理也。故善用兵者,携手若使一人,不得已也。

将军之事,静以幽,正以治,能愚士卒之耳目,使之无知;易其事,革其谋,使民无识;易其居,迂其途,使民不得虑。帅与之期,如登高而去其梯;帅与之深入诸侯之地, 而发其机。若驱群羊,驱而往,驱而来,莫知所之。聚三军之众,投之于险,此将军之事也。

九地之变,屈伸之利,人情之理,不可不察也。

凡为客之道,深则专,浅则散。去国越境而师者,绝地也;四徹者,衢地也;入深者,重地也;入浅者,轻地也;背固前隘者,围地也;无所往者,死地也。

是故散地吾将一其志,轻地吾将使之属,争地吾将趋其后,交地吾将固其结,衢地吾将谨其恃,重地吾将继其食,泛地吾将进其途,围地吾将塞其阙,死地吾将示之以不活。

故兵之情:围则御,不得已则斗,过则从。

是故不知诸侯之谋者,不能预交;不知山林、险阻、沮泽之形者,不能行军;不用乡导者,不能得地利。四五者,一不知,非霸王之兵也。夫王霸之兵,伐大国,则其众不得聚; 威加于敌,则其交不得合。是故不争天下之交,不养天下之权,信几之私,威加于敌,故其城可拔,其国可隳(hui, 灰)

施无法之赏,悬无政之令。犯三军之众,若使一人。犯之以事,勿告以言;犯之以害,勿告以利。投之亡地然后存,陷之死地然后生。夫众陷于害,然后能为胜败。

故为兵之事,在顺祥敌之意,并敌一向,千里杀将,是谓巧能成事。是故政举之日,夷关折符,无通其使,厉于廊庙之上。以诛其事。敌人开阖,必亟入之,先其所爱, 微与之期,践墨随敌,以决战事。是故始如处女,敌人开户,后如脱兔,敌不及拒。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1