link:github链接
1.数据结构绪论
1.3数据结构起源
数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系和操作等相关问题的学科。
1.4基本概念和术语
数据/数据元素/数据项/数据结构
数据结构:相互之间存在一种或多种特定关系的数据元素的集合。
也就是数据的组织形式
1.5逻辑结构和物理结构
逻辑结构:集合,线性,树形,图形
物理结构:数据的逻辑结构在计算机中的存储形式
顺序存储和链式存储
1.6抽象数据类型
ADT abstract data type
指一个数学模型和在该模型上的一组操作
2.算法
算法:解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示为一个或多个操作。
2.2数据结构与算法关系
谈到算法是为了更好的理解数据结构
2.5算法的特性
输入(0/多)、输出(1/多)、有穷性(不会无限循环)、确定性(无二义性)、可行性(可编程)
2.6 算法设计的要求
正确性:语法,对合法的input产生正确的output,对不合法的input产生说明,考虑极端情况
可读性:便于阅读理解和交流
健壮性:不合法的input不会招致崩溃
时间效率高,存储量低
2.7算法效率的度量方法
事后统计/事前分析
与问题输入规模n的关系
2.8函数的渐进增长
判断算法效率时,系数和常数不重要,更关注最高阶项的阶数
2.9算法时间复杂度
大O记法:最高阶项,无系数
常数阶O(1)/线性阶O(n)/平方阶/对数阶
2.10常见的时间复杂度
1<logn<n<nlogn<n*n<2n<n!<nn
2.11最坏情况与平均情况
无特指则分析最坏时间复杂度
2.12算法空间复杂度
可以以空间换时间
分析算法所用的辅助单元
3.线性表
零个或多个数据元素的有限序列
3.3线性表的抽象数据类型
初始化,是否为空,清空,按位置索引,按值定位,插入,删除,返回个数
3.4线性表的顺序存储结构
用一段地址连续的存储单元依次存储线性表的数据元素
描述顺序存储结构需要三个属性:起始位置,最大存储容量(capacity),当前长度(size)
数据的长度:sizeof 所占字节数
线性表长度:元素个数,size
线性表,随机存取结构,存取性能时间O(1)
3.5顺序存储结构的插入与删除
插入:插入位置之后的元素全都要后移,O(n)
删除:删除位置之后的元素全都要前移,O(n)
优缺点:
优点:无需为元素之间的逻辑关系额外增加存储空间,快速存取
缺点:插入删除复杂度高,长度变化时,难以确定空间容量,造成存储空间碎片。
3.6线性表的链式存储结构
特点:用一组任意的存储单元存储线性表数据元素,存储地址可以不连续。每个节点存储数据和后继节点的地址。
数据域/指针域
头指针:指向头节点的指针,,常为链表的名字,必不为空
头节点:dummy head,数据无意义,指针只想第一个数据元素,用来统一第一数据节点与其他节点的操作。不是必须要素。
p->data
p->next
3.7单链表的读取
读取特定元素需要从头节点开始找
复杂度O(n)
因为链表没有定义长度,多以只能用工作指针后移
3.8单链表的插入与删除
将s插入到p和p->next中:
new listnode s;
s->next=p->next;p->next=s;
将p->next从p和p->next->next中删除:
q=p->next;p->next=q->next;
free(q);
对于频繁插入/删除数据项的操作,链表相对于vector优势明显
3.9单链表的整表创建
生成节点-》插入
3.10单链表的整表删除
删除节点,释放节点
3.11链表与vector的优缺点
频繁查找用数组,频繁插入删除用list
数目变化大用list
3.12 静态链表
对于没有指针的语言,可用数组实现静态链表。每个数据节点存储数据和下一个元素的index。
插入/删除:将未被使用和已被删除的节点组成备用链表
优缺点:与数组相比,插入删除复杂度降低,但是未解决表长度不确定的问题以及失去了数组 O(1)存取的特性
3.13循环链表
尾节点指针指向头节点
3.14双向指针
data
*next
*prev
4.栈与队列
栈是限定仅在表尾进行插入和删除操作的线性表
队列是尾部插入,头部删除的线性表
4.2栈的定义
栈顶:允许插入删除
后进先出,LIFO,last in first out
入栈(压栈),出栈
4.3栈的抽象数据类型
操作:初始化,销毁,清空,判断是否为空,读栈顶,pop,push,size
4.4栈的顺序结构存储及实现
数组尾端做栈顶
4.5两栈共享空间
从数组的两端向中间靠拢,每次push检查两个栈顶会不会相遇
条件:两个栈的空间需求相反,数据类型相同
4.6栈的链式存储结构及实现
头指针作为栈顶指针,头结点不需要了
C语言会用链栈,因为没有动态数组
4.7栈的作用
简化程序设计,思考范围缩小,聚焦问题核心,高级语言基本都有栈的封装
4.8栈的应用–递归
斐波那契数列:兔子繁殖,出生两个月后,每对生一对
斐波那契数列直接递归
递归函数:直接调用自己或者间接调用自己的函数
递归函数要有终止条件
迭代使用循环结构
递归使用选择结构
迭代代码易懂,更消耗时间和内存(调用函数,栈帧开销)
调用时,函数的局部变量,参数值,返回地址都被压入栈中。
4.9栈的应用–四则运算表达式求值
后缀(逆波兰)表示法 RPN reverse polish notation
括号成对出现,用栈很合适
所有的符号在要运算的数字的后面出现,去掉了括号
运算规则:遇到数字进栈,遇到符号,栈顶弹出两个元素进行运算
中缀表达式转后缀表达式规则:数字输出,符号则判断与栈顶符号的优先级,右括号或优先级低于栈顶符号,则栈顶元素依次出栈(当前的是右括号,则截止到左括号出栈)输出,当前符号进栈。
计算机处理中缀表达式:1.转后缀2.后缀计算
4.10队列的定义
queue,fifo
先进先出,只可以在尾部插入,头部删除
应用:操作系统,客服系统
顺序存储:头指针队头元素,尾指针队尾元素的下一个位置,则front=rear时,空队列
假溢出?:循环队列解决
防止front=rear时有满或空两种情况:将满定义为front与rear差1时
队列满的条件:(rear+1)%queuesize==front
队列长度公式(rear-front+queuesize)%queuesize
时间复杂度不高,但面临数组溢出的问题
4.13 队列的链式存储结构及实现
单链表,只能尾进头出
空间上,链队列更灵活,时间上,循环队列更快些
5.串
string:字符串,0/多个字符组成的有限序列
空串
串的相邻字符有前驱和后继的关系
子串在主串中的位置时子串第一个字符在主串中的序号
串的比较:
更小:串头相同,更短者;第一个不同的字符ascii编码更小者。
线性表更关注单个元素操作:增删改查
串更多的是查找字串位置,得到指令位置子串,替换子串等
串的结尾有\0表示结束
c中串操纵为了防止溢出注意malloc和free
5.6朴素的模式匹配算法
子串的定位操作同非常乘坐串的模式匹配。
朴素:主串遍历,每个位置作为起始位置与子串尝试匹配,n主串长度,m子串长度,最坏O(n-m+1)*m
5.7KMP模式匹配算法
避免重复遍历
设计一个next数组表征子串自身的重复性,用来跳过不必要的比较。
next数组设计:
next【0】=0;
next【1】=1;
next【i(》=2)】看【0:i-1】的串的前缀和后缀有几个重和,n个重合则值为n+1
比较不等的情况下,子串指针通过next数组回溯,减少复杂度。o(m+n)
主串与子串之间存在许多“部分匹配”的情况下有优势。
next数组可改良解决子串头长重复字符的问题。将next【0】传递给身后与他相同字符对用的next值
改进的kmp:在计算next的同时,如果a位字符与他next指向的字符相等,则a的nextval指向b的nextval,不等则和naxt一样。
回文,s==reverse(s)
6.树
树:一个root,子树不相交
一对多的数据结构
度:节点拥有的子树数目
节点:根节点/内部节点(度>0)/叶节点(度为0)
节点之间的关系:子节点/父节点/兄弟节点/祖先/子孙
深度:root深度最浅
森林:互不相交的树的集合
6.4 树的存储结构
一个存储结构设计的是否合理,取决于该存储结构的运算是否合适,是否方便,时间复杂度好不好。
孩子兄弟表示法
node:data | first child | right brother
将树转化为二叉树
6.5 二叉树的定义
左子树和右子树次序不能任意颠倒
斜树:左/右斜树
满二叉树:内部节点度为2. 同等深度的二叉树里,满二叉树节点数最多。
完全二叉树:构造满二叉树的中间状态
6.6 二叉树的性质
1.第i层最多有2^(n-1)个结点
2.深度为k的二叉树,最多有2^k - 1 个结点
3.叶节点数=度为2的节点数+1
4.完全二叉树深度【log2N】 +1 //【】,向下取整
完全二叉树的层次排列:节点i,【i/2】是其父节点,2i是其左孩子,2i+1是其右孩子
6.7 二叉树的存储结构
完全二叉树可以用数组存储,普通的也可以,将不存在的节点用^无效数据表示来占坑
链式存储:data+*左孩子 + *右孩子 (还可以指向父节点,三叉链表)
6.8 遍历二叉树
前/中/后 序遍历:指访问根节点的次序,左一直在右前。比如后序遍历,左右中。
层次遍历
遍历方法把树的节点变成某种意义的线性序列。
前中后的遍历和迭代写法
根据前序和中序 或者 后序和中序 可以推导出另一种遍历顺序。但是前序+后序不能确定一颗二叉树。
6.9 二叉树的建立
递归,生成节点(new/malloc),节点赋值
6.10 线索二叉树
创建时就记住前驱和后继。
利用空地址(n个节点有n-1个边,但是有2n个指针域)来存储某种遍历次序下的前驱和后继节点的位置。
指向前驱和后继的指针称为线索。
线索二叉树是将二叉树转化为了双向链表
线索化
节点中增加两个bool变量,ltag和rtag,表征当前左右指针是否是左右孩子。
线索化就是在便利的过程中修改空指针,将空指针改为指向前驱或者后继。
应用场景:二叉树经常需要某种遍历序列的前驱和后继。
6.11 树、森林与二叉树的转换
树–》二叉树:长子兄弟表示法
森林–》二叉树:各自转成二叉树,后续树的根作为右孩子接到前边。
6.12 赫夫曼树及其应用
压缩编码方法:赫夫曼编码
根据数据出现的频率来编码,将频率高的数据用更短的bit来编码,前缀编码,所有编码不是另外编码的前缀。
最优树/赫夫曼树:带权路径长度最小的二叉树
书的路径长度是根到每一结点的路径长度之和
赫夫曼编码需要接收端配合
7.图
图:顶点和边构成的集合。
图形结构中,节点之间的关系是任意的。
线性表–》元素,树–》结点,图–》顶点vertex
图中不允许没有顶点,顶点集合有穷非空,边集可以是空的
各种图定义:无向边,有向边(弧)
无向边用()来表示,有向边用<>来表示
简单图:没有顶点到其自身和重复的边
无向图中,如果任意两个顶点都存在边,则成称为无向完全图。n顶点–》n*(n-1)/2
稀疏图/稠密图
带权重的图通常称为网
子图,subgraph
顶点之间,相邻接,边与顶点相关联(依附)。顶点的度(degree)是与其相关联的边数。
有向图:入度和出度
顶点到顶点的路径不唯一
回路/环/简单环(只有头尾重复)
任意两个顶点都有路径(连通的),称为连通图。
连通分量:极大连通子图
有向图:强连通图,强连通分量
连通图的生成树:极小连通子图,包含n个顶点,n-1条边,构成树
7.4 图的存储结构
邻接矩阵:顶点用一维数组存储,边用二维邻接矩阵存储
无向图的边数组是一个对称矩阵
邻接矩阵处理稀疏图很浪费–》邻接表
邻接表:把数组和链表相结合的方法。
数组:存储顶点和第一个邻接点的指针,所以数组的每个entry都构成链表。
对于带权值的网,节点定义中再加一个weight即可
单链表创建:头插法
对于有向图来说,将邻接表(出度)和逆邻接表(入度)结合起来,十字链表(orthogonal list)
十字链表,顶点表节点结构:data|firstin|firstout
边表结点结构:tailvex|headvex|headlink|taillink
十字链表,容易求得顶点的入读和出度
邻接多重表:关注无向图的对边的操作,一条边在边表中用两个节点表示
边集数组:两个一维数组组成,一个存储顶点的信息,另一个存储边的信息
7.5 图的遍历
从图中某一顶点出发,遍历其他顶点,且每个顶点被访问一次,叫做图的遍历。
深度优先遍历(类似于树的前序遍历)/广度优先遍历(类似于树的层次遍历)
深度优先搜索+回溯,遍历图的顶点
对于点多边少的稀疏图来说,邻接表使算法的时间空间效率大大提高
bfs和dfs在时间复杂度上相同,不同的是对顶点访问顺序的不同
根据应用场景选择深度优先或者广度优先
7.6 最小生成树
构造连通网的最小代价生成树称为最小生成树。
普利姆算法(稠密图有优势),克鲁斯卡尔算法(稀疏图有优势)
prim算法:以顶点为起点,逐步找各顶点上最小权值的边来构建最小生成树。
kruskal算法:找最小权值的边来构建,利用边集数组结构
7.7 最短路径
迪杰斯特拉算法:dijkstra on*n
按路径长度递增的次序产生最短路径。一步步求出起点到终点之间顶点的路径。
弗洛伊德算法:floyd
二重循环初始化+三重循环权值修正
7.8 拓扑排序
有向无环图,顶点表示活动,弧表示活动之间的优先关系,AOV网。
拓扑排序,其实就是对一个有向图构造拓扑序列的过程。
7.9 关键路径
对一个流程图获得最短时间,就要分析他的拓扑关系,找到当中最关键的流程。
8.查找
查找表
关键字
静态查找表:
动态查找表:可做插入删除
编程优化:在查找方向的尽头放置哨兵,辨别错误
二分查找,斐波那契查找
8.5 线性索引查找
索引就是把一个关键字与它对应的记录相关联的过程。
线性索引 树形索引 多级索引
线性索引,索引表:稠密索引,分块索引,到排索引
稠密索引:数据集的每个记录对应一个索引项。索引项按关键码有序。
分块索引:对数据集分块,使其分块有序,然后对每个块建立索引。块内无序,快间有序。索引表的条目存储,最大关键码,块长,块首指针。查块用二分,块内遍历 。复杂度 根号n
倒排索引:由属性值查找该属性值的各记录地址,因此称为倒排。即建立一张表,记录此关键码和出现过次关键码的指针或者主关键码。(搜索引擎)
8.6 二叉排序树
二叉搜索树,左《root《右
插入删除:旋转,3+4操作
8.7 平衡二叉树(AVL树)
平衡因子:左子树高度-右子树高度,只会为-1,0,1
查找/插入/删除:o lgn
8.8 多路查找树(B树)
为了避免频繁访问磁盘而设计。
打破一个节点只存储一个元素的限制。
多路查找树multi-way-search-tree:每一个节点的孩子数可以多于两个,且每个节点可以存储多个元素。查找树,元素之间存在某种特定关系。
2-3树,2-3-4树,B树,B+树
2-3树:每个节点具有两个孩子或者三个孩子
2结点,1元素+2孩子,类似二叉搜索树,不过两个孩子同时出现或者同时缺失。
3结点,2元素+3孩子,同样有序
2-3插入的传播效应导致了根节点的拆分,则树的高度就会增加。
2-3-4树:还有4结点,3元素+4孩子
B树平衡的多路查找树,2-3,2-3-4,是B树的特例。结点最大的孩子数组称为B树的阶。
在B树的查找过程,就是顺指针查找结点和在节点中查找关键字的交叉过程。
B树如何减少访问外存的次数?
使B树的阶数和硬盘的页面大小匹配,采用类似页式管理的方法来管理数据结构,让根节点贮存在内存中,大大减少访问外存的次数。每次磁盘访问都可以获得最大数量的数据。
B树的数据结构就是为了内外存的数据交互准备的。
B+树:B树对下层结点的遍历需要频繁访问根节点。
改进:节点中增加一个指向中序遍历后继的指针(上一层节点中出现过)
8.9 散列表查找(哈希表)概述
直接通过key找到value
value/value的位置和key有映射关系。
散列表/哈希表
散列:面向查找的数据结构,记录之间没有逻辑关系。
存储和查找:通过哈希函数处理key,计算存储地址
设计一个简单/均匀/存储利用率高的散列表:
如何处理冲突?
8.10 散列函数的构造方法
好的散列函数:计算简单+地址均匀
常用方法:
1.直接定址法:线性函数:y=a*key+b
需要事先知道关键字的分布,适合查找表较小且连续的情况。
2.数字分析法:抽取数的某些位并作各种操作(环移,反转,互加)形成地址
通常适合处理key的数字位数较大的情况,也要求key均匀
3.平方取中法:平方,再取中间某些位
适合不知道关键字分布,且位数又不是很大的情况。
4.折叠法
将key从左到右分割成几段,叠加再取后几位
实现不需要知道key的分布,适合key位数较多的情况。
5.除留余数法:最常用
y=k mod p(p小于表长,最好为质数)
6.随机数法
y=random (key),适合key的长度不等时
选择散列函数参考:计算时间,关键字长度,散列表大小,关键字分布,查找频率
8.11 处理散列冲突的方法
1.开放定址法:
线性探测法,冲突了寻找临近位置是否为空并放入。
会出现堆积现象(不冲突的两个key争夺地址)
二次探测法:寻找新位置的步长由1-2-3改为1,-1,4,-4,9.。。两个方向,步长平方增长
随机探测法:探测的步长随机生成(查找时用相同的随机种子,所以没问题)
开放定址法在散列表未填满时,总能找到不发生冲突地址。常用。
2.再散列函数法:事先准备多个散列函数,发生冲突时,更换散列函数。
3.链地址法:将冲突的数据挂在同一个单链表上,冲突时加节点
4.公共溢出区法:将冲突的数据统一放置于溢出区
8.12 散列表查找实现
散列查找取决于:
散列函数是否均匀,处理冲突的方法,散列表的装填因子(利用率)
9.排序
排序的稳定性:两个相等的值排序完前后相对位置没变,则称为稳定。
内排序/外排序:排序过程中,待排序记录是否全部放置在内存中。
排序算法主要受:时间性能(比较和移动),辅助空间,算法时间复杂性
简单算法:冒泡,简单选择,直接插入
改进算法:希尔排序,堆排序,归并排序,快速排序
冒泡排序:两两比较相邻记录的关键字,如果反序则交换,知道没有反序的记录为止。
冒泡优化:增加flag,在i循环时判断,若某一遍i循环里没有发生交换,直接退出
选择排序:冒泡的改进,交换次数少,每次在未排序序列中找到最值,与未排序头交换,已排序+1,未排序-1。
On2 不稳定
插入排序:构建有序序列,对于未排序元素,从后往前扫描查找插入位置,找到后插入。
on2,在stl中作为快排的补充,数据较少时选择插入排序。数据基本有序速度很快。
希尔排序:
插入排序改进,插入排序每次与前面一个比较,然后再往前一个,而希尔排序每次往前K个。增量逐渐减小。
当增量为1的时候,希尔排序与插入排序就完全是一样的过程;
增量的选择比较关键,不稳定。
堆排序:对选择排序的改进
堆:root是最值,用数组表示堆
构建堆,弹出顶,重新构建堆
归并排序:采用分治思想,递归分成小的子序列,使子序列有序后再合并得到完全有序的序列
快速排序:选择基准,分区,递归处理