【CS基础】数据结构笔记

本文综述了数据结构中的关键算法(如最小生成树、排序和查找)、时间复杂度与空间复杂度、B树与哈希,以及计算机网络的数据链路层、网络故障排查和编译原理的基础知识点。涵盖了排序算法、图论、哈希表、二叉树、栈与队列等核心概念,并讨论了NP问题、矩阵运算和网络通信原理。
摘要由CSDN通过智能技术生成

系列文章目录



前言


提示:以下是本篇文章正文内容,下面案例可供参考

-+# 1.数据结构
考试范围
线性表:线性表的定义与基本操作,线性表的存储结构以及基本操作;
堆栈与队列:
堆栈与队列的基本概念与基本操作、存储结构以及基本操作;
堆栈和队列的算法应用;
树与二叉树:
树,二叉树,完全二叉树,满二叉树;
二叉树的顺序存储与链表存储;
二叉树的前序遍历、中序遍历、后序遍历和按层次遍历;
图:
图的基本概念,图的存储结构;
图的遍历;
查找:
查找表的概念;
静态查找以及性能分析;
二叉排序树查找与分析;
内部排序:
插入排序法(直接插入排序,折半插入排序);
选择排序法(简单选择排序,树形选择排序,堆排序);
归并排序法;

1.0 数据结构&物理结构

数据的逻辑结构包括4种,
(1)集合:数据元素之间除了有相同的数据类型再没有其他的关系
(2)线性结构:数据元素之间是一对一的关系
(3)树形结构:数据元素之间是一对多的关系
(4)图状结构:数据元素之间是多对多的关系。
物理结构包括顺序存储结构和链式存储结构。

1.1 最小生成树算法有哪些和适用情况

kruskal算法比较常用,适合稀疏图(大多数题目的图都是稀疏图),其复杂度主要与边数m有关。而prim比较适合稠密图,因此常用邻接矩阵存图,复杂度主要与点数n有关。

1.1.1 什么是最小生成树

在给定一张无向图,如果在它的子图中,任意两个顶点都是互相连通,并且是一个树结构,那么这棵树叫做生成树。当连接顶点之间的图有权重时,权重之和最小的树结构为最小生成树!

1.1.2 prime算法

.清空生成树,任取一个顶点加入生成树

2.在那些一个端点在生成树里,另一个端点不在生成树里的边中,选取一条权最小的边,将它和另一个端点加进生成树

3.重复步骤2,直到所有的顶点都进入了生成树为止,此时的生成树就是最小生成树

1.1.3 kruscal算法

【主要是依次添加最小的且在不同联通分量的边】!!
Kruskal算法是一种贪心算法,我们将图中的每个edge按照权重大小进行排序,每次从边集中取出权重最小且两个顶点都不在同一个集合的边加入生成树中!注意:如果这两个顶点都在同一集合内,说明已经通过其他边相连,因此如果将这个边添加到生成树中,那么就会形成环!这样反复做,直到所有的节点都连接成功!

1.2 希尔排序问什么会有比较好的效果

【妙蛙】
至于楼主问为啥希尔能突破 O ( N 2 ) O(N^2) O(N2)的界,可以用逆序数来理解,假设我们要从小到大排序,一个数组中取两个元素如果前面比后面大,则为一个逆序,容易看出排序的本质就是消除逆序数,可以证明对于随机数组,逆序数是 O ( N 2 ) O(N^2) O(N2)的,而如果采用“交换相邻元素”的办法来消除逆序,每次正好只消除一个,因此必须执行 O ( N 2 ) O(N^2) O(N2)的交换次数,这就是为啥冒泡、插入等算法只能到平方级别的原因,反过来,基于交换元素的排序要想突破这个下界,必须执行一些比较,交换相隔比较远的元素,使得一次交换能消除一个以上的逆序,希尔、快排、堆排等等算法都是交换比较远的元素,只不过规则各不同罢了

1.3 解释迪杰斯特拉,解释弗洛伊德算法。

1.3.1 迪杰斯特拉算法

基本思想

  1. 通过Dijkstra计算图G中的最短路径时,需要指定一个起点D(即从顶点D开始计算)。
  2. 此外,引进两个数组S和U。S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),而U则是记录还未求出最短路径的顶点(以及该顶点到起点D的距离)。
  3. 初始时,数组S中只有起点D;数组U中是除起点D之外的顶点,并且数组U中记录各顶点到起点D的距离。如果顶点与起点D不相邻,距离为无穷大。
  4. 然后,从数组U中找出路径最短的顶点K,并将其加入到数组S中;同时,从数组U中移除顶点K。接着,更新数组U中的各顶点到起点D的距离。
  5. 重复第4步操作,直到遍历完所有顶点。

