数据结构 06 线性结构
pta练习题,需要多回顾,多小结,记得常回来看看~
1 自行车停放
7-1 自行车停放
有n辆自行车依次来到停车棚,除了第一辆自行车外,每辆自行车都会恰好停放在已经在停车棚里的某辆自行车的左边或右边。(e.g.停车棚里已经有3辆自行车,从左到右编号为:3,5,1。现在编号为2的第4辆自行车要停在5号自行车的左边,所以现在停车棚里的自行车编号是:3,2,5,1)。给定n辆自行车的停放情况,按顺序输出最后停车棚里的自行车编号。
输入格式:
第一行一个整数 n。 第二行一个整数x。表示第一辆自行车的编号。 以下 n-1 行,每行 3 个整数 x,y,z。 z=0 时,表示编号为x 的自行车恰停放在编号为 y 的自行车的左边。 z=1 时,表示编号为 x 的自行车恰停放在编号为 y 的自行车的右边。
输出格式:
从左到右输出停车棚里的自行车编号,。
输入样例:
在这里给出一组输入。例如:
4
3
1 3 1
2 1 0
5 2 1
输出样例:
在这里给出相应的输出。例如:
3 2 5 1
题解:
解题思路
本题要求模拟自行车的停放过程,每辆新自行车需停在已有自行车的左边或右边。由于需要频繁地在指定位置的左右插入新元素,且最终需按顺序输出,我们可以使用数组记录邻居关系的方法,避免复杂的链表操作。
核心思路:
- 用两个数组
left[]
和right[]
分别记录每个自行车的左邻居和右邻居(-1
表示无邻居) - 对于每辆新自行车,根据停放位置(左 / 右)更新相关自行车的邻居关系
- 最终从最左侧的自行车开始,按右邻居顺序遍历输出
#include <stdio.h>
#include <stdlib.h>#define MAX_NUM 100000 // 假设自行车编号不超过100000int main() {int n, first;scanf("%d", &n);scanf("%d", &first);// 用两个数组分别记录每个自行车的左邻居和右邻居// -1表示没有邻居int* left = (int*)malloc(sizeof(int) * (MAX_NUM + 1));int* right = (int*)malloc(sizeof(int) * (MAX_NUM + 1));// 初始化所有值为-1(表示无邻居)for (int i = 0; i <= MAX_NUM; i++) {left[i] = -1;right[i] = -1;}// 第一辆自行车的左右邻居都为-1left[first] = -1;right[first] = -1;// 处理后续n-1辆自行车for (int i = 0; i < n - 1; i++) {int x, y, z;scanf("%d %d %d", &x, &y, &z);if (z == 0) {// 停在y的左边int k = left[y]; // y原来的左邻居left[x] = k; // x的左邻居是kright[x] = y; // x的右邻居是yleft[y] = x; // y的左邻居现在是x// 如果k存在,更新k的右邻居为xif (k != -1) {right[k] = x;}} else {// 停在y的右边int k = right[y]; // y原来的右邻居right[x] = k; // x的右邻居是kleft[x] = y; // x的左邻居是yright[y] = x; // y的右邻居现在是x// 如果k存在,更新k的左邻居为xif (k != -1) {left[k] = x;}}}// 找到最左边的自行车int current = first;while (left[current] != -1) {current = left[current];}// 从左到右输出所有自行车编号while (current != -1) {printf("%d ", current);current = right[current];}printf("\n");// 释放动态分配的内存free(left);free(right);return 0;
}
代码解析
数据结构设计:
left[]
:left[x]
表示编号为x
的自行车的左邻居编号(-1
表示无左邻居)right[]
:right[x]
表示编号为x
的自行车的右邻居编号(-1
表示无右邻居)- 数组大小定义为
MAX_NUM + 1
,确保能覆盖所有可能的自行车编号
初始化操作:
- 所有邻居初始化为
-1
(无邻居) - 第一辆自行车的左右邻居均为
-1
- 所有邻居初始化为
核心插入逻辑:
- 当
z=0
(x 停在 y 左边):- 记录 y 原来的左邻居
k
- 建立 x 与 k、x 与 y 的邻居关系
- 若 k 存在,更新 k 的右邻居为 x
- 记录 y 原来的左邻居
- 当
z=1
(x 停在 y 右边):- 记录 y 原来的右邻居
k
- 建立 x 与 y、x 与 k 的邻居关系
- 若 k 存在,更新 k 的左邻居为 x
- 记录 y 原来的右邻居
- 当
结果输出:
- 先找到最左侧的自行车(左邻居为
-1
的节点) - 从最左侧节点开始,通过
right[]
依次遍历所有自行车并输出
- 先找到最左侧的自行车(左邻居为
优势分析
- 相比链表实现,数组操作更简洁,避免了指针操作的复杂性
- 时间复杂度为
O(n)
,效率高(查找邻居只需O(1)
时间) - 空间复杂度为
O(MAX_NUM)
,对于题目约束下的输入规模完全适用
2 符号配对
请编写程序检查C语言源程序中下列符号是否配对:/*
与*/
、(
与)
、[
与]
、{
与}
。
输入格式:
输入为一个C语言源程序。当读到某一行中只有一个句点.
和一个回车的时候,标志着输入结束。程序中需要检查配对的符号不超过100个。
输出格式:
首先,如果所有符号配对正确,则在第一行中输出YES
,否则输出NO
。然后在第二行中指出第一个不配对的符号:如果缺少左符号,则输出?-右符号
;如果缺少右符号,则输出左符号-?
。
输入样例1:
void test()
{int i, A[10];for (i=0; i<10; i++) { /*/A[i] = i;
}
.
输出样例1:
NO
/*-?
输入样例2:
void test()
{int i, A[10];for (i=0; i<10; i++) /**/A[i] = i;
}]
.
输出样例2:
NO
?-]
输入样例3:
void test()
{int idouble A[10];for (i=0; i<10; i++) /**/A[i] = 0.1*i;
}
.
输出样例3:
YES
题解:
符号配对问题的题解(基于栈的简洁实现)
解题思路
本题要求检查 C 语言源程序中 4 种符号对的配对情况:/*
与*/
、(
与)
、[
与]
、{
与}
。我们可以通过栈来实现符号配对检查,核心思路是:
- 先过滤输入中的无关字符,只保留需要检查的符号
- 用特殊符号
<
代表/*
,>
代表*/
,简化双字符符号的处理 - 遇到左符号(
(
、[
、{
、<
)时入栈 - 遇到右符号(
)
、]
、}
、>
)时,检查是否与栈顶左符号匹配 - 遍历结束后,若栈为空则所有符号配对成功,否则存在未匹配的左符号
代码实现
#include <stdio.h>
#include <string.h>#define MAX_SIZE 100char stack[MAX_SIZE]; // 用于存储括号的栈
int top = -1; // 栈顶指针// 入栈操作
void push(char c) {if (top < MAX_SIZE - 1) {stack[++top] = c;}
}// 出栈操作
char pop() {if (top >= 0) {return stack[top--];}return '\0'; // 返回空字符表示栈为空
}// 判断栈是否为空
int isEmpty() {return top == -1;
}// 获取栈顶元素
char getTop() {if (top >= 0) {return stack[top];}return '\0'; // 返回空字符表示栈为空
}int main() {char x[101], y[101] = ""; // x存储输入行,y存储提取的符号序列int flag = 1; // 标记是否有错误// 读取输入直到遇到句点.while (fgets(x, 101, stdin)) {int i = 0;if (strcmp(x, ".\n") == 0)break;for (; x[i] != '\0'; i++) {// 提取单字符符号if (x[i] == '[' || x[i] == ']' || x[i] == '{' || x[i] == '}' || x[i] == '(' || x[i] == ')') {strncat(y, &x[i], 1);}// 处理双字符符号/*(用<表示)else if (x[i] == '/' && x[i + 1] == '*') {strcat(y, "<");i++; // 跳过*,避免重复处理}// 处理双字符符号*/(用>表示)else if (x[i] == '*' && x[i + 1] == '/') {strcat(y, ">");i++; // 跳过/,避免重复处理}}}// 遍历符号序列检查配对int i = 0;for (; y[i] != '\0'; i++) {char topChar = getTop();// 左符号入栈if (y[i] == '[' || y[i] == '{' || y[i] == '(' || y[i] == '<') {push(y[i]);continue;}// 处理右符号if (isEmpty()) { // 右符号无匹配的左符号printf("NO\n");if (y[i] == '>')printf("?-*/\n"); elseprintf("?-%c\n", y[i]);flag = 0;break;} // 检查是否匹配(ASCII码差值为1或2:()差1,[]和{}差2)else if (y[i] - topChar != 1 && y[i] - topChar != 2) {printf("NO\n");if (topChar == '<')printf("/*-?\n");elseprintf("%c-?\n", topChar);flag = 0;break;} else { // 匹配成功,出栈pop();}}// 所有符号处理完后检查栈是否为空if (flag == 1) {if (isEmpty()) {printf("YES\n");} else { // 存在未匹配的左符号char topChar = getTop();printf("NO\n");if (topChar == '<')printf("/*-?\n");elseprintf("%c-?\n", topChar);}}return 0;
}
代码解析
符号预处理:
- 用
y
数组存储提取的符号序列,过滤无关字符 - 双字符符号
/*
和*/
分别用<
和>
代替,简化处理逻辑 - 读取输入直到遇到只含
.
的行(结束标志)
- 用
栈操作设计:
push
:左符号入栈pop
:匹配成功时出栈getTop
:获取栈顶元素(待匹配的左符号)isEmpty
:判断栈是否为空(用于检测右符号无匹配左符号的情况)
配对检查逻辑:
- 左符号直接入栈
- 右符号处理:
- 栈为空 → 缺少左符号(输出
?-右符号
) - 与栈顶左符号不匹配 → 输出
左符号-?
- 匹配成功 → 栈顶元素出栈
- 栈为空 → 缺少左符号(输出
- 所有符号处理完毕后,若栈非空 → 存在未匹配的左符号(输出
左符号-?
)
匹配规则:
- 利用 ASCII 码特性:
()
的 ASCII 码差值为 1,[]
和{}
的差值为 2,简化匹配判断
- 利用 ASCII 码特性:
该实现简洁高效,通过符号预处理和栈操作,准确检测第一个不匹配的符号,完全符合题目要求。
3 判断回文
回文是指正读反读均相同的字符序列,如“abba”和“abdba”均是回文,但“good”不是回文。试写一个程序判定给定的字符向量是否为回文,用栈实现。(提示:将一半字符入栈)
输入格式:
输入任意字符串。
输出格式:
若字符串是回文,输出:xxxx是回文。
若字符串不是回文,输出:xxxx不是回文。
输入样例:
abba
输出样例:
abba是回文。
输入样例:
abdba
输出样例:
abdba是回文。
输入样例:
good
输出样例:
good不是回文。
题解:
判断回文(基于栈的实现)
解题思路
回文是指正读和反读都相同的字符串。使用栈判断回文的核心思路是:
- 将字符串的前半部分字符依次入栈
- 对于后半部分字符,逐个与栈顶元素比较
- 如果所有字符都匹配,则是回文;否则不是回文
对于长度为奇数的字符串,中间字符无需比较,直接忽略。
代码实现
#include <stdio.h>
#include <string.h>#define MAX_SIZE 100// 栈结构定义
char stack[MAX_SIZE];
int top = -1;// 入栈操作
void push(char c) {if (top < MAX_SIZE - 1) {stack[++top] = c;}
}// 出栈操作
char pop() {if (top >= 0) {return stack[top--];}return '\0'; // 栈空时返回空字符
}int main() {char str[MAX_SIZE];// 读取输入字符串fgets(str, MAX_SIZE, stdin);// 去除字符串末尾的换行符str[strcspn(str, "\n")] = '\0';int len = strlen(str);int i;// 将前半部分字符入栈for (i = 0; i < len / 2; i++) {push(str[i]);}// 确定后半部分的起始位置(长度为奇数时跳过中间字符)int start = (len % 2 == 0) ? len / 2 : len / 2 + 1;int isPalindrome = 1; // 标记是否为回文// 比较后半部分与栈中字符for (i = start; i < len; i++) {if (str[i] != pop()) {isPalindrome = 0;break;}}// 输出结果if (isPalindrome) {printf("%s是回文。\n", str);} else {printf("%s不是回文。\n", str);}return 0;
}
代码解析
栈的基本操作:
push
:将字符压入栈顶,栈顶指针上移pop
:从栈顶弹出字符,栈顶指针下移
核心逻辑:
- 首先读取输入字符串,并去除可能的换行符
- 计算字符串长度
len
,将前len/2
个字符依次入栈 - 根据字符串长度的奇偶性,确定后半部分的起始位置:
- 偶数长度:从
len/2
开始 - 奇数长度:从
len/2 + 1
开始(跳过中间字符)
- 偶数长度:从
- 逐个比较后半部分字符与栈中弹出的字符:
- 若所有字符匹配,则是回文
- 若有任意字符不匹配,则不是回文
示例说明:
- 对于 "abba"(长度 4,偶数):
- 前 2 个字符 'a'、'b' 入栈
- 从位置 2 开始比较:'b' 与栈顶弹出的 'b' 匹配,'a' 与栈顶弹出的 'a' 匹配 → 是回文
- 对于 "abdba"(长度 5,奇数):
- 前 2 个字符 'a'、'b' 入栈
- 跳过中间的 'd',从位置 3 开始比较:'b' 与栈顶弹出的 'b' 匹配,'a' 与栈顶弹出的 'a' 匹配 → 是回文
- 对于 "good"(长度 4,偶数):
- 前 2 个字符 'g'、'o' 入栈
- 从位置 2 开始比较:'o' 与栈顶弹出的 'o' 匹配,'d' 与栈顶弹出的 'g' 不匹配 → 不是回文
- 对于 "abba"(长度 4,偶数):
该实现利用栈的 "后进先出" 特性,高效地完成了回文判断,时间复杂度为 O (n)(n 为字符串长度),空间复杂度为 O (n/2),符合题目要求。
4 武松喝酒景阳冈
武松又来景阳冈喝酒了,这次酒老板给他出了个难题:老板拿出很多碗酒,在桌子上摆成圆形,然后告诉武松,本地规矩,喝酒要数数,数到9的倍数或者数字里含有9才能喝,同时,本地人很讨厌7,所以如果数字是7的倍数或者数字里含有7就不能喝。比如,9,19可以喝,数到27就不能喝。老板告诉武松,酒碗按顺时针方向从1开始编号,从1号开始数起。老板说,如果武松能告诉他,他依次喝的酒碗的编号,就让他过去。你能帮帮他吗?
输入格式:
输入:在一行中给出1个整数N,表示酒的碗数,N不超过3000。
输出格式:
对每一组输入,在一行中输出酒碗的编号,中间用一个空格分隔,首尾不能有多余的空格。
输入样例:
在这里给出一组输入。例如:
3
输出样例:
在这里给出相应的输出。例如:
3 1 2
题解:
武松喝酒景阳冈问题(C 语言题解)
解题思路
本题核心是模拟武松按规则挑选酒碗的过程,规则为 “数到 9 的倍数或含 9 的数能喝,但 7 的倍数或含 7 的数不能喝”,且酒碗按圆形排列(喝完的酒碗需移除,后续从下一个开始数)。
实现思路如下:
- 用动态数组存储当前剩余的酒碗编号(初始为 1~N)。
- 从 1 开始循环计数,逐个判断数字是否满足 “能喝” 条件。
- 满足条件时,输出对应酒碗编号并从数组中移除该酒碗。
- 不满足条件时,继续按圆形顺序(数组循环)检查下一个酒碗。
- 重复步骤 2~4,直到所有酒碗都被输出。
代码实现
#include <stdio.h>
#include <stdlib.h>// 判断数字是否包含9(含9则返回1,否则返回0)
int f1(int n) {if (n == 9)return 1;while (n > 10) {int x = n % 10; // 取数字的个位if (x == 9) {return 1;}n /= 10; // 移除个位,继续检查高位}if (n == 9) // 处理10以内的数字return 1;return 0;
}// 判断数字是否包含7(含7则返回1,否则返回0)
int f2(int n) {if (n == 7)return 1;while (n > 10) {int x = n % 10; // 取数字的个位if (x == 7) {return 1;}n /= 10; // 移除个位,继续检查高位}if (n == 7) // 处理10以内的数字return 1;return 0;
}int main() {int n;scanf("%d", &n); // 输入酒碗总数// 动态分配数组,存储当前剩余的酒碗编号(初始为1~n)int *b = (int *)malloc(n * sizeof(int));for (int i = 0; i < n; i++) {b[i] = i + 1;}int size = n; // 记录当前剩余的酒碗数量int index = 0; // 当前检查的酒碗在数组中的索引(模拟圆形循环)int cnt = 0; // 计数变量(从1开始,判断是否满足喝酒条件)// 循环直到所有酒碗都被输出(size为0时结束)while (size > 0) {cnt++; // 每次循环计数+1// 检查当前计数是否满足“能喝”条件:// 条件1:是9的倍数 或 含9;条件2:不是7的倍数 且 不含7if ((cnt % 9 == 0 || f1(cnt) == 1) && f2(cnt) == 0 && cnt % 7 != 0) {// 输出当前酒碗编号,控制格式(最后一个编号后不输出空格)printf("%d", b[index]);if (size != 1) {printf(" ");}// 从数组中删除当前酒碗(后续元素前移覆盖)for (int i = index; i < size - 1; i++) {b[i] = b[i + 1];}size--; // 剩余酒碗数量-1// 若删除的是数组最后一个元素,索引重置为0(回到开头)if (index >= size) {index = 0;}} else {// 不满足条件,移动到下一个酒碗index++;// 若索引超出当前数组范围,重置为0(实现圆形循环)if (index >= size) {index = 0;}}}printf("\n");free(b); // 释放动态分配的内存,避免内存泄漏return 0;
}
代码解析
1. 辅助函数:判断数字特征
- f1(int n):检查数字
n
是否包含 9。通过 “取个位(n%10
)+ 删个位(n/10
)” 的循环,遍历数字的每一位,若有一位是 9 则返回 1,否则返回 0。 - f2(int n):检查数字
n
是否包含 7。逻辑与f1
完全一致,仅判断目标数字改为 7。
2. 核心逻辑:酒碗选择与输出
- 动态数组初始化:用
malloc
分配大小为n
的数组,存储初始酒碗编号 1~n,size
记录当前剩余酒碗数量。 - 圆形循环模拟:用
index
作为数组索引,每次不满足条件时index++
;若index
超出size
(数组边界),则重置为 0,实现 “喝完一圈再从头数” 的圆形效果。 - 酒碗删除与输出:
- 满足条件时,先输出当前酒碗编号(注意最后一个编号后不输出空格);
- 通过 “后续元素前移” 的方式删除当前酒碗(例如数组
[1,2,3]
删除索引 1 的2
后,变为[1,3]
); size
减 1,若删除的是最后一个元素,index
重置为 0,避免后续索引越界。
3. 示例验证(输入样例 N=3)
- 初始数组:
[1,2,3]
,size=3
,index=0
,cnt=0
。 - cnt=1~8:均不满足条件(无 9 相关,或含 7 / 是 7 的倍数),
index
循环移动(0→1→2→0→1→2→0→1)。 - cnt=9:满足条件(9 的倍数,不含 7 且不是 7 的倍数),输出
b[1]=3
,数组变为[1,2]
,size=2
,index
重置为 0。 - cnt=10~17:排除含 7(17)、7 的倍数(14),其余无 9 相关,
index
移动(0→1→0→1→0)。 - cnt=18:满足条件(9 的倍数),输出
b[0]=1
,数组变为[2]
,size=1
,index
重置为 0。 - cnt=19:满足条件(含 9),输出
b[0]=2
,size=0
,循环结束。 - 最终输出:
3 1 2
,与样例一致。
该实现完全贴合题目规则,通过动态数组和索引循环模拟圆形酒碗排列,逻辑清晰且效率满足 N≤3000 的要求。
5 集合减法
给定两个非空集合A和B,集合的元素为30000以内的正整数,编写程序求A-B。
输入格式:
输入为三行。第1行为两个整数n和m,分别为集合A和B包含的元素个数,1≤n, m ≤10000。第2行表示集合A,为n个空格间隔的正整数,每个正整数不超过30000。第3行表示集合B,为m个空格间隔的正整数,每个正整数不超过30000。
输出格式:
输出为一行整数,表示A-B,每个整数后一个空格,各元素按递增顺序输出。若A-B为空集,则输出0,0后无空格。
输入样例:
5 5
1 2 3 4 5
3 4 5 6 7
输出样例:
1 2
题解:
集合减法(排序 + 双指针法)题解
解题思路
集合减法 A-B
的目标是找出所有 “属于 A 且不属于 B” 的元素,并按递增顺序输出。当集合元素数量较大时,直接遍历的双重循环效率过低(时间复杂度 O (n*m)),容易超时。
采用排序 + 双指针的方法可将效率优化至 O (n log n + m log m),核心思路如下:
- 先对集合 A 和 B 分别排序(升序),利用有序数组的特性简化比较逻辑。
- 使用双指针遍历两个有序数组:
- 指针
i
遍历 A,指针j
遍历 B。 - 通过比较
A[i]
和B[j]
的值,高效判断A[i]
是否在 B 中。
- 指针
- 筛选出符合条件的元素,由于 A 已排序,结果自然按递增顺序排列。
代码实现
#include <stdio.h>
#include <stdlib.h>// 排序比较函数(升序)
int compare(const void *a, const void *b) {return *(int *)a - *(int *)b;
}int main() {int n, m;scanf("%d %d", &n, &m);// 1. 读取集合A和Bint A[10000], B[10000];for (int i = 0; i < n; i++) {scanf("%d", &A[i]);}for (int i = 0; i < m; i++) {scanf("%d", &B[i]);}// 2. 对A和B分别排序(升序)qsort(A, n, sizeof(int), compare);qsort(B, m, sizeof(int), compare);// 3. 双指针筛选A-B的元素int result[10000];int res_len = 0; // 结果数组长度int i = 0, j = 0; // i遍历A,j遍历Bwhile (i < n && j < m) {if (A[i] < B[j]) {// A[i]不在B中(因B已排序,后续元素更大),加入结果result[res_len++] = A[i];i++; // 移动A的指针} else if (A[i] == B[j]) {// A[i]在B中,跳过这两个元素i++;j++;} else {// A[i]比当前B[j]大,移动B的指针找更大的元素j++;}}// 4. 处理A中剩余元素(均大于B的最大元素,肯定不在B中)while (i < n) {result[res_len++] = A[i];i++;}// 5. 按要求输出结果if (res_len == 0) {printf("0\n"); // 空集输出0} else {for (int k = 0; k < res_len; k++) {printf("%d ", result[k]); // 每个元素后带空格}printf("\n");}return 0;
}
代码解析
1. 排序的作用
- 对 A 和 B 排序后,元素按升序排列,使得后续比较可通过 “线性遍历” 完成,避免重复检查。
- 例如:A 排序后为
[1,2,3,4,5]
,B 排序后为[3,4,5,6,7]
,通过有序性可直接判断1
和2
不在 B 中。
2. 双指针核心逻辑
A[i] < B[j]
:由于 B 已排序,B[j]
之后的元素都大于B[j]
,因此A[i]
必然不在 B 中,直接加入结果集,同时移动i
(检查 A 的下一个元素)。A[i] == B[j]
:A[i]
在 B 中,不属于A-B
,同时移动i
和j
(跳过这两个元素)。A[i] > B[j]
:当前B[j]
太小,不可能与A[i]
匹配,移动j
(检查 B 的下一个更大元素)。
3. 处理剩余元素
- 当 B 遍历完毕(
j >= m
),A 中未处理的元素(i < n
)均大于 B 的最大元素,必然不在 B 中,全部加入结果集。
4. 结果输出
- 若结果集为空(
res_len == 0
),输出0
。 - 否则按顺序输出结果集中的元素,每个元素后带一个空格(符合题目格式要求)。
5. 时间复杂度分析
- 排序 A:O (n log n),排序 B:O (m log m)。
- 双指针遍历:O (n + m)(每个元素最多被访问一次)。
- 总时间复杂度:O (n log n + m log m),远优于双重循环的 O (n*m),可高效处理 n 和 m 为 10000 的情况,避免超时。
该方法利用排序和双指针的线性遍历特性,既保证了结果的递增顺序,又大幅提升了效率,是解决集合减法问题的通用优化方案。