一、近两周的数据结构实验课:
①第15周数据结构实验课:
1、出栈序列合法性&栈的最小容量:
给定入栈序列,判定出栈序列的合法性&给定入栈与出栈序列判定栈的最小容量——解题思路:建立一个队列来保存入栈序列,建立一个队列保存出栈序列,建立一个空栈用作临时栈。
当然了,建立数组存储序列也可以,实际上操作还是和队列差不多的。
然后就是一个循环——每次从入栈序列队列取出元素入栈后,比较栈顶元素和出栈序列队列队头元素,如果相等,弹栈+出队。循环的跳出条件为任一队列为空,然后在循环跳出后判定临时栈是否为空。如果栈不为空,说明出栈序列不合法。
相对应的,对于栈最小容量的求解,只需要增加一个变量,每次有元素入栈之后都判定一下当前栈的容量和变量保存的容量的大小,如果当前大,则更新该变量,最后输出即可。
代码实现:
2、递归枚举所有的字母排列数:(输入的数字n代表需要枚举前n个字母)
#include<vector>
#include<iostream>
using std::cin;
using std::vector;
using std::cout;
char char_list[] = { 'a','b','c','d','e','f','g' };
bool visited[7] = { 0 };
//参数分别为:还需要枚举多少次字符,一次需要枚举多少个字符
vector<char>ans;
int index = 0;
void DFS(int n,int nums)
{
if (n == 0)
{
for (auto i : ans)
cout << i;
cout << "\n";
return;
}
for (int i = 0; i < nums; i++)
{
ans.push_back(char_list[i]);
DFS(n - 1, nums);
ans.pop_back();
}
}
int main()
{
int n;
cin >> n;
DFS(n, n);
}
如果需要祛重的话:
if (!visited[i])
{
//如果没访问过,则设为已访问,再接着这个点继续访问
visited[i] = 1;
ans.push_back(char_list[i]);
DFS(n - 1, nums);
ans.pop_back();
visited[i] = 0;
}
总之,重点从递过去的过程,以及归回来的过程(比如回溯过程的清除访问标记&数列pop)这两个过程来理解整个题目!
//扒答案之后得到的另一种解法:
用字符串处理,本质上也是和vector的处理思路类同。祛重也是同样的操作:
②第16周数据结构实验课:
1、接收层序遍历方式(上->下,左->右)的输入来构造树:
思路——按照层序遍历输出的模式来接受输入:
counts代表的是后续会输入的一个变量,它的值代表着将要输入的顶点数
int index = 0;
int counts;
void createTree()
{
if (!counts)
return;
queue<TreeNode*>Q;
char value;
cin >> value;
root = new TreeNode(value);
Q.push(root);
index++;
while (!Q.empty())
{
TreeNode*temp=Q.front();
Q.pop();
if (index >= counts)
break;
cin >> value;
temp->left = new TreeNode(value);
index++;
Q.push(temp->left);
if (index >= counts)
break;
cin >> value;
temp->right = new TreeNode(value);
index++;
Q.push(temp->right);
}
}
使用队列来保证这样的先后顺序---这就是BFS的思想,这一层的结点出队的同时,下一层对应的结点入队。画画这个队列的出入过程即可理解这个循环的操作!
2、关于递归的查错方式:
能不通过断点就不通过断点,最好是通过递归出口,加上模拟递过程&归过程(回溯过程很重要!!!)来思考递归书写上的问题(有时候还会抄错函数名,比如把inorder复制到postorder里面然后忘记改名)。如果实在要打断点来看,一定要尽可能的去看递归深度小的递归过程,比较好理解。
3、当采取下标的形式时,要注意转换思路,从传参为纯指针变为下标相关的参量:
#include<iostream>
#include<unordered_set>
using std::cin;
using std::cout;
using std::unordered_set;
struct TreeNode
{
int lowcast;
int left;
int right;
};
TreeNode* ans;
void postorderTraverse(TreeNode root)
{
if (root.lowcast == -1||root.lowcast==0)
return;
postorderTraverse(ans[root.left]);
postorderTraverse(ans[root.right]);
cout << root.lowcast << " ";
}
void preorderTraverse(TreeNode root)
{
if (root.lowcast == -1 || root.lowcast == 0)
return;
cout << root.lowcast << " ";
preorderTraverse(ans[root.left]);
preorderTraverse(ans[root.right]);
}
void inorderTraverse(TreeNode root)
{
if (root.lowcast == -1 || root.lowcast == 0)
return;
inorderTraverse(ans[root.left]);
cout << root.lowcast << " ";
inorderTraverse(ans[root.right]);
}
int main()
{
int N;
cin >> N;
ans = new TreeNode [N + 1];
for (int i = 1; i < N+1; i++)
{
int M;
cin >> M;
ans[M].lowcast = M;
cin >> ans[M].left;
cin >> ans[M].right;
}
//找根:
unordered_set<int>assist;
for (int i = 1; i < N+1; i++)
{
assist.emplace(ans[i].left);
assist.emplace(ans[i].right);
}
int get_lowcast;
for (int i = 1; i < N + 1; i++)
{
if (assist.find(ans[i].lowcast)==assist.end())
{
get_lowcast = i;
break;
}
}
inorderTraverse(ans[get_lowcast]);
}
※虽然但是,接收层序遍历的输入,明显可以用数组存储,再用数组作遍历操作:
所以第一个题的代码可以改成:
4、关于满二叉树的层高的问题——
※输入的所有的结点,都是幌子。只需要根据完全二叉树的性质来看即可:
③第17周数据结构实验课:
1.任意二叉树的层序遍历:
重点在于数组下标和顶点编号的一一对应(看清楚编号从0开始还是1开始!),采用结构体的方式来存储左右孩子的下标&自己的下标(val)
一一对应的时候,记得首先要对自己的下标进行初始化,同时根据题目的编号规则来决定从1开始还是从0开始
寻求根节点的菜逼解法:
发答案之后康康老师的优化解法!
紧接着就是基于队列实现的层序遍历解法——
2.小根堆的判定——不同于小根堆的生成,判定的话只需要递过程就可以结束(看看当前节点是不是比它俩孩子都小就够了)。而仅存在递过程时,往往只需要循环就可以解决。仅存在归过程的时候,或许仍然需要递归的方式来看
红框是由于下标从0开始才做了这样的(i+1)*2-1的操作,而下标从1开始的话,只需要简单粗暴的i*2&i*2+1即可。由此在这种数组存储的完全二叉树中,直接从下标为1的位置开始存储即可,免得不必要的麻烦。
3.最小堆的生成:(看标准答案&扒一扒AVL与红黑的修正思路!)
蠢逼解法:由于递过程和归过程都需要判断&修正,因此递归体的结构应当是:
递过程对当前节点的处理
递归调用函数
归过程的处理
于是:
void change_to_min(int lowcast)
{
if (lowcast > n)
return;
int index = 0;//0不换,1和左边换,2和右边换
int min=ans[lowcast];
if (lowcast * 2 <= n)
{
if (ans[lowcast * 2] < min)
{
min = ans[lowcast * 2];
index = 1;
}
}
if (lowcast * 2 + 1 <= n)
{
if (ans[lowcast * 2 + 1] <min)
{
min = ans[lowcast * 2 + 1];
index = 2;
}
}
if (!index)
return;
if (index == 1)
{
ans[lowcast * 2] = ans[lowcast];
ans[lowcast] = min;
}
else
{
ans[lowcast * 2 + 1] = ans[lowcast];
ans[lowcast] = min;
}
//不需满足左小右大之类的左右顺序规则
//只是和min做交换即可
change_to_min(lowcast * 2);
change_to_min(lowcast * 2 + 1);
int index_back = 0;//0不换,1和左边换,2和右边换
int min_back = ans[lowcast];
//这样可能会使得左右孩子的次序颠倒,如果是先左再右这样来判别
if (lowcast * 2 <= n)
{
if (ans[lowcast * 2] < min_back)
{
min_back = ans[lowcast * 2];
index_back = 1;
}
}
if (lowcast * 2 + 1 <= n)
{
if (ans[lowcast * 2 + 1] < min_back)
{
min_back = ans[lowcast * 2 + 1];
index_back = 2;
}
}
if (!index_back)
return;
if (index_back == 1)
{
ans[lowcast * 2] = ans[lowcast];
ans[lowcast] = min_back;
}
else
{
ans[lowcast * 2 + 1] = ans[lowcast];
ans[lowcast] = min_back;
}
}
上下两部分做的工作完全相同,只是改了些变量的名字避免重定义问题而已。
标准code:在插入过程中即不断地进行回溯更新
#include<iostream>
using namespace std;
int h[50];
int n = 10;
void sifup(int i)
{
if (i == 1)
{
return;
}
while (i / 2 != 0)
{
if (h[i] < h[i / 2])
{
swap(h[i], h[i / 2]);
}
else
{
//此时说明插入的结点已经到了正确的位置,return即可
return;
}
i = i / 2;
}
}
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> h[i];
sifup(i);
}
for (int i = 1; i <= n; i++)
{
cout << h[i] << " ";
}
cout << endl;
return 0;
}
4.无向图的深搜:
深搜的条件是以编号小的结点先开始搜,于是递归时只需要先对当前结点的相邻结点作编号大小的排序,然后再跑一个循环分别搜即可
这个edges,实际上就是“遇事不决加变量”的思想的体现,在对结点的初始化的过程中,顺便存储好这个结点对应的边数即可。方便后续排序的时候知道上界。
(注意加visited数组!!!而且一定要初始化!!!!)
还有的,非常重要的一点——无向图一定要做好对称化处理!输入阶段就可以完成处理:
标准code:直接使用下标进行一一对应,而不去记录各种结点的信息!!!
#include<iostream>
using namespace std;
bool visited[100];
typedef struct
{
int arc[100][100];
int numnodes,numedges;
}mgraph;
void DFS(mgraph &g,int i)
{
int j;
visited[i]=true;
cout<<i<<" ";
for(j=0;j<g.numnodes;j++)
{
if(g.arc[i][j]==1&&!visited[j])
{
DFS(g,j);
}
}
}
int main()
{
mgraph g;
int i,j,k,w;
cin>>g.numnodes>>g.numedges;
for(i=0;i<g.numnodes;i++)
{
for(j=0;j<g.numnodes;j++)
{
g.arc[i][j]=0;
}
}
for(k=0;k<g.numedges;k++)
{
cin>>i>>j;
g.arc[i][j]=1;
g.arc[j][i]=1;
}
DFS(g,0);
return 0;
}
5.无向图的广搜:
要求遵循小顶点优先的原则,同样的,先排序,入队的时候小的优先入队即可。大致的输入的接受&结点结构和深搜类似(标准code也是!一定要记得还有下标一一对应这种写法!!)
6.最小生成树:跑Prim即可,比Kruskal好写点,不用维护并查集数组
这个题目只是需要最终的最小权值,所以:
※注意循环是循环vernum次还是vernum-1次——一般是vernum-1次,因为先从0开始,0就直接加入了,然后再处理vernum-1个顶点即可
以及:非常重要的对陈化处理:
④第18周数据结构实验课整理:
1、二分查找——注意一点,有序序列不代表就是一定是升序!虽然样例给的是升序,但不要被迷惑
※蠢逼写法:写两个名字不同的函数
留尾巴——如何利用多态性&函数指针之类的思想来完成复用??
#include<iostream>
using namespace std;
int arr[1000];
int N;
int x = 1;
bool BinarySearch_down(int a)
{
int low = 0, high = N - 1;
while (low <= high)
{
int mid = (low + high) / 2;
if (arr[mid] == a)
return true;
if (arr[mid] < a)
{
x++;
high = mid - 1;
continue;
}
if (arr[mid] > a)
{
x++;
low = mid + 1;
continue;
}
}
return false;
}
bool BinarySearch_up(int a)
{
int low = 0, high = N - 1;
while (low <= high)
{
int mid = (low + high) / 2;
if (arr[mid] == a)
return true;
if (arr[mid] > a)
{
x++;
high = mid - 1;
continue;
}
if (arr[mid] < a)
{
x++;
low = mid + 1;
continue;
}
}
return false;
}
int main()
{
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> N;
for (int i = 0; i < N; i++)
{
cin >> arr[i];
}
int a;
cin >> a;
if (N == 1 && a == arr[0])
{
cout << "1" << endl;
return 0;
}
if (arr[1] > arr[0])
{
bool m = BinarySearch_up(a);
if (!m)
{
cout << "NO" << endl;
return 0;
}
cout << x;
}
else
{
bool m = BinarySearch_down(a);
if (!m)
{
cout << "NO" << endl;
return 0;
}
cout << x;
}
}
2、二叉排序树的构建——不要忘记插入的时候通过temp&pre的前后指针来找插入位置的思想
严格卡好左小右大!构建完之后跑一下中序遍历看看是否严格升序,很可能会写的时候写反左右——同时,不要写顺手写成while(!root_node)
#include<iostream>
using namespace std;
struct TreeNode
{
int val;
TreeNode* right;
TreeNode* left;
TreeNode() :val(0), right(0), left(0) {}
TreeNode(int a) :val(a), right(0), left(0) {}
};
TreeNode* root;
void insert(TreeNode* root_node, int val)
{
TreeNode* temp = root_node;
while (root_node)
{
temp = root_node;
if (val > root_node->val)
{
root_node = root_node->right;
continue;
}
root_node = root_node->left;
}
//往temp右边插入
if (val > temp->val)
{
temp->right = new TreeNode(val);
return;
}
temp->left = new TreeNode(val);
}
int x=0;
bool BSTSearch(TreeNode*root_node,int val)
{
while (root_node)
{
x++;
if (val > root_node->val)
{
root_node = root_node->right;
continue;
}
if (val < root_node->val)
{
root_node = root_node->left;
continue;
}
return true;
}
return false;
}
int main()
{
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int N;
cin >> N;
for (int i = 0; i < N; i++)
{
int a;
cin >> a;
if (!i)
{
root = new TreeNode(a);
continue;
}
insert(root, a);
}
int T;
cin >> T;
bool a = BSTSearch(root, T);
if (!a)
{
cout << "NO" << endl;
return 0;
}
cout << x << endl;
}
3&4、迪杰斯特拉——第四题的获取路径的方法扒一下答案看看除了从终点起入栈直到源点,再出栈得到路径这样做以外的别的处理方式
看清楚是有向图还是无向图!!!!!!!!!!!!!!
Path[i] = origin-'A';
//默认就是从origin中转,Path不要默认写作-1,而应当初始化为源点!
//尤其是单源点到单终点的问题,常常会循环跑不全最后导致某个点path仍-1导致路径输出异常
//迪杰斯特拉的二叉堆形式优化写法:
//过程即是把所有的和源点相关的顶点入队
//取出堆顶元素,更新相邻顶点的minimum path值
不过这样的话,堆顶元素存的就得是个Node,因为要记录最短路径值&自己的下标来更新相邻顶点
5、读入病人ID————注意不要用整型数来存储ID,会自动舍弃高位的0(毕竟他是个数学意义上的数字,存储高位0本身没有任何意义)。
二、2022夏季PAT甲级考试整理:
①经验:别死磕,读好题再开敲,带好笔和纸画好图,多玩限时训练来适应这种模式。
②题目整理:
1、无聊的猜天数问题,但是要注意,一定别忘了“循环”问题,也就是周一的上一天是周日,周日下一天是周六,这个很容易被忽略导致少过样例。但是,即使有样例没过,也千万不要在一道题死磕,没有任何意义!
补充一点,循环问题,有一个很重要的处理方式,也就是取余。
可以在作差之后+7,再拿整体和7取余!
菜逼专属暴力枚举写法:
#include<iostream>
using namespace std;
int a[3], b[3];
string ToString(int a)
{
switch (a)
{
case -1:return "Saturday";
case 0: return "Sunday";
case 1: return "Monday";
case 2: return "Tuesday";
case 3: return "Wednesday";
case 4: return "Thursday";
case 5: return "Friday";
case 6: return "Saturday";
case 7: return "Sunday";
}
return "";
}
int main()
{
for (int i = 0; i < 3; i++)
{
cin >> a[i];
}
for (int i = 0; i < 3; i++)
{
cin >> b[i];
}
//如果a[0]对
if (a[0] == b[0])
{
cout << ToString(a[0] + 1) << endl;
cout << "yesterday" << endl << "yesterday";
return 0;
}
//这个代表着 a的昨天与b的今天相等
//如果a的昨天是周六
if (a[0] == b[1] - 1||a[0]==6&&b[1]==0)
{
cout << ToString(a[0] + 1) << endl;
cout << "yesterday" << endl << "today";
return 0;
}
if (a[0] == b[2] - 2)
{
cout << ToString(a[0] + 1) << endl;
cout << "yesterday" << endl << "tomorrow";
return 0;
}
if (a[1] == b[0]+1||a[1]==0&&b[0]==6)
{
cout << ToString(a[1]) << endl;
cout << "today" << endl << "yesterday";
return 0;
}
if (a[1] == b[1])
{
cout << ToString(a[1]) << endl;
cout << "today" << endl << "today";
return 0;
}
if (a[1] == b[2] - 1 || a[1] == 6 && b[2] == 0)
{
cout << ToString(a[1]) << endl;
cout << "today" << endl << "tomorrow";
return 0;
}
if (a[2] == b[0]+2||a[2]==0&&b[0]==5&&a[2]==1&&b[0]==6)
{
cout << ToString(a[2]-1) << endl;
cout << "tomorrow" << endl << "yesterday";
return 0;
}
if (a[2] == b[1] + 1 || a[2] == 0 && b[1] == 6)
{
cout << ToString(a[2]-1) << endl;
cout << "tomorrow" << endl << "today";
return 0;
}
if (a[2] == b[2])
{
cout << ToString(a[2]-1) << endl;
cout << "tomorrow" << endl << "tomorrow";
return 0;
}
return 0;
}
2.二叉堆的类似实现:
补充内容:(1)一个提高读取文件的速度的小妙招:
一定要注意这两个加速方式的使用!!!!
(2):set与map的使用区别:
对于此题目的书写:
重点在于对于堆中元素存在性的查找,以及对于堆中元素的更新。这也是引起超时的一大原因。
而且注意频繁使用的操作,要封装成inline的形式,尽可能的优化速度!
※重写之后的代码:
3、DFS的正确性检查:
三、PAT甲级相关练习题目整理