时间复杂度 O|V|^2
【Prime&迪杰斯特拉算法】

  • 使用情况:Prim算法用于构建最小生成树——即树中所有边的权值之和最小。例如,构建电路板,使所有边的和花费最少。只能用于无向图。而迪杰斯特拉可以有向图
  • 松弛操作:最小生成树只关心所有边的和最小,所以有v.key = w(u, v),即每个点直连其他点的最小值(最多只有两个节点之间的权值和)
    最短路径树只搜索权值最小,所以有v.key = w(u, v) + u.key,即每个点到其他点的最小值(最少是两个节之间的权值和)
    简单总结就是,Dijkstra的松弛操作加上了到起点的距离,而Prim只有相邻节点的权值。

【注意!!!】若需记录路径
迪杰斯特拉记录路径的办法就是开一个数组, 记录一下该节点的上一个节点是谁最后在递归输出就可以了
在这里插入图片描述

1.3.1 Floyd算法

弗洛伊德算法是解决任意两点间的最短路径的一种算法,可以正确处理有向图或有向图或负权(但不可存在负权回路)的最短路径问题,同时也被用于计算有向图的传递闭包。
【算法】
参考连接link
通过Floyd计算图G=(V,E)中各个顶点的最短路径时,需要引入两个矩阵,矩阵S中的元素a[i][j]表示顶点i(第i个顶点)到顶点j(第j个顶点)的距离。矩阵P中的元素b[i][j],表示顶点i到顶点j经过了b[i][j]记录的值所表示的顶点。

假设图G中顶点个数为N,则需要对矩阵D和矩阵P进行N次更新。初始时,矩阵D中顶点a[i][j]的距离为顶点i到顶点j的权值;如果i和j不相邻,则a[i][j]=∞,矩阵P的值为顶点b[i][j]的j的值。 接下来开始,对矩阵D进行N次更新。第1次更新时,如果”a[i][j]的距离” > “a[i][0]+a[0][j]”(a[i][0]+a[0][j]表示”i与j之间经过第1个顶点的距离”),则更新a[i][j]为”a[i][0]+a[0][j]”,更新b[i][j]=b[i][0]。 同理,第k次更新时,如果”a[i][j]的距离” > “a[i][k-1]+a[k-1][j]”,则更新a[i][j]为”a[i][k-1]+a[k-1][j]”,b[i][j]=b[i][k-1]。更新N次之后,操作完成!

1.4 排序算法

在这里插入图片描述

1.4.1 插入排序-直接插入排序

将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

1.4.2 插入排序-希尔排序

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
按增量序列个数k,对序列进行 k 趟排序;
每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

1.4.3 交换排序-冒泡排序

比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

1.4.4 交换排序-快速排序

【SPLIT算法】

def SPLIT(A,low,high):
    i = low
    x = A[low]
    for j in range(low+1,high+1):
        if A[j] <= x:
            i += 1
            if i != j:
                t = A[i]
                A[i] = A[j]
                A[j] = t
    t = A[low]; A[low] = A[i];A[i] = t
    w = i
    return [A,w]

遍历一次的时间复杂度是O(N),需要遍历多少次呢?至少lg(N+1)次,最多N次。则快速排序的时间复杂度在最坏情况下是O(N2),平均的时间复杂度是O(N*lgN)。

1.4.5 选择排序-直接选择排序

  • 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
  • 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  • 重复第二步,直到所有元素均排序完毕。

1.4.6 选择排序-堆排序

  • 创建一个堆 H[0……n-1];

  • 输出堆顶,将堆底送到堆顶,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;

  • 重复步骤 2,直到堆的尺寸为 1。

【构造堆】
思路:过程就像冒泡一样,从最序号最大的父节点即n/2向下取整开始,查看是否满足最大堆,如果不满足,则调整(调整之后,还要查看被调整的节点是否依然满足最大堆性质,如果不满足,则需要往下遍历调整,这部分在后面的举例中会有说明),如果满足,则继续查看前一个父节点是否满足,直接最终的0节点。

