数据结构笔记

     浅析KMP算法之Next数组
	
首先我们需要知道KMP是干嘛的,通俗易懂的来讲是查找相同字符串的一种捷径
我们人的本能的趋简避繁,例如aababcbdaabacbbbd这样的字符串,我们需要在其中找到子串aabacb我们怎么操作?
为什么会将子串第一个字符依次对比字符串的每一个字符来判断:
=----------1---------=
aababcbdaabacbbbd
aabacb
=----------2---------=
aababcbdaabacbbbd
 aabacb
=----------3---------=
aababcbdaabacbbbd
  aabacb
=----------4---------=
aababcbdaabacbbbd
   abacb
=----------5---------=
aababcbdaabacbbbd
    abacb
=----------6---------=
aababcbdaabacbbbd
     abacb
=----------7---------=
aababcbdaabacbbbd
      abacb
=----------8---------=
aababcbdaabacbbbd
       abacb
=----------9---------=
aababcbdaabacbbbd
        abacb
=----------10---------=
aababcbdaabacbbbd
         abacb
=----------end完结撒花---------=
怎么样是不是很麻烦吧?//麻烦个嘚,我就喜欢
不是,既然计算机大拿说了麻烦咱就认行吗?
于是开头字母分别为KMP的三个人设计了一个算法叫KMP算法(具体思路自己百度,以下只解释最重要也是最难理解的Next数组该怎么求)
Next数组说白了就是求子串中每一个最长相等前后缀(听不懂没关系,往下看)
这里声明一个规则:
前缀:不包含未元素的串
后缀:不包含首元素的串
我们来举个例子这是子串|a|a|b|a|a|f|
(1)|a|
前缀以及后缀都为0  导致最长相等前后缀为0
(2)|a|a|
前缀为|a|
后缀为|a|相等
最长相等前后缀为1
(3)|a|a|b|
前缀为|a|a|
后缀为|a|b|不相等
在这有两个元素的前后缀的基础上再下一次比较
前缀|a|
后缀|b|依然不相等//注意这里的|a|和|b|不是后缀中的|a|b|拆分的,是前缀|a|a|的第一个|a|以及后缀|a|b|最后的那个|b|
最长相等前后缀为0
(4)|a|a|b|a|
前缀为|a|a|b|
后缀为|a|b|a|当然不相等
在这有三个元素的两个前后缀的基础上再下一次比较
前缀|a|a|
后缀|b|a|依然不相等
同理再比较
前缀|a|
后缀|a|相等
所以最长相等前后缀为1
(5)|a|a|b|a|a|
前缀|a|a|b|a|
后缀|a|b|a|a|不等
再前缀|a|a|b|
后缀|b|a|a|不等
再|a|a|
后缀|a|a|相等长度为2
最长相等前后缀为2
(6)|a|a|b|a|a|f|
前缀|a|a|b|a|a|
后缀|a|b|a|a|f|不等
前缀|a|a|b|a|
后缀|b|a|a|f|不等
前缀|a|a|b|
后缀|a|a|f|不等
前缀|a|a|
后缀|a|f|不等
前缀|a|
后缀|f|不等
所以最长相等前后缀为0
所以Next数组的值就是,这6个算出来的数值
即|0|1|0|1|2|0|
以上为理论部分(聪明的人会看到这里的)

以下是代码实现部分(C++)
#include < stdio.h >
#include < iostream >//c++的头文件
#include< string >
using namespace std;//c++的命名空间,如果没有这句话,很多地方会这样写std::cout等等
class solution {//定义类,c++语法
public://公共方法(好像java才说方法)以及属性
    void getnext(int* next, const string& s) {//传入next数组以及字串
        int j = 0;//j初始化为0
        next[0] = 0;//首元素设置为0
        for (int i = 1; i < s.size(); i++) {//我们假设i指向后缀的末尾,j指向前缀的末尾
            while (j > 0 && s[i] != s[j]) {//while是用来处理前后缀不相同的情况
                j = next[j - 1];//这句代码的意思是回退,回退到前一个数组元素的值为数组下标处
            }
            if (s[i] == s[j]) {//if用来处理前后缀相同的情况
                j++;//如果想相同,则会将j++,让j来表示最长相等前后缀的情况
            }
            next[i] = j;//让此处的最长相等前后缀为j的值
        }
    }
};
int main()
{
    int next[8];
    string s = "abcabcab";
    solution*a = new solution();
    a->getnext(next,s);
    for(int i=0;i<8;i++)
    printf("%d",next[i]);
}
        //放心,代码你十有八九看不懂,不过没关系,毕竟你要是自己研究出来了
        //你就是KMP的发明者了,今天看几遍,过几天再来,慢慢的就理解了
	
     图的最小生成树
    

给定图,利用Kruskal算法(克鲁斯卡算法)生成最小生成树

(1) 找出最小的边 即 (2) 继续找出最小的边 即 (3) 继续找出最小的边 即 (4) 继续找出最小的边(注意:这里之所以是选40而不是15的边是因为C和D已经有了通路)


给定图,利用Prim算法(普里姆算法)生成最小生成树,给定A为初始点

(1) 选出与A相连的最小权值的边 即 (2) 选出与AD相连的最小权值的边 即 (3) 选出与ADE相连的最小权值的边 即 (4) 选出与ADEC相连的最小权值的边(注意:不能选择已经用通路的两点的边) 即 总结两种算法的结果是相同的,但是其过程是不同的,解答这类题目的答案就是需要步骤的
     哈夫曼树
    

