栈
栈的定义
栈(stack)是限定仅在表尾进行插入和删除操作的线性表
栈是一种后进先出(Last In First Out)的线性表,简称LIFO结构
栈的顺序存储结构与链式存储结构
栈的顺序存储结构如下图
栈的链式存储结构如下图
比较:
- 顺序栈与链栈在时间复杂度上是一样的,均为O(1)
- 对于空间性能,顺序栈需要事先确定一个固定的长度,可能会存在内存空间浪费的问题,但它的优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一些内存开销,但对于栈的长度无限制
- 如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈
栈的应用——四则运算表达式求值
程序中解决四则运算是比较麻烦的,因为计算有优先级,波兰逻辑学家发明了一种不需要括号的后缀表达法,称为逆波兰表示
如
9 + (3 - 1) x 3 + 10 ÷ 2
转换成后缀表达式为
9 3 1 - 3 * + 10 2 / +
转换规则:
从左到右遍历表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分,若是符号,则判断其与栈顶符号的优先级,是右括号或优先级低于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止
计算规则:
从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果
队列
队列的定义
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表
队列是一种先进先出(First In First Out)的线性表,简称FIFO
允许插入的一端称为队尾,允许删除的一端称为队头
队列的顺序存储结构——循环队列
队列的头尾相接的顺序存储结构称为循环队列
队列的链式存储结构
队列的链式存储结构,就是线性表的单链表,只不过它只能尾进头出
比较
- 循环队列与链队列的时间复杂度都为O(1)
- 循环队列需要事先申请好空间,使用期间不释放,而对于链队列,每次申请和释放结点也会存在一些时间开销
- 对于空间上来说,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题,而链队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受
- 在可以确定队列长度最大值的情况下,建议用循环队列,如果无法预估队列的长度时,则用链队列
串
串(string)是由零个或多个字符组成的有限序列,又名叫字符串
串的比较
串的比较是通过组成串的字符之间的编码来进行的,而字符的编码指的是字符在对应字符集中的序号(如ASCII值)
串的存储结构
串的存储结构与线性表相同,分为两种:串的顺序存储结构和串的链式存储结构
串的顺序存储结构
串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列,按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区,一般是用定长数组来定义
串的链式存储结构
串结构中的每个元素数据是一个字符,如果一个结点对应一个字符,就会存在很大的空间浪费,因此可以考虑一个结点存放多个字符,最后一个结点若是未被占满时,可以用”#”或其他非串值字符补全,如下图所示
每个结点存多少个字符会直接影响串处理的效率,需要根据实际情况做出选择
串的链式存储结构除了在连接串与串操作时有一定方便之外,总的来说不如顺序存储灵活,性能也不如顺序存储结构好
朴素的模式匹配算法
子串的定位操作通常称做串的模式匹配,如从主串S=”goodgoogle”中,找到子串T=”google”这个子串的位置,通常需要下面的步骤
- 主串S第一位开始匹配,匹配失败
- 主串S第二位开始匹配,匹配失败
- 主串S第三位开始匹配,匹配失败
- 主串S第四位开始匹配,匹配失败
- 主串S第五位开始匹配,S与T,6个字母全匹配,匹配成功
时间复杂度为O(n+m),其中n为主串长度,m为要匹配的子串长度
极端情况下,主串为S=”00000000000000000000000001”,子串为T=”0001”,在匹配时,每次都得将T中字符循环到最后一位才发现不匹配,此时的时间复杂度为O((n-m+1)*m)
树
树的定义
树(Tree)是n(n≥0)个结点的有限集合。n=0时称为空树,在任意一棵非空树中:
- 有且仅有一个特定的称为根(Root)的结点
- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2…Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)
结点分类
树的结点包含一个数据元素及若干指向其子树的分支,结点拥有的子树数称为结点的度(Degree),度为0的结点称为叶结点(Leaf)或终端结点;度不为0的结点称为非终端结点或分支结点
树的度是树内各结点的度的最大值
结点间关系
结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent)
同一个双亲的孩子之间互称兄弟(Sibling)
树的其他相关概念
结点层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。若某结点在第i层,则其子树的根就在第i+1层
在同一层的结点互为兄弟
如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树
森林(Forest)是m(m≥0)棵互不相交的树的集合
线性结构与树结构对比
线性结构
- 第一个数据元素:无前驱
- 最后一个数据元素:无后继
- 中间元素:一个前驱一个后继
树结构
- 根结点:无双亲,唯一
- 叶结点:无孩子,可以多个
- 中间结点:一个双亲多个孩子
树的存储结构
双亲表示法
在每个结点中,附设一个指示器指示其双亲结点到链表中的位置
该存储方式根据结点的parent指针很容易找到它的双亲结点,时间复杂度为O(1)
缺点: 如果需要知道某个结点的所有孩子,需要遍历整棵树
孩子表示法
把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空,然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中,如下图所示
缺点: 如果需要知道某个结点的双亲,需要遍历整棵树
改进: 双亲孩子表示法
孩子兄弟表示法
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的,因此,可以设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟
这个表示法的最大好处是它把一棵复杂的树变成了一棵二叉树
二叉树
二叉树的定义
二叉树(Binary Tree)是n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成
二叉树特点
- 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点
- 左子树和右子树是有顺序的,次序不能任意颠倒
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树
特殊的二叉树
- 斜树(左斜树、右斜树)
- 满二叉树
完全二叉树
- 对一棵具有n个结点的二叉树按层序编号,如果编号为i(1≤i≤n)的结点与同样深度的满二叉树中编号为i的结点在二叉树位置完全相同,则这棵二叉树称为完全二叉树
满二叉树一定是一棵完全二叉树,但完全二叉树不一定是满二叉树
二叉树的性质
- 在二叉树的第i层上至多有 2(i−1) 个结点(i≥1)
- 深度为k的二叉树至多有 2k−1 个结点(k≥1)
- 对任何一棵二叉树T,其叶子结点数=度为2的结点数+1
- 具有n个结点的完全二叉树的深度不大于 log2n +1的最大整数
如果对一棵有n个结点的完全二叉树的结点按层序编号(每层从左到右),对任一结点i(1≤i≤n)有:
- 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点i/2
- 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i
- 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1
二叉树的存储结构
二叉树顺序存储结构
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系,比如双亲与孩子的关系,左右兄弟的关系等
上图浅色代表不存在的结点,不存在的结点用^表示,会造成对存储空间的浪费,所以顺序存储结构一般只用于完全二叉树
二叉链表
二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域
遍历二叉树
二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次
前序遍历
先访问根结点,然后前序遍历左子树,再前序遍历右子树
中序遍历
从根结点开始(并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树
后序遍历
从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点
层序遍历
从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问
线索二叉树
在二叉链表上,只能知道每个结点指向其左右孩子结点的地址,而不知道某个结点的前驱是谁,后继是谁,可以利用如下结构,存放指向结点在某种遍历次序下的前驱和后继结点的地址
这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树
- ltag为0时指向该结点的左孩子,为1时指向该结点的前驱
- rtag为0时指向该结点的右孩子,为1时指向该结点的后继
如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,就比较适合用线索二叉链表的存储结构
树、森林与二叉树的转换
树转换为二叉树
将树转换为二叉树的步骤如下
- 加线,在所有兄弟结点之间加一条连线
- 去线,对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线
- 层次调整,以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明,注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子
森林转换为二叉树
森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作
步骤如下:
- 1.把每个树转换为二叉树
- 2.第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来,当所有的二叉树连接起来后就得到了由森林转换来的二叉树
二叉树转换为树
二叉树转换为树是树转换为二叉树的逆过程
步骤如下:
- 1.加线,若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点…都作为此结点的孩子,将该结点与这些右孩子结点用线连接起来
- 2.去线,删除原二叉树中所有结点与其右孩子结点的连线
- 层次调整,使之结构层次分明
二叉树转换为森林
判断一棵二叉树能够转换成一棵树还是森林,标准很简单,只要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树,转换成森林的步骤如下:
- 1.从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除…,直到所有右孩子连线都删除为止,得到分离的二叉树
- 2.再将每棵分离后的二叉树转换为树即可
树与森林的遍历
树的遍历分为两种方式
- 一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树,如下图遍历结果为ABEFCDG
- 另一种是后根遍历,即先依次后根遍历每棵子树,然后再访问根结点,如下图遍历结果为EFBCGDA
森林的遍历也分为两种方式
- 前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样方式遍历除去第一棵树的剩余树构成的森林,如下图遍历结果为ABCDEFGHJI
- 后序遍历:是先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林,如下图遍历结果为BCDAFEJHIG
森林的前序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同
赫夫曼树及其应用
赫夫曼树定义与原理
从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称做路径长度
树的路径长度就是从树根到每一结点的路径长度之和
如果考虑到带权的结点,结点的带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积,树的带权路径长度为树中所有叶子结点的带权路径长度之和
带权路径长度WPL最小的二叉树称做赫夫曼树
二叉树a的 WPL = 5x1+15x2+40x3+30x4+10x4 = 315
二叉树b的 WPL = 5x3+15x3+40x2+30x2+10x2 = 220
构造赫夫曼树的步骤:
- 1.先把有权值的叶子结点按照从小到大的顺序排列成一个有序序列,即:A5,E10,B15,D30,C40
- 2.取头两个最小权值的结点作为一个新结点 N1 的两个子结点,相对较小的是左孩子
- 3.将 N1 替换A与E,插入有序序列中,保持从小到大排列,即 N115 ,B15,D30,C40
- 4.重复步骤2,3,直到只含一棵树为止
此时构造出来的赫夫曼树的 WPL = 40x1+30x2+15x3+10x4+5x4 = 205