1.4.7 归并排序

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
自下而上的迭代;

  • 递归法(Top-down)
    申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
    设定两个指针,最初位置分别为两个已经排序序列的起始位置
    比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
    重复步骤3直到某一指针到达序列尾
    将另一序列剩下的所有元素直接复制到合并序列尾
  • 迭代法(Bottom-up)
    原理如下(假设序列共有n个元素):
    将序列每相邻两个数字进行归并操作,形成ceil(n/2)个序列,排序后每个序列包含两/一个元素
    若此时序列数不是1个则将上述序列再次归并,形成ceil(n/4)个序列,每个序列包含四/三个元素
    重复步骤2,直到所有元素排序完毕,即序列数为1

1.4.8 基数排序(Radix sort)

原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。

MSD:先从高位开始进行排序,在每个关键字上,可采用计数排序
LSD:先从低位开始进行排序,在每个关键字上,可采用桶排序

1.4.9 桶排序

桶排序**(Bucket sort)**或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是比较排序,他不受到O(n log n)下限的影响。

【复杂度】
平均时间复杂度:O(n + k)
最佳时间复杂度:O(n + k)
最差时间复杂度:O(n ^ 2)
空间复杂度:O(n * k)
稳定性:稳定
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

1.5 寻找中项和第k小元素

1.5.1 select快速选择算法

  • 当数据量少,可以直接装进数组的话,可以采用类似快排的思想,每一步都是把大于某值的数放在一边,小于某值的放在另一边,如果大数区间容量大于n,就在大数区间继续划分直到大数区间容量为n,否则在小数区间划分,直到小数区间的大数区容量为n-k;时间复杂度为O(n)
  • 这里用到的是递归,以5个为一组,找中项的中项,作为划分数组的标准。

1.5.2 序列输入时使用的 Heap-Select 算法

考虑一个输入序列,要求在序列输入完毕的时候得出这个序列的第 k 大(小)的元素.

  • 要选择第 k 小的元素时, 我们考虑用一个 k 大小的大顶堆. 对数组从头开始遍历(等价于数组线性输入), 头 k 个元素用于建立 k 大小的大顶堆. 对于从 k + 1 到 N 的元素. 当该元素小于堆顶元素的时候,将该元素插入到堆中,将堆顶元素出堆. 遍历(输入)结束后, 堆顶元素即为我们要找的元素.

  • 相应的选择第 k 大的元素时, 我们考虑用一个 k 大小的小顶堆.对数组从头开始遍历. 头 k 个元素用于建立 k 大小的小顶堆. 对于从 k + 1 到 N 的元素. 当该元素大于堆顶元素的时候,将该元素插入到堆中,将堆顶元素出堆. 遍历(输入)结束后, 堆顶元素即为我们要找的元素.

这样可得这个算法的时间复杂度为 O(k) + O(N * log k) ==> O(N * log k)

由于要调用空间构造堆,空间复杂度为 O(k)

关于这个算法的正确性,用归纳法, 从已经输入k的数组中挑选头k个最大(小)的元素。 然后继续下去即可。

1.6 解释一下什么是时间复杂度&空间复杂度

1.6.1 时间复杂度

  • 时间复杂度实际上以一种度量,并不是真正意义上的算法运行时间,相当于给你一把尺子去量一下这个算法的耗时他主要是为了描述,时间复杂度的关注点是算法中基本操作的重复次数,根据这个次数来估计算法耗时。同时为了简化问题,时间复杂度只考虑了最高项的阶数,因为问题规模足够大时其他项的贡献可以忽略。
  • 因为大O符号表示法并不是用于来真实代表算法的执行时间的,它是用来表示代码执行时间的增长变化趋势的。
  • 一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数,记作T(n)=O(f(n)),它称为算法的渐进时间复杂度,简称时间复杂度。

1.6.2 空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义。

1.7 B树是什么?主要作用是什么?

B-树中所有结点中孩子结点个数的最大值成为B-树的阶,通常用m表示,从查找效率考虑,一般要求m>=3。一棵m阶B-树或者是一棵空树,或者是满足以下条件的m叉树。
1)每个结点最多有m个分支(子树),且至多含m-1个关键字。如果是根结点且不是叶子结点,则至少要有两个分支,非根非叶结点至少有ceil(m/2)个分支,这里ceil代表向上取整。
2)如果一个结点有n-1个关键字,那么该结点有n个分支。这n-1个关键字按照递增顺序排列。
3)每个结点的结构为:
n k1 k2 … kn
p0 p1 p2 … pn
其中,n为该结点中关键字的个数;ki为该结点的关键字且满足ki<ki+1;pi为该结点的孩子结点指针且满足pi所指结点上的关键字大于ki且小于ki+1,p0所指结点上的关键字小于k1,pn所指结点上的关键字大于kn。
在这里插入图片描述
【上图中叶子结点都在第四层上,代表查找不成功的位置】
4)结点内各关键字互不相等且按从小到大排列。
5)叶子结点处于同一层;可以用空指针表示,是查找失败到达的位置。