已知

A B C D E
0.15 0.16 0.07 0.21 0.41
(1)画出相对应的哈夫曼(Huffman)树 (2)计算带权路径长度WPL (3)求A、B、C、D、E的哈夫曼(Huffman)编码

(1)取最小的两者不断重构(结合以后的生成的数再放到整个比较) 以下是详细步骤 1>ABCDE中A与C的数值最小于是乎先画出A C 2>把A和C的合并数值0.22当成AC再去和剩下的B、D、E比较大小,选出最小两个(也就是B和D),这个时候刚才画的就别动,在旁边重新画一棵树 3>思想同上(别告诉我你现在还没有看明白😒😒😒) 以上就是对应的完整的哈夫曼树了 (2)计算WPL(看带字母的节点,然后看这个节点到顶点经过了几条边,经过几条就数值*几就行) 例如A,从顶点到A经过了4条边(幼儿园的都知道怎么数🤣🤣🤣) 也就是0.15*4,总之把所有字母的边*数加起来就是WPL ∴WPL=1*0.41+3*0.15+3*0.07+3*0.06+3*0.21//*是乘号✖,我靠这样看起来很头疼吗??这不就是程序员必备的? ∴WPL=1✖0.41+3✖0.15+3✖0.07+3✖0.06+3✖0.21//满足你 (3)求哈夫曼编码(将哈夫曼树的左边画0,右边画1,你就知道了) 然后就是从顶点开始往字母走,假如我们选D,D的编码就是011(简单吧🫵) 答案就是: A:000 B:010 C:001 D:011 E:1 别觉得哈夫曼编码就是固定长度(🤣🫵)
     哈希函数
    设散列表长为11,记录关键字集合为{15,17,28,41,37,31,56,23,45,51,73},散列函数:H(Key) = key MOD 11,冲突处理采用线性探测法,请给出散列表的构造过程,再求平均查找程度ASL
(1)先看散列函数,H(Key) = Key MOD 11.(对每个数都取余,与11相除,取余)
    
余数 4 6 6 8 4 9 1 1 1 7 7
原数 15 17 28 41 37 31 56 23 45 51 73
(2)我们先写出一个0-10(11个)
0 1 2 3 4 5 6 7 8 9 10
我们规定,按照余数,放进这个表格中,如果冲突线性探测就是往后顺延一位(如果一位不行就再继续顺) 我们一步步来,把余数4对应的15放到4处
0 1 2 3 4 5 6 7 8 9 10
15
此时记录15到位4处动了(1)次,接着把17放到6处
0 1 2 3 4 5 6 7 8 9 10
15 17
记录17动了(1)次,接着放28,因为28是6,和之前的17冲突了,采用线性探测就是往后顺延一位
0 1 2 3 4 5 6 7 8 9 10
15 17 28
记录28动了(2)次,接着41放8处
0 1 2 3 4 5 6 7 8 9 10
15 17 28 41
记录41动了(1)次,接着37,与15冲突了往后顺延1位
0 1 2 3 4 5 6 7 8 9 10
15 37 17 28 41
记录37动了(2)次,接着31,放9处
0 1 2 3 4 5 6 7 8 9 10
15 37 17 28 41 31
记录31动了(1)次,接着56放1处
0 1 2 3 4 5 6 7 8 9 10
56 15 37 17 28 41 31
记录56动了(1)次,接着23放1处冲突顺延到2处
0 1 2 3 4 5 6 7 8 9 10
56 23 15 37 17 28 41 31
记录23动了(2)次,接着45放1处冲突顺延到2处再冲突再顺延3处
0 1 2 3 4 5 6 7 8 9 10
56 23 45 15 37 17 28 41 31
记录了45动了(3)次,接着把51放7处与28冲突顺延到8冲突到9冲突于是放到10
0 1 2 3 4 5 6 7 8 9 10
56 23 45 15 37 17 28 41 31 51
记录51动力4次,接着73放7处冲突,再8冲突,再9冲突,再10冲突,再0放
0 1 2 3 4 5 6 7 8 9 10
73 56 23 45 15 37 17 28 41 31 51
以上就是表的构造过程了 15:1次 17:1次 28:2次 41:1次 37:2次 31:1次 56:1次 23:2次 45:3次 51:4次 73:5次 求ASL(平均查找程度就是将以上数加起来再除他们的总个数)

ASL= 1 + 1 + 2 + 1 + 2 + 1 + 1 + 2 + 3 + 4 + 5 10 = 2.3

还有一种题目是冲突采用链地址表示法 前面求余数的步骤一样就是构造的时候采用链表的思想
余数 4 6 6 8 4 9 1 1 1 7 7
原数 15 17 28 41 37 31 56 23 45 51 73
就是构造以下基表竖着的
0 ^
1 -> 56 -> 23 -> 45 ^
2 ^
3 ^
4 -> 15 -> 37 ^
5 ^
6 -> 17 -> 28 ^
7 -> 51 -> 73 ^
8 -> 41 ^
9 -> 31 ^
10 ^
求ASL,就是1*第一列的个数+2*第二列的个数+3*第三列的个数... 此题就是ASL=1*6+2*4+3*1
     图的深度优先与广度优先
            
