文章共分为三篇
第一篇:数据结构 -《大话数据结构》读书笔记(1)
一、数据结构绪论
二、算法
三、线性表
第二篇:数据结构 -《大话数据结构》读书笔记(2)
四、栈与队列
五、串
六、树
七、图
第三篇:数据结构 -《大话数据结构》读书笔记(3)
八、查找
九、排序
四、栈与队列
4.1 栈的定义
- 栈
栈是限定仅在表尾进行插入和删除操作的线性表。
我们把允许插入和删除的一端称为 栈顶,另一端称为 栈底,不含任何数据元素的栈称为 空栈。栈又称为后进先出的 线性表,简称 LIFO 结构。
栈的特殊之处在于它限制了这个线性表的插入和删除位置,它始终只在栈顶进行
。
-
栈的插入操作,叫作
进栈
,也称压栈
、入栈
。
-
栈的删除操作,叫作
出栈
,也有的叫作弹栈
。
4.2 栈的应用
栈的引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的问题核心。反之,像数组等,因为要分散精力去考虑数组的下标增减等细节问题,反而掩盖了问题的本质。
所以现在很多高级语言,比如 Java、C#等都有对栈结构的封装,你可以不用关注它的实现细节,就可以直接使用 Stack 的 push 和 pop 方法,非常方便。
栈的应用——递归
在高级语言中,调用自己和其它函数没有本质的不同。我们把一个直接用自己或通过一系列的调用语句间接地调用自己的函数,称作递归函数。
每个递归函数必须至少有一个条件,满足时递归不再执行,即不再引用自身而是返回值退出。
递归和迭代的区别是:迭代使用的是循环结构
,递归使用的是选择结构
。 递归能使程序的结构更清晰、更简洁、更容易让人理解,从而减少读懂代码的时间
。但是大量的递归调用会建立函数的副本,会耗费大量的时间和内存
。迭代则不需要反复调用函数和占用额外的内存。因此我们应该视不同情况选择不同的代码实现方式。
在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。
4.3 队列定义
- 队列
队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。
队列是一种先进先出的线性表,简称 FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。假设队列是 q = (a1, a2, ……, an),那么 a1 就是队头元素,而 an 就是队尾元素。我们在删除时,总是从 a1 开始;插入时,列在最后。如下图:
同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾
进行,删除数据只能在队头
进行。
4.3.1 循环队列
解决假溢出的办法就是后面满了,就再从头开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。
4.3.2 队列的链式存储结构及实现
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。
队列的链式存储结构——入队操作
入队操作时,其实就是在链表尾部
插入结点。
队列的链式存储结构——出队操作
出队操作时,就是头结点的后继结点出队
,将头结点的后继改为它后面的结点
,若链表除头结点外只剩一个元素时,则需将 rear 指向头结点。
4.3.3 循环队列与链队列的对比
对比:时间上,基本操作都是常数时间,都为 O(1)
的,不过循环队列是事先申请好空间,使用期间不释放,而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是有细微差异。空间上,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题。而链队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接收。所以在空间上,链队列更加灵活。
总的来说,在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。
五、串
5.1 串的定义:
串是由零个或多个字符组成的有限序列,又叫字符串。
一般记为 s=“a1a2……an”(n>=0),其中,s 是串的名字,用双引号括起来的字符序列是串的值,双引号不属于串的内容。a1 可以是字母、数字或其他字符,i 就是该字符在串中的位置。串中的字符数目 n 称为串的长度,定义中谈到“有限”,指的是长度 n 是一个有限的数值。零个字符的串称为空串,它的长度为零。所谓序列,说明串的相邻字符之间具有前驱和后继的关系。
空格串
,是只包含空格的串。空格串与空串不同,空格串是有长度的,而且可以不止一个空格。
子串
和主串
,串中任意个数的连续字符组成的子序列称为该串的子串,相应的,包含子串的串称为主串。
子串在主串中的位置就是子串的第一个字符在主串中的序号。
5.2 串的比较
串的比较是通过组成串的字符之间的编码来进行的,而字符的编码指的是字符在对应字符集中的序号。
C 语言中,比较两个串是否相等,必须是它们的串是否相等,必须是它们串的长度以及它们对应位置的字符都相等时,才算是相等。即给定两个串:s=“a1a2……an”,t=“b1b2……bm”,当且仅当 n = m,且 a1 = b1 ,a2 = b2,……,an=bm 时,我们认为 s=t。
5.3 串的存储结构
串的存储结构与线性表相同,分为两种。
5.3.1 串的顺序存储结构
串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列的。按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区。一般是用定长数组来定义。
5.3.2 串的链式存储结构
对于串的链式存储结构,与线性表是相似的,但由于串结构的特殊性,结构中的每个元素数据是一个字符,如果也简单的应用链表存储串值,一个结点对应一个字符,就会存在很大的空间浪费。因此,一个结点可以存放一个字符,也可以考虑存放多个字符,最后一个结点若是未被栈占满时,可以用“#”或其他非串值字符补全,如下图:
这里一个结点存多少字符才合适就变得很重要,这会直接影响着串处理的效率,需要根据实际情况做出选择。
但串的链式存储结构除了在连接串与串操作时有一定方便之外,总的来说不如顺序存储灵活,性能也不如顺序存储结构好。
六、树
6.1 树的定义
树是 n (n >= 0) 个结点的有限集。n=0 时称为空树。在任意一棵非空树种:(1)有且仅有一个特定的称为根的结点;(2)当 n>1 时,其余结点可分为 m (m > 0) 个互不相交的有限集,其中每一个集合本身又是一棵树,并且称为根的子树。如下图所示:
6.1.1 结点分类
树的结点包含一个数据元素及若干指向子树的分支。结点拥有的子树称为结点的度。度为 0 的结点称为叶节点或终端结点;度不为 0 的结点称为非终端结点或分支结点。除根结点之外,分支结点也成为内部结点。树的度是树内各结点的度的最大值。
如上图,这棵树结点的度的最大值是结点 D 的度,为 3,所以树的度也为 3。
6.1.2 结点间关系
结点的子树的根称为该结点的孩子,相应的,该结点称为孩子的双亲。同一个双亲的孩子之间互称兄弟。结点的祖先是从根到该结点所经分支上的所有结点。
如上图,对于 H 来说,D、B、A 都是它的祖先。反之,以某结点为根的子树中的任一结点都称为该结点的子孙。B 的子孙有 D、G、H、I。
6.1.3 树的其它相关概念
结点的层次从根开始定义起,根为第一层,根的孩子为第二层。若某结点在第 i 层,则其子树的根就在第 i + 1 层。其双亲在同一层的结点互为堂兄弟。
在上图中,D、E、F 是堂兄弟,G、H、I、J 也是。树中结点的最大层次称为树的深度或高度,当期树的深度为 4。
如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则为无序树。
森林是 m (m>=0) 棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。
对比线性表与树的结构,它们有很大的不同,如下图:
6.2 二叉树
6.2.1 二叉树定义
二叉树是 n (n>=0) 个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根节点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
6.2.2 二叉树特点:
- 每个结点最多有两棵子树,所以二叉树不存在度大于 2 的结点。注意不是只有两棵子树,而是最多有。没有子树或者有一颗子树都是可以的。
- 左子树和右子树是有顺序的,次序不能任意颠倒。
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
6.2.3 二叉树具有五种基本形态:
- 空二叉树
- 只有一个根结点
- 根结点只有左子树
- 根结点只有右子树
- 根结点既有左子树又有右子树
6.2.4 特殊二叉树
- 斜树
所有的结点都只有左子树的二叉树叫左斜树。所有结点都只有右子树的二叉树叫右斜树。
- 斜树
-
满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
-
-
完全二叉树
对一棵具有 n 个结点的二叉树按层序编号,如果编号为 i (1 <= i <= n) 的结点与同样深度的满二叉树中编号为 i 的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。
-
6.3 遍历二叉树
二叉树的遍历是指从根结点出发,按照某种次序
依次访问
二叉树所有结点,使得每个结点被访问一次且仅被访问一次。
二叉树遍历方法
-
前序遍历
若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。
前序遍历次序:ABDGHCEIF
- 中序遍历
若二叉树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后访问根结点,最后中序遍历右子树。
中序遍历次序:GDHBAEICF
- 后序遍历
若二叉树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。
后序遍历次序:GHDBIEFCA
- 层次遍历
若二叉树为空,则空操作返回,否则从树的第一层,
层次遍历次序:ABCDEFGHI
七、图
7.1 图的定义
图是由顶点的有穷非空集和顶点之间边的集合,通常表示为:G(V. E),其中,G 表示一个图,V 是图 G 中顶点的集合,E 是图 G 中边的集合。
- 在线性表中,数据元素之间是被串起来的,仅有线性关系,每个数据元素只有一个直接前驱和一个直接后继。
- 在树形结构中,数据元素之间有着明显的层次关系,并且每一层上的数据元素可能和下一层中多个元素相关,但只能和上一层中一个元素相关。
- 图是一种较线性表和树更加复杂的数据结构,在图结构中,节点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。
线性表中我们把数据元素叫元素;树种将数据元素叫结点;在图中数据元素,我们称之为顶点。
线性表中可以没有数据元素,称为空表;树种可以没有结点,叫做空树;但在图结构中,不允许没有顶点。在定义中,若 V 是顶点的集合,则顶点集合 V 有穷非空。
线性表中,相邻的元素之间具有线性关系;树结构中,相邻两层的结点具有层次关系;而图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
7.2 各种图定义
-
无向边:若顶点 V1 到 V2 之间的边没有方向,则称这条边为无向边,用无序偶对来表示。如果途中任意两个顶点之间都是无向边,则称该图为无向图。如下图:
由于是无方向的,连接顶点 A 与 D 的边,可以表示成无序对 (A,D),也可以表示成 (D,A)。
-
有向边:若顶点 V1 到 V2 的边有方向,则称这条边为有向边,也称为弧。用有序偶<Vi, Vj> 来表示,Vi 称为弧尾,Vj 称为弧头。如果图中任意两个顶点之间的边都是有向边,则称该图为有向图。如下图:
连接顶点 A 到 D 的有向边就是弧,A 是弧尾,D 是弧头,<A, D> 表示弧,注意不能写成<D, A>。
在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有 n 个顶点的无向完全图有 n * (n - 1) / 2 条边。
在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有 n 个顶点的有向完全图有 n * (n - 1) 条边。
从这里可以得出结论,对于具有 n 个顶点和 e 条边数的图,无向图 0<= e <= n(n-1)/2,有向图 0 <= e <= n(n-1)。
7.3 图的遍历
从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历。
7.3.1 深度优先遍历
深度优先遍历,也有称为深度优先搜索,简称 DFS。它从图中某个顶点 V 出发,访问此顶点,然后从 V 的未被访问的邻接点出发深度优先遍历图,直至图中所有和 V 有路径相通的顶点都被访问到。
7.3.2 广度优先遍历
广度优先遍历,又称为广度优先搜索,简称 BFS。
对比图的深度优先遍历和广度优先遍历算法,它们在时间复杂度上是一样的,不同之处仅仅在于对顶点访问的顺序不同。可见两者在全图遍历上是没有优劣之分的,只是视不同的情况选择不同的算法。