1.7.1 插入

【新关键字总是插在叶子结点上】
参考链接link

  • (1)定位:插入位置一定是最底层某个非叶结点
  • (2) 插入:当插入关键字后,结点内关键字个数大于m-1,则需结点分裂

1.7.2 删除

在这里插入图片描述

1.8 B+树&哈希(散列)

1、在查询速度上,如果是等值查询,那么Hash索引明显有绝对优势,因为只需要经过一次 Hash 算法即可找到相应的键值,复杂度为O(1);当然了,这个前提是键值都是唯一的。如果键值不是唯一(或存在Hash冲突),就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据,这时候复杂度会变成O(n),降低了Hash索引的查找效率。所以,Hash 索引通常不会用到重复值多的列上,比如列为性别、年龄的情况等(当然B+tree索引也不适合这种离散型低的字段上);

  2、Hash 索引是无序的,如果是范围查询检索,这时候 Hash 索引就无法起到作用,即使原先是有序的键值,经过 Hash 算法后,也会变成不连续的了。因此

       ①、Hash 索引只支持等值比较查询、无法索成范围查询检索,B+tree索引的叶子节点形成有序链表,便于范围查询。

       ②、Hash 索引无法做 like ‘xxx%’ 这样的部分模糊查询,因为需要对 完整 key 做 Hash 计算,定位bucket。而 B+tree 索引具有最左前缀匹配,可以进行部分模糊查询

大多数场景下,都会有组合查询,范围查询、排序、分组、模糊查询等查询特征,Hash 索引无法满足要求,建议数据库使用B+树索引。

  2、在离散型高,数据基数大,且等值查询时候,Hash索引有优势。

1.8.1 B+树

参考链接link
在这里插入图片描述与B树区别
在这里插入图片描述
B+树实例
在这里插入图片描述

1.9 什么是NP难问题

P问题:有多项式时间算法,算得很快的问题。

NP问题:算起来不确定快不快的问题,但是我们可以快速验证这个问题的解。

NP-complete问题:属于NP问题,且属于NP-hard问题。

NP-hard问题:比NP问题都要难的问题。

著名的NP类问题:旅行家推销问题(TSP)。即有一个推销员,要到n个城市推销商品,他要找出一个包含所有n个城市的环路,这个环路路径小于a。我们知道这个问题如果单纯的用枚举法来列举的话会有(n-1)! 种,已经不是多项式时间的算法了,(注:阶乘算法比多项式的复杂)。那怎么办呢?我们可以用猜的,假设人品爆炸猜几次就猜中了一条小于长度a的路径,TSP问题解决了,皆大欢喜。可是,我不可能每次都猜的那么准,也许我要猜完所有种方案呢?所以我们说,这是一个NP类问题。也就是,我们能在多项式的时间内验证并得出问题的正确解,可是我们却不知道该问题是否存在一个多项式时间的算法,每次都能解决他(注意,这里是不知道,不是不存在)。

1.9.1 P类问题(是NP子集)

P类问题:存在多项式时间算法的问题。(P:polynominal,多项式)

1.9.2 NP类问题

NP类问题:能在多项式时间内验证得出一个正确解的问题。(NP:Nondeterministic polynominal,非确定性多项式)
P类问题是NP问题的子集,因为存在多项式时间解法的问题,总能在多项式时间内验证他。

1.9.3 NPC问题

3.NPC类问题(Nondeterminism Polynomial complete):存在这样一个NP问题,所有的NP问题都可以约化成它。换句话说,只要解决了这个问题,那么所有的NP问题都解决了。其定义要满足2个条件:

首先,它得是一个NP问题;然后,所有的NP问题都可以约化到它。

要证明npc问题的思路就是: 先证明它至少是一个NP问题,再证明其中一个已知的NPC问题能约化到它。

1.9.4 NP难问题

NP-hard Problem:对于这一类问题,用一句话概括他们的特征就是“at least as hard as the hardest problems in NP Problem”, 就是NP-hard问题至少和NP问题一样难。

NP-Hard问题是这样一种问题,它满足NPC问题定义的第二条但不一定要满足第一条(就是说,NP-Hard问题要比 NPC问题的范围广,NP-Hard问题没有限定属于NP),即所有的NP问题都能约化到它,但是它不一定是一个NP问题。
在这里插入图片描述