1 -> 3 -> 2 -> 4 ^
2 ^
3 -> 4 -> 5 ^
4 ^
5 -> 2 -> 4 ^
我们来分析一下深度优先 第一步 我们给1标上了①,接下来给3表上了②,于是这时候跳到了
1 -> 3 -> 2 -> 4 ^
2 ^
3 -> 4 -> 5 ^
4 ^
5 -> 2 -> 4 ^
跳到了3处,往后给4表上③,这时候跳到
1 -> 3 -> 2 -> 4 ^
2 ^
3 -> 4 -> 5 ^
4 ^
5 -> 2 -> 4 ^
这时候跳到了
1 -> 3 -> 2 -> 4 ^
2 ^
3 -> 4 -> 5 ^
4 ^
5 -> 2 -> 4 ^
注意这时候,4后面没有任何东西,于是回退到上一个的最后继续标
1 -> 3 -> 2 -> 4 ^
2 ^
3 -> 4 -> 5 ^
4 ^
5 -> 2 -> 4 ^
标了于是跳到5处,给5的下一个2标上
1 -> 3 -> 2 -> 4 ^
2 ^
3 -> 4 -> 5 ^
4 ^
5 -> 2 -> 4 ^
到此为止全部的数都标完了 于是 深度优先遍历就是:1、3、4、5、2 接下来我们讨论一下广度优先遍历(简单的一批,就是从上到下,从左到右)
1 -> 3 -> 2 -> 4 ^
2 ^
3 -> 4 -> 5 ^
4 ^
5 -> 2 -> 4 ^
就是1、3、2、4、5
     二叉树的前中后序遍历
这类题目一般给前中/后中,来确定后/前序遍历,因为只有前中/后中才能确定唯一的二叉树,而前后序不能确定
    
(1)给定前序ABDGHCEFI与中序GDHBAECIF,写出后序遍历。 什么?我前中后序遍历都不知道是什么? 接下来我们来学习,前中后,简单来说,前中后描述的是根所在的位置 前(根左右)中(左根右)后(左右根) 上图中,前序遍历,也就是根左右,我们一步步来看 首先根:A 其次左:B //很好,接下来并不是右C,而是E,因为B也是一棵子树二叉树的根 也就是根B的左:E //接下开E又是一棵子树的根 也就是根E的左:F //因为F不再是任何树的根,这时候就可以到右 也就是根E的右:G //完成以后,我们回退到E?不,因为E的左右都写完了,于是我们再回退B,根B写了左但还没写右 也就是根B的右:D //此时D又是根,于是再写D的左 也就是根D的左:H //此时H不是根了,就可以写右 也就是根D的右:I //这时候,根D左右写完,回退B,B的左右也写完了,回退A,A只写完了左,右还没有写,于是 也就是根A的右:C //到此为止,我们的前序遍历就算写完了 前序:ABEFGDHIC
本来我想图省事,直接说,中后序同理的😏😏😏 上图中,中序(左根右)一步步来 左:F //这个左是整个树的左 根:E //这应该能理解吧?不能理解?废了,重开吧 右:G //此时,一棵子树以及遍历完了,按理说是返回到B,但是B是根,把B看成根,也就是E子树为左,D子树为右 也就是根:B 现在你肯定想说,接下来不就是D了吗?no、no、no,时刻谨记D是根,我们优先左再根再右,也就是根D树的左H 左:H 根:D 右:I 现在D树完成,回退到B,B树的左右也完成了,回退到A,A左完成,根据左根右,也就是左完成,现在是根,于是 根:A 右:C 这还用我说?🤏🤏🤏 中序:FEGBHDIAC
上图中,后序(左右根),一步步来说 左:F 右:G 根:E //现在B的左树已经写完,根据左右根规则,我们看B的右边的树,也就是以D为根的树,但是我们肯定不是先写D,要先左 左:H 右:I 根:D //好了,左右子树都写完,就写根了 根:B 现在总体看,A树的左子树写完了,再看A的右树 右:C 根:A 完结撒花😃😃😃😃😃 后序:FGEHIDBCA
我了个天,扫盲结束。回到一开始我们的题目 (1)给定前序ABDGHCEFI与中序GDHBAECIF,写出后序遍历。 你肯定有点思路,但我知道并不多,我们来用一种流氓的思路来解决他 画以下的类似二维坐标系的图 G D H B A E C I F G D H B A E C I F I F E C H G D B A 怎么样,画出上面的图,我们肯定知道A是根节点,别问为什么,因为前序遍历第一个肯定是总树的根节点啊😡😡😡😡 接下来我们画线 G D H B A E C I F G D H B A E C I F I F E C H G D B A 画这些点和线真的累死我啦🤨😥😭 看起来是不是很简单,但是如果你现在直接把我题目抄在草稿纸上,自己去画,绝对会有地方卡住 这就是为什么你有时候学习,以为学的很好了,看起来很简单,但是自己一动手就废了 注意看我画的图,你怎么知道A必须连接B?为什么不左连接D,右连B?等等问题 在这里我们遵循几个原则 1.这是二叉树,最多只有两个子树,也就是最多只能有两个孩子 2.左孩子的x不会大于根的x 3.根一定在孩子上方 4.与最近的结合 行了,以上是我瞎总结的,这些就是经验,教科书又臭又长,但是学懂了,根本不需要记住那么多东西 就是自然而然的。 然后就是喜闻乐见的,答题环节 后序遍历:GHDBEIFCA //你可能还是困惑为什么E在C前面,I在F前面,真的想一巴掌拍晕你,左右根,A的右树开始是C,但是C是A右子树的根啊,所以开始E,然后C的右,右F也是一棵子树,F也是根所以先I再F啊 (2)给定后序ACDBGIHFE与中序ABCDEFGHI求前序遍历 在解答这道题前别着急写,别以为学了点东西就以为天下无敌了,乱拳打不死老师傅 仔细想想中序遍历不管他,我们前一题给的是前序遍历,在y轴上当然A最在最上面,因为A是全部的根 但是后序呢?最后一个字母E才是根,如果还按之前的y轴排,E在最下面,那成啥了? A B C D E F G H I A B C D E F G H I A C D B G I H F E 按照同样的思路连线,你可以自己尝试一下,再来看我画的 A B C D E F G H I A B C D E F G H I A C D B G I H F E 我知道你如果自己试了的话,有可能在C为什么不连A,而是B连A,这个问题在于,B作为一个根,你将其从上到下看成一个金箍棒 如果C连的左子树为A,那么A大概率在B这根金箍棒的右边而不是左边 下面是题目的答案 前序遍历:EBADCFHGI 一开始学习的时候我问过一个问题: 第三中,给定前后序,怎么求中序啊?(不知道你会不会有这种想法) 还记得我们一开始说的吗?只有前中以及后中才能确定一棵树,而前后并不能确定唯一的一棵树
     常见的几种排序
    希尔排序、快速排序、堆排序、归并排序、基数排序、折半查找
       
