• 一. 基本概念1. 概念及定义数据(Data)数据元素数据对象数据结构2. 四种逻辑结构3. 四种基本存储结构顺序存储结构链式存储结构索引存储结构散列存储结构4. 数据类型、抽象数据类型4.1 数据类型4.2 抽象数据类型(ADT)二. 算法1. 算法基本概念2. 算法效率度量2.1 时间复杂度2.2 空间复杂度三. 线性表1. 线性表定义2. 线性表的基本操作3. 顺序表3.1 顺序表的静态分配3.2 顺序表的动态分配3.3 顺序表的插入3.4 顺序表的删除3.5 顺序表的查找4. 单链表4.1 单链表建立4.1.1 尾插法4.1.2 头插法建立链表4.2 单链表的插入4.3 单链表的删除4.4 链表的查找4.5 习题:链表的逆置5. 双链表5.1 双链表的初始化5.2 双链表的插入5.3 双链表的删除6. 循环链表6.1 循环单链表的初始化6.2 循环双链表的初始化7. 静态链表7.1 静态链表的初始化8. 顺序表和链表的比较四. 栈1. 栈的顺序存储实现1.1 栈的定义1.2 栈的增加1.3 出栈操作1.4 共享栈2. 链栈2.1 链栈初始化2.2 链栈的入栈2.3 链栈的出栈2.4 链栈打印3. 栈的应用—括号匹配4. 栈的应用—表达式求值4.1 中缀表达式转后缀方法4.2 后缀表达式计算4.3 代码实现5. 栈在递归中的应用五. 队列1. 队列的顺序实现1.1 队列初始化1.2 入列1.3 出列1.4 队列元素个数1.5 其他情况2. 链式队列2.1 链式队列的初始化2.2 链式队列的入队2.3 链式队列出队操作3. 双端队列4. 队列的应用——树的遍历5. 特殊矩阵的压缩存储5.1 一维数组5.2 二维数组5.3 对称矩阵压缩存储5.4 三角矩阵5.5 三对角矩阵的压缩存储5.6 稀疏矩阵压缩存储六. 串1. 串的存储方法2. 串的顺序存储2.1 定长顺序存储2.2 堆分配存储3. 串的链式存储3.1 低密度存储3.2 高密度存储4. 字符串基本操作4.1 求字串4.2 字符串比较4.3 定位操作5. 朴素模式匹配算法6. KMP算法匹配字符串7. 求next数组8. KMP算法进一步优化1. 树的基本概念2. 树常考的性质3. 二叉树3.1 几个特殊二叉树满二叉树完全二叉树二叉排序树平衡二叉树3.2 二叉树存顺序储结构完全二叉树顺序存储非完全二叉树顺序存储3.3 二叉树的链式存储3.4 二叉树遍历三种遍历方法二叉树层序遍历3.5 通过遍历序列构造二叉树3.6 线索二叉树线索二叉树概念线索二叉树代码实现线索二叉树找前驱与后继4. 树的存储与遍历4.1 树与森林的存储方法双亲表示法孩子表示法孩子兄弟表示法森林转换二叉树二叉树转换为森林4.2 树和森林的遍历先根遍历树的后根遍历(深度优先遍历)树的层次遍历(广度优先遍历)森林的先序遍历森林的中序遍历5. 哈夫曼树5.1 哈夫曼树的构造5.2 哈夫曼树的应用八. 并查集1. 集合查操作2. 集合的合并操作3. 并查集的优化4. 并查集优化2

    一. 基本概念

    数据结构三要素:逻辑结构、数据的运算、物理结构(存储结构)

    1. 提取操作对象,找出操作对象之间的关系并用数学的语言描述就是数据结构。

      操作对象指:每位学生的信息(学号、姓名、性别.....)

      操作的算法指:查询、插入、修改、删除等。

    2. 数据结构可分为:线性数据结构(数组,队列,线性表等)、非线性数据结构(集合,树,图等)。

      线性数据结构:有且仅有一个开始和一个终端结点,并且所有结点都最多只有一个直接前趋和一个直接后继。如:学生管理系统,是一对一关系

      非线性数据结构:一个结点可能油多个直接前趋和直接后继。如:目录结构(树)、最短路径(图),是一对多关系或多对多关系。

      最短路径:

      导航节点

    1. 概念及定义

    数据(Data)

    数据是能输入计算机且能被计算机处理的各种符号的集合。

    数据特点:

    1. 信息的载体
    2. 是对客观事物符号化的表示
    3. 能够被计算机识别存储和加工

    数据包括:

    1. 数值型的数据:整数、实数等。
    2. 非数值型的数据:文字、图像、图形、声音等。

    数据元素

    数据元素是数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理。而一个数据元素可由多个数据项组成,数据项是构成数据元素不可分割最小单位。

    也可称为元素,或称为记录、结点或顶点。如上面最短路径图,其中的每个点我们称之为结点。

    数据对象

    数据对象是性质相同的数据元素的集合,是数据的一个子集。

    如:学籍表可以看作是一个数据对象,由若干条学生信息构成的子集。

    数据对象图例:

    数据对象图例

    数据结构

    意义:数据元素不是孤立存在的,它们之间存在着某种关系,数据元素相互之间的关系称为结构( Structure )

    数据结构是指相互之间存在一种或多种特定关系的数据元素集合。或者说,数据结构是带结构的数据元素的集合。

    2. 四种逻辑结构

    逻辑结构和数据的运算是定义一种数据结构前提。

    集合结构(大纲不考):结构中的数据元素之间除了同属于一个集合的关系外,无任何其它关系。

    集合结构例子:

    集合结构例子

    线性结构:结构中的数据元素之间存在着一对一的线性关系,除了除了第一个元素,所有元素都有唯一前驱;除了最后一个元素,所有元素都有唯一后继。

    基本运算:①查找第i个数据元素。②在第i个位置插入新的数据元素。③删除第i个位置数据元素.....

    线性结构例子:

    线性结构例子

    树形结构:结构中的数据元素之间存在着一对多的层次关系。

    树形结构例子:

    树形结构例子

    图状结构或网状结构:结构中的数据元素之间存在着多对多的任意关系。

    图结构例子:

    图结构例子

    逻辑结构分类:

    逻辑结构分类

    3. 四种基本存储结构

    当我们定义完一种数据结构后,需要用计算机来实现这种数据结构,此时用到基本存储结构。

    顺序存储结构

    含义:

    1. 用一组连续的存储单元依次存储数据元素,数据元素之间的逻辑关系由元素的存储位置来表示。
    2. C语言中用数组来实现顺序存储结构

    顺序存储结构示例:

    顺序存储结构

    链式存储结构

    含义:

    1. 用一组任意的存储单元存储数据元素,数据元素之间的逻辑关系用指针表示。
    2. C语言中用指针来实现链式存储结构。

    链式存储结构:

    链式存储结构

    如:上图bat对应指针(地址)是165,后面130是下一个元素地址,即cat。没有即为NULL

    索引存储结构

    含义:

    1. 在存储结点信息的同时,还建立附加的索引表。
    2. 通俗讲索引就是目录,一般形式是:关键字、地址。
    3. 关键字是能唯一标识一个结点的数据项。
    4. 若每个结点在索引表中都有一个索引项,则该索引表称之为稠密索引。若一组结点在索引表中只对应一个索引项,则该索引表称之为稀疏索引。

    索引存储结构:

    索引存储结构2

    散列存储结构

    根据结点的关键字直接计算出该结点的存储地址。如:哈希表。

    4. 数据类型、抽象数据类型

    4.1 数据类型

    数据类型是一个值的取值范围和定义在此范围上的一组操作(加减、取模等)的总称。

    1. 原子类型:其值不可再分的数据类型。如int类型(可进行加、减、乘、除、取模等运算),bool类型(可进行与、或、非等操作)
    2. 结构类型:其值可以再分解为若干成分(分量)的数据类型。如定义一个可操作横坐标和纵坐标的函数。

    4.2 抽象数据类型(ADT)

    抽象数据类型是抽象数据组织及与之相关的操作。如:逻辑结构、数据运算、物理结构(存储结构),这个过程。

    二. 算法

    1. 算法基本概念

    算法就是如何高效地处理这些数据,以解决实际问题。

    其定义是对特定问题求解步骤地一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。

    算法特点:

    1. 有穷性。一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。
    2. 确定性。算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出。
    3. 可行性。算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
    4. 输入。 一个算法有零个或多个输入, 这些输入取自于某个特定的对象的集合。 输出。一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。

    以上特点只要有一个不满足,就不能称之为算法。

    一个好的算法所具备特质:

    1. 正确性。算法能够正确解决求解问题。
    2. 可读性。算法应具有良好的可读性,以帮助人们理解。考试时候要写注释
    3. 健壮性。输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。
    4. 高效率和低存储。即算法时间复杂度低,空间复杂度低。

    算法基本概念总结:

    算法基本概念总结

    2. 算法效率度量

    算法复杂度度量可以从两个方面:①时间复杂度、②空间复杂度

    2.1 时间复杂度

    用事前预估算法时间开销T(n)与问题规模n的关系(T表示"time")。

    方法:

    通过以上规则我们还要注意以下几点:

    1. 顺序执行的代码只会影响常数项,所以可以忽略不记。
    2. 循环执行代码中,我们只需要挑一个循环体基本操作,分析他的执行次数即可。
    3. 如果有多层嵌套循环,我们只需要关注最深层代码循环次数即可。

    例1:分析以下代码时间复杂度

    答案:T(n)=O(log2n)

    解析:我们直接看循环体,循环体中,赋值语句i每次翻倍,假设循环了x次,则循环结束时刚好满足2x>n,所以x=log2n+1

    则,时间复杂度T(n)=O(log2n)

    例2:分析以下代码时间复杂度

    答案:最好情况:元素n在第一个位置,即最好时间复杂度T(n)=O(1) 最坏情况:元素n在最后一个位置,最坏时间复杂度T(n)=O(n) 平均情况:假设元素n在任意一个位置的概率相同为1n,平均时间复杂度T(n)=O(n)

    平均情况解析:循环次数x=(1+2+3+...+n)1n=(n(1+n)2)1n=1+n2,即T(n)=O(x)=O(n)

    注意:我们在评价一个算法时候一般只看最坏情况和平均情况,而最好情况一般参考意义不大。

    时间复杂度总结:

    时间复杂度总结

    2.2 空间复杂度

    空间复杂度需要时间复杂度中运算知识。空间复杂度我们用S(n)表示。

    例1:计算以下程序空间复杂度

    答案:S(n)=O(n)

    解析:函数所需要参数nint占四个字节,下面的iint类型所以也是四字节,数组长度是n且为int类型,所以是4n,那么这个程序所需要空间为:4n+8。规范写法为S(n)=O(n)

    结论:常数项同样不考虑,注意数组大小等。

    例2:计算以下程序空间复杂度

    答案:S(n)=O(n2)

    解析:二维int类型n长度数组占大小为:4n2,下面一维数组是4n,所以S(n)=O(n2)+O(n)+O(1)=O(n2)

    例3:计算以下程序空间复杂度

    答案:S(n)=O(n)

    解析:函数递归n次,每次变量大小占16,所以应该为S(n)=16n=O(n)

    结论:递归问题空间复杂=递归调用深度。

    例4:我们对例3进行改进,讲声明变量改为数组

    答案:S(n)=O(n2)

    解析:函数递归调用n次,每次数组长度为n,所以空间复杂度为:1+2+3+...n=n(1+n)2=12n2+12n,即答案。

    空间复杂度总结:

    空间复杂度总结

    三. 线性表

    1. 线性表定义

    定义:线性表是具有相同数据类型n(n0) 个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。若用L命名线性表,则其一般表示为:

    L=(a1,a2...ai,ai+1...an)

    几个概念:

    1. ai是线性表中的"第i个"元素线性表中的位序
    2. a1是表头元素;an是表尾元素。
    3. 除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。

    2. 线性表的基本操作

    InitList(&L):初始化表。构造一个空的线性表L,分配内存空间。 DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。

    ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。

    LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。 GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。

    其他常用操作: Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。 PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。 Empty(L):判空操作。若L为空表,则返回true,否则返回false。

    以上括号内的是参数。

    总结:

    1. 对数据的操作:创建,销毁、增删改查

    2. C语言函数的定义:<返回值类型>函数名(<参数1类型>参数1,<参数2类型>参数2.....)

    3. 函数名和参数的形式、命名都可改变。对于命名一般用驼峰命名法:

      方法名、参数名、成员变量、局部变量需要使用小驼峰命名法

      类名需要使用大驼峰命名法

    4. 什么时候要传入引用"&":对参数的修改结果需要"带回来"。解释如下:

      运行结果:

      调用test前x=1 test函数内部x=1024 调用test后x= 1024

      这里test函数中的x是引用类型,所以不用返回值就可以修改主函数中的x值。

    线性表知识点总结:

    线性表知识点总结

    3. 顺序表

    顺序表是线性表的一种。其定义是:用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置,上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。

    顺序表优缺点:

    我们可以用C语言提供的sizeof关键字来判断一个类型或者数据的大小。

    顺序存储:

    顺序存储

    3.1 顺序表的静态分配

    给各个数据元素分配连续的存储空间,大小为MaxSizesizeof(ElemType)。即数组大小×数据类型。

    注意:C语言要初始化数据。否则会出现"脏数据"情况。

    静态分配存在一定局限性:无法提前预知数组大小。

    3.2 顺序表的动态分配

    C语言中动态申请和释放内存空间函数:

    动态分配案例:

    顺序表的特点:

    1. 随机访问,即可以在O(1)时间内找到第i个元素。
    2. 存储密度高,每个节点存储数据元累
    3. 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
    4. 插入、删除操作不方便,需要移动大量元素

    3.3 顺序表的插入

    以上就是对顺序表进行元素插入操作。

    我们可以对以上ListInsert()方法进行时间复杂度分析:

    1. 最好情况:新元素插入到表尾,不需要移动元素。

      i=n+1,循环0次,最好时间复杂度=O(1)

    2. 最坏情况:新元素插入到表头,需要将原有的n个元素全部向后移动

      i=1,循环n次;最坏时间复杂度=O(n)

    3. 平均情况:假设新元素插入到任何一个位置的概率相同,即i=1,2,3...,length+1的概率都是p=1n+1

      i=1时,循环n次;i=2时,循环n1次;i=3时,循环n2...i=n+1时,循环0次。

      所以平均循环次数=np+(n1)p+(n2)p+...+p=n(n+1)21n+1=n2

      由此可得时间复杂度为O(n)

    3.4 顺序表的删除

    运行结果:已删除第3个元素,删除元素值为=3;

    我们可以对以上ListDelete()方法进行时间复杂度分析:

    1. 最好情况:删除表尾元素,不需要移动其他元素

      i=n,循环0次,最好时间复杂度=O(1)

    2. 最坏情况:删除表头元素,需要将后续的n1个元素全都向前移动

      i=1,循环n1次;最坏时间复杂度=O(n)

    3. 平均情况:假设删除任何一个元素的概率相同,即i=1,2,3,...length的概率都是p=1n

      i=1,循环n1次,i=2,循环n2...i=n时,循环0

      平均循环次数为=(n1)p+(n2)p+...+p=n(n1)21n=n12

      由此可得时间复杂度为O(n)

    顺序表基本操作总结:

    顺序表基本操作

    3.5 顺序表的查找

    顺序表按位查找和按值查找总结:

    顺序表按位查找和按值查找

    4. 单链表

    链表不同于顺序表的顺序存储方式,用的是链式存储。

    单链表的每个结点除了存放数据元素,同时还需要包含指向下一个结点的指针。由于每个结点只包含一个指针所以叫单链表。

    单链表优缺点:

    单链表定义如下:

    上面的"LNode"称之为结点,data称为数据域,*next称为指针域。

    带头节点的单链表:

    上面前四行代码是将"struct LNode"命名为"LNode",并且用"LinkList"表示一个指向"struct LNode"的指针。

    不带头结点单链表

    4.1 单链表建立

    4.1.1 尾插法

    先初始化一个带头结点的单列表,之后每次取个数据元素插入表尾部。是比较常用的方法。

    运行结果:

    输入:1,2,399

    尾插法图像示例:

    f47832ac0b274b19b1fb2e5ba8d5fcb9

    4.1.2 头插法建立链表

    头插法将每个数据插入在链表头部。头插法可以用于链表的逆置

    运行效果和尾插法一样。

    头插法图像示例:

    7ae0a7bd64d74b5d9a149f7849e38f93

    4.2 单链表的插入

    核心思路是将插入位置前一个结点的指针域指向新建立的结点,再将新节点的指针与指向后一个结点(前一个结点指针域中的值为后一个结点)。

    代码如下:

    主函数:

    指定结点前插操作:

    指定结点前插操作

    输入:1,2,3,4,5

    运行结果:1,2,3,66,4,5

    4.3 单链表的删除

    通过按照输入序号删除对应结点,其原理和上面的插入类似。

    主函数代码:

    关于删除指定节点p,其本质就是将p的后继结点复制给p结点,再释放后继结点,并将p结点重新指向下一个结点。这种方法有个局限性,就是我们在删除最后一个结点时,由于后一个结点为null所以程序会出错,我们就只能使用传统办法循环一个个找了。

    输入:1,2,3,4,5

    运行结果:1,2,4,5

    4.4 链表的查找

    最常用的查找是按值查找和按序号查找

    主函数:

    4.5 习题:链表的逆置

    逆置方法有很多种:

    方法一:可以通过遍历链表元素,用头插法将遍历元素值插入新链表。

    输入值:1,23,4,5

    运行结果:5,4,23,1

    方法二

    方法是先让新结点p=l->next

    再让头节点指针l->next=NULL断开链表。

    用结点lNodeNext=p来记录结点pp=p->next指向下一轮待逆置的结点。

    最后让lNodeNext用头插法插在l链表内即可

    链表逆置

    链表逆置2

    输入值:1,23,4,5

    运行结果:5,4,23,1

    5. 双链表

    由于单链表只能指向下一个指针,而双链表同时指向前驱节点和后继结点。

    5.1 双链表的初始化

    同样的这里的DNodeDLinklist等价,只是为了区分链表(DLinklist)和结点(DNode)

    5.2 双链表的插入

    双链表的插入:

    双链表的插入

    双链表最后一个节点插入:

    双链表的最后一个节点插入

    5.3 双链表的删除

    同样和之前但链表删除方法一致。

    双链表的总结:

    双链表的总结

    6. 循环链表

    循环链表就是在单链表(双链表)的基础上讲最后一个结点指向头节点。

    特点是:从一个结点出发可以找到其他任何一个结点。常用于对表头和表尾操作频率高的情况下。

    6.1 循环单链表的初始化

    循环单链表结构:

    循环链表

    其他操作基本与前面一致。注意判断最后一个结点时,应该判断是否指向头节点。

    6.2 循环双链表的初始化

    循环链表:表头结点的prior指向表尾结点;表尾结点的next指向头结点。

    循环双链表不需要考虑表头表尾的界限操作。

    循环双链表结构:

    循环双链表

    其他操作

    循环链表总结:

    循环链表总结

    7. 静态链表

    与单链表不同的是静态链表是分配一整片连续的内存空间,各个结点集中安置。

    静态链表每个数据元素4B,每个游标4B ( 每个结点共8B)。设起始地址为addr,e1的存放地址为addr + 8*2。2是接下来要寻找的数组下标。

    适用场景:①不支持指针的低级语言;②数据元素数量固定不变的场景(如操作系统的文件分配表FAT)。

    静态链表结构:

    静态链表

    7.1 静态链表的初始化

    初始化静态链表:

    初始化静态链表

    8. 顺序表和链表的比较

    两种表的适用场景:

    1. 表长难以预估、经常要增加/删除元素用链表(订餐系统)。
    2. 表长可预估、查询(搜索)操作较多用顺序表(课堂点名)。

    逻辑结构:顺序表和链表都属于线性结构。

    存储结构:

    基本操作比较:

    在遇到一个开放式问题探讨时:从逻辑结构,存储结构和基本操作这三方面回答。

    四. 栈

    栈和线性表的结构很相似,但栈只允许在一端进行插入或删除操作的线性表。栈和我们生活种摞起来的盘子类似。

    摞起来的盘子:

    盘子

    栈结构和常见术语:

    栈结构和术语

    进栈顺序:a1a2a3a4a5

    出战顺序:a5a4a3a2a1

    栈特点:后进(push)先出(pop)(LIFO)

    栈的基本操作:创建、销毁、增加、删除、查询、判空。

    1. 栈的顺序存储实现

    1.1 栈的定义

    和线性表类似,我们先要用结构体初始化栈。

    1.2 栈的增加

    1.3 出栈操作

    1.4 共享栈

    通过上面代码实现我们可以发现栈的一个明显缺点是栈的大小不可变,可以通过链式栈或者分配大空间来解决这个问题。但分配大片空间会导致空间冗余,这时候可以用共享栈解决。

    共享栈就是两个栈共享一片空间。

    共享栈实现:

    共享栈结构:

    共享栈

    共享栈两个top指针一个从下网上存储,一个从上往下存储。判断栈满的条件是:top0 + 1 == top1

    2. 链栈

    定义:用链式存储结构实现的栈

    实现:实际上我们用头插法建立的单链表就是一个链栈。

    2.1 链栈初始化

    2.2 链栈的入栈

    用头插法入栈

    2.3 链栈的出栈

    链栈的出栈操作,栈顶指针是第一个指针(头指针的下一个),所以将其往后挪一位就好了。

    2.4 链栈打印

    最后其它操作和单链表一致。

    3. 栈的应用—括号匹配

    括号匹配就是相邻括号成对出现。如:[],{},().

    具体思路是用栈实现括号匹配:

    1. 依次扫描所有字符,遇到左括号入栈,遇到右括号存入数组(也可以不用数组,当输入右括号直接和栈中括号匹配)
    2. 查看是否匹配。匹配失败情况: ①左括号单身②右括号单身③左右括号不匹配

    括号匹配思路:

    栈练习--括号匹配

    4. 栈的应用—表达式求值

    表达式通常由三部分组成:①操作数②运算符③界限符(括号等)

    常见表达式有以下几种:

    1. 中缀表达式:a+baba+bca+bcd

      特点:运算符在两个数中间

    2. 后缀表达式(逆波兰表达式):ab+abab+cab+cd

      特点:运算符在两个操作数后面

    3. 前缀表达式(波兰表达式):+abab+abc+abcd

      特点:运算符在操作数前面

    4.1 中缀表达式转后缀方法

    遵循左优先原则。

    ①确定运算顺序

    ②选择下一个运算符,按照[ ]的方式组合成一个新的操作数

    ③如果还有运算符没被处理,继续②

    ((15÷(7(1+1)))×3)(2+(1+1))转换为后缀步骤:

    1. 1 1 +
    2. 7 1 1 +
    3. 15 7 1 1 + ÷
    4. 15 7 1 1 + ÷ 3 ×
    5. 15 7 1 1 + ÷ 3 × 1 1 +
    6. 15 7 1 1 + ÷ 3 × 2 1 1 + +
    7. 15 7 1 1 + ÷ 3 × 2 1 1 + +

    4.2 后缀表达式计算

    通过上面我们将中缀表达式转为后缀表达式15 7 1 1 + ÷ 3 × 2 1 1 + +

    计算后缀表达式也不难:从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算 合体为一个操作数。注意:两个操作数的左右顺序。

    步骤:

    1. 第一个运算符是+,先算1+1
    2. 第二个运算符是72
    3. 第三个运算符是÷15÷5
    4. 第四个运算符是×3×3=9
    5. 第五个运算符是+1+1
    6. 第六个运算符是+2+2=4
    7. 最后一个运算符是94得最后结果

    后缀表达式计算图示:

    后缀表达式计算

    4.3 代码实现

    代码实现需要遵循以下几点:

    ①遇到操作数直接入栈

    ②遇到界限符'(',直接入栈,遇到')',依次弹出栈内的运算符,直到栈顶元素为'('。

    ③运算符运算弹出规则,应该是:操作符栈顶运算符大于或等于当前输入运算符则弹出栈顶操作符。数字栈依次弹出两个数字num1,num2,运算是num2+...num1

    :((15÷(7(1+1)))×3)(2+(1+1))

    Ⅰ.先分析运算符生效顺序,如下图:

    运算符生效顺序

    Ⅱ. 从左到右依次扫描入栈:操作符栈(charStack),操作数栈(numStack)

    Ⅲ. 定义操作符优先级:+/A×/÷B(C.

    Ⅳ. 进行扫描运算:

    ①输入'(',由于操作符栈为NULL,直接入栈。

    ②输入'(',操作符栈不为NULL,且优先级等于操作栈顶的元素'(',但由于括号不参与运算,所以直接入栈。

    ③输入15,数字直接入栈。

    ④输入'÷',由于'÷'优先级低于操作符栈顶元素'(',直接入栈。

    ⑤输入'(',括号直接入栈。

    ⑥输入7,数字直接入栈。

    ⑦输入'',''优先级低于操作符栈顶元素'(',入栈。

    ⑧输入'(',直接入栈

    ⑨输入1,入栈

    ⑩输入'+','+'优先级低于操作符栈顶元素'(',入栈

    ⑪输入1,入栈。此时栈中元素情况如下:

    操作符栈和操作数栈:

    运算栈

    ⑫输入')',栈顶操作符一次出栈直到为NULL或者为'('。此时弹出操作符栈顶元素'+',弹出操作数栈前两个元素1,1。之后运算1+1得到新的数字重新放回操作栈顶部,再次执行弹出元素为'(',这次运算结束。

    运算栈2

    ⑬输入')',再次重复上面,弹出操作符栈顶元素'',弹出操作数栈两个元素2,7,运算72。得到新的数字重新放回操作栈顶部,再次执行弹出元素为'(',这次运算结束。

    运算栈3

    ⑭输入')',重复上面过程,弹出操作符栈顶元素'÷',弹出操作数栈两个元素5,15,运算15÷5。得到新的数字重新放回操作栈顶部,再次执行弹出元素为'(',这次运算结束。

    运算栈4

    ⑮输入'×',此时操作符栈顶元素为'(',优先级低于栈顶元素,直接入栈。

    ⑯输入'3',直接入栈

    运算栈5

    ⑰输入')',弹出操作符栈顶元素'×',弹出操作数栈两个元素3,3,运算3×3。得到新的数字重新放回操作栈顶部,再次执行弹出元素为'(',这次运算结束。

    运算栈6

    ⑱输入'',此时操作栈为NULL,直接入栈

    ⑲输入'(',入栈

    ⑳输入2,入栈

    ㉑输入'+',优先级小于操作栈顶元素'(',入栈

    ㉒输入'(',直接入栈

    ㉓输入1,入栈

    ㉔输入'+',优先级低于操作栈栈顶元素'(',入栈

    ㉕输入1,入栈

    运算栈7

    ㉖输入')',弹出操作符栈顶元素'+',弹出操作数栈两个元素1,1,运算1+1。得到新的数字重新放回操作栈顶部,再次执行弹出元素为'(',这次运算结束。

    运算栈8

    ㉗输入')',弹出操作符栈顶元素'+',弹出操作数栈两个元素2,2,运算2+2。得到新的数字重新放回操作栈顶部,再次执行弹出元素为'(',这次运算结束。

    运算栈9

    ㉘弹出操作栈顶元素'',弹出操作数栈两个元素进行最后运算,得到结果为5

    详细代码

    5. 栈在递归中的应用

    先给出一段代码,看一下函数调用背后过程

    主函数用执行第一句代码后会在栈中存放a,b,c的地址

    栈递归中应用:

    栈递归中应用

    接着执行func1函数所对应代码,当执行func1时,会将后面应该执行代码,这里是表达式c=a+b地址同样压入栈中,图示如下(表达式地址用#1代替)

    栈递归中应用2

    同样func2执行过程同上一步,表达式x=x+10086地址假设为#2

    栈递归中应用3

    执行完func2函数后,之后弹栈,根据栈顶地址,执行#2对应代码(func1)。接着再弹栈执行#1对应代码,即主函数中c=a+b

    以上就是递归基本原理。适合用递归算法解决是:可以把原始问题转换为属性相同,但规模较小的问题。

    实现递归两个重要条件:递归表达式,递归边界条件。

    阶乘问题:

    阶乘问题

    上面代码当执行到第10层时,n=1返回1,弹栈;执行第9层代码此时n=1,表达式就是2*1;之后以此类推。

    上面也可以看出递归缺点就是非常占用栈空间,所以太多层递归可能会导致栈溢出。

    五. 队列

    队列是只允许在队尾一端插入(入队),在队头一端删除(出队)的数据结构。

    特点:先进入先出(FIFO)。

    栈的基本操作:创建、销毁、增加、删除、查询、判空。

    1. 队列的顺序实现

    1.1 队列初始化

    1.2 入列

    上面第五行取余操作,可以将对列变为循环队列。

    循环队列结构图:

    循环队列

    1.3 出列

    和上面入列一样,这一队列变为循环队列,存储数据和读取数据更为方便。

    1.4 队列元素个数

    执行上面代码会实现当前队列中的元素个数。同时队列s.size中保存的也是元素个数。即 (s.rear-s.front+MaxSize)%MaxSize<<endl~s.size

    1.5 其他情况

    上面的都是队头队尾指针初始化时,指向同一位置,但有时候会碰见队尾指针在队尾情况

    队尾指针在队尾情况:

    队尾指针在队尾

    此时入队操作有所改变,我们要先让尾指针向后移一位,再进行插入。

    而初始化方式也有所不同,让尾指针指向队尾元素,当第一次入队时通过取余操作,往后移动到第一位。

    判空操作和判满操作也不同:

    判满操作同上,除了size判满操作,我们也可以通过设置辅助遍历tag来记录上次是入队操作,还是出队操作。由于只有入队操作会让队满,而只有出队操作会让队空,所以当入队时设置tag=1,当出队时设置tag=0

    2. 链式队列

    2.1 链式队列的初始化

    初始化创造两个结构体,第一个是结点,第二个存储链队列地址。

    带头结点初始化:

    带头结点初始化方式

    不带头结点初始化:

    不带头结点初始化方式

    2.2 链式队列的入队

    带头结点入队:

    带头结点入队

    不带头结点入队:

    不带头结点入队

    2.3 链式队列出队操作

    3. 双端队列

    双端队列定义:

    双端队列定义

    具体又可以细分为:输入受限的双端队列、输出受限的双端队列

    输入受限的双端队列和输出受限的双端队列:

    输出或输出受限的双端队列

    考点:和栈一样会考某组数据出栈顺序是否合法。可以用卡特兰数计算出一组数据有多少种合法出战组合。

    :1n+1C2nn::1,2,3,4:14+1C84=14

    4. 队列的应用——树的遍历

    队列有一个重要应用:树的层次遍历。

    树的遍历:

    树的遍历

    遍历步骤:

    1. 遍历1号节点,将一号两个子节点放入队列队尾:123
    2. 遍历完1号节点后出队,遍历2号节点两个子节点放入队尾:2345
    3. 遍历2号后出队,遍历3号节点两个子节点放入队尾:34567
    4. 之后遍历4节点,4节点没有子节点,遍历完后直接弹出:567
    5. 遍历5节点,子节点入队:56789
    6. 遍历5节点后,6节点没有子节点,遍历完后直接弹出:789
    7. 遍历7节点,将子节点放入队列:7891911
    8. 依次遍历弹出即可

    5. 特殊矩阵的压缩存储

    5.1 一维数组

    数组a[10]在内存中存储如下:

    数据存储:

    数据存储

    各个元素大小相同,且物理上连续存放。所以我们可以根据数组起始地址得到任何一个数组内存地址。

    计算方法:a[i]存放地址=+isizeof(ElemType)。数组下标i默认从0开始,如果从1开始,计算公式要改为i-1

    5.2 二维数组

    数组b[2][4]内存存储如下:

    二维数组存储:

    二维数组存储

    存储方式有两种:行优先(一行一行存),列优先(一列一列存)。

    计算方法:MN列二维数组b[m][n],按照行优先存储,则b[i][j]的存储地址=+(iN+j)sizeof(ElemType)

    MN列二维数组b[m][n],按照列优先存储,则b[i][j]的存储地址=+(jM+i)sizeof(ElemType)

    5.3 对称矩阵压缩存储

    n阶方阵中任意一个元素ai,j都有ai,j=aj,i,则该矩阵为对称矩阵。

    对称矩阵:

    对称矩阵

    其特点就是关于主对角线下标i=j对称

    对称矩阵特点:

    对称矩阵特点

    存储数组:数组从0开始,大小为:(1+n)·n21的一维数组存储

    存储方式:只存储主对角线+/下三角区域

    5.4 三角矩阵

    下三角矩阵:除了主对角线和下三角区,其余的元素都相同

    上三角矩阵:除了主对角线和上三角区,其余的元素都相同

    下三角矩阵:

    下三角矩阵

    上三角矩阵:

    上三角矩阵

    存储策略:按行优先原则将橙色区元素存入一维数组,并在最后一个位置存储常数c.

    三角矩阵行优先存储:

    三角矩阵行优先存储

    ai,j元素在数组中下标k

    k={i(i1)2+j1,ij(线)n(n+1)2,i<j()

    5.5 三对角矩阵的压缩存储

    又称为带状矩阵,当满足|ij|>1,ai,j=0(1i,jn)

    三对角矩阵:

    三对角矩阵

    压缩策略:按行优先原则(列优先),只存储带状部分(非0部分)

    三对角矩阵压缩存储:

    三对角矩阵压缩存储

    ai,j(|ij|1)元素在数组中是第几个:

    i1行共3(i1)1个元素,ai,ji行第ji+2个元素,ai,j是第2i+j2个元素。

    若已知数组下标k,如何得到i,j

    ①前i1行共3(i1)1个元素

    ②前i行共3i1个元素

    ③显然3(i1)1<k+13i1,即i(k+2)3i向上取整即可满足这个不等式:i=[k+23]

    5.6 稀疏矩阵压缩存储

    稀疏矩阵:非零元素远远少于矩阵元素个数。

    稀疏矩阵:

    稀疏矩阵

    压缩存储策略:

    矩阵常见考题:

    矩阵考题

    六. 串

    串,即字符串。是一种特殊的线性表,数据元素之间呈线性关系。

    串的线性关系:

    串的线性关系

    术语:

    1. 子串:串中任意个连续的字符组成的子序列。空串也是字符串的子串。
    2. 主串:整个字符串
    3. 字符在主串中的位置:字符在串中的序号,从1开始。
    4. 字串在主串中的位置:字串的第一个字符在主串中的位置,也是从1开始。如:T=iPhone pro MaxproT中位置是8
    5. 只有长度为0时,才是空串。空格也算字符串,每个占1B

    串的基本操作增删改查等,通常以子串为操作对象。

    字符集编码:任何数据存到计算机中一定是二进制数,需要确定一个字符和二进制数的对应规则,这就是"编码"。一般考试用ASCII编码,一个字符大小为1B

    1. 串的存储方法

    一般采用以下几种存储方法:

    串的几种存储:

    串存储方法

    方案一缺点是从0开始,与串的序列不符。方案二缺点是char[0]大小不能超过255。所以经常采用方案四。

    2. 串的顺序存储

    2.1 定长顺序存储

    可以采用静态数组方式实现串的顺序存储

    这样做缺点是数组长度是定长的,不能改变。

    2.2 堆分配存储

    用动态数组方式实现

    3. 串的链式存储

    3.1 低密度存储

    存储密度低,每个字符1B,而每个指针4B

    串低密度存储:

    串低密度存储

    3.2 高密度存储

    每个结构体有一个长度为4的字符型数组

    串高密度存储:

    串高密度存储

    最后一个节点存储不满,可以用特殊字符标记,建议用\0

    4. 字符串基本操作

    4.1 求字串

    要求:用sub返回串S的第pos个字符起长度为len的子串。

    4.2 字符串比较

    S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0.

    4.3 定位操作

    若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置,否则函数值为0

    每次从S中取T大小的字符,逐个对比即可。

    5. 朴素模式匹配算法

    字符串模式匹配:在一大片字符串(主串)中搜索某个字符串(模式串)。如:一个文档中搜索一段话。

    这里解释较容易混淆概念:子串是主串的一部分,一定存在。模式串不一定能在主串中找到。

    朴素模式匹配思路:在主串中依次取和模式串长度相同字符串进行匹配。如:主串长度为n,模式串长度为m,将主串中所有长度为m的子串依次进行对比,直到找到一个完全匹配的子串,或所有的子串都不匹配为止。最多对比nm+1个子串。

    代码实现:用两个标记指针i,j,分别指向主串和模式串的第一个元素,每匹配依次,两个指针后移,匹配到不相同元素时让i=ij+2,j=1,重新匹配。

    时间复杂度:最坏情况,每个子串都要对比m个字符,共nm+1个子串,复杂度=O((nm+1)m)=O(nmm2+m)=O(nm)(由于很多时候往往nm,所以nm数量级要远远大于m2,m.所以只保留nm)

    6. KMP算法匹配字符串

    KMP算法是朴素模式匹配算法优化。利用好模式串本身带有的信息可以跳过中间很多没有必要的对比,从而使算法效率得到提升。给出一组子串和一个模式串:

    1. 从主串1位置开始匹配,假定模式串前5个元素都匹配成功,但最后一个匹配失败。如下:

      主串0 模式串0

      对于模式串T='abaabc',当第6个元素匹配失败时,可令主串指针i不变,模式串指针j=3,直接跳过2,3循环对比

      主串1 模式串1

    2. 从主串5位置开始匹配,假定模式串的前5个元素都匹配成功,但最后一个匹配失败。如下:

      主串2

      模式串2

      同样对于模式串T='abaabc',当第6个元素匹配失败时,可令主串指针i不变,模式串指针j=3,直接跳过6,7循环对比。如下:

      主串3

      模式串3

    3. 从主串1位置开始匹配,假定模式串前4个元素都匹配成功,但第5个匹配失败。对于模式串T='abaabc',当第5个元素匹配失败时,也就代表主串中前四个元素信息可以确定是abaa。此时可以让主串指针i不变,模式串指针指向j=2。如下:

      主串4

      模式串4

    4. 从主串1位置开始匹配,假定模式串前3个元素都匹配成功,但第4个匹配失败。对于模式串T='abaabc',当第4个元素匹配失败时,也就代表主串中前三个元素信息可以确定是aba。此时可以让主串指针i不变,模式串指针指向j=2。如下:

      主串5

      模式串5

    5. 以此类推,主串1位置开始匹配,假定模式串前2个元素都匹配成功,但第3个匹配失败。对于模式串T='abaabc',当第3个元素匹配失败时,也就代表主串中前二个元素信息可以确定是ab。可以让主串指针i不变,模式串指针指向j=1。如下:

      主串6

      模式串6

    6. 同样的假定模式串第2个元素匹配失败,可令主串i不变,模式串j=1

    7. 当第1个元素匹配失败时,匹配下一个相邻子串,令j=0,i++,j++

    优化后的主串指针i不"回溯"。对于模式串T='abaabc',回溯有以下情况:

    回溯情况:

    回溯情况

    我们可以用一个next[]数组存放模式串回溯信息。上面匹配情况next数组如下:

    next数组:

    next数组

    当第一个元素匹配失败时,令j=next[1];当第二个元素匹配失败时,令j=next[2];当第三个元素匹配失败时,令j=next[3]....

    判断代码:

    模式匹配部分完整代码:

    KMP算法最欢时间复杂度O(m+n).其中求next数组时间复杂度为O(m),模式匹配时间复杂度为O(n)

    7. 求next数组

    求next数组过程很关键,要利用next数组进行匹配,主串指针不用回溯,从而减少时间复杂度。

    求字符串google的next数组过程:字符串google长度是6,那么next数组长度也应该是6。主串指针为i,模式串指针为j

    总结:next数组的作用:当模式串的第j个字符失配时,从模式串的第next[j]的继续往后匹配

    步骤总结:在不匹配的位置前边,划一根分界线,模式串一步一步往后退,直到分界线之前能匹配上,或模式串完全跨过分界线为止此时j指向哪儿,next数组值就是多少。代码如下:

    8. KMP算法进一步优化

    本质是对next数组的优化,将next数组优化为nextval数组。

    以模式串abaabc为例,其对应next数组如下:

    求next数组9

    next[3]时模式串指针j应该回溯到1的位置,此时会发现模式串回溯位置是字符a,而回溯之前匹配失败位置3处的值也是a,所以主串所指位置i处一定不是a,这样又要将模式串进行next[1]的回溯,相当于多一次不必要的回溯。

    next[3]=next[1],而主串S[i]next[3]

    求next数组10

    直接办法是让next[3]=next[1]=0,此时如果模式串第三个位置匹配失败,执行next[3]=0,回溯到0处,此时直接跳过主串S[3]处必定失败的匹配。

    同理,next[5]处值也可以优化为next[2]处的值,即next[5]=next[2]=1。优化后next数组如下:

    求next数组11

    其代码实现核心是失败位置next[i]所指向的元素是否等于主串S[i]处的元素。如果等于则优化为:nextval[i]=nextVal[next[i]]

    树是一种递归定义的结构。树的结构如下:

    非空树结构

    以上是非空树结构,还有空树结点为0的树。

    1. 树的基本概念

    非空树特点:

    1. 有且仅有一个根节点

    2. 没有后继的结点称为"叶子结点"(或终端结点)

    3. 有后继的结点称为"分支结点"(或非终端结点)

    4. 除了根节点,任何一个节点都有且仅有一个前驱

    5. 每个节点可以有0个或多个后继

    6. 除根节点外,其余节点可以分为若干个子树,这些子树特点是互不相交

      子树

    结点之间的关系描述:

    结点之间的关系描述

    结点、树的属性描述

    属性: 结点的层次(深度)——从上往下数

    树结点的层次

    结点的高度——从下往上数

    树结点的高度

    树的高度(深度)——树总共多少层

    结点的度——一个结点有几个分支(包括根结点)。非叶子结点的度>0,叶子结点度=0

    树的度——各结点的度的最大值。如上面树的度=3,是结点D

    有序树:逻辑上看,树种结点的各子树从左到右是有次序的,不能互换。如:

    结点之间的关系描述

    无序树:树中结点的各子树从左至右是无次序的,可以互换。如:

    无序树

    森林:森林是m(m0)棵互不相交的树的集合,可以允许有现象。

    森林

    如果以上森林加一个共同根节点A,森林就变成了树。这是一个重要考点。

    2. 树常考的性质

    常考点总结:

    常考点总结

    3. 二叉树

    二叉树是n(n0)个结点的有限集合

    ①或者是空二叉树,即n=0

    ②或者由一个根结点和两个互不相交的被称为根的左子树喝右子树组成,左子树和右子树又分别是一颗二叉树。

    二叉树

    特点:每个结点至多只有两棵子树,左右子树不能颠倒(二叉树是有序树);二叉树是递归定义的数据结构。

    二叉树五种状态:

    二叉树五种状态

    3.1 几个特殊二叉树

    满二叉树

    满二叉树:一棵高度为h,且含有2h1个结点的二叉树

    满二叉树

    特点:

    ①只有最后一层有叶子结点;

    ②不存在度为1的结点;

    ③按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点i的父结点为[i/2]

    完全二叉树

    完全二叉树:当且仅当其每个结点都与高度为h满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。

    特点:

    ①只有最后两层可能有叶子结点;

    ②且最多只有一个度为1的结点。

    ③按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点i的父结点为[i/2]

    i[n/2]为分支结点,i>[n/2]为叶子结点

    ⑤一个结点如果有孩子,一定是左孩子。

    满二叉树是完全二叉树,而完全二叉树不一定是满二叉树。

    完全二叉树

    二叉排序树

    二叉排序树是一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:

    ①左子树上所有结点的关键字均小于根结点的关键字;

    ②右子树上所有结点的关键字均大于根结点的关键字。

    ③左子树和右子树又各是一棵二叉排序树。

    二叉排序树

    例:如果找60这个元素,先从根结点开始,根结点19<60往右结点走,右结点50<60再往右走,此时右结点66>60故往左走找到60元素的结点。

    二叉排序树例子

    平衡二叉树

    平衡二叉树,树上任一结点的左子树和右子树的深度之差不超过1

    特点是:根节点子树越多,高度越低,搜索排序的效率越高。

    平衡二叉树

    同样平衡二叉树排序搜索效率很高。

    3.2 二叉树存顺序储结构

    完全二叉树顺序存储

    完全二叉树几个常考的基本操作:

    二叉树顺序存储3

    若完全二叉树中共有n个结点,则:

    要存储下面完全二叉树结构:

    二叉树顺序存储

    这里结构体中的isEmpy变量是该结点有没有元素,有的话为false,初始化时默认为true。其树元素在顺序表中存储如下:

    二叉树顺序存储2

    可以让第一个位置空缺,保证数组下标和结点编号一致。

    非完全二叉树顺序存储

    要存储下面二叉树结构:

    非完全二叉树

    如果不是完全二叉树,依然按层序将各节点顺序存储,无法从结点编号反映出结点间的逻辑关系。

    二叉树顺序存储2

    这种存储方法不能用完全二叉树操作解决,所以是错误的。

    解决方法是将二叉树结点编号与完全二叉树对应起来。将二叉树重新编号如下:

    非完全二叉树存储2

    其存储结构如下:

    非完全二叉树存储

    此时可以通过结点间的编号确定结点之间关系:

    但是判断结点方法不能像完全二叉树那样,只能通过创建的isEmpy字段判断。如:判断5号结点是否有左孩子,由于结点编号关系可知左孩子=2i=1010号结点的isEmpy=true所以5号结点没有左孩子。

    缺点:

    这样存储会导致顺序表有大量闲置空间,最坏情况是:高度为h且只有h个结点的单支树(所有结点只有右孩子),也至少需要2h1个存储单元.

    二叉树顺序存储最坏情况:

    二叉树顺序存储最坏情况2

    结论:二叉树的顺序存储结构,只适合存储完全二叉树

    3.3 二叉树的链式存储

    存储以下二叉树结构:

    二叉树链式存储

    实现结构图:

    二叉树链式存储结构图

    ^代表这个结点指针域为空。

    由于每个结点都有两个指针域,如果一个二叉树有n个结点,那么它总共就有2n个指针域。除了根结点,每个结点头上都至少有一个指针连接,即有n1个指针,故空指针个数为:2n(n1)=n+1

    完整定义代码:

    找到指定结点左/右孩子:只用通过指针就可以找到。

    找到p结点父结点:只能通过遍历找到谁的左右指针是指向p结点的,这样很耗时,所以我们定义三叉链表解决这个问题:

    3.4 二叉树遍历

    二叉树递归特性:二叉树要么是个空二叉树,要么就是由"根节点+左子树+右子树"组成的二叉树。

    三种遍历方法

    按照这种特性可以制定三种常考的遍历规则

    先序遍历:根左右(NLR) 中序遍历:左根右(LNR) 后序遍历:左右根(LRN)

    二叉树三种遍历方式

    上图二叉树三种遍历方式为:

    先序遍历:ABC 中序遍历:BAC 后序遍历:BCA

    较为复杂二叉树如下:

    二叉树三种遍历方式2

    遍历方式如如下:

    先序遍历:ABDECFG 中序遍历:DBEAFCG 后序遍历:BCA

    算法表达式分析数:对于表达式:a+b(cd)e/f的二叉树存储如下:

    算数表达式的分析树

    先序遍历:+abcd/ef

    中序遍历:a+bcde/f

    后序遍历:abcd+ef/

    由此可知:先序遍历==前缀表达式、中序遍历==中缀表达式(需要加界限符)、后续表达式==后缀表达式

    三种遍历实现方式如下:

    先序遍历(PreOrder)的操作过程如下:

    1. 若二叉树为空,则什么也不做

    2. 若二叉树非空: ①访问根结点

      ②先序遍历左子树

      ③先序遍历右子树

    从根节点出发,画一条路:如果左边还有没走的路,优先往左边走,走到路的尽头(空结点)就往回走。如果左边没路了,就往右边走。如果左、右都没路了,则往上面走。先序遍历是在第一次路过时访问结点。

    先序遍历图解

    中序遍历和后续遍历同上,差别在于访问结点顺序不一样。

    应用:求下面树的深度

    遍历求树的高度

    上面算法通过递归特性,通过退栈,每退一次右子树的右结点,则r+1。遇到NULL结点时,进行退栈。

    二叉树层序遍历

    层序遍历是每次都要判断一个结点p的左右结点是否为空,不为空的话,左右孩子入队,p结点出队。遍历如下的二叉树:

    二叉树层次遍历

    算法思想: ①初始化一个辅助队列 ②根结点入队 ③若队列非空,则队头结点出队,仿问该结点,并将其左、右孩子插入队尾(如果有的话) ④重复,知道队列为空

    代码实现:

    结构定义:

    结构定义第8行,我们并不需要保存整个树结点,只需要保存这个树结点的指针即可。

    3.5 通过遍历序列构造二叉树

    给定以下中序遍历序列:BDCAE

    其二叉树树结构可能为以下几种情况:

    不同二叉树的遍历序列

    同样可以得到一个结论:若只给出二叉树的前///层序遍历序列中的一种,不能唯一确定一棵二叉树。

    但如果给出中序+前序/后续/层序就能反推出树的结构。

    遍历序列特点:

    遍历序列反推二叉树结构

    1给出前序遍历序列:DAEFBCHGI;中序序列:EAFDHCBGI

    1. 先由前序遍历序列得出根结点D;在中序遍历序列D的左边为左子树,右边为右子树

      前序遍历+中序遍历

    2. 左子树元素为:EAF,由前序遍历序列得出父结点A,其左子树为E,右子树为F

      前序遍历+中序遍历2

    3. 右子树HCBGI,根结点前序遍历序列得出根结点B;其左子树为HC,右子树为GI

      前序遍历+中序遍历3

    4. 再根据前序遍历序列得出左子树HC根结点为C;右子树GI的根结点为G。故整棵二叉树结构如下:

      前序遍历+中序遍历4

    三种遍历序列特点:

    遍历序列反推二叉树结构2

    注意:必须要知道中序遍历序列才能推出二叉树结构。

    3.6 线索二叉树

    线索二叉树概念

    二叉树线索

    如上图二叉树结构,假设此时从G结点出发对这棵树进行中序遍历,显然是不能实现的,即从一个指定结点开始遍历树是不能完全实现的,因为以G结点为例,显然不能找到后继结点B3

    再看F结点,结点的前驱是A,找到指定结点的前驱方法是:

    这样缺点是:每次必须从根结点开始,找前驱、后继很不方便。

    此时就需要线索二叉树解决这个问题。

    一个有n个结点的二叉树,有n+1个空链域。这些空链域可用来记录前驱、后继的信息。

    上面的二叉树结构我们可以将其线索化,将结点空链域指向其前驱或后继(左指针指向前驱,右指针指向后继)。

    中序遍历线索二叉树:

    中序遍历线索二叉树

    指向前驱后继的指针称为"线索"。代码实现:

    线索二叉树结构定义:

    线索结构图

    tag==0,表示指针指向孩子

    tag==1,表示指针是"线索"

    中序线索二叉树的链式存储:

    中序线索二叉树的链式存储

    先序、后序原理同中序。

    三种序列二叉树线索化对比:

    三种序列二叉树线索化对比

    线索二叉树代码实现

    方法一:以中序遍历为例用pre指针记录前驱,q指针向下遍历,直到q==p时,pre指针指向结点就是p的前驱。

    方法二:同样以中序遍历为例,一边遍历一边线索化

    上面代码第40行最后还要检查prerchild是否为NULL,如果是,则令rtag=1

    要特别注意先序线索化,由于先序线索化要先访问根结点:

    先序遍历注意点

    当上面代码执行到D结点时,由于D左结点没有子结点,所以q->lchild=pre;,在执行InThread(T->lchild);时,q指针会回到B结点,导致出现死循环。解决方法是,在执行InThread(T->lchild);时先判断tag变量的值,看是否为真结点。修改代码如下:

    后序遍历线索化同中序遍历线索化。

    线索二叉树找前驱与后继

    总结:

     中序线索二叉树先序线索二叉树后序线索二叉树
    找前驱×
    找后继×

    线索二叉树考点在于:怎么手算二叉树线索化、找线索二叉树前驱和后继。

    4. 树的存储与遍历

    树是一种递归定义的数据结构。

    树是n(n0)个结点的有限集合,n=0时,称为空树,这是一种特殊情况。在任意一棵非空树中应满足:

    1. 有且仅有一个特定的称为根的结点。
    2. n>1时,其余结点可分为m(m>0)个互不相交的有限集合T1,T2....Tm,其中每个集合本身又是一棵树,并且称为根结点的子树。

    树结构:

    树结构

    4.1 树与森林的存储方法

    双亲表示法

    双亲表示法:每个结点中保存指向双亲的"指针"。

    树特点是树种任意一个结点都有且仅有一个双亲,所以在每个结点种保存指向"双亲"的指针。如要保存下面树结构:

    双亲表示法

    结构体如下:

    双亲表示法存储结构:

    双亲表示法存储结构

    如果新增加元素,无需按照逻辑上的次序存储。如:在k结点添加左孩子k和右孩子l,此时kl结点插入顺序无所谓。

    删除方案有两种:

    注意以上操作都要结点数n-1

    如果删除的是根结点,那么要让根结点所有子结点同时删除,此时双亲表示法缺点就暴露出来:查指定结点的孩子只能从头遍历,对比该结点的parent值是否是删除根结点的位置。

    如果被删除根结点子元素很多,那么使用第一种删除方案会导致空数据增多,增加遍历的时间复杂度。

    孩子表示法

    孩子表示法:顺序存储各个节点,每个结点中保存孩子链表头指针

    孩子表示法

    树结构结构体代码如下:

    树结构存储结构如下:

    孩子表示法存储结构

    孩子兄弟表示法

    孩子兄弟表示法:链式存储的方式存储一棵树。可以将树转化为二叉树的存储结构。

    存储以下树结构:

    孩子表示法

    树的结构存储如下:

    *firstchild指针看作二叉树中的左指针,*nextsibling看作二叉树中的右指针。

    存储过程如下:

    1. 先存储根结点

    2. 根结点左指针*firstchild指向第一个孩子B结点,接着*nextsibling指针指向B结点的兄弟结点。

      孩子兄弟表示法1

    3. B结点的第一个孩子是E结点,*firstchild指向EE结点兄弟是F*nextsibling指针指向F

      孩子兄弟表示法2

    4. E的第一个孩子是K*firstchild指向K

      孩子兄弟表示法3

    5. C的第一个孩子是G*firstchild指向G

      孩子兄弟表示法4

    6. D的第一个孩子是H*firstchild指向H;剩下的IJ都是H的兄弟结点,故H*nextsibling指针指向II*nextsibling指针指向J

      孩子兄弟表示法5

    以上操作我们将树转换为了二叉树。其优点是可以用熟悉的二叉树操作来处理树。

    再看一个树转换二叉树例子:

    树和二叉树的转化

    简单来说就是:树的左边都是其孩子结点,右边都是兄弟结点。

    森林转换二叉树

    森林。森林是m(m0)棵互不相交的树的集合

    上面的孩子兄弟表示法可以将树转换成二叉树。同样的,森林也可以转换为二叉树来存储和遍历。

    将以下森林转换为二叉树:

    森林转换为二叉树

    首先将这些森林的树转换为二叉树:

    森林转换为二叉树1

    由于这些树的根结点BCD是平级关系,所以相连称为兄弟关系。

    森林转换为二叉树2

    二叉树转换为森林

    二叉树转换为森林是对上一节的逆用。将以下二叉树转换为森林:

    二叉树转换为森林

    根据右边结点是兄弟结点,左边是对应兄弟结点的子结点特性:

    1. ACFL是兄弟结点,即森林树的根结点。
    2. 剩下结点依次为上面结点子结点即可

    二叉树转换为森林:

    二叉树转换为森林2

    4.2 树和森林的遍历

    先根遍历

    先根遍历:若树非空,先访问根结点,再依次对每棵子树进行先根遍历。

    伪代码实现如下:

    对下面树进行遍历:

    先根遍历

    先根遍历顺序:A(B(EK)F)(CG)(DHIJ)

    总结:树的先根遍历序列与这棵树相应二叉树的先序序列相同。

    树的后根遍历(深度优先遍历)

    后根遍历:若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。

    伪代码如下:

    对下面树进行遍历:

    先根遍历

    后根遍历顺序:((KE)FB)(GC)(HIJD)A

    总结:树的后根遍历序列与这棵树相应二叉树的中序序列相同。

    树的层次遍历(广度优先遍历)

    用队列实现:

    1. 若树非空,则根节点入队
    2. 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
    3. 重复
    4. 直到队列为空

    森林的先序遍历

    先序遍历森林:若森林为非空,则按如下规则进行遍历:

    1. 访问森林中第一棵树的根结点。
    2. 先序遍历第一棵树中根结点的子树森林。
    3. 先序遍历除去第一棵树之后剩余的树构成的森林。

    遍历以下森林结构:

    森林的遍历

    先序遍历顺序:(B(EKL)F)(CG)(D(HM)IJ)

    森林的先序遍历效果等同于依次对各个树进行先根遍历

    以上算法设计时,有两层的嵌套递归,所以推荐将森林转换为二叉树,其先序遍历效果等同于依次对二叉树进行先序遍历

    森林转换二叉树:

    森林转换二叉树遍历

    森林的中序遍历

    先序遍历森林:若森林为非空,则按如下规则进行遍历:

    1. 中序遍历第一棵树中根结点的子树森林。
    2. 访问森林中第一棵树的根结点。
    3. 中序遍历除去第一棵树之后剩余的树构成的森林。

    遍历以下森林结构:

    森林的遍历

    中序遍历顺序:((KLE)FB)(GC)((MH)IJD)

    森林的先序遍历效果等同于依次对各个树进行后根遍历

    同样的以上算法设计时,有两层的嵌套递归,所以推荐将森林转换为二叉树,其中序遍历效果等同于依次对二叉树进行中序遍历

    森林转换二叉树:

    森林转换二叉树遍历

    总结:

    森林二叉树
    先根遍历先序遍历先序遍历
    后根遍历中序遍历中序遍历

    5. 哈夫曼树

    结点的权:有某种现实含义的数值(如:表示结点的重要性等)

    结点的带权路径长度:从树的根到该结点的路径长度( 经过的边数)与该结点上权值的乘积

    树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL, Weighted Path Length)

    WPL=i=1nwili

    计算以下树叶子结点的带权路径长度:

    哈夫曼树

    WPL=12+32+42+52=26

    哈夫曼树WPL值

    WPL=31+33+24+15=25

    哈夫曼树WPL值2

    WPL=15+24+31+33=25

    有了带权路径长度这个概念可以得到哈夫曼树定义:在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树。

    5.1 哈夫曼树的构造

    给定n个权值分别为w1,w2...w3,的结点,构造哈夫曼树的算法描述如下:

    1. 将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F
    2. 构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
    3. F中删除刚才选出的两棵树,同时将新得到的树加入F中。
    4. 重复步骤23,直至F中只剩下一棵树为止。

    有以下结点集合,构造哈夫曼树:

    构造哈夫曼树

    步骤:

    1. 首先将集合中最小的两个结点作为孩子结点,其根结点为两个孩子结点权值之和。

      构造哈夫曼树2

      ac结点权值较小作为孩子结点,其根结点是权值为3的结点。

    2. 再将上面根结点与剩下结点集合中权值最小的结点e做合并,根结点为3e结点权值之和5

      构造哈夫曼树3

    3. 重复上述操作将5b结合,其根结点为88在与d结合,根结点为15

      构造哈夫曼树4

      这颗树的WPL为:

      WPLmin=17+23+32+41+42=31

      这颗带权路径长度最小的二叉树就是哈夫曼树。

    特点如下:

    5.2 哈夫曼树的应用

    哈夫曼树应用是:哈夫曼编码用于压缩。

    哈夫曼编码:即给定一个字符集,设计一个编码方案。

    :假设给定一个字符集(A,B,C,D),其中C出现80次,A出现10次,B出现8次,D出现2次。

    分析:规定哈夫曼树左路径是0,右路径是1。以上字母出现的次序就是哈夫曼树的权值,出现2次的D和出现8次的B结合成一个结点权重为10的结点。权重为10的结点和出现10次的A形成新的根结点20,根结点在和C形成权重为100的根结点。组成的哈夫曼树如下:

    构造哈夫曼树6

    此时C的编码为0A的编码为10D的编码为110B的编码为111。将编码0101010111110编译为对应的字母:CAAABD

    则此哈夫曼树的MPLmin=180+210+32+38=130

    哈夫曼编码特点

    1. 可变长度编码:允许对不同字符用不等长的二进制位表示。如上面的A编码不能设置为1,因为此时会和110,111冲突。
    2. 固定长度编码:每个字符用相等长度的二进制位表示
    3. 若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。非前缀编码在解码的时候有歧义,如将上面A编码改为1
    4. 有哈夫曼树得到哈夫曼编码:字符集中的每个字符作为一个叶子结点,各个字符出现的频度作为结点的权值,根据之前介绍的方法构造哈夫曼树。
    5. 同样,哈夫曼编码不唯一。
    6. 上面哈夫曼编码可以用于压缩存储。

    八. 并查集

    注:并查集( Disjoint Set)是逻辑结构:是集合的一种具体实现,只进行"并"和"查"两种基本操作。

    如果给定多个元素,将各个元素划分为若千个互不相交的子集,这些子集就叫做集合。

    可以用森林结构表示各个子集。以下子集:

    子集

    可以用森林表示结构:

    森林表示集合结构

    集合两个常见操作:①查:给定元素,找到该元素所属的集合(根结点);②并:将一棵树的根结点并到另一个树下。

    可以看到以上两个操作需要都需要由下往上找到根结点,所以我们需要用到树的双亲表示法来存储集合结构

    并查集存储结构:

    并查集存储结构

    其中s[]表示的是数据元素的父结点,即parent指针指向的结点。

    集合初始化代码如下:

    将上面元素根结点都设置为-1表示每个元素当前都是一个独立的子集。之后可以根据业务的具体需求对元素进行合并操作。

    1. 集合查操作

    集合关于查的操作主要有以下两个:

    查找代码如下:

    如我们要找到L元素所属集合:Ls[]的值为4,即所指元素为EEs[]的值为1,指向B;而Bs[]0指向AAs[]-1代表此为根元素,即L从属于A

    若结点数为n,算法时间复杂度为O(n)。如果一个树深度很高如下图:

    并查集查找最坏情况

    那么此时算法时间复杂度就增加,所以查找时间复杂度和树的高度h有有关。优化的思路是在每次合并操作树的时候,尽量不让树太高。

    2. 集合的合并操作

    合并两个集合操作:让一棵树成为另一棵树的子树即可。

    合并集合代码如下:

    上面的Root1Roor2代表要合并的两个树根结点的数组下标。

    如果给定的元素不是根结点,那要查找两个元素的根结点,再进行合并操作。

    3. 并查集的优化

    若结点数为n,算法时间复杂度为O(1)

    优化合并算法可以降低查询算法的时间复杂度,优化思路是:

    1. 用根节点的绝对值表示树的结点总数
    2. Union操作, 让小树合并到大树

    并查集存储结构:

    并查集存储结构

    可以优化为:

    并查集优化合并算法

    上面A所对应的s[]值为-6,代表树的结点为6。所以s[]的负值的绝对值代表了该树有多少结点。

    合并优化后代码:

    假设合并AC两棵树,其对应的Root1=0Root2=2;显然Root2>Root1即树A结点树大于树C。再执行S[Root2]=Root1;将小树合并到大树。合并后的树如下:

    并查集优化合并算法2

    该算法优化后可以让树的高度不超过[log2n]+1。Union操作优化后,Find操作最坏时间复杂度: O(log2n)

    4. 并查集优化2

    可以类似与缓存那样,将Find查找的结点所经过路径的所有结点直接成为根结点的子结点。此时,可以让之后查找的路径变短,这种让查找路径变短的方法称为压缩路径。

    如我们要找L结点所属集合,此时L结点查找必经过EB结点:

    并查集优化合并算法3

    优化后树的结构图如下:

    优化后树的结构图

    优化后树的存储结构如下:

    优化后树的存储结构图

    压缩路径:Find操作,先找到根节点,再将查找路径上所有结点都挂到根结点下。

    代码实现方式如下:

    每次Find操作,先找根,再"压缩路径",可使树的高度不超过0(α(n))α(n)是一个增长很缓慢的函数,对于常见的n值,通常α(n)4,因此优化后并查集的FindUnion操作时间可以用常数级时间复杂度O(1)表示,可以看出时间开销都很低。

     


    1 参考佩亚诺余项运算
    2 表示向上取整
    3 这里的后继结点不是指子结点,而是通过递归遍历时的访问顺序。