栈的典型应用补充
迷宫求解
参考https://blog.csdn.net/baidu_38304645/article/details/82940648
行编辑程序
一个简单的行编辑程序的功能为;接受用户从终端输入的程序或数据。并存入用户的数据区,由于用户在终端上进行输入时,不能保证不出差错,因此,若在编辑程序中,每接受一个字符即存入数据区的做法显然是不恰当的。较好的做法是,设立一个输入缓冲区,可以接受用户输入的每一行字符,然后逐行存入用户数据区。允许用户输入出差错,并在发现错误时及时改正。例如当用户发现刚刚键入的一个字符是错的,那么退格,或指定退格符"#",以表示前一个字符无效;如果发现当前键入的一行内错误较多,则使用退行符"@",表示当前行的字符无效。我放佛看见了vim。
输入缓冲区为一个栈结构,每当从终端接受了一个字符之后先作判别,如果不是退格符也不是退行符,则压入栈顶,如果是退格符,则弹出栈顶,如果为退行符,则清空栈。
代码
void LineEdit()
{
InitialStack(S);
ch=getchar();
while(ch!=EOF)
{
while(ch!=EOF && ch!="\n")
{
switch(ch)
{
case '#':Pop(S);break;
case '@' ClearStack(S);break;
default:Push(S,ch);break;
}
ch=getchar();
}
//遭遇换行或者EOF,将栈中元素传入数据区
ClearStack(S);
if(ch!=EOF) ch=getchar();
}
DestroyStack(S);
}
循环队列满的条件(rear+1)%M=front;
牺牲了一个元素,约定当队列头指针在队列尾指针的下一位置时,为满。
因为队列插入的时候,rear++,删除的时候front++;
广义表和数组
参考http://data.biancheng.net/view/189.html
这个主要知道广义表可以递归定义。
如果非空,表尾也是广义表。表头只有一个元素。
数组和广义表可看成是一种特殊的线性表,其特殊在于: 表中的元素本身也是一种线性表。内存连续。根据下标在O(1)时间读/写任何元素。
二维数组,多维数组,广义表、树、图都属于非线性结构
数组
数组的顺序存储:行优先顺序;列优先顺序。数组中的任一元素可以在相同的时间内存取,即顺序存储的数组是一个随机存取结构。
关联数组(Associative Array),又称映射(Map)、字典( Dictionary)是一个抽象的数据结构,它包含着类似于(键,值)的有序对。 不是线性表。
矩阵的压缩:
对称矩阵、三角矩阵:直接存储矩阵的上三角或者下三角元素。注意区分i>=j和i<j的情况
对角矩阵:除了主对角线和主对角线相邻两侧的若干条对角线上的元素之外,其余元素皆为零。
稀疏矩阵:非零元素个数远小于矩阵元素总数。三元组或十字链表,十字链表更适合矩阵的加法乘法等操作。
三元组顺序表。三元组顺序表虽然节省了存储空间,但时间复杂度比一般矩阵转置的算法还要复杂,同时还有可能增加算法的难度。因此,此算法仅适用于t<<m*n的情况。
稀疏矩阵在采用压缩存储后将会失去随机存储的功能。因为在这种矩阵中,非零元素的分布是没有规律的,为了压缩存储,就将每一个非零元素的值和它所在的行、列号做为一个结点存放在一起,这样的结点组成的线性表中叫三元组表,它已不是简单的向量,所以无法用下标直接存取矩阵中的元素。
对于用三元组存储稀疏矩阵,每个元素要用行号,列号,元素值来表示,在用三元组表示稀疏矩阵,还要三个成员来记住矩阵的行数列数,总的元素数,即总共需要(非零元素个数)n+1个元素。
三元组转置(1)将数组的行列值相互交换(2)将每个三元组的i和j相互交换(3)重排三元组的之间的次序便可实现矩阵的转置
线索二叉树
参考https://github.com/Jack-Lee-Hiter/AlgorithmsByPython/blob/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.md
线索二叉树:对二叉树所有结点做某种处理可在遍历过程中实现;检索(查找)二叉树某个结点,可通过遍历实现;如果能将二叉树线索化,就可以简化遍历算法,提高遍历速度,目的是加快查找结点的前驱或后继的速度。
如何线索化?以中序遍历为例,若能将中序序列中每个结点前趋、后继信息保存起来,以后再遍历二叉树时就可以根据所保存的结点前趋、后继信息对二叉树进行遍历。对于二叉树的线索化,实质上就是遍历一次二叉树,只是在遍历的过程中,检查当前结点左,右指针域是否为空,若为空,将它们改为指向前驱结点或后继结点的线索。前驱就是在这一点之前走过的点,不是下一将要去往的点。
加上结点前趋后继信息(结索)的二叉树称为线索二叉树。n个结点的线索二叉树上每个结点有2个指针域(指向左孩子和右孩子),总共有2n个指针域;一个n个结点的树有n-1条边,那么空指针域= 2n - (n-1) = n + 1,即线索数为n+1。指针域tag为0,存放孩子指针,为1,存放前驱/后继节点指针。
线索树下结点x的前驱与后继查找:设结点x相应的左(右)标志是线索标志,则lchild(rchild)就是前驱(后继),否则:
LDR–前驱:左子树中最靠右边的结点;后继:右子树中最靠左边的结点
LRD–前驱:右子树的根,若无右子树,为左子树跟。后继:x是根,后继是空;x是双亲的右孩子、x是双亲的左孩子,但双亲无右孩子,双亲是后继;x是双亲的左孩子,双亲有右孩子,双亲右子树中最左的叶子是后继
DLR–对称于LRD线索树—将LRD中所有左右互换,前驱与后继互换,得到DLR的方法。
为简化线索链表的遍历算法,仿照线性链表,为线索链表加上一头结点,约定:
头结点的lchild域:存放线索链表的根结点指针;
头结点的rchild域: 中序序列最后一个结点的指针;
中序序列第一结点lchild域指向头结点;
中序序列最后一个结点的rchild域指向头结点;
在二叉树上加上结点前趋、后继线索后,可利用线索对二叉树进行遍历,此时,不需栈,也不需递归。基本步骤:
p=T->lchild; p指向线索链表的根结点;
若线索链表非空,循环:
循环,顺着p左孩子指针找到最左下结点;访问之;
若p所指结点的右孩子域为线索,p的右孩子结点即为后继结点循环: p=p->rchild; 并访问p所指结点;(在此循环中,顺着后继线索访问二叉树中的结点)
一旦线索“中断”,p所指结点的右孩子域为右孩子指针,p=p->rchild,使 p指向右孩子结点;
赫夫曼树(最优二叉树)
一些概念
路径:从一个祖先结点到子孙结点之间的分支构成这两个结点间的路径;
路径长度:路径上的分支数目称为路径长度;
树的路径长度:从根到每个结点的路径长度之和。
结点的权:根据应用的需要可以给树的结点赋权值;
结点的带权路径长度:从根到该结点的路径长度与该结点权的乘积;
树的带权路径长度=树中所有叶子结点的带权路径之和;通常记作 WPL=∑wi×li
哈夫曼树:假设有n个权值(w1, w2, … , wn),构造有n个叶子结点的二叉树,每个叶子结点有一个 wi作为它的权值。则带权路径长度最小的二叉树称为哈夫曼树。最优二叉树。
一些概念
前缀码的定义:在一个字符集中,任何一个字符的编码都不是另一个字符编码的前缀。霍夫曼编码就是前缀码,可用于快速判断霍夫曼编码是否正确。霍夫曼树是满二叉树,若有n个节点,则共有(n+1)/2个码子
给定n个权值作为n的叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为霍夫曼树(Huffman Tree)。霍夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
假设哈夫曼树是二叉的话,则度为0的结点个数为N,度为2的结点个数为N-1,则结点总数为2N-1。哈夫曼树的结点个数必为奇数。
哈夫曼树不一定是完全二叉树,但一定是最优二叉树。
若度为m的哈夫曼树中,其叶结点个数为n,则非叶结点的个数为[(n-1)/(m-1)]。边的数目等于度。
那么这个如果没有要求二叉的话,那可太简单了。但是因为要求为二叉树,则n个结点的话,如果n>2,显然就不能直接连在根上了。
根据
n
0
+
n
1
+
n
2
=
n
n_0+n_1+n_2=n
n0+n1+n2=n
n
1
+
2
n
2
=
n
−
1
n_1+2n_2=n-1
n1+2n2=n−1
n
0
=
m
n_0=m
n0=m
则可以解出
n
1
和
n
2
n_1和n_2
n1和n2。
n
2
=
m
−
1
,
n
1
=
n
−
2
m
+
1
n_2=m-1,n_1=n-2m+1
n2=m−1,n1=n−2m+1
完全二叉树的
n
1
=
1
或
者
0
n_1=1或者0
n1=1或者0,那么赫夫曼树如何?
先看一个例子:
如果要将百分制转化为5级,最直接的:
if(a<60) b='E';
else if (a<70) b='D';
else if (a<80) b='c';
else if (a<90) b='B';
else b='A';
也就是90-100为A
80-90为B
70-80为C
60到70为D
0-60为E
我们的判定程序从a<60开始判断,我们知道如果一个学生为E,判断次数为1次,D为两次,C为三次,B为两次,A为一次。
然而实际的分布一般为正态分布。
E D C B A
5% 15% 40% 30% 10%
然而如果用上面的程序,比较次数就太多了。
假设有10000个输入数据,上面的判读需要31500次。
而如果使用最优霍夫曼树进行比较,比较次数可降低为22000次。
前一种判定树为:
赫夫曼树为:
从中间的80开始判断很正确,因为正态分布。
在计算机数据处理中,哈夫曼编码使用变长编码表对源符号(如文件中的一个字母)进行编码,其中变长编码表是通过一种评估来源符号出现机率的方法得到的,出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码,这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。
例如,在英文中,e的出现机率最高,而z的出现概率则最低。当利用哈夫曼编码对一篇英文进行压缩时,e极有可能用一个比特来表示,而z则可能花去25个比特(不是26)。用普通的表示方法时,每个英文字母均占用一个字节,即8个比特。二者相比,e使用了一般编码的1/8的长度,z则使用了3倍多。倘若我们能实现对于英文中各个字母出现概率的较准确的估算,就可以大幅度提高无损压缩的比例。
哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。树的路径长度是从树根到每一结点的路径长度之和,记为WPL=(W1L1+W2L2+W3L3+…+WnLn),N个权值Wi(i=1,2,…n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,…n)。可以证明哈夫曼树的WPL是最小的。
一. 目的:
找出存放一串字符所需的最少的二进制编码
二. 构造方法:
首先统计出每种字符出现的频率!(也可以是概率)//权值
例如:频率表 A:60, B:45, C:13 D:69 E:14 F:5 G:3
第一步:找出字符中最小的两个,小的在左边,大的在右边,组成二叉树。在频率表中删除此次找到的两个数,并加入此次最小两个数的频率和。
F和G最小,因此如图,从字符串频率计数中删除F与G,并返回G与F的和 8给频率表
重复第一步:
频率表 A:60, B:45, C:13 D:69 E:14 FG:8
最小的是 FG:8与C:13,因此如图,并返回FGC的和:21给频率表。
重复第一步:
频率表 A:60 B: 45 D: 69 E: 14 FGC: 21
如图
重复第一步
频率表 A:60 B: 45 D: 69 FGCE: 35
重复第一步
频率表 A:60 D: 69 FGCEB: 80
频率表 AD:129 FGCEB: 80
添加 0 和 1,规则左0 右1(感觉似乎反过来也行1.为什么要保证长编码不与短编码冲突?
冲突情况:如果我们已经确定D,E,F,G 用 01 ,010 ,10,001的2进制编码来传输了。那么想传送FED时,我需要传送 1001001,接收方可以把它解析为FDG(10 01 001),当然也能解析为FED(10 010 01),他两编码一样的,这就是编码冲突,(这里编码冲突的原因,也是因为编码时,D的编码是E的编码的左起子串了)显然这是不行的,就像我说压脉带,你如果是日本人会理解为 (你懂得),这就是发出同一种语,得出不同的意的情况。所以不能让一个字母的二进制代表数,为另一个字母的二进制代表数的子串。但为什么实际情况只要求编码时,一个编码不是另一编码的左起子串呢而不是绝对意义上的非子串呢,因为计算机从数字串的左边开始读,如果从右边读,那么可以要求是非右起(无奈)。你又可以问了为什么编码要求是非左起或非右起不直接规定不能是子串呢(也行,不过=>),比如说原文中B就是C,F,G的子串,那这不就不符合规则了么。这里是为了哈夫曼的根本目的,优化编码位占用问题,如果完全不能有任何子串那么编码将会非常庞大。但这里计算机是一位一位的·读取编码的,只需要保证计算机在读取中不会误判就行。并且编码占用最少。)
频率表 A:60, B:45, C:13 D:69 E:14 F:5 G:3
每个 字符 的 二进制编码 为(从根节点 数到对应的叶子节点,路径上的值拼接起来就是叶子节点字母的应该的编码)
字符 编码
A 10
B 01
C 0011
D 11
E 000
F 00101
G 00100
那么当我想传送 ABC时,编码为 10 01 0011
参考https://blog.csdn.net/qq_29519041/article/details/81428934
这样,权值越大的结点离根越近。
思考:
大家观察 出现得越多的字母,他的编码越短 ,出现频率越少的字母,他的编码越长。
在信息传输过程中,如果这个字母越多,那么我们希望他越瘦小(编码短)这样占用的编码越少,其实编码长的字母也是让频率比它多的字母把编码短的位子都占用后,他才去占用当前最短的编码。至此让总的编码长度最短。
且要保证长编码的不与短编码的字母冲突:
比如 不能出现 读码 读到 01 还有长编码的 字母为011,如果短编码为一个长编码的左起子串,这就是冲突,意思就是说读到当前位置已经能确定是什么字母时不能因为再读取一位或几位让这个编码能表示另外的字母,
但哈夫曼树(最优二叉树)在构造的时候就避免了这个问题。为什么能避免呢,因为哈夫曼树的它的字母都在叶子节点上,因此不会出现一个字母的编码为另一个字母编码左起子串的情况。
1.为什么要保证长编码不与短编码冲突?
冲突情况:如果我们已经确定D,E,F,G 用 01 ,010 ,10,001的2进制编码来传输了。那么想传送FED时,我需要传送 1001001,接收方可以把它解析为FDG(10 01 001),当然也能解析为FED(10 010 01),他两编码一样的,这就是编码冲突,(这里编码冲突的原因,也是因为编码时,D的编码是E的编码的左起子串了)显然这是不行的,就像我说压脉带,你如果是日本人会理解为 (你懂得),这就是发出同一种语,得出不同的意的情况。所以不能让一个字母的二进制代表数,为另一个字母的二进制代表数的子串。但为什么实际情况只要求编码时,一个编码不是另一编码的左起子串呢而不是绝对意义上的非子串呢,因为计算机从数字串的左边开始读,如果从右边读,那么可以要求是非右起(无奈)。你又可以问了为什么编码要求是非左起或非右起不直接规定不能是子串呢(也行,不过=>),比如说原文中B就是C,F,G的子串,那这不就不符合规则了么。这里是为了哈夫曼的根本目的,优化编码位占用问题,如果完全不能有任何子串那么编码将会非常庞大。但这里计算机是一位一位的·读取编码的,只需要保证计算机在读取中不会误判就行。并且编码占用最少。
code:0110101001110
左起子串:011
右起子串:110
绝对非子串:1110111 此串在code中完全找不到
2.那么哈夫曼树怎么避免左起子串问题呢?
因为哈夫曼是从叶子节点开始构造,构造到根节点的,而且构造时,都是计算两个权值的节点的和再与其他叶子节点再生成一个父节点来组成一个新的树。并且不允许任何带有权值的节点作为他们的父节点。这也保证了所有带有权值的节点都被构造为了叶子节点。然后最后编码的时候是从根节点开始走到叶子节点而得出的编码。在有权值的节点又不会出现在任何一条路的路途中的情况,只会出现在终点的情况下,因此不会出现01代表一个字母011又代表一个字母。
又如原文ABC编码为10010011的情况,当计算机读到10时,由于有左起子串不冲突的原则。那么计算机完全可以保证当前的10就是A字母,然后再往下读010011的部分,然后当读到01时,也完全能确定B,C同理,而不用说因为会出现冲突的编码而接着继续读取之后的编码来确定前面的编码。这样对信息的判断和效率是相当的不利的,也不是说不可以。即使你ABCD,分别用01,011,0111,01111来代替也行,传输后也能精确识别,但是数据量极大呢,想代替整个中文编码呢,那0后面得多少个1才能代表完。因此哈夫曼就是为了获得最少编码量代替最多字符串,并且不冲突,系统不会误判而产生的。
3.这里要提一下同权不同构
已经有朋友问起这个问题了。这里要说一下哈夫曼树的构造并不是唯一的。
考虑如下情况:
有权值分别为 5,29,7,8,14,23,3,11的情况,可以如下图一样构造。
带权路径长度:
(5+3+7+8)*4+
(11+14)*3+
(23+29)*2
=271
也可以如下图构造:
带权路径长度:
(3+5)*5+
7*4+
(8+11+14)*3+
(23+29)*2
=271
这两种不同的方式构造出来的哈夫曼树,得出的带权路径长度相等,那么选哪颗树都可以,这就叫同权不同构。
最小生成树
每次遍历一个连通图将图的边分成遍历所经过的边和没有经过的边两部分,将遍历经过的边同图的顶点构成一个子图,该子图称为生成树。因此有DFS生成树和BFS生成树。
生成树是连通图的极小子图,有n个顶点的连通图的生成树必定有n-1条边,在生成树中任意增加一条边,必定产生回路。若砍去它的一条边,就会把生成树变成非连通子图
最小生成树:生成树中边的权值(代价)之和最小的树。最小生成树问题是构造连通网的最小代价生成树。
Kruskal算法:令最小生成树集合T初始状态为空,在有n个顶点的图中选取代价最小的边并从图中删去。若该边加到T中有回路则丢弃,否则留在T中;依此类推,直至T中有n-1条边为止。
Prim算法、Kruskal算法和Dijkstra算法均属于贪心算法。
Dijkstra算法解决的是带权重的有向图上单源最短路径问题,该算法要求所有边的权重都为非负值。
Dijkstra算法解决了从某个原点到其余各顶点的最短路径问题,由循环嵌套可知该算法的时间复杂度为O(NN)。若要求任一顶点到其余所有顶点的最短路径,一个比较简单的方法是对每个顶点当做源点运行一次该算法,等于在原有算法的基础上,再来一次循环,此时整个算法的复杂度就变成了O(NN*N)。
Bellman-Ford算法解决的是一般情况下的单源最短路径问题,在这里,边的权重可以为负值。该算法返回一个布尔值,以表明是否存在一个从源节点可以到达的权重为负值的环路。如果存在这样一个环路,算法将告诉我们不存在解决方案。如果没有这种环路存在,算法将给出最短路径和它们的权重。
最小生成树是遍历图中所有节点的最小加权路径。
实际场景就是TSP问题。或者假设要在n个城市间建立通信联络网,则连通n个城市需要n-1条线路。如何最节省经费呢?
可以用无向图来表示这个连通网,n个顶点,边为带权边,而且要保证图的连通性。那么其实DFS和BFS都可以实现连通性(无向图),强连通性(有向图)。
参考https://www.bilibili.com/video/BV1Eb41177d1?from=search&seid=10382698866861251410
Prim
参考https://www.bilibili.com/video/BV1Ut41197XQ
最重要的是MST的性质
这个可以用反证法证明:假设网N中的任何一棵MST不包含(u,v),,设T为一棵MST,则将(u,v)加入T中为T’,则T’中必然成环,环包括(u,v)。而T中原来必包含(u’,v’)其中u’在U中,v’在V中,且u和u’,v和v’均连通。则删除(u’,v’),则可以消除环,同时结果也为生成树T’’,而(u,v)的代价不高于(u’,v’),则T’'的代价不高于T,这和T为MST矛盾。
解释:
先任意找一个顶点。找出最小的边。
依此类推。这样算法的复杂度为O(n^2)。如果用二叉堆可以优化为O(ElogV)。因为而小顶堆找最小值的复杂度为O(logV)。而不是O(V)。
Kruskal
先对边按照权值排序,依次将边放入图中,若无环,则保留边。否则放弃此边。
取下一条边。
终止条件为保留边的数量为n-1个,则停止,因为再加边就要成环了。这种思想也是MST的性质,
这种算法主要是要对边排序。这种方法首先前面如果没有成环,则没有问题的,因为最小的必然是从小到大相加,如果成环,证明上一次的的m条边连接了m+1个顶点。这个可以说是U,其它的为U-V,那么要找最小的(u,v)。
克鲁斯卡尔如何判断环呢?用到了并查集。
https://www.cnblogs.com/kubixuesheng/p/4403280.html
拓扑排序
DAG的应用。
AOV
如何判断AOV中是否有回路呢?
这个序列并不唯一。
最后为
找不到没有前驱的顶点了。
关键路径
AOE
这里是假设必须在180之前做完。
这个最大最小还是好理解的。为何最早取最大的呢?因为如果C需要A,
B做完才能做,那么显然如果A最早10分钟做,而B最早20开始,那么你必须等B,A做完了没用。
同样假设C做完了才做A和B,A最晚120开始,B最晚140开始,那肯定就着120,否则将超时。
B+树
https://blog.csdn.net/u013411246/article/details/81088914
B+树
B+树是B-树的变体,也是一种多路搜索树:
1.其定义基本与B-树同,除了:
2.非叶子结点的子树指针与关键字个数相同;
3.非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间);
5.为所有叶子结点增加一个链指针;
6.所有关键字都在叶子结点出现;
如:(M=3)
B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在
非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;
B+的特性:
1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
2.不可能在非叶子结点命中;
3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
4.更适合文件索引系统;
B*树
是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针;
B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3
(代替B+树的1/2);
B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据
复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父
结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;
B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分
数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字
(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之
间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;
所以,B*树分配新结点的概率比B+树要低,空间使用率更高;
小结
二叉搜索树:二叉树,每个结点只存储一个关键字,等于则命中,小于走左结点,大于
走右结点;
B(B-)树:多路搜索树,每个结点存储M/2到M个关键字,非叶子结点存储指向关键
字范围的子结点;
所有关键字在整颗树中出现,且只出现一次,非叶子结点可以命中;
B+树:在B-树基础上,为叶子结点增加链表指针,所有关键字都在叶子结点
中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中;
B*树:在B+树基础上,为非叶子结点也增加链表指针,将结点的最低利用率
从1/2提高到2/3;