(1) 归并排序 对{40,24,80,39,43,18,20,10,90,70} 进行从小到大排序过程(两两分组比较) [40] [24] [80] [39] [43] [18] [20] [10] [90] [70] [24,40] [39,80] [18,43] [10,20] [70,90] [24,39,40,80] [10,18,20,43] [70,90] [10,18,20,24,39,40,43,80] [70,90] [10,18,20,24,39,40,43,70,80,90] 这应该看得懂吧?一目了然了都😏😏
(2) 快速排序 对{46,58,15,45,90,18,10,62}进行快速排序 以第一个数为基准提出来 ___ 58 15 45 90 18 10 62 ___ 58 15 45 90 18 10 62<-- 这时候比较最后一个数与第一个数46的大小,62>46(不动) ___ 58 15 45 90 18 10<-- 62 这时候比较向前移动一位比较10和46大小,10<46(将10移动到46原本在的第一位) 10 58 15 45 90 18 ___ 62 这个时候比较的指向是从前面也就是10的后面开始 10 58<-- 15 45 90 18 ___ 62 58>45(将58移动到10原本的位置) 10 ___ 15 45 90 18 58 62 这时候再从后面也就是58的前面 10 ___ 15 45 90 18<-- 58 62 18<46(移动到58原本所在的前面) 10 18 15 45 90 ___ 58 62 再从18的后面开始比较 10 18 15<-- 45 90 ___ 58 62 15<46(不动) 10 18 15 45<-- 90 ___ 58 62 45<46(不动) 10 18 15 45 90<-- ___ 58 62 90>46(移动到后面的空位去) 10 18 15 45 ___ 90 58 62 到此为止全部元素都和46比较过了,于是把46放到空位处 10 18 15 45 46 90 58 62 此时46的位置已经确定了,46所在的位置就是这里,你可能也感觉出来了,这一轮比较就是把比46小的一股脑丢46前面,比46大的一股脑丢到后面去 既然46确定了,只需要比较两块内容就行了,也就是46前面的,和46后面的就行,还是按照之前的方法 ___ 18 15 45<-- 46 ___ 58 62<-- 哈哈哈,开启双开模式,前面提10,后面提90 45>10(不动);62<90(动) 10 18 15<-- 45 46 62 58<-- ___ 10 18<-- 15 45 46 62 58 90 我会把比较完成的数标红 10 18 15 45 46 62 58 90 10 ___ 15 45<-- 46 ___ 58<-- 90 10 ___ 15<-- 45 46 58 ___ 90 10 15 18 45 46 58 62 90
(3) 希尔排序 利用希尔排序对关键字系列{40,24,80,39,43,18,20,10,90,70}从小到大排序(增量5,3,1) 增量的意思就是每个x个为一个组比较大小 当增量为5时 40,24,80,39,43,18,20,10,90,70 4024,80,39,43,1820,10,90,70 402480,39,43,182010,90,70 402480 39,43,18201090,70 402480 39,43,18201090,70 最终我们得出增量为5的分组(颜色不同来区分) 402480 39,43,18201090,70 (把小的放前,大的放后就行) 182010 39,43,40248090,70 就是以上的第一次增量,当增量为3的时候 1820, 39,4340,24,8090,70 1820, 24,4340,39,8090,70 完成,以下是增量为1的 18,20, 24,43,40,39,80,90,70 18,20,24,39,40,43,70,80,90 完成
(4)基数排序 对{457,131,481,219,392,674,350,815,315}进行基数排序 Q_0 Q_1 Q_2 Q_3 Q_4 Q_5 Q_6 Q_7 Q_8 Q_9 350 481 392 674 475 815 315 137 219 接下来就是收集(从上到下,从左到右):{350,481,392,674,475,815,315,137,219} Q_0 Q_1 Q_2 Q_3 Q_4 Q_5 Q_6 Q_7 Q_8 Q_9 815 315 219 137 350 674 475 481 392 接下来就是收集(从上到下,从左到右):{815,315,219,137,350,674,475,481,392} Q_0 Q_1 Q_2 Q_3 Q_4 Q_5 Q_6 Q_7 Q_8 Q_9 137 219 315 350 392 475 481 674 815 接下来就是收集(从上到下,从左到右):{137,219,315,350,392,475,481,674,815} 至此你也看出来了,它到底是怎么运作来实现排序的 也不难,先个位数,再十位数,再百位数,一步步确保
(5)折半查找 对{5,13,17,29,37,41,61,79,89}利用折半查找值为29的元素 _0_ _1_ _2_ _3_ _4_ _5_ _6_ _7_ _8_ 5 13 17 29 37 41 61 79 89 low mid high 我们规定了首尾都有一个指针,其次mid的指针由下面式子给出 mid=low+high2=0+82=4 是的,折半的想法,此时mid指向的值为37比我们要找的值29大 这就是说明我们的中间的指针是比这个大的(😏废话) 所以我们把高指针的指向重新定义为mid(中间指针)的前一个 如果你对为什么是前一个没有疑虑,那么你很聪明 因为我们比较的就是中间指针肯定是要么大要么小,如果相同的就结束了啊 所以这个相当于不属于我们下一次查找的范围了 _0_ _1_ _2_ _3_ _4_ _5_ _6_ _7_ _8_ 5 13 17 29 37 41 61 79 89 low mid high mid=low+high2=0+32=1(向下取整) mid指向的是13,比29小,我们把low(小)指针改为mid(中间指针)的后面一个 _0_ _1_ _2_ _3_ _4_ _5_ _6_ _7_ _8_ 5 13 17 29 37 41 61 79 89 low mid high mid=low+high2=2+32=2(向下取整) 此时mid指向的是17还是比29小,于是把low指针指向mid的下一个 _0_ _1_ _2_ _3_ _4_ _5_ _6_ _7_ _8_ 5 13 17 29 37 41 61 79 89 low mid high mid=low+high2=3+32=3 bingo!mid指向29,刚好是我们要找的,好吧,麻烦是麻烦了点 但是编程实现起来比其他简单啊🤨
(6)堆排序(大根堆/小根堆) 对{5,1,6,9,2}如何构造大/小根堆 怎么说呢,就是大小根堆简单就是根是最大/小的就是 我们操作的步骤第一步就是将给出的数列按完全二叉树排列(完全二叉树不知道的话那,废了) 我们这样5个数,我们取一半(向下取整) 52=2 我们从第二个数开始即1【2】开始比较它与它的左右孩子们的大小,大的放根上就行 然后再第1个数即5【1】比较它与它的左右孩子大小 以上就是大根堆的构造过程,小根堆同理但是这只是简单数字比较少的时候的情况 一旦数字过多就会出现,一些没讲过的情况例如 {9,43,-54,4,-13,10,36}(初步一看就不是最大根堆/最小根堆,因为第一个数既不是最大也不是最小) 我们构造大根堆,第一步还是总个数/2 72=3(向下取整) 比较第三个与它的左右孩子的大小,大的放根上 接着比较第二个数与它的左右孩子的大小,大的放根上 接着比较第一个数与它的左右孩子的大小大的放根上 OK,此时就是最大根堆了,哎呀呀,我想说的那种情况没有发生, 就是那种像最后一步的时候,9和43不是换了嘛,9到新位置以后如果它的左右孩子有比它小的,也要再调换 然后就是仅仅这样不算是堆排序,这一步只是把最大值选出来了 我们可以通过移除最大值,来继续排但这样多此一举 不如直接让这个根(最大值)和我们的第七个数(大概率最小值)调换 调换以后,我们可以忽略它,然后重新排序 按照我们说的,继续 嘿嘿 62=3 还是比较第三个数和它的左右孩子,但是这次,它只有左孩子了 然后就是第二个数和它的左右孩子,9还是最大的,然后我们比较 第一个数,发现36是最大的调上去 到这里并没有结束,我之前说的情况发生了 就是第六个数10比第三个数(掉下来的)-54大,此时同样需要换 最终就是 然后就是再把根36和最后位第六位调换,然后重新排,以此往复 直到排完就是一个以完全二叉树的从小到大的排序了 小根堆是同理的。(暂时更到这里,累死我了😭😣🤤😬😰😨🥶🥵🥴😵‍💫🤧🤮🤢🫵😗)
    顺序存储线性表的操作(初版)纯手打(原创)
	
        /*
* 线性表的ADT(抽象)
* 1.初始化。2.增删改查。3.判空。4.销毁。
* InitList(&L) 初始化
* ListInsert(&L,i,e) 插入操作,因为要修改表所以传入其本身
* ListDelete(&L,i,&e) 删除操作,因为也要删除表中元素所以传入其本身
* ListAlert(&L,i,e) 修改操作,因为同样修改表中元素所以传入其本身
* ListGetElem(L,i) 获取操作,因为并不需要修改表,所以只需要副本即可
* ElemIsNull(L) 判空操作,同样因为不需要修改表,所以只需要副本即可
* GetLength_List(L) 获取表长 同理
* DestroyList(&L) 销毁操作,因为要销毁表本身,所以要传入本身
* ShowList(L)
*/
#include< stdio.h >
#include< iostream >
#include< stdlib.h >//开辟内存空间必需的头文件
using namespace std;

