数据结构
- 第零章 引入
- 0.1 元素类型说明
- 0.2 数组定义
- 0.3 内存动态分配
- 0.4 参数传递
- 0.5 操作算法中用到的预定义常量和类型
- 0.6 结构类型的比较
- 第一章绪论
- 1.1 数据结构的研究内容
- ❀内容小结
- 1.2 基本概念和术语
- 1.2.1 数据、数据元素、数据项、数据对象
- 1.2.2 数据结构
- 1.2.2.1 逻辑结构
- 1.2.2.2 存储结构
- 1.2.3 数据类型和抽象数据类型
- (1)数据类型
- (2)抽象数据类型
- ❀内容小结
- 1.3 抽象数据类型的表示与实现
- 1.4 算法和算法分析
- 1.4.1 算法时间效率的度量
- 1.4.2 时间复杂度
- 1.4.3 空间复杂度
- ❀内容小结
- 第二章 线性表
- 2.1 线性表的定义和特点
- 2.2 案例引入+分析与实现
- 2.2.1 一元多项式的运算
- 2.2.2 稀疏多项式
- 2.2.3 图书信息管理系统
- ❀内容小结
- 2.3 线性的类型定义
- 2.4 线性表的顺序表示和实现
- 2.4.1 线性表的顺序存储表示
- (1)顺序表的定义
- (2)顺序表的实现
- I.静态分配
- II.动态分配
- ❀内容小结
- 2.4.2 顺序表基本操作的实现
- (1)线性表初始化
- (2)销毁线性表
- (3)清空线性表
- (4)求线性表长度
- (5)判断是否为空
- (6)取值
- (7)查找
- 按位查找
- 按值查找
- (8)插入
- (9)删除
- ❀ 内容小结
- 2.5 线性表的链式表示和实现
- ❀内容小结
- 2.5.1 单链表的定义和表示
- (1)不带头结点的单链表
- (2)带头结点的单链表
- (3)不带头结点VS带头结点
- ❀内容小结
- 2.5.2 单链表基本操作的实现
- (1)单链表初始化-带头结点
- (2)判断链表是否空
- (3)销毁
- (4)清空
- (5)求表长
- (6)取值
- (7)查找
- 按位查找
- 按值查找
- (8)插入
- 按位序插入-带头结点
- 按位序插入-不带头结点
- 指定结点的后插操作
- 指定结点的前插操作
- (9)删除
- 按位序删除-带头结点
- 指定结点的删除
- (10)建立单链表
- 头插法
- 尾插法
- ❀内容小结
- 2.5.3 循环链表
- (1)循环单链表
- (2)循环双链表
- 初始化
- 插入
- 删除
- ❀内容小结
- 2.5.4 双向链表
- (1)结构定义
- (2)初始化
- (3)插入
- (4)删除
- (5)遍历
- ❀内容小结
- 2.5.5 静态链表
- (1)定义
- (2)基本操作
- ❀内容小结
- 2.5.6 单链表、循环链表、双向链表的时间效率比较
- ❀内容小结
- 2.6 顺序表和链表的比较
- ❀内容小结
- 2.7 线性表的应用
- 第三章 栈和队列
- 3.1 栈和队列的定义和特点
- 3.1.1 栈的定义和特点
- 3.1.2 队列的定义和特点
- 3.2 案例引入+分析与实现
- 3.3.1 栈-进制转换
- 3.3.2 栈-括号匹配的检验
- 3.3.3 表达式求值
- 3.3.4 队列-舞伴问题
- 3.3 栈的表示和操作的实现
- 3.3.1 栈的抽象数据类型的类型定义
- 3.3.2 顺序栈的表示和实现
- 3.3.3 链栈的表示和实现
- 3.4 栈与递归
- 3.5 队列的表示和操作的实现
- 3.5.1 队列的抽象数据类型定义
- 3.5.2 队列的顺序表示和实现
- 3.5.3 队列的链式表示和实现
- 第四章 串、数组和广义表
- 4.1 串
- 4.1.1 串的定义
- 4.1.2 案例引入、分析与实现
- (1)病毒感染检测
- 4.1.3 串的类型定义、存储结构及其运算
- (1) BF算法
- (2) KMP算法
- 4.2 数组
- 4.2.1 数组的抽象数据类型定义
- 4.2.2 数组的顺序存储
- 4.2.3 特殊矩阵的压缩存储
- 4.3 广义表
- 第五章 树和二叉树
- 5.1 树和二叉树的定义
- 5.1.1 树的定义
- 5.1.2 树的基本术语
- 5.1.3 二叉树的定义
- (1)满二叉树
- (2)完全二叉树
- 5.2 案例引入、分析与实现
- 5.2.1 数据压缩问题
- 5.2.2 利用二叉树求解表达式的值
- 5.3 树和二叉树的抽象数据类型定义
- 5.4 二叉树的性质和存储结构
- 5.4.1 性质
- 5.4.2 存储结构
- (1)顺序存储
- (2)二叉链表
- (3)三叉链表
- 5.5 遍历二叉树和线索二叉树
- 5.5.1 遍历二叉树
- (1)先序遍历
- (2)中序遍历
- (3)后序遍历
- (4)遍历算法的分析
- (5)层次遍历
- 5.5.2 二叉树遍历算法的应用
- (1)二叉树的建立
- (2)复制二叉树
- (3)计算二叉树的深度
- (4)计算结点总数
- (5)计算叶子结点数
- 5.5.3 线索二叉树
- 5.6 树和森林
- 5.6.1 树的存储结构
- (1)双亲表示法
- (2)孩子链表
- (3)双亲与孩子结合
- (4)孩子兄弟表示法
- 5.6.2 树与二叉树的转换
- 5.6.3 森林与二叉树的转换
- 5.6.4 树森林的遍历
- (1)树的遍历
- (2)森林的遍历
- 5.7 哈夫曼树及其应用
- 5.7.1 哈夫曼树的基本概念
- 5.7.2 构造哈夫曼树
- 5.7.3 哈夫曼树构造算法的实现
- (1)顺序存储——一维结构数组
- 5.7.4 哈夫曼码
- (1)设计哈夫曼编码
- (2)哈夫曼编码的算法实现
- (3)文件的编码和解码
- 第六章 图
- 6.1 图的定义和基本术语
- 6.2 案例引入、分析与实现
- 6.2.1 六度空间理论
- 6.3 图的类型定义
- 6.4 图的存储结构
- 6.4.1 数组表示法(邻接矩阵)
- (1)邻接矩阵的表示
- (2)邻接矩阵的建立
- (3)邻接矩阵的优缺点
- 6.4.2 链式存储结构
- (1)邻接表
- (2)邻接多重表
- (3)十字链表
- 6.5 图的遍历
- 6.5.1 深度优先遍历
- 6.5.2 广度优先遍历
- 6.6 图的应用
- 6.6.1 最小生成树
- (1)构造最小生成树MST
- Prim算法
- Kruskal算法
- 6.6.2 最短路径
- (1)Dijkstra算法.....
- (2)Floyd算法
- 6.6.3 拓扑排序
- 6.6.4 关键路径
- 求关键路径步骤
- 第七章 查找
- 7.1 查找的基本概念
- 7.2 线性表的查找
- 7.2.1 顺序查找(线性查找)
- 7.2.2 折半查找(二分或对分查找)
- 7.2.3 分块查找(索引顺序查找)
- 7.2.4 线性表查找方法比较
- 7.3 树表的查找
- 7.3.1 二叉排序树
- (1)二叉排序树存储结构
- (2)查找
- (3)插入
- (4)生成
- (5)删除
- 7.3.2 平衡二叉树
- 7.4 哈希表的查找
- 7.4.1 散列表的基本概念
- 7.4.2 散列函数的构造方法
- (1)直接定址法
- (2)除留余数法
- 7.4.3 处理冲突的方法
- (1)开放定址法
- (2)链定址法
- 7.4.4 散列表的查找
- 第八章 排序
- 8.1 基本概念和排序方法概述
- 8.2 插入排序
- 8.2.1 直接插入排序
- 8.2.2 折半插入排序
- 8.2.3 希尔排序
- 8.3 交换排序
- 8.3.1 冒泡排序
- 8.3.2 快速排序
- 8.4 选择排序
- 8.4.1 简单选择排序
- 8.4.2 堆排序
- 8.5 归并排序
- 8.6 基数排序
- 8.7 外部排序
- 8.8 各种排序的综合比较
- 8.8.1 时间性能
- 8.8.2 空间性能
- 8.8.3 排序的稳定性能
- 8.8.4 关于“排序方法的时间复杂度的下限”
本篇是关于王卓老师+王道咸鱼的数据结构学习笔记。
递归思想:依次往下调用(这个调用是调用的自身函数),调用到最后一层时得到的是最里层(即最下层)的数据,然后一层一层往上代(相当于嵌套函数),即可得到最终结果。
第零章 引入
0.1 元素类型说明
【对应2.4.1——线性表的顺序存储表示】
ElemType是元素类型。若顺序表中存放的是ABCD,则可写为char;存放的是多项式系数,则写为float。
也可以单独提出来先定义。如typedef char ElemType;
0.2 数组定义
- 数组静态分配:数组的名字中存放的是数组中的首元素data[0]的地址,即此数组的基地址。
- 数组动态分配:*data形式上是指针变量,但是它也可以表示一个数组,可以用它来存放数组中第一个元素的地址。
0.3 内存动态分配
- malloc()的参数要求是整数,即这个分配的这块空间的字节数。
- MaxSize是根据需要自定义的。
- ElemType决定了动态申请的空间怎样去划分。若是char类型,则是1字节1个空间;若是int类型,则是4字节1空间。。* 代表分配好的这个结果是一个指针。。外面的()表示强制类型转换。
- (ElemType*)表示指向数据元素的指针。
- L.data即获得了数组中这一大片空间的基地址。
- free()中的参数要求是一个指针变量。
0.4 参数传递
swap函数中最后m、n释放了。因此最终结果a、b并没有得到改变。
*m改变的是其中的内容。因此a、b值互换。
交换的是m、n的地址,对a、b没有影响。
- 注意sub()中b[]方括号中不可以指明大小
- 最终结果相当于重新给a赋值。最终输出world
也可以理解为j是一个地址。i和j共同使用同一个空间(即地址相同)。
对m、n操作实际上就是对a、b操作。因此最后a、b值成功互换。
0.5 操作算法中用到的预定义常量和类型
0.6 结构类型的比较
第一章绪论
1.1 数据结构的研究内容
数据结构就是操作对象及操作对象之间的关系。
早期,计算机主要用于数值计算。
现在,计算机越来越多地用于非数值计算。
❀内容小结
1.2 基本概念和术语
1.2.1 数据、数据元素、数据项、数据对象
- 数据是计算机程序加工的原料。
- 数值型数据通常是指可以加减乘除、平方开方等的算数运算的。
- 对于非数值型问题,通常会关心 每个个体的具体信息 以及 每个个体之间的关系。
1.2.2 数据结构
- 同一个数据对象里的数据元素,可以组成不同的数据结构。
- 不同的数据元素,可以组成相同的数据结构。
1.2.2.1 逻辑结构
1.2.2.2 存储结构
例中字符串数组是有先后顺序的。(此顺序用内存中的存储位置来表示——在前面的元素存储时存在前面,即按照元素的位置存储)
指针即地址。
通讯录便是常见的索引存储结构。
可与《查找》一章联系学习。
1.2.3 数据类型和抽象数据类型
(1)数据类型
(2)抽象数据类型
定义一个ADT,就是在“定义”一种数据结构。
【确定了ADT的存储结构,才能“实现”这种数据结构】
基本操作名(参数表):
- 如求圆的面积,其中半径为r。则记为 area® 。。【这种便是赋值参数】
- 如求乘方,x的y次方。则记为power(x,y)
- 如对一个图像G进行放大n倍的操作。则记为scale(&G,n)。。【注意:加上&则表示最后返回G作为结果,这种便是引用型参数。】
复数构造即加减的具体实现:
#include<stdio.h>
typedef struct {float realpart; //定义实部float imagpart; //定义虚部
}Complex;//构造复数
Complex assign(float real,float imag) {Complex c;c.realpart = real;c.imagpart = imag;return c;
}//两复数求和
Complex add(Complex A,Complex B){Complex c;c.realpart = A.realpart + B.realpart;c.imagpart = A.imagpart + B.imagpart;return c;
}
//两复数求差
Complex minus(Complex A, Complex B) {Complex c;c.realpart = A.realpart - B.realpart;c.imagpart = A.imagpart - B.imagpart;return c;
}
int main()
{ Complex b,c,d,e;b = assign(2.0, 4.0);c = assign(1.0, 1.0);d = add(b, c);e = minus(b, c);printf("实部为2虚部为4的复数是:b=%f+%f i\n", b.realpart, b.imagpart);printf("实部为1虚部为1的复数是:c=%f+%f i\n", c.realpart, c.imagpart); printf("两复数相加是:d=%f+%f i\n", d.realpart, d.imagpart);printf("两复数相减是:d=%f+%f i\n", e.realpart, e.imagpart);return 0;
}
❀内容小结
1.3 抽象数据类型的表示与实现
main()函数部分:
typedef用于声明一个已有的数据类型的新名字。
1.4 算法和算法分析
算法是求解问题的步骤。
- 输入取自于某个特定的对象的集合。
- 输出是与输入有着某种特定关系的量。
矛盾的意思是有时候为了节省时间可能会消耗空间,有时为了节省空间可能会花费更多的时间。【因此需要综合平衡】
1.4.1 算法时间效率的度量
【 更多的是选择事前分析法】。
- 事后统计法存在的问题:
若假设每条语句所需的时间均为单位时间,则可以暂时不考虑。
- 行一 是从1到n,循环变量执行n次。但是还需要最后执行一次来判断,即条件不成立则退出循环。因此执行n+1次。
- 行二 是外层循环的循坏体,循环体要执行n次;每执行1次这句语句,都要执行n+1次。因此执行n(n+1)次。
- 行三 是中层循环的循环体,外层循环每执行一次,中层循环执行n次,而外层循环执行了n次。因此执行了n*n次。
1.4.2 时间复杂度
由例可知:很多算法执行时间与输入的数据有关。
一般只考虑最坏和平均。
口诀:【常对幂指阶】
例题:
也可直接写成O(lgn)。即不用去考虑底数具体是多少。
1.4.3 空间复杂度
❀内容小结
第二章 线性表
2.1 线性表的定义和特点
单位历年中,第一年6,第二年17…以此构成前后结点的线性关系。
2.2 案例引入+分析与实现
2.2.1 一元多项式的运算
把每一项的系数拿出来存成一个线性表。
系数的下标 i 还可隐含地表示指数。
2.2.2 稀疏多项式
如相加:
线性表A中有4项,B中有3项。
- 假设每项指数均不同,则最多可得7项。
- 极端情况下每一项刚好指数一样,系数是相反数,则最少可得0项。
2.2.3 图书信息管理系统
存储方式:
❀内容小结
2.3 线性的类型定义
这里指的类型实际上就是抽象数据类型。
数据对象就是一些元素,元素的个数n>=0;
<ai-1,ai>序偶关系,ai-1是ai的前驱。
2.4 线性表的顺序表示和实现
2.4.1 线性表的顺序存储表示
(1)顺序表的定义
元素之间的关系由存储单元的邻接关系来体现。
找地址是O(1)
数组长度定义模板
(2)顺序表的实现
I.静态分配
【顺序表的实现示例代码】
ps:为给数据元素设置默认值
【初始化时,设置初始值是必须做的】
初始就分配很大的内存空间是不可行的。
II.动态分配
ps:最终如下
例:
❀内容小结
左边框是静态,右边框是动态。
2.4.2 顺序表基本操作的实现
画线的较难。
ps:加上&
(1)线性表初始化
- 行1:&L即通过引用型变量将构造好了的顺序表L返回。
- 若成功构造则是为变量L分配空间。
- 行2:动态分配,获得这个数组中的基地址。
- 行4:若成功分配,注意此时里面没有元素,因此长度为0。
- 行3:异常处理。OVERFLOW是溢出的意思,返回的是-2。
(2)销毁线性表
注意:销毁的前提条件是该线性表存在。
(3)清空线性表
length代表线性表元素的个数,将其令为0。则表示线性表内无元素,为空。
(4)求线性表长度
(5)判断是否为空
返回1表示为空,返回0表不为空。
(6)取值
算法的时间复杂度为O(1)。
(7)查找
按位查找
【1.静态分配】
【2.动态分配】
按值查找
这是进行顺序查找的算法。
也可以写成如下:
详见0.6 结构类型的比较
(8)插入
注意:
- 如果有n个元素,那么插入的位置只能是0~n+1位置上。
- 如果线性表容量已经放满,还要插入数据的话,会存在溢出情况。
【综上,需要首先判断插入位置i是否合法以及存储空间是否已满】
插入中的移动方式:先后面的元素往后移,再前面的元素往后移
以下是王卓老师的算法
算法时间主要耗费在移动元素的操作上。
(9)删除
第2步是可以省略的。即直接删除。
删除中的移动方式:先前面的元素往前移,再后面的元素往前移
算法时间主要耗费在移动元素的操作上。
❀ 内容小结
2.5 线性表的链式表示和实现
❀内容小结
2.5.1 单链表的定义和表示
- 包含两部分数据,可用高级语言中的结构体。这个结构中包含两个成员,即data、next。
- data的数据类型,是由处理问题的本身决定的。如data是多项式中的系数,则是float。因此把需要的元素定义成【ElemType】。
- next是指针型。它的类型取决于它所存放地址的元素是什么类型。如data是int整型,则next应定义为:int *p;(意思是指向整型变量的指针)。p=&a;(意思是p里存放的是a的指针)。
- next指向的是和自己的结构完全一样的变量的地址。【因此出现下一点中嵌套的定义】
- struct Lnode是指向这种类型的指针。(这种类型又包括data、next两个成员的一个指针)。。即用自己来定义自己【嵌套的定义】
- 注意:最后一行中Lnode,*LinkList都是类型。【若要定义指向结点的指针,可以是 Lnode *L; 也可以是LinkList L;】
【补充,可忽略】
(1)不带头结点的单链表
(2)带头结点的单链表
(3)不带头结点VS带头结点
❀内容小结
2.5.2 单链表基本操作的实现
(1)单链表初始化-带头结点
(2)判断链表是否空
(3)销毁
【销毁单链表是将所有结点,包括头结点和头指针都销毁。内存中不存在该单链表】
- 用指针变量p操作当前想要操作的结点。
- 让p一开始指向头结点,然后把指向的结点释放。【一个变量要想指向某一个空间,就把这个空间的地址赋给他】。如若想指向变量a,就是p=&a ; 。【头结点的地址在头指针里存放】。因此让指针p指向头结点,则是 p=L;
- 得先让L指向下一个结点,才能删除头结点。【头结点的指针域存放了下一节点的地址】,即L=L->next; 。
- delete p; 是c++中语法。在c语言中是free§;
- L!=NULL也可表示成L。
(4)清空
【清空链表只是将单链表中元素清除,成为一个空链表】
- 第一步p=L->next; 即p指向首元结点。
- 得先把下一个结点确定好,才能释放掉前结点。即引入指针变量q预先保存下一个结点的地址。
- 最后一步p、q指针都指向空后,要将头指针设置为空。
(5)求表长
让指针变量p指向首元结点,若不为空,计数+1且指向下一个结点。到结点为空时结束。
- 定义指针变量p可以用 Lnode *p; 这样可读性更好。
- i是元素个数计数。初始为0。
- while§表示p指向的结点不为空。
(6)取值
- 如果输入的值超过了元素个数,当指针所指为空,则不需往后找了。
- 如果i=-1,即小于初始下标的值。一开始便不用找了,因为是错的。
- 初始化一行:首先p指向首元结点,j=1表示这是第一个结点。
- while(p&&j<i)中j<i即 若还没到第i个结点,则一直找。
(7)查找
按位查找
按值查找
- 循环条件:p不为空且当前指针所指的结点值同要找的数据不相同时。
- 返回:找到了就是那个结点,找不到则返回空。
(8)插入
按位序插入-带头结点
按位序插入-不带头结点
指定结点的后插操作
指定结点的前插操作
ps:或如下写法:
【以下是王卓老师部分,可忽略】
- 不可以。先执行2的话p->next指向的地址就变了。
- 如果非要先执行2,则找个暂存指针q存放ai的地址。
(9)删除
按位序删除-带头结点
指定结点的删除
- 插入算法中i的合理取值范围是1到n+1,删除算法中i的合理取值是1到n。
- p指向的是ai-1结点,q指向的是ai结点。
(10)建立单链表
头插法
也叫前插法。
尾插法
【因此设置一个表尾指针r】
也叫后插法。
❀内容小结
- 查找:最好的情况是第一个元素就找到,即1次;最坏的情况是最后一个元素才找到,即n次。
2.5.3 循环链表
使用循环链表时,最多的是使用带尾指针的循环链表。
(1)循环单链表
(2)循环双链表
初始化
插入
删除
❀内容小结
2.5.4 双向链表
(1)结构定义
(2)初始化
(3)插入
GetElemP_DuL(L,i)是查找链表L中的第i个元素的操作。
(4)删除
(5)遍历
❀内容小结
2.5.5 静态链表
(1)定义
王道书
(2)基本操作
❀内容小结
2.5.6 单链表、循环链表、双向链表的时间效率比较
双向循环链表是消耗了空间来提升时间效率。
❀内容小结
2.6 顺序表和链表的比较
- 都属于线性表,都是线性结构。
❀内容小结
2.7 线性表的应用
- 基地址+(表长-1)=最后一个元素地址。即pa_last=LA.elem+LA.length-1;
空间复杂度是O(1)
第三章 栈和队列
3.1 栈和队列的定义和特点
- 栈如枪装子弹的弹夹; 队列如日常生活的排队。
- 栈只能在队尾插入,在队尾删除。
- 队列只能在队尾插入,在队头删除。
3.1.1 栈的定义和特点
3.1.2 队列的定义和特点
3.2 案例引入+分析与实现
3.3.1 栈-进制转换
3.3.2 栈-括号匹配的检验
3.3.3 表达式求值
3.3.4 队列-舞伴问题
3.3 栈的表示和操作的实现
3.3.1 栈的抽象数据类型的类型定义
3.3.2 顺序栈的表示和实现
- top和base可以定义成整型,用它们存放数组的下标。【这种方式就是用下标来引用数组中的元素】
- top和base还可以定义成指针。两个指针相减即两个指针之间的元素个数。 【注意这两个指针必须指向同一数组才可以相减】
if(S.base)表示当前栈不为空。
3.3.3 链栈的表示和实现
3.4 栈与递归
函数的调用就是栈。
递归的时间效率较差。
3.5 队列的表示和操作的实现
3.5.1 队列的抽象数据类型定义
3.5.2 队列的顺序表示和实现
- 定义*base用来指向数组中的首元素。
- front、rear称作指针,但不是指针变量。是用来表示数组中元素的下标位置的。如front指向队头元素的下标。
- Q.base[Q.rear]=x; 将元素放在尾指针所指的位置
- 当Q.rear+1达到MAXQSIZE时,则对MAXQSIZE模后可以得到0。(即回到了队尾)
- x=Q.base[s.front]; 将表头指针所指的元素赋给x,即要删除的值。
分配的是一个数组空间,数组的首元素地址实际上就是一个指针。因此定义的是Q.base。【即base指向数组空间的首地址】
循环队列的差值可能是负数,加上MAXQSIZE,求余便可得到循环队列的个数。
采用的是少用一个元素空间的方法。
3.5.3 队列的链式表示和实现
*QuenePtr 指向结点的指针类型。
在内存中分配新结点,实际上很少会内存连一个结点的位置都没有,因此一般很少判断是否溢出。
绿色字是直接使用Q.rear。不额外利用新的空间。
第四章 串、数组和广义表
4.1 串
4.1.1 串的定义
- 空串是里面不包含任何字符,连空格都没有。即""
- 空格串里面有若干个空格。即" "
- 主要d中含有一个空格,因此长度是8。
- d的字串不包含c是因为字串的字符应该是连续的。d中的空格断开了BEIJING。
4.1.2 案例引入、分析与实现
(1)病毒感染检测
可以用BF或KMP算法实现。
复制一次,长度变成2m,每次截取m个字符,看是否匹配。匹配了就说明感染了。
匹配的字数,看m等于几。如果是3个字符,则比较3遍就够了:
4.1.3 串的类型定义、存储结构及其运算
实际中,顺序串用的更多。因为对字符串进行插入和删除操作是比较少的;多数是匹配和查找运算(用顺序更方便)。
- 顺序:逻辑关系直接映射物理结构。
- 后面在研究串的算法时,通常从1号位置往后存储,0号位置闲置不用。
- 假设一个块放50个。
- 常用下面那种结构,称为块链。
(1) BF算法
BF即暴力破解法。简单但时间效率相对较差。
子串即查找的内容。
第0位不存储。第一位发现一样,则继续匹配下一位。
发现不匹配后,再往后移动一位。用新的4位来比较。此时需要将i回退到第二个字符的位置,j从头开始。
i=i-j+2:
- +T从1移动到 j,移动了j-1个长度;此时S移动的也是j-1。S目前位置是i,用目前的位置i减去移动的位置j-1得到移动前初始位置1(S也是从1开始移动的)。因此【i-j+1】表示移动前的初始位置。
- 【(i-j+1)+1】就是初始位置的下一个位置(满足了往后移一位再比较)。
回溯就是造成这个算法效率比较低的主要原因。
如下:注意j是回到1
返回值是 i的最终位置 减去 查找字符串的串长。
pos表示位置。
- 这个算法是从第一个位置开始查找。
- 红框中>=还是>的问题见下面一点。
定义了pos,则开始比较的值更为灵活。
分析时间复杂度:(模式T含有m个字符)
- 最好的情况:第一次就找到了,则m个字符循环比较了m次。时间复杂度为O(m)。
- 最差的情况:
- 平均:是O((n/2)*m),也就是O(mn)
- 书上是>,王卓老师讲义是>=。由以下代码实现,表面出现结果一致,均不影响最终答案。
- 原因为:最终j=T.length时,满足了 if的条件。但是while循环还是没有结束的,还是满足while循环条件的。因此还要执行一下while循环。执行一次while循环后,i+1,j+1。然后就j一定大于T.length,此时条件不满足while循环条件,就终止了。因此【=是满足的】
#include <stdio.h>
#include <stdlib.h>
#include<string.h>
#define MAXSIZE 20typedef struct SString
{char ch[MAXSIZE+1];int length;
}SString;
int GetLength(char *L)//得到字符数组的长度
{int n = 0;char *p = L;while(*p!='\0'){n++;p++;}return n;
}
void initSString(SString * L)//初始化串
{char a[MAXSIZE];//定义一个辅助数组scanf("%s",a);char *p= L->ch;//定义一个字符指针,指向串里面的数组strcpy(++p,a);//在数组的下标为1的位置开始赋值,注意为了方便,我们不采用0开始的下标L->length = GetLength(a);//顺便给长度赋值
}
int index(SString S,SString T,int pos)//返回模式L2在主串L1中第pos个字符开始第一次出现的位置。如果不存在,则返回值为0
{int i = pos;//初始化int j = 1;while(i<=S.length&&j<=T.length)//俩串都未比较到串尾{if(S.ch[i]==T.ch[j]){i++;j++;}else//指针回退,重新开始比较{i = i-j+2;j=1;}}if(j>=T.length){return i-T.length;}else{return 0;}
}
int main()
{SString S;SString T;printf("请输入主串\n");initSString(&S);printf("请输入模式串\n");initSString(&T);
// printf("请输入要从主串的第几个元素进行匹配\n");int a=1;
// int a;
// scanf("%d",&a);if(index(S,T,a)){printf("匹配成功,在主串的第%d个位置匹配成功\n",index(S,T,a));}else{printf("匹配失败\n");}return 0;
}
(2) KMP算法
❀天勤率辉-KMP算法(易懂版)与王卓老师结合
建议看 天勤率辉-KMP算法(易懂版) 入门,真的很容易懂!
- KMP算法:快速地从一个主串中找出一个想要的子串。(可以做到仅仅后移模式串,比较指针不回溯)
- 这里指针位置所指元素不匹配。(指针左边元素完全匹配)
- 可看到指针前面已经匹配的串中 左右两端都有AB子串,他们完全匹配。则称为模式串的公共前后缀。
- 这一步是KMP算法核心:直接移动模式串,使得【公共前后缀】的前缀直接移动到原先后缀所在的位置。
- 【最长公共前后缀】:如果模式串中有多对公共前后缀,要取最长的那对。
- 其实找公共前后缀时只用看模式串即可。
- 使公共前后缀的前缀移动到原先后缀所在的位置,这样做保证了当前比较指针所在的位置 左边的串上下是一致的
- 当前移动位置,模式串和主串匹配了,指针继续往后扫描。又发现如下不匹配。
- 再次寻找公共前后缀。
- 移动模式串,将公共前后缀的前缀移到后缀位置上
- 此时发现模式串已经超出主串的范围,就可以判定失败。即主串不含有与模式串相匹配的子串。
上述是未查找到的情况,接下来我们试验正确查找到的情况
发现最后是匹配的。因此匹配成功
❗注意字符串存放在一个字符数组中,数组在内存中不可能移动。上述所说移动只是一个形象化的方式。
我们以如下模式串研究:
- 为了方便处理,我们将该模式串放在数组中,并标出数组下标。【注意模式串是从下标为1的位置开始存储的】→(从0开始也可以,原理一样,只是大多数学校是从1)
- 假设该模式串可能和任意主串进行KMP算法。因此每一个位置上都可能发生不匹配。
- 假设第一个位置就发生了不匹配。
- 则把模式串移一位。重新开始比较。
- 在代码中的描述应该为:让模式串中1号位置的字符与主串下一个位置的字符开始比较。
- 如果二号位发生不匹配。
- 此时指针前的子串长度只有1,公共前后缀长度为0,于是移动后落在模式串中 指针左边的子串长度也为0。
- 在代码中的描述是:比较指针所指的位置就是主串中发生不匹配的位置,称为当前位置。此种情况下,拿 模式串的1号位 与 主串中当前位置 进行比较。
- 若3号位 发生不匹配。
- 继续找公共前后缀,此时指针左边的子串为AB,公共前后缀长度为0。与2号位情况相同。
- 若4号位发生不匹配。
- 寻找到公共前后缀
- 后面的类似前面的。
- 可发现一个规律【若公共前后缀长度为n,则在n+1号为与主串当前位开始比较】
- 可以看到除了第一句话外,其余所有的描述都类似。因此将第一句话标记为0。(当看到0,就按照第一句话描述的方式去处理)
- 则代码中可表示f(0)则按第一句话做。后面的每句话把开头的 号位的数字拿出来作为 这句话所要执行的操作的代号。
- 再结合上面的数组下标,将其放在一个数组中。
- 由此可以判断出模式串中任一位置发生不匹配时的操作。如模式串1位置发生不匹配,数组1位置值为0,则找到之前分析得到的操作(1号位与主串下一位比较)
- 因此 它叫做下一步数组,即next数组。
next[j]存储j的下一个位置。
- next[j]和主串没关系,只和模式串有关系。
- 【从头开始的k-1个元素指的就是前缀】。如一个字符串abcd:a是前缀;ab是前缀;abc是前缀;但是abcd不是前缀。
- 【j前面的k-1个元素指的是后缀】。如一个字符串abcd:d是后缀;cd是后缀;bcd是后缀;但是abcd不是后缀。
- 如果字符串的前缀和j这个位置前面的若干个元素相等的话,则获得k的值。【k的值可能不止一个,因此需要找到最大值作为下一个j的位置】
- 如果上述没有找到k,此时j也不为1,则是其他情况。
next[j]代码:
4.2 数组
可以看成非线性原因:一个元素不止有1个前驱和后继。如a11前驱有a01和a10。
因此把二维数组看成特殊的线性结构,是线性结构的扩展。
typedef array1 arry2[m]
- arry2[m]中有m个元素,这m个元素是array1类型;
- 而array1类型是有n个元素的一维数组;
- 因此这句话是一共有m个 含有n个元素的一维数组。因此也定义出一个二维数组。
4.2.1 数组的抽象数据类型定义
画波浪线的地方表示的是前驱后继的序偶关系。
4.2.2 数组的顺序存储
C、Java都是行优先。
最后一个的下标是mn-1(从0开始存储)
- 先分析元素前有几行,如a[ 2 ] [ 1 ]前面有2行。。则a[ i ][ j ]前面有 i 行。 这i行每行都有n个元素,因此总共是in个元素。
再分析元素前有几列,如a[ 2 ] [ 1 ]前面有1列。。则a[ i ][ j ]前面有 j 行。 前面则有 j 个元素。- 因此a[ i ][ j ] 前面总共有 in+j 个元素。
- 这些元素总共占用的空间便是 L*(in+j)。
- a[ i ][ j ] 的地址便是首元素地址+L*(in+j)=a+L*(in+j)。
知道某个元素的位置:先确定他前面存了多少页,在数出该元素所在页前面的行数,再确定所在行前面的个数。
4.2.3 特殊矩阵的压缩存储
【K值就对应的值在一维数组中存放的位置】。
aij对应的k值如何求?
- 要判断他前面有多少个元素。
- 首先判断前面有多少行:前面有i-1行,第一行1个元素、第二行2个元素…因此前i-1行共有元素1+2+3+…+(i-1);
- 然后判断本行 aij前有多少个元素:j-1。
- 因此aij前面共有【1+2+3+…+(i-1)】+【j-1】。即k值。
用二维数组去存。
- 一条一条对角线的去存。主对角线存在第0行;其他对角线两边对称地存。
- 每一行有1个指针指向这一行的非0元素。同理每列。
4.3 广义表
第五章 树和二叉树
5.1 树和二叉树的定义
5.1.1 树的定义
树的定义就是个递归嵌套的定义。
凹入表示:用凹入的深度表示层次,凹入得越深表示层次越低。
5.1.2 树的基本术语
- 结点:数据元素以及指向子树的分支。
- 一个树中根结点只有一个。
- 结点的度可以直接看后继结点的个数。
- 有共同双亲的结点称为兄弟结点。
- M的祖先:A、D、H。
- D的子孙:H、I、J、M。
- 树的深度:树中结点的最大层次。(也称为树的高度)。
5.1.3 二叉树的定义
❗二叉树不是树,也不是有序树。
(1)满二叉树
(2)完全二叉树
5.2 案例引入、分析与实现
5.2.1 数据压缩问题
等长会比较浪费。
c是非前缀码,其坏处是如果码中出现一个0便搞不清楚是a、b还是c中的第一个0。
b是前缀码。既不等长又是前缀码 是我们常用的。实现方法:哈夫曼树和哈夫曼编码。
5.2.2 利用二叉树求解表达式的值
- 叶子结点上是运算数,叶子结点的双亲结点都是运算符。
- 对这个二叉树进行【后序遍历】运算即可得到表达式。
5.3 树和二叉树的抽象数据类型定义
数据元素的说明即根、结点、左孩子右孩子的说明。
- 基本操作
5.4 二叉树的性质和存储结构
5.4.1 性质
二叉树每一个结点的后继结点最多有两个。
- n是结点总数;B是边数;n1是度为1的结点数。
- 第一张图从下往上分析:每一个结点到它的双亲结点都要有一个分支,而只有根节点没有。因此如果有n个结点,那么边数等于结点数-1,即B=n-1。
- 第二张图从上往下分析:度为2的结点会产生2条边,度为1的结点产生1条边,叶子结点不产生边。
证:假设有n个结点,在第k层上。
证明还是用归纳法(不需知道)
5.4.2 存储结构
(1)顺序存储
- TElemType是根据树的存储来决定的,如可以是int、char…
- bt是一个数组。
对应的是满二叉树的编号。
(2)二叉链表
- 每个结点都可能有 2个结点,因此最多可能有2n个链域。
- 此题应该倒过来分析,孩子 一定有双亲,即有一条边(每个结点的前驱最多只有一个,而只有根结点没有)。
- 因此一共有n-1个结点有指针域,即一共有n-1个指针域。
(3)三叉链表
方便找双亲
5.5 遍历二叉树和线索二叉树
5.5.1 遍历二叉树
重点研究前三种
(1)先序遍历
算法的执行:
(2)中序遍历
中序遍历递归算法
中序遍历非递归算法
- 定义这个算法时,变声明了一个指针T,指向根结点。
- 当D的右子树访问完时,即A的左子树访问完时。【此时即p为空,由循环条件便去访问栈中元素】。便可访问根结点A。然后再访问A的右子树。
(3)后序遍历
(4)遍历算法的分析
- 三个的时间算法复杂度是一样的,每个结点都需要路过一次。
- 遇到某一个结点,如果不访问它,就需要找地方先存储,等回来的时候再访问。这时候便需要用到【存储空间】。因此需要有一个【栈】存储暂时不访问的结点。【最坏的情况是一个单支结点,即需要暂时存储n个结点】
❀例题
前后序可以确定根,中序可确定左右子树。
(5)层次遍历
p指代的是指向根结点的指针。
- 最后发现队列中元素为空了,层次遍历 便结束。
5.5.2 二叉树遍历算法的应用
(1)二叉树的建立
- 只知先序序列,不可以唯一确定二叉树。
- 因此补充好空结点,这样输出的才是唯一的。【可以用 空格符或# 表示空结点】
BiTree &T建立的是以链式存储的二叉树。
(2)复制二叉树
和先序遍历是类似的,都是根左右。
(3)计算二叉树的深度
(4)计算结点总数
+1是根结点。
- 箭头表示执行流程。
- 往下执行函数,到了最后一层可得到值,再一层一层返回,得到最终值。
(5)计算叶子结点数
- 不为空,则又继续下一层,即以左孩子为参数,如果左孩子为空,则以右孩子为参数。
- 左右孩子都为空即为叶子结点。
- 左子树判断完了后,返回上层,开始右子树的逐层调用。
5.5.3 线索二叉树
二叉链表中空指针域有n+1个。
- 如果没有后继,就继续为空。
- C为中序遍历第一个结点,因此左孩子的指针域继续为空。后继结点为B(看中序节点),因此右孩子指针结点指向B。
5.6 树和森林
- 树去掉根结点就变成森林。
- 给所有森林加一个共同的根结点即为树。
5.6.1 树的存储结构
(1)双亲表示法
R没有双亲,即双亲域为-1
(2)孩子链表
(3)双亲与孩子结合
(4)孩子兄弟表示法
- 往右走 可以找到所有兄弟结点
- 于是 往左走,然后一直往右走 可以找到所有孩子。
还是找双亲困难。但是可以根据需要 增加双亲结点的数据域(像(3))。
5.6.2 树与二叉树的转换
- 树的二叉链表存储(孩子兄弟表示法)
5.6.3 森林与二叉树的转换
5.6.4 树森林的遍历
(1)树的遍历
(2)森林的遍历
先序:123
中序:213
后序:312
5.7 哈夫曼树及其应用
- 改进:
5.7.1 哈夫曼树的基本概念
- 0是根结点到自身的长度。
- 包含结点数相同 的树 的路径长度可能是不相同的。
【路径长度最短的不一定是完全二叉树】
某种含义不一定,具体看树用于某种场合。如五分制成绩中的权代表分数占的比例(5%是<60所占比例)。
- 满二叉树不一定是哈夫曼树。
- 哈夫曼树中权越大的叶子离根越近。
- 具有相同带权结点的哈夫曼树不唯一。
5.7.2 构造哈夫曼树
- n-1个新结点,每个结点都是度为2。【所有结点的度都不为1】
- 得到的哈夫曼树最高就是n-1层。因为经过n-1次合并。
5.7.3 哈夫曼树构造算法的实现
(1)顺序存储——一维结构数组
- weight是权值。
- H可以表示指针,也可以表示数组。
接下来,重复2.3。
剩下一个parent为0 的即结束。
最后一排的lch是13。
5.7.4 哈夫曼码
(1)设计哈夫曼编码
(2)哈夫曼编码的算法实现
- 从下往上找并判断是左孩子还是右孩子。(左孩子记为0,右孩子记为1)
- 一直到根结点为止
- 找到的编码需翻转,如G从下往上的为00001,则最终G的哈夫曼编码应该为10000。
- HC[i] 由字符串构成的数组,即最终的哈夫曼编码。
- start即 哈夫曼编码的位数。哈夫曼的最大层数(n-1)即哈夫曼编码的最大位数。
- 将哈夫曼编码作为字符串来看待,因此cd[start]的最后一个位置用来存放字符串结束标志 \0
- 倒着存储,因此从第n-1位置开始存储。
(3)文件的编码和解码
得到的一种哈夫曼编码,共需1596位。
第六章 图
6.1 图的定义和基本术语
- 无向图称为边,有向图称为弧。
- ()表示两个没有先后关系;<>表示两个有先后关系,即序偶。
【注:路径上的各顶点均不互相重复,称这样的路径为简单路径】
6.2 案例引入、分析与实现
6.2.1 六度空间理论
6.3 图的类型定义
信息即权。
6.4 图的存储结构
6.4.1 数组表示法(邻接矩阵)
(1)邻接矩阵的表示
- 邻接矩阵是n*n的,n是顶点数。
- 邻接矩阵是唯一的
(2)邻接矩阵的建立
(3)邻接矩阵的优缺点
6.4.2 链式存储结构
(1)邻接表
- nextarc 链域:指示下一条边或弧。
- 可用info存放边的权值或其他信息。
用的头插法
(2)邻接多重表
(3)十字链表
- firstin第一条入弧,firstout第一条出弧
- tailvex弧尾位置,headvex弧头位置,hlink弧头相同的下一条弧,tlink弧尾相同的下一条弧
6.5 图的遍历
6.5.1 深度优先遍历
一条路走到底后回退到上一个位置。(最后回退到顶点的位置)
ps:相关实现过程往下翻
起点是2号顶点,因此辅助数字visited[2]=1
访问1顶点
访问3
访问5
访问5时,发现相邻结点均访问过,因此回退到3
访问3时,也都访问过,回退到1
访问4
访问6
然后发现均访问过,则一步步往后退
6.5.2 广度优先遍历
从起始点开始,访问所有的邻接点,然后再访问邻接点的邻接点。直到所有顶点都被访问。
v1入队> v1出队,即访问v1
v1的邻接点v2、v3入队
6.6 图的应用
6.6.1 最小生成树
此通信网便是生成树。因此解题便需要找到最小生成树。
(1)构造最小生成树MST
Prim算法
【点开始:任意取一个点,找连接中最小的边;形成整体后再找最小的边】
【适用于点少、边数多的算法】
Kruskal算法
【先写出所有顶点,找权值最小的,验证是否构成一个环(若构成一个环,则退回上一步找笔直大一点的环)】
【适用于点多的图】
若v5v6的权值为5,则有两种
6.6.2 最短路径
(1)Dijkstra算法…
接下来看v0直接到各个顶点 与 v0经过v2到达各个顶点的 比较,若减少则修改值,若没减少则不变。
(v2找到了最短路径,后面的都不用看了)
有两个一样的,则选择下标小的那个
(2)Floyd算法
对角线为0,即不考虑自身的
6.6.3 拓扑排序
对如图所示的图进行拓扑排序,可得到如下结果:
(这三个结点均存在前驱,因此不可删除)
不可删除,则证明存在环。
通常选取下标最小的点
>
6.6.4 关键路径
关键路径就是影响工程进度的关键
最迟发生时间是需要 所有流程需要时间的
求关键路径步骤
汇点的最迟发生时间与最早发生时间相同
e(i):a1是从v1到v2,于是查找v1的ve(v1),即0,因此a1的e为0。
l(i):a1是 从v1到v2,于是查找v2的vl(v1),再减去a1的持续时间(即权值6)。即6-6=0
计算差值
差值为0的就是关键活动。
由关键活动构成的路径就是关键路径(蓝色标志)。
第七章 查找
7.1 查找的基本概念
7.2 线性表的查找
7.2.1 顺序查找(线性查找)
合并后:
时间效率分析:
7.2.2 折半查找(二分或对分查找)
查找过程
假设数的排列是递增的
【情况一 找21】
发现要查找的数比中间数小,于是在左边查找
(4+5)/2 结果取4
【情况二 找63】
7.2.3 分块查找(索引顺序查找)
块间有序,块内可以无序
7.2.4 线性表查找方法比较
7.3 树表的查找
7.3.1 二叉排序树
(1)二叉排序树存储结构
(2)查找
(3)插入
(4)生成
(5)删除
【如下是删除关键字40和80】
7.3.2 平衡二叉树
把中间的数放在第一层,然后最小的放在左边,最大的放在右边。
7.4 哈希表的查找
7.4.1 散列表的基本概念
7.4.2 散列函数的构造方法
(1)直接定址法
线性函数中 自变量和因变量是一一对应的关系。(就不会有冲突的函数值)
(2)除留余数法
7.4.3 处理冲突的方法
(1)开放定址法
(2)链定址法
7.4.4 散列表的查找
第八章 排序
8.1 基本概念和排序方法概述
8.2 插入排序
8.2.1 直接插入排序
- 若j比x大,则a[j]后移,j–指向前一个元素,再比较
a[j]小于x则把x插入到 j的下一个位置
8.2.2 折半插入排序
8.2.3 希尔排序
8.3 交换排序
8.3.1 冒泡排序
3是因为使用了临时变量,交换赋值了3次。
8.3.2 快速排序
8.4 选择排序
8.4.1 简单选择排序
8.4.2 堆排序
8.5 归并排序
8.6 基数排序
缺点:不是对所有情况都适用
8.7 外部排序
8.8 各种排序的综合比较
8.8.1 时间性能