数据结构PTA全八章个人复现wp
纯个人wp,轻点骂,骂就是骂得对QWQ
第一章
线性结构:元素间一对一
非线性结构:集合,树(存在有2个以上前后驱的结点),图
数据元素:数据的基本单位
数据项:数据最小单位
数据结构:带有结构的各种数据元素的集合
逻辑结构是理论上的东西,是虚拟的
在此简单循环中x(n)和自增变量y的1/2在同一数量级
第二章
做完上述所有操作后p指向了hb的最后一个结点,此时只需连到ha的头上即可,然后返回题目要求的头指针ha
存储密度:结点数据所占空间/结点占的空间
顺序表=1
链表<1(还有指针域)
单链表先插后断(先断就会找不到next结点)
s连到目标结点,目标结点的尾结点连回s,p断链并连到s,s的尾指针连回p
用q指向想删的结点,完成头结点接到第三个结点,此时需注意pq是否指向同一个结点,若是则要更新尾指针p使其指向头结点
因为是顺序存储,所以按数组想就行,直接可访问结点,增加结点需一个简单循环
同上一题
注意本题说的是在最后插入和删除,所以顺序表也能以O(1)完成
C:遍历肯定是一个N的循环,求第i个结点要循环i次,取最坏是n,所以也是O(N)
AD:知道单链表结点的地址就像数组知道下标一样,所以是O(1)
查找最后一个结点,最节省时间的肯定是知道头结点的双循环链表,因为可以通过头结点的前指针直接访问尾结点
两个要求:O(1)和空间最小
空间小:不用双链表
同时O(1):必须是知道尾指针的循环链表
查找已知数据域的结点显然是O(1)
t连上去,头结点改变,更新头结点
s的尾指针连回p,s连到目标结点(p的下一个结点),由p访问到目标结点并使它连回s,最后把p连到s
(buhui)
第三章
连续进栈,且先进后出
顺序栈是一种基于数组的数据结构,其实现方式是利用一个一维数组存储栈中的元素,同时使用一个变量top来指示栈顶元素在数组中的位置。当进行进栈和出栈操作时,只需要改变top的值即可。
因此,对于大小为m的顺序栈而言,进栈和出栈操作都不应受到操作次数的限制,选F
经典节约空间的操作,两个栈头对头,即栈底分别在两端
顺序结构下
除非1第一个出栈,不然1不可能在2的前面出栈
链式存储是动态申请内存
栈只能在栈顶进行
通常使用堆栈来处理…
循环队列和普通队列相比,它的主要优势之一就是在执行出队操作时可以避免大量元素的移动。这是因为进行出队操作时,只需要将队头指针front往后移一位,就可以删除队头元素
(rear - front + n) % n,其中n为环形队列的容量
反了,在队尾插入,队头删除(因为先进先出)
选F,(buhui)以下来自gpt的胡编:
该命题未必成立。
当删除 L 中某个物品后,可能会出现两种情况:一是 FF(L’) 等于 FF(L); 二是 FF(L’) 小于 FF(L)。如果 FF(L’) 等于 FF(L),则显然 FF(L’) 至多是 FF(L)。如果 FF(L’) 小于 FF(L),那么问题就来了。因为L’ 是从 L 中删除一个物品得到的,根据贪心算法中子问题最优解推出原问题最优解的思路,FF(L′) 的结果可能比删除前少一个箱子,也可能相同或多一个箱子。具体例子可能如下:
假设L为 [0.7, 0.5, 0.6, 0.4],用 First Fit 算法装箱需要的箱子数为 2(第一个箱子装 0.7 和 0.4,第二个箱子装 0.5 和 0.6)。如果从 L 中删除 0.7 这个物品,得到 L’ = [0.5, 0.6, 0.4],同样用 First Fit 算法装箱,则需要的箱子数仍然是 2(第一个箱子装 0.5 和 0.4,第二个箱子装 0.6)。因此,在这个例子中 FF(L’) 等于 FF(L),意味着 FF(L’) 至多是FF(L)。
但是,如果我们将L修改为L=[0.9, 0.5, 0.8, 0.7, 0.4],用 First Fit 算法装箱,则需要的箱子数为 3(第一个箱子装 0.9 和 0.1,第二个箱子只能装 0.5,第三个箱子装 0.8 和 0.7)。如果从 L 中删除任意一个物品后得到 L’,再用 First Fit 算法装箱,可能会有不同结果,例如:
如果删除 0.9 得到 L’=[0.5, 0.8, 0.7, 0.4],则需要 3 个箱子;
如果删除 0.5 得到 L’=[0.9, 0.8, 0.7, 0.4],则需要 4 个箱子;
如果删除 0.8 得到 L’=[0.9, 0.5, 0.7, 0.4],则需要 3 个箱子。
因此,在这个例子中,FF(L’) 不一定等于 FF(L),也可能大于FF(L)。所以我们无法肯定地说“FF(L′) 至多是 FF(L)”。
总之,具体情况具体分析,对于不同的 L 和 L’,它们与它们对应的 FF 值之间的大小关系不能一概而论,需要具体问题具体分析。
少用一个存储空间的方法是为了解决无法判别队列满还是空。
疑似选A,以下为好兄弟gpt的解释:
正确答案是A。在递归调用过程中,系统会为每一层递归调用创建一个栈帧,用于存储该层调用时需要保存的信息,例如返回地址、函数参数、局部变量等,在递归返回时将这些信息从栈帧中弹出,实现递归的回溯。
选项B是错误的,因为递归的调用顺序是按照先入后出的顺序进行的。
选项C也是错误的,因为递归的调用过程中,栈会一直处于被占用的状态,直到递归结束才可以释放,而不是在某个时刻就不再需要使用栈了。
选项D也是错误的,因为递归函数的调用次数并不完全取决于对应工作栈的元素个数,而是由程序控制流程和递归调用条件所限制。
在队尾插,先插,后更新尾指针
循环队列中队尾指针指向最后一个元素的下一个位置,队头指针指向第一个元素
需要注意的是,这是个队列,它的头是在尾的“后面”(以顺时针来说,因为头是删除元素的)
所以队满就是头被r+1在循环的意义上追上,即f=(r+1)%n
以特例刚初始化时(r=f=0)记,即r==f
删除:(f+1)%n
增加:(r+1)%n
n为总空间数
同上题
首先明白一点,线性队列的假溢出指的是队列中还有空闲空间却无法继续插入元素的情况,而(少用一个存储元素的)循环队列可以解决这个问题,让它可以继续插入元素。
所以引入循环队列的本质目的是为了克服假溢出,而非更好的利用空间,虽然它不可否认的使我们可以更好的利用空间,但这只是副作用。
另外:循环队列有无法判断队满的问题,而少用一个存储元素就是在解决这个问题。
具体:尾指针的next是头指针时,明明还空着一个位子却插不了元素(因为此时r+1=f,误判为了队满),而加个空位置可以起到“缓冲的作用”,从而解决该问题
第四章(水)
广义表的每个元素可以是原子也可以是另一个广义表,所以说它是递归的
稀疏矩阵不能用顺序结构存储且不递归
LOC(i,j)=LOC(1,1)+[n*(i-1)+j-1]\b
LOC(5,5)=10+[100(5-1)+5-1]2=818
大意了,没看到这题是以行为主。
BA+(4*10+7)*3,选A
注意:2-17错了,选B
100+(7*8+4)*3=280,故选B
注意:2-19又错了,选B
10*(1+1+1)2+32=66
第四章
可以每层都是1
满树
h=log(n+1)
格局小了,应该选F
用的是层次遍历
完全二叉树叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。没有左孩子,那么一定没有右孩子
还可以用数组顺序存储。
如果i=1,则它是根节点;
如果i>1,则它的父节点的下标为i/2(向下取整);
如果2i<=n,则它的左子节点的下标为2i;
如果2i+1<=n,则它的右子节点的下标为2i+1。
用二叉链表存储,注意是唯一对应的
二叉链表存储,根节点因为没有兄弟,所以一定没有右子树
根据哈夫曼树的生成过程(不断选两个最小的树合并)可知,哈夫曼树没有度数为1的结点。
设度数为2的结点有n个,由二叉树的性质(度数为0的比2的多一个)得,度数为0的结点有n+1个,所以总结点数为2n+1
不知道哪错了,选F
树的度:度数最大结点的度数
所以二叉树不一定度为2
二叉树是一种特殊的有序树
哈夫曼树上层也可能出现叶子结点
哈夫曼树是最优二叉树,不一定是完全二叉树,也不一定是平衡二叉树(左右子树高度差不超1)
这两种遍历方法中无论哪个,右孩子一定是最后一个,若想先序和中序相同,那一定是去掉左孩子,只留根节点和右孩子,顺序是先根后右
树没回路
要有相同的父节点才是兄弟
到达根结点路径上的所有结点是祖先
画个示意图易得要保留根结点和任一结点
懒得找规律了,直接代特值d=2。按照顺序存储二叉树的方法,假想一颗层数为3的满二叉树,取3号结点,它的父结点为1,左孩子是6,右孩子是7,代入特值,只有C正确
原本用栈是为了便于回溯,使用三叉链表可以直接通过双亲指针回溯
随便画棵树,标号,再转成二叉树,遍历一下发现应选中序
用先序遍历确定每次的根结点,去分割中序遍历的序列,可得二叉树为(CDB)(FE)A,易得后序序列
森林转二叉树,第一棵树的根结点作为总根结点,第一棵树作为左子树,第二三棵树合成一下,再作为右子树连到总树上。所以右子树的根结点是m2+m3
已知2(h-1)=N,则由等比数列求和得2h-1=2N-1
构建哈夫曼树,按查找长度及频次计算
(33+43+53+63+82+102)/(3+4+5+6+8+10)=2.5
若漏算权重,会算出C2.67
按图填回去字母,先序遍历一次得A
构建哈夫曼树,得树高5
WPL=(2+3)*4+3*6+2*(12+7+10)=96
总结点数为n,那就一共开了2n个指针,即总指针数2n。树的边数为n-1,即有n-1个非空的指针域,所以减一下得到空指针域是n+1个。2n-(n-1)=n+1
(总度数=边数=n-1)=(Σ=10+2+30+80)
解得总结点数n=123,减去非叶子结点数41,得叶子结点数为82
使结点数尽可能多且是完全二叉树,那么就要有6层,第5层有24=16个结点,其中有8个叶子结点,那么就有8个非叶子结点,所以第6层最多有2*8=16个结点。加上前面5层的总数25-1=31,得总结点数为47
考查二叉树转回森林,从左向右按左倾的方向搜索,每次只需要每次将向右的线连回对应的根结点
倒着看后序序列,去分割中序序列,还原出二叉树
总度数=n-1,n0=n-n1-n2-n3=8
后序遍历的最后一个元素是根结点
k是由中序序列得到的分解点,所以左子树在数组b中的终点是k-1,右子树的起点是k+1
由对数组b的分析可得左子树有k-1-s+1=k-s个元素,终点为i+k-s+1,同理右子树有t-k-1个元素,起点为j-t+k
根据频率构建哈夫曼树,左0右1,写出每个字母的哈夫曼编码
WPL=5*(2+3)+4*4+3*7+2*22+3*(10+15)+2*37=255
算成WPL了,W(T)=63
第六章
每条边都连接两定点产生两个度,总度和必为偶数
边数大于等于顶点数-1
举个三角形图的例子
设结点数为n,边数m,空间复杂度O(n+m)。只要两个节点之间有边相连,就会分配存储空间
设结点数为n,边数m,空间复杂度O(n2)
如果无向图G必须进行两次广度优先搜索才能访问其所有顶点,则G一定有2个连通分量(经过几次广度优先搜索,就有几个连通分量)
若c到a小于10,则会更新b到a的最短路径距离
假设刚开始有两条路径可以到达T,第一条上有1000条边,但总权值只有1,另一条只有一条权重为1.1的边,那么都加1时第一条路径实际增加了1000的权值
最小生成树指的是整个路径最小,不是单个结点的路径最小
直接记
若e是最短边,则在Kruskal算法中,它必定会被先选中,被加入到最小生成树中
深度优先算法
深度优先:前序遍历
广度优先:层次遍历
选B
选B
以下代码来自acm佬@如沐晨光_
# include<bits/stdc++.h>
using namespace std;
const int N = 510;
const int inf = 1e9;
int g[N][N];
bool sex[N];
int d[N];
vector<int> a, b;
signed main() {
int num;
cin >> num;
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++) {
if (i == j);
else
g[i][j] = inf;
}
for (int i = 1; i <= num; i++) {
char se;
int n;
cin >> se >> n;
if (se == 'F') sex[i] = 1;
for (int j = 1; j <= n; j++) {
int a, b;
scanf("%d:%d", &a, &b);
g[i][a] = min(g[i][a], b);
}
}
for (int k = 1; k <= num; k++)
for (int i = 1; i <= num; i++)
for (int j = 1; j <= num; j++) {
g[i][j] = min(g[i][k] + g[k][j], g[i][j]);
}
for (int i = 1; i <= num; i++)
for (int j = 1; j <= num; j++)
if (sex[i] != sex[j])
d[i] = max(d[i], g[j][i]);
int d1 = inf, d2 = inf;
for (int i = 1; i <= num; i++)
{
if (sex[i]) d1 = min(d1, d[i]);
else d2 = min(d2, d[i]);
}
for (int i = 1; i <= num; i++)
{
if (sex[i]) continue;
if (d[i] == d2) a.push_back(i);
}
for (int i = 1; i <= num; i++)
{
if (!sex[i]) continue;
if (d[i] == d1) b.push_back(i);
}
swap(a,b);
printf("%d",a[0]);
for(int i=1;i<(int)a.size();i++)
printf(" %d",a[i]);
puts("");
printf("%d",b[0]);
for(int i=1;i<(int)b.size();i++)
printf(" %d",b[i]);
}
第七章(摆)
FTTTTF
BACDC DBBCA CD
转载自https://blog.csdn.net/BLADCS/article/details/105308961
bool Insert( List L, ElementType X ){
if(L->Last==MAXSIZE-1) return false;
if(L->Data[L->Last] > X){
L->Data[L->Last+1]=X;
L->Last++;
return true;
}
int tp=0;int find=0;int tmp=0;
for(int i=0;i <= L->Last;i++){
if(L->Data[i] == X) return false;
if(L->Data[i] < X && find==0){
tp = L->Data[i];
L->Data[i]=X;
find=1;
}
if(find==1){
tmp=L->Data[i+1];
L->Data[i+1]=tp;
tp=tmp;
}
}
L->Last++;
return true;
}
第八章
希尔选择冒泡插入快速排序O(n2),堆归并O(nlogn)
对大文件排序(外部排序)时,多使用路归并排序
例如有一个含10000个记录的文件,首先通过10次内部排序得到10个初始归并段R1~R10 ,其中每一段都含1000个记录。然后对它们作如图所示的两两归并,直至得到一个有序文件为止。
希尔排序和堆排序利用了顺序存储的随机访问特性
快速排序的基本思想是分治思想,每次选择一个枢轴元素,将表中其他元素分成左右两部分,使左边部分的元素都小于等于枢轴元素,右边部分的元素都大于枢轴元素,然后递归地对左右两部分进行快速排序,直到所有子问题都变成了规模为 1 的问题为止。
如果原始数据已经有序,则每次选取的枢轴元素都会是当前序列中的最大或最小值,导致每次只能减少一个元素,执行效率极低,时间复杂度退化为 O(n2)。
快速排序每趟确定的是枢轴元素,同时受到初始序列是否有序影响
n不论是左子树还是右子树,n的父结点一定是 [n/2],注意中括号的取整规则,正数就是下取整
那么 [n/2] 这个结点还是有子结点的,从 [n/2] + 1 开始一直到 n 都是叶子结点,叶子结点就可能会是最大值
快速排序:平均O(nlogn),最坏O(n2)
最后一个元素正好应该排最前面,插入排序最后一趟所有元素都要后移
希尔排序每趟排好的是“内序”
插入排序通过构建有序序列,让未排序数据插入
选择排序很呆,不管初始有序无序都要全跑一遍
而冒泡排序如果初始有序,则一个泡也冒不出来,即一次可以结束
在这4种排序算法中,需要的辅助空间最大的是归并排序。因为归并排序需要用到 O(n) 的辅助空间来存储每一次合并操作所需的过程值,以及最后合并时临时存储所有数组元素的辅助数组。而选择排序和快速排序不需要额外空间,希尔排序只需要O(1)的额外空间(也有一些变种需要较多辅助空间)
不稳定的排序:选择,快速,希尔,堆
稳定的排序:插入,气泡,归并
O(nlogn)的:归并,堆,希尔,快速
注意到4,8,15的位置改变方式,显然是希尔排序
选最前面的10个数,只有冒泡和堆排序可以10次解决问题,同时堆排速度快
快速排序:以28为界分成两部分,前小后大,然后取r,f指针,从后向前搜索,当f的值大于r则交换,并换一头搜索,直到r,f重合,此时一次划分结束
小顶堆:放入二叉树后,所有分支<=孩子结点关键码
尽管初始有序也要全部循环完
堆排序和冒泡排序里选到更快的堆排序
初始时完全逆序,比较总数即为n-1加到1
归并排序每趟排的是子序列中的内序
初始逆序,冒泡排序比较n(n-1)/2次
选择排序:直接选择,堆排序
快速排序要快速确定子区间,所以要用到随机访问特性,即顺序存储
归并和直接插入稳定,直接插入O(n2),二路归并O(nlogn)
按快速排序跑一趟发现左右指针在81相遇,即枢轴元素是81,选D