//线性表的实现
//1.有限的序列
//2.序列中的每个元素都有唯一的前驱和后继,除了开头和结尾两个节点
//3.逻辑顺序与物理顺序相同
#define MaxSize 30//定义线性表的最大长度
//线性表的存储类型描述
typedef struct
{
	int data[MaxSize];
	int length;
}List;//List是别名 是整个结构体定义语句的替代者

bool InitList(List& L);
int ListInsert(List& L, int i, int e);
bool ListDelete(List& L, int i, int& e);
bool ListAlert(List& L, int i, int e);
int ListGetElem(List L, int i);
bool ElemIsNull(List L);
int GetLength_List(List L);
void DestroyList(List& L);
int ShowList(List L);

bool InitList(List &L)//初始化
{
	for (int i = 0; i < MaxSize; i++)
	{
		L.data[i] = 0;
		L.length = 0;
	}//一般初始化都是这样吧?如果没有赋值就会出现混乱
	return true;

}
int GetLength_List(List L)//获取表长
{
	//首先这个函数我本来不想添加的,因为结构体中的一个表长元素已经定义了表长
	//实现的角度来看就是将,结构体中的length赋值返回
	return L.length;
	//其实我是很纠结要不要减一的,被题目骗怕了
	//但是仔细一想,表长就是元素的个数,而不是数组小标
}
int ListInsert(List &L, int i,int  e)//插入
{
	//插入的算法的实现,就是传入插入的位置以及插入的元素值
	//其实我们拿到了表的本身也好改
	//我们先遍历到需要插入的,nonono,我突然想到,这位置不就是数组的下标-1吗?我把链表搞混了
	//但是我们的基本盘不变,要让程序变得壮硕
	//我们还要判断表能不能插入,有没有位置
	while(i <= 0 || i > MaxSize)
	{
		printf("插入输入不合法!请重新输入\n");
		scanf_s("%d%d", &i, &e);
	}
	if (L.length == MaxSize)
	{
		return 1;//1代表表现在已经不能插入数据了
	}
	//接下来我们就可以插入操作了记住数组的插入等,是需要将其后面的元素全部后移的
	//此时我们的插入位置i,并不是下标,而是下标+1=插入位置i
	for (int j = L.length -1 ; j >= i - 1;j--)
	{
		L.data[j + 1] = L.data[j];//我们这时候执行的是将插入位置(包括插入位置)的元素全部后移
	}
	//后移完成以后,我们将元素插入
	L.data[i - 1] = e;
	L.length++;//记得将长度加一
	return 2;//2代表程序已经成功插入元素
}