1.10 矩阵相乘的时间复杂度

  • 如果用朴素的算法,mxn的矩阵和nxk的矩阵相乘的运算量是O(mnk),原因是,计算结果是一个mk矩阵,这说明至少需要进行mk次运算,而每次运算还要进行n次的求和运算(左边的每一行*右边的每一列)
  • 当然,如果用并行计算的话,比如python里的tensor.dot函数替代for循环,时间复杂度会大大降低

1.11 KMP算法(用于字符串匹配)

1.11.1 暴力匹配

朴素匹配:这个方法也称为暴力匹配,因为他就是我们最容易想到的一种字符串匹配的算法。就是从源字符串开始搜索,若出现不能匹配,则从原搜索位置+1进行搜索。

那么它的时间复杂度就需要O(n*m),可以看出时间复杂度很高

1.11.2 KMP算法

参考链接link
利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量地移动到有效的位置。
【注意】当匹配失败时,j要移动的下一个位置k。存在着这样的性质:最前面的k个字符和j之前的最后k个字符是一样的。
如果用数学公式来表示是这样的
P [ 0 至 k − 1 ] = = P [ j − k 至 j − 1 ] P[0 至 k-1] == P[j-k 至 j-1] P[0k1]==P[jkj1]
在这里插入图片描述
txt 中匹配 pat,前者是在 pat 中匹配 pat[1…end],状态 X 总是落后状态 j 一个状态,与 j 具有最长的相同前缀。所以我把 X 比喻为影子状态,似乎也有一点贴切。

1.11 散列表

1.11.1 哈希函数。

  • 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。
  • 数字分析法:通过对数据的分析,发现数据中冲突较少的部分,并构造散列地址。例如同学们的学号,通常同一届学生的学号,其中前面的部分差别不太大,所以用后面的部分来构造散列地址。
  • 平方取中法:当无法确定关键字里哪几位的分布相对比较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为散列地址。这是因为:计算平方之后的中间几位和关键字中的每一位都相关,所以不同的关键字会以较高的概率产生不同的散列地址。
  • 取随机数法:使用一个随机函数,取关键字的随机值作为散列地址,这种方式通常用于关键字长度不同的场合。
  • 除留取余法:取关键字被某个不大于散列表的表长 n 的数 m 除后所得的余数 p 为散列地址。这种方式也可以在用过其他方法后再使用。该函数对 m 的选择很重要,一般取素数或者直接用 n。