bool ListDelete(List &L, int i, int &e)//删除
{
	//表的元素删除操作,这不是删除表,而是删除表中的元素
	//删除是和增加反着来的,就是删了以后它后面的全部元素前移
	//第一步我们还是健壮性
	if (!ElemIsNull(L))
	{
		return 0;//返回0代表的是表是空的,没办法进行删除
	}
	//删除的位置一定是在表长范围内的
	if (i > L.length)
		return 0;
	//第二步就是删除了,我们还是先前移,再删除
	//等等,你有没有发现,我既然前移了,我直接把你覆盖不就是删除了?
	//牛逼,我们先把元素传出去
	e = L.data[i - 1];
	for (int j = i - 1; j < L.length - 1; j++)//为什么这里是表长 -1 ,而且还是小于号?就是我们不是想前移动吗?
		//假如此时j是倒数第二个,j++不就是倒数第一个了,这就是最后一次循环

	{
		L.data[j] = L.data[j++];
	}
	return 1;//成功删除返回一
}

bool ListAlert(List&L,int i,int e) //修改
{
	//判断表是否空,同时,修改的这个元素是否在表中,也就是这个删除的位置一定是小于等于表长的
	if (!ElemIsNull(L))
		return false;
	if (i > L.length)
		return false;
	//表的修改,我有点感觉多余了,但是这也是我学数据库得来的,增删改查一个不少
	L.data[i - 1] = e;
	return true;//修改成功返回真
}
int ListGetElem(List L,int i)//获取元素值
{
	//同样判断表空以及位置是否越界
	if (!ElemIsNull(L))
	{
		printf("表空\n");
		return 0;
	}
	if (i > L.length)
	{
		printf("你要删除的不存在\n");
		return 0;
	}
	//直接返回就行了
	return L.data[i - 1];
}
bool ElemIsNull(List L)//判空
{
	if (L.length == 0)
	{
		return false;
	}
	if (L.length != 0)
	{
		return true;
	}
}
void DestroyList(List &L)//销毁
{
	//销毁的操作,但是这个不是动态分配的内存所以不能手动销毁
	//这个是栈上的内存

}
int ShowList(List L)
{
	if (!ElemIsNull)
	{
		printf("表是空的或者不存在\n");
		return 0;
	}
	else {
		for (int i = 0; i < L.length; i++)
			printf("|%d|\n", L.data[i]);
	}
	return 1;

}