1.11.2 处理冲突方法

  • 开放地址法(也叫开放寻址法):实际上就是当需要存储值时,对Key哈希之后,发现这个地址已经有值了,这时该怎么办?不能放在这个地址,不然之前的映射会被覆盖。这时对计算出来的地址进行一个探测再哈希,比如往后移动一个地址,如果没人占用,就用这个地址。如果超过最大长度,则可以对总长度取余。这里移动的地址是产生冲突时的增列序量。
    (包括线性探测法、平方探测法、再散列法
  • 链地址法:链地址法其实就是对Key通过哈希之后落在同一个地址上的值,做一个链表。其实在很多高级语言的实现当中,也是使用这种方式处理冲突的,我们会在后面着重学习这种方式。
  • 建立一个公共溢出区:这种方式是建立一个公共溢出区,当地址存在冲突时,把新的地址放在公共溢出区里。

1.12 二叉树

  • 满二叉树
    树每层都含有最多结点
  • 完全二叉树
    树中间无空隙
  • 二叉排序树
    左子树都小于根,右子树都大于根
  • 平衡二叉树
    树上任意结点左子树和右子树深度差不超过1
  • 性质:
    性质1:二叉树第i层上的结点数目最多为 2 i − 1 ( i ≥ 1 ) 2^{i-1} (i≥1) 2i1(i1)
    性质2:深度为k的二叉树至多有 2 k − 1 2^{k}-1 2k1个结点(k≥1)。
    性质3:包含n个结点的完全二叉树的高度至少为log2 (n+1)。
    性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。

1.13 栈的应用:括号匹配&表达式求值

  • 用一个名为 left 的栈代替之前思路中的 left 变量,遇到左括号就入栈,遇到右括号就去栈中寻找最近的左括号,看是否匹配:
  • 将中缀表达式转换成后缀式(逆波兰表达式)
  1. 从左到右读进中序表达式的每个字符。

  2. 如果读到的字符为操作数,则直接输出到后缀表达式中。

  3. 如果遇到“)”,则弹出栈内的运算符,直到弹出到一个“(”,两者相互抵消。

  4. “(”的优先级在栈内比任何运算符都小,任何运算符都可以压过它,不过在栈外却是优先级最高者。

  5. 当运算符准备进入栈内时,必须和栈顶的运算符比较,如果外面的运算符优先级高于栈顶的运算符的优先级,则压栈;如果优先级低于或等于栈顶的运算符的优先级,则弹栈。直到栈顶的运算符的优先级低于外面的运算符优先级或者栈为空时,再把外面的运算符压栈。

  6. 中缀表达式读完后,如果运算符栈不为空,则将其内的运算符逐一弹出,输出到后缀表达式中。
    -!!!!原表达式对应的表达式树的后序遍历对应后缀表达式

  • 通过后缀表达式计算表达式值:
    顺序扫描波兰表达式的每一项,数则进栈,运算符则连续从栈中退出两个操作数,并将计算结果重新入栈。

1.14 拓扑排序

十四、介绍一下拓扑排序以及是如何实现的?

拓扑排序可以决定哪些子工程必须要先执行,哪些子工程要在某些工程完成后才能执行。把以顶点为活动,边为活动间先后顺序关系的有向图成为顶点活动网,简称为AOV 网。一个AOV网应该是有向无环图,不应该存在回路,如果要存在回路的话则该回路上的所有活动都无法执行。在AOV网中如果不存在回路,则可以把所有的活动排列成一个序列,称该序列为拓扑序列,拓扑序列并不是唯一的。形成拓扑序列的过程称为拓扑排序。

拓扑排序的步骤:
(1)在有向图中任意选择一个没有前驱的节点输出
(2)从图中删去该节点以及与它相连的边
(3)重复以上步骤,直到所有的顶点都输出或者当前图中不存在无前驱的顶点为止,后者代表该图是有环图,所以可以通过拓扑排序来判断一个图是否存在环。

1.15 其他

1.15.1 矩阵的压缩存储

  • 对称矩阵、三角矩阵:直接存储矩阵的上三角或者下三角元素。注意区分i>=j和i
  • 稀疏矩阵:三元顺序表存储、

1.16 有序链表插入的时间复杂度

在一个具有n个结点的有序单链表中插入一个新结点并仍然保持有序的时间复杂度是:O(n)
有序顺序表,可以用二分查找,复杂度为o(lgn)
而本题中为有序单链表,需要遍历找到插入的位置,复杂度为O(n)

3.计算机网络

3.1 数据链路层的功能

3.2 网络不通怎么办(先ping一下)之类

5.编译原理

编译原理中处理int、char、定长数组、指针有什么区别?
1.参考link
从编译器角度分析C语言中数组名和指针的区别:
第二**,指针是一个变量,而数组名不是。数组名是数组的首地址**,即它本身就是一个地址,对应到汇编语言级别就是一个常量,一个固定的数(地址)。因此数组名不能进行++,–等运算。

在大多数编译器中,对数组的引用a[i]总是被编译器改写成*(a+i)的格式。也就是说,编译器每遇到a[i],都会把它当作*(a+i)来处理。我们都知道,addr表示内存中(addr)的位置存储的值,比如0×8048000就表示地址为0×8048000的内存中所存储的值。所以a[i]就表示a的值加上i所得到的数作为一个内存地址里面所存储的值。

,对于指针p,它是一个变量,其值存储在地址&p中,这个值在编译时是不能得到的。因为是变量,所以指针可以作为表达式中的左值操作,如++或--,而被认为是常量的数组名却不可以,正如你可以骑走一辆自行车,但是不能骑走一棵树。另外,C语言把数组下标改写成指针偏移量的根本原因是指针和偏移量是底层硬件所使用的基本模型。

第三,对数组的引用,如a[i],或(a+1),需要访存一次;而对指针的引用,如(p+1),需要访存两次。**

如果理解了第二条的解释,这个应该就不难理解。因为a被认为是常数,所以取*(a+1)的值只需将a所表示的常数加1,然后从得到的地址里访存取一次即可。而对于指针,需要先从&p这个地址里把p的值取出来,然后加1,再从得到的地址里访存取一次,一共需要两次访存。

第四,假设a是一个数组名,而p是一个指针,当你使用 a 和 &a 时,得到值是一样的,都是数组的起始地址。而使用 p 和 &p 时,得到的值是不一样的, p 表示指针 p 所指向的地址,而 &p 表示 p 这个变量的地址。再假设

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值