int main()
{
	int i = 0;
	int j=0, e=0;
	List L;
	printf("请选择你的操作:\n1.初始化\n2.增加元素\n3.修改元素\n4.删除元素\n5.查找元素\n6.打印所有元素值\n7.结束\n");
	scanf_s("%d", &i);
	while (i != 7) {
		switch (i) {
		case 1:

			if (InitList(L))
			{
				printf("初始化成功\n");
			}
			break;
		case 2:

			printf("请输入插入位置以及值\n");
			scanf_s("%d%d", &j, &e);
			ListInsert(L,j,e);
			break;
		case 3:

			printf("请输入需要修改的元素位置\n");
			scanf_s("%d", &i);
			printf("请输入需要修改的值\n");
			scanf_s("%d", &j);
			ListAlert(L, i, j);
			break;
		case 4:

			printf("请输入需要删除的位置\n");
			scanf_s("%d", &j);
			ListDelete(L,j,e);
			printf("第%d个元素值%d删除成功\n", j, e);
			break;
		case 5:

			printf("请输入你要查找的第几个元素\n");
			scanf_s("%d",&j);
			e = ListGetElem(L, j);
			printf("第%d个元素的值为%d\n",j,e);
			break;
		case 6:
			ShowList(L);
			break;
		case 7:
			return 0;
			break;
		}
		printf("请选择你的操作:\n1.初始化\n2.增加元素\n3.修改元素\n4.删除元素\n5.查找元素\n6.打印所有元素值\n7.结束\n");
		scanf_s("%d", &i);
	}
	return 0;
}
    

    后缀表达式求值与转换
    四则运算表达式转化为后缀表达式的规则:
    
        1.数字直接写
        2.运算符号如果级别高于栈顶的,直接入栈写上,如果等于或者低于栈顶的让栈顶先出,自己再进去
        3.左括号不管入,遇到右括号,将左括号上面的所有出栈
    
    我们来举个例子:9+(3-1)×3+10
    数字直接写:9
    然后就是+号,符号入栈
	
    
    
    
   
        
        
        
        
        
        
        
        +
        
    
    然后就是(入
    	
    
    
    
   
        
        
        
        
        
        
        
        +
            (
        
    
    9+(3-1)×3+10,然后就是3,直接写
    93
    然后就是-入
     	
    
    
    
   
        
        
        
        
        
        
        
        +
            (
            -
        
    
    9+(3-1)×3+10,然后就是1,数字直接写
     931
    然后就是)右括号,你瞧,直接打包出来,其实也没有就一个-号,收获不大啊,竹篮打水一场空
     931-
       	
    
    
    
   
        
        
        
        
        
        
        
        +
        
    
    然后就是x,和此时的栈顶+比较,谁牛逼,显然x牛点,所以入栈
           	
    
    
    
   
        
        
        
        
        
        
        
        +
            x
        
    
    9+(3-1)×3+10,然后就是3,数字直接写
    931-3
    然后就是+,比较栈顶的x,哦豁,没有它厉害,乖乖让x出来
    就是
    931-3x
    此时栈:
          	
    
    
    
   
        
        
        
        
        
        
        
        +
        
    
    还没有完,还要继续比较现在的栈顶,是+和即将入的+是相等的,让栈顶出来
    931-3x+
           	
    
    
    
   
        
        
        
        
        
        
        
        
    
    这个时候,我们的+再入栈
      931-3x+
           	
    
    
    
   
        
        
        
        
        
        
        
                 +
        
    
    9+(3-1)×3+10,然后就是10,数字直接写
    931-3x+10
    然后没了,栈里面的给我滚出来吧
    931-3x+10+
    
以上就是中转后缀表达式,下面讲讲如何模拟计算机一样,直接计算后缀表达式 1.当是数字的时候直接入栈 2.当是运算符号的时候 3.就将栈的最上面两个数拿出进行运算 后 再将结果进栈 记住(栈顶元素永远在运算符号的右边) 看9 3 1 - 3 * + 10 2 / + 9 3 1依次入栈,没有异议 9 3 1 接下来是-,取出栈顶的两个元素,分别是1 3,1是栈顶,放-号的右边 就是3-1=2 将2入栈 9 2 9 3 1 - 3 * + 10 2 / +接下来是 3 入栈 9 2 3 9 3 1 - 3 * + 10 2 / +接下来是*,取出来运算 2*3=6,即便是*这种不在意次序的,你也务必养成习惯将栈顶放右边 6入栈 9 6 9 3 1 - 3 * + 10 2 / +接下来是+,取出运算 9+6=15,15入栈 15 9 3 1 - 3 * + 10 2 / +接下来是10,数字入栈 15 10 9 3 1 - 3 * + 10 2 / +接下来是 2入栈 15 10 2 9 3 1 - 3 * + 10 2 / +接下来是/,取出两个运算 10/2=5入栈 15 5 9 3 1 - 3 * + 10 2 / +接下来是+,取出来运算 15 + 5 = 20 好了,这就是最后结果了,不信你自己算嘛,给你9+(3-1)x 3+10/2
    时间复杂度、空间复杂度

#include < iostream >
#include < vector >
using namespace std;
bool find(vector< int > &nums);
int main()
{
    //时间复杂度,假设输入的数据量大小为N
    //时间复杂度统计的是【计算的操作数量】而不是【运行的绝对时间】
    //其次【计算的操作数量】和【运行的绝对时间】的正相关(并不是相等)
    //再次,运行时间受到【运行环境、语言、处理器】的影响
    //⬯[最差]、⊝[平均]、Ω[最佳]
    vector< int > a;
    a.push_back(1);
    a.push_back(2);
    a.push_back(7);
    if (find(a))
    {
        cout << "找到啦!" << endl;
    }
    else
    {
        cout << "没有找到" << endl;
    }
}
bool find(vector< int > &nums)
{
    for (int i=0;i < nums.size();i++)
    {
        if (nums[i] == 7)
        {
            return true;
        }
    }
    return false;
}
//最好的情况就是【7,a,b,c,e,d,da,a,s,sa,】第一次就找到了
//最坏的情况就是整个数组都没有7,循环了一遍也没有
//常见的算法时间复杂度有:⬯(1)<⬯(log N)<⬯(N)<⬯(N log N)<⬯(N²)<⬯(2^N)<⬯(N!)
//⬯(1)表示运行次数不随输入数据大小N的变化而变化
int algo(int N)
{
    int a = 1;
    int b = 2;
    int x = a * b + N;
    return 1;

}
//⬯(N)运行次数和N大小是线性关系
int algo1(int N)
{
    int a = 0;
    for (int i = 0; i < N; i++)//运行了N次
    {
        a++;
    }
    return a;
}
//⬯(N²)表示运行次数和N还是线性关系,但是是N²的关系
int algo2(int N)
{
    int a = 0;
    for (int i = 0; i < N; i++)//此处运行了N次
    {
        for (int j = 0; j < N; j++)//又运行了N次
        {
            a++;
        }
    }//总体上运行了N的平方次
}
//⬯(2^N)是指数级,类似我们的细胞分裂,一分为二,二分为四,四分为八
int algo3(int N)
{
    if (N <= 0)
        return 0;
    int a = algo3(N - 1);//递归调用,自我递归叫做直接递归
    int b = algo3(N - 1);//这两个式子,就是让每一个都可以一分为二
    return a + b;
}
//⬯(N!)这个是一开始分裂的是N个,然后每一层分裂比上一层少一个
int algo4(int N)
{
    if (N <= 0)return 1;
    int a = 0;
    for (int i = 0; i < N; i++)
    {
        a += algo4(N - 1);//为什么这里用循环,因为循环的次数的外层循环×上内层循环的次数,每向内一层的循环次数-1就能达到我们想要的效果
    }
    return a;
}
//⬯(log N)对数阶和指数阶是相反的,指数阶是每轮分裂,这个是每轮结合,也就是八合四,四合二,二合一
int algo5(int N)
{
    int a = 0;
    float i = N;
    while (i > 1)
    {
        i = i / 2;
        a++;
    }
    return a;
}
//⬯(N log N),相信你已经明白了,不就是套个N的循环嘛,yes
int algo6(int N)
{
    int a = 0;
    float i = N;
    while (i > 1)//while循环次数为log N,因为每次减半
    {
        i = i / 2;
        for (int j = 0; j < N; j++)//循环次数为N
        {
            a += 1;
        }
    }
    return a;
 }
//------------------------------------------------------------------------
//空间复杂度
//栈帧空间
int test()
{
    return 0;
}
int algor(int N)
{
    for (int i = 0; i < N; i++)
    {
        test();//这个循环中每次调用函数test以后,栈帧空间就会被释放
    }
}//因此这个函数的空间复杂度为⬯(1)

//---------------------------------------

int algor1(int N)
{
    if (N <= 1)
    {
        return 1;
    }
    return algor1(N - 1) + 1;//注意这是个递归函数,每次使用了,栈帧空间是不释放的
}//因此空间复杂度为⬯(N)

    链表例题
     问题如下:我们实现图书的管理员手上有一张链表图书表,其中int为图书编号,现在我们需要倒序输出图书的编号
    
#include< stdio.h >
#include< iostream >
using namespace std;
struct book
{
	int head;
	book* prev;
	book* next;
};
int main()
{
	int a = 0;
	cout << "请输入你想要添加书本的数量" << endl;
	cin >> a;
	cout << sizeof(book) << endl;
	book* b = (book*)malloc(sizeof(book));

	book* p = b;
	for (int i = 0; i < a; i++)
	{
		int j = 0;
		cout << "请输入第"<< i+1<<"本书的编号" << endl;
		cin >> j;
		b->head = j;
		//book* temp = b;
		b->next = (book*)malloc(sizeof(book));
		b->next->prev = b;
		b = b->next;
		//b->prev = temp;
	}
	b = b->prev;
	cout << "书本倒序的编号为:" << endl;
	for (int i = 0;i < a;i++)
	{

			cout << b->head << endl;
			b = b->prev;

	}

	return 0;
}
        我们具体来分析一下思路,结构体定义,没话可说吧?我一开始只定义了后续节点的指针,导致我倒序输出,没办法,当然硬实现还是可以的,反正耍赖谁都会
        我是那种理论学的太多了,实践就傻眼了,结构体初始化,我竟然还是数组的那种思想
        就是我以为一开始就初始化好就行了,但是细看就是内存谁知道在哪里,怎么初始化?我这段话看起来可能有点语无伦次
        事实上也是如此,好了,就是说链表其实是动态的主动的给分配内存的,b->next = (book*)malloc(sizeof(book));的作用就是分配内存


    
    栈与队列
     栈是限定在表尾进行修改操作的线性表
    允许操作的一端为栈顶(top),另一端为栈底(bottom)
    
ADT 栈
Data
	 同线性表,元素具有相同类型,相邻元素具有前驱和后继关系
Operation
	InitStack(*S):初始化,建立栈
	DestroyStack(*S):若栈存在就摧毁
	ClearStack(*S):将栈清空
	StackEmpty(S):若栈为空,返回true,否则返回false
	GetTop(S,*e):若栈存在且非空,用e返回栈顶元素
	Push(*S,e):若栈存在,插入新元素e到栈中成为新栈顶
	Pop(*S,*e):删除栈S中栈顶元素,并用e返回其值
	StackLength(S):返回栈S的元素个数
endADT
        别觉得不重要,当你真的到那种境界的时候,看以上的东西就够了
        别问,问就是答:我当然没有到那种境界了
----------------------------------------
typedef struct
{
	int data[MaxSize];//栈
	int top;//栈顶指针
}stack;
-----------------------------------------