C语言编程练习题
题目一:随机方向贪吃蛇
1. 菜单功能(核心新增)
- 程序启动后,首先显示游戏菜单,菜单包含3个选项:① 开始游戏 ② 查看历史最大长度 ③ 退出程序;
- 菜单操作:通过键盘输入对应选项(如输入1选择开始游戏、输入2查看历史最大长度、输入3退出),确认后执行对应操作;
- 游戏死亡后,先显示死亡提示(如“蛇已死亡,游戏结束!”),提示“按任意键返回菜单”,按下任意键后回到主菜单,可重新选择选项;
- 历史最大长度:记录所有游戏局中蛇身达到的最长长度,初始值为蛇的初始长度,每次游戏结束后,若当前蛇长大于历史最大长度,则更新历史最大长度;查看时直接显示“历史最大长度:XXX”。
2. 游戏机制
- 选择“开始游戏”后,蛇在控制台界面中自动移动,无需玩家手动控制方向;
- 移动规则:每移动3步后,从“上、下、左、右”中随机选择一个新方向,不能选择与当前移动方向相反的方向(例如当前向右,新方向不能向左);未到3步时,保持当前方向匀速移动;
- 无关卡设计,游戏持续进行,仅当触发“蛇头撞自身身体”时结束,撞墙壁不结束;游戏结束后自动记录当前蛇长,更新历史最大长度(若需)。
3. 碰撞与穿墙规则
- 蛇头碰到控制台边界(上下左右边缘)时,触发穿墙效果:从对应边界的对侧穿出(例如蛇头从顶部边界穿出,直接出现在底部对应x坐标位置;从左侧边界穿出,直接出现在右侧对应y坐标位置);
- 蛇头碰到自身身体的任意一节,游戏结束,无其他结束条件;结束后显示死亡提示,按任意键返回主菜单。
4. 食物规则
- 初始时在控制台随机位置生成一个食物(用▲表示);
- 蛇头碰到食物后,蛇身长度+1,并在新的随机位置生成下一个食物(食物不能生成在蛇身身上);
- 记录蛇身长度,用于更新历史最大长度。
5. 界面要求
- 菜单界面:简洁清晰,显示选项及操作提示(如“请输入选项[1-3]:”);
- 游戏界面:用字符绘制游戏区域,蛇头用◆表示,蛇身用●表示,食物用▲表示,墙用■表示,空白区域用空格;
- 界面大小固定(建议20行×40列,可自行调整,需在代码中明确);
- 游戏结束后,显示“蛇撞到自身,游戏结束!”,随后提示“按任意键返回菜单”,按下任意键后回到主菜单。
6. 输入输出
- 菜单操作:输入1/2/3选择对应功能,输入错误时提示“输入错误,请重新输入”并重新显示菜单;
- 游戏过程:无需玩家输入任何指令,自动运行,仅在死亡后需要按下任意键返回菜单;
- 查看历史最大长度:输入2后,显示“历史最大长度:XXX”,提示“按任意键返回菜单”,按下任意键回到主菜单;
- 退出程序:输入3后,程序正常退出。
7. 技术要求
- 随机数生成需使用rand()和srand()函数,确保方向随机且符合规则;
- 菜单的循环显示、游戏的循环运行,需通过循环语句实现,确保逻辑连贯;
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <windows.h>
#include <io.h>
#include <process.h>
#include <time.h>
#include <stdlib.h>
#include <conio.h>
#define WIDTH 40
#define HEIGHT 20
#define INIT_LEN 3
int max_len = INIT_LEN;
int snake_x[1000];
int snake_y[1000];
int len;
int dir;
int step_cnt;
int food_x, food_y;
int game_over;
void menu() {
system("cls");
printf("===== 随机方向贪吃蛇 =====\n");
printf(" 1. 开始游戏\n");
printf(" 2. 查看历史最大长度\n");
printf(" 3. 退出程序\n");
printf("==========================\n");
printf("请输入选项[1-3]:");
}
void append(char* dest, int* pos, const char* src) {
int i = 0;
while (src[i] != '\0') {
dest[*pos] = src[i];
(*pos)++;
i++;
}
}
void gotoxy(int x, int y) {
COORD pos = { x,y };
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}
void rand_dir() {
int new_dir;
do {
new_dir = rand() % 4 + 1;
} while ((dir == 1 && new_dir == 2) ||
(dir == 2 && new_dir == 1) ||
(dir == 3 && new_dir == 4) ||
(dir == 4 && new_dir == 3));
dir = new_dir;
}
void create_food() {
int flag;
do {
flag = 1;
food_x = rand() % (WIDTH - 2) + 1;
food_y = rand() % (HEIGHT - 2) + 1;
for (int i = 0; i < len; i++) {
if (snake_x[i] == food_x && snake_y[i] == food_y) {
flag = 0;
break;
}
}
} while (!flag);
}
int hit_self() {
for (int i = 1; i < len; i++) {
if (snake_x[0] == snake_x[i] && snake_y[0] == snake_y[i]) {
return 1;
}
}
return 0;
}
void cross_wall() {
if (snake_x[0] <= 0)
snake_x[0] = WIDTH - 2;
if (snake_x[0] >= WIDTH - 1)
snake_x[0] = 1;
if (snake_y[0] <= 0)
snake_y[0] = HEIGHT - 2;
if (snake_y[0] >= HEIGHT - 1)
snake_y[0] = 1;
}
void move_snake() {
step_cnt++;
if (step_cnt >= 3) {
rand_dir();
step_cnt = 0;
}
for (int i = len - 1; i > 0; i--) {
snake_x[i] = snake_x[i - 1];
snake_y[i] = snake_y[i - 1];
}
switch (dir) {
case 1: snake_y[0]--; break;
case 2: snake_y[0]++; break;
case 3: snake_x[0]--; break;
case 4: snake_x[0]++; break;
}
cross_wall();
if (hit_self()) {
game_over = 1;
if (len > max_len)
max_len = len;
return;
}
if (snake_x[0] == food_x && snake_y[0] == food_y) {
len++;
create_food();
}
}
void game() {
len = INIT_LEN;
for (int i = 0; i < len; i++) {
snake_x[i] = WIDTH / 2 + i;
snake_y[i] = HEIGHT / 2;
}
dir = 3;
step_cnt = 0;
game_over = 0;
create_food();
while (!game_over) {
gotoxy(0, 0);
char screen[4096];
int pos = 0;
for (int j = 0; j < HEIGHT; j++) {
for (int i = 0; i < WIDTH; i++) {
if (j == 0 || j == HEIGHT - 1 || i == 0 || i == WIDTH - 1) {
append(screen, &pos, "■");
} else if (i == snake_x[0] && j == snake_y[0]) {
append(screen, &pos, "◆");
} else if (i == food_x && j == food_y) {
append(screen, &pos, "▲");
} else {
int is_snake = 0;
for (int z = 1; z < len; z++) {
if (i == snake_x[z] && j == snake_y[z]) {
append(screen, &pos, "●");
is_snake = 1;
break;
}
}
if (!is_snake)
append(screen, &pos, " ");
}
}
append(screen, &pos, "\n");
}
char len_str[30];
sprintf(len_str, " 当前长度:%d", len);
append(screen, &pos, len_str);
screen[pos] = '\0';
printf("%s", screen);
Sleep(200);
move_snake();
}
gotoxy(10, 9);
printf("蛇撞到自身,游戏结束!");
gotoxy(10, 11);
printf("按任意键返回菜单");
getch();
}
void ssize() {
system("cls");
printf("==========================\n");
printf(" 历史最大长度:%d\n", max_len);
printf("==========================\n");
printf("按任意键返回菜单...\n");
getch();
}
void menu_loop() {
while (1) {
menu();
int choice;
scanf("%d", &choice);
switch (choice) {
case 1: game(); break;
case 2: ssize(); break;
case 3:
system("cls");
printf("程序正常退出\n");
exit(0);
default:
system("cls");
printf("输入错误,请重新输入\n");
Sleep(1000);
break;
}
}
}
int main() {
srand((unsigned)time(NULL));
menu_loop();
return 0;
}
计算机的随机原理 计算机没有真随机 都是假随机 为了不让随机数显的太规律 就引入了随机种子的概念 根据随机种子去产生随机数
C语言中的随机数生成原理
计算机无法生成真正的随机数,通常使用伪随机数生成器(PRNG)来模拟随机性。伪随机数序列由算法生成,看似随机但实际上是可预测的。
伪随机数生成器的工作原理
伪随机数生成器通过一个初始值(种子)和确定性算法生成一系列数字。如果种子相同,生成的序列完全相同。C语言中的rand()函数是典型的线性同余生成器(LCG),其公式为:
[ X_{n+1} = (a \times X_n + c) \mod m ]
其中:
- (X_n) 是当前随机数
- (a)、(c)、(m) 是预定义的常数
- 结果通过取模运算限制在一定范围内
随机种子的作用
种子(srand()的参数)决定了伪随机序列的起点。如果不设置种子或使用固定种子(如srand(1)),每次程序运行时rand()会生成相同的序列。为了使序列看起来更随机,通常使用当前时间作为种子:
#include <stdlib.h>
#include <time.h>
srand(time(NULL));
int random_number = rand();
随机数的局限性
伪随机数存在以下问题:
- 序列周期性重复,取决于算法实现
- 可预测性,知道种子和算法即可重现序列
- 不均匀分布,某些实现可能导致数值分布不均
改进随机性的方法
使用更复杂的算法或外部熵源可以提升随机性。例如:
- 密码学安全的随机数生成器(如
/dev/urandom) - 混合多个熵源(时间、硬件噪声等)
// 从/dev/urandom读取随机字节(Linux)
unsigned int seed;
FILE* f = fopen("/dev/urandom", "rb");
fread(&seed, sizeof(seed), 1, f);
fclose(f);
srand(seed);
应用场景建议
- 普通场景:
rand()配合时间种子足够 - 安全敏感场景:使用专用库(如OpenSSL的
RAND_bytes()) - 科学计算:选择高质量PRNG(如Mersenne Twister)
题目二:KMP算法实现与原理详解
KMP算法全称为Knuth-Morris-Pratt算法,是一种高效的单模式串字符串匹配算法,核心解决了暴力匹配(BF算法)中“主串指针频繁回退、导致大量重复比对”的低效问题。与BF算法最坏时间复杂度O(n×m)(n为主串长度,m为子串长度)相比,KMP算法通过预处理子串生成辅助数组(next数组),实现主串指针永不回退,整体时间复杂度优化至O(n+m),适用于长文本查找关键字、日志检索、报文解析等场景。
核心核心逻辑:不重复比对主串中已比对过的字符,利用子串自身的“前缀与后缀重复特征”,让子串指针在不匹配时回退到最优位置,减少无效比对。
1. 核心概念(重中之重,理解后才能实现算法)
- 前缀:对于子串pattern,不包含最后一个字符、以第一个字符开头的所有连续子串。
示例:子串"ababc"(下标0-4),前缀包括:"a"(下标0)、"ab"(0-1)、"aba"(0-2)、"abab"(0-3),不包含整个子串。
- 后缀:对于子串pattern,不包含第一个字符、以最后一个字符结尾的所有连续子串。
示例:子串"ababc",后缀包括:"c"(下标4)、"bc"(3-4)、"abc"(2-4)、"babc"(1-4),不包含整个子串。
- 最长公共前后缀长度(LPS):子串的某一段前缀和某一段后缀完全相同,且长度最长,这个长度就是最长公共前后缀长度。
示例1:子串"abab"(0-3),前缀有"a"、"ab"、"aba",后缀有"b"、"ab"、"bab",最长且相同的是"ab",长度为2;
示例2:子串"abc",前缀有"a"、"ab",后缀有"c"、"bc",无相同前后缀,长度为0;
示例3:子串"aaaa",前缀"aaa"与后缀"aaa"相同,最长公共前后缀长度为3。
2. next数组(KMP算法核心,辅助子串指针回退)
2.1 next数组的定义
next数组是与子串长度相同的整型数组,数组的下标j对应子串的第j个字符(从0开始),next[j]的值表示:子串中前j+1个字符组成的子串(即pattern[0..j])的最长公共前后缀长度。
2.2 next数组的手动计算示例(必看,掌握计算逻辑)
以子串pattern = "ababc"(长度5,下标0-4)为例,分步计算next数组:
- j=0:子串为"a"(仅1个字符),无前缀、无后缀,最长公共前后缀长度为0 → next[0] = 0;
- j=1:子串为"ab",前缀"a",后缀"b",无相同前后缀 → next[1] = 0;
- j=2:子串为"aba",前缀"a"、"ab",后缀"a"、"ba",最长相同前后缀为"a",长度1 → next[2] = 1;
- j=3:子串为"abab",前缀"a"、"ab"、"aba",后缀"b"、"ab"、"bab",最长相同前后缀为"ab",长度2 → next[3] = 2;
- j=4:子串为"ababc",前缀"a"、"ab"、"aba"、"abab",后缀"c"、"bc"、"abc"、"babc",无相同前后缀 → next[4] = 0;
最终next数组为:[0, 0, 1, 2, 0]。
2.3 next数组的核心作用
当主串str的第i个字符与子串pattern的第j个字符不匹配时,无需将主串指针i回退,也无需将子串指针j重置为0,而是让j回退到next[j-1]的位置,继续与主串的第i个字符比对。这样就跳过了所有无效的重复比对,大幅提升匹配效率。
示例:主串"ababxabcabx",子串"ababc",当i=4(主串字符"x")、j=4(子串字符"c")不匹配时,j回退到next[3] = 2,用子串第2个字符("a")继续与主串第4个字符("x")比对,无需回退i。
3. KMP匹配完整流程( step-by-step 详解)
假设主串为str(长度n),子串为pattern(长度m),next数组已预处理完成,匹配流程分为3个阶段,结合具体示例说明:
示例:主串str = "ababcabcabx"(n=11),子串pattern = "ababc"(m=5),next数组 = [0,0,1,2,0]
- 初始化:主串指针i = 0,子串指针j = 0;
- 匹配阶段:循环遍历主串(i < n):
情况1:str[i] == pattern[j] → 字符匹配,i和j同时向后移动1位(i++,j++);
示例:i=0、j=0(a==a)→ i=1、j=1;i=1、j=1(b==b)→ i=2、j=2;以此类推,直到i=4、j=4(c==c),j=5(等于子串长度m=5),匹配成功。
- 情况2:str[i] != pattern[j] → 字符不匹配,分两种子情况:
子情况2.1:j > 0 → j回退到next[j-1]的位置,继续比对(i不回退);
示例:若主串为"ababxabcabx",当i=4、j=4时,str[4] = 'x' != pattern[4] = 'c',j=next[3] = 2,继续用pattern[2]与str[4]比对。
- 子情况2.2:j == 0 → 子串第一个字符就不匹配,j保持0不变,i向后移动1位(i++);
示例:若主串第一个字符为'd',与pattern[0]='a'不匹配,i++,继续比对主串下一个字符。
- 结束判断:
匹配成功:当j == m(子串指针遍历完整个子串),说明子串完全匹配,返回匹配的起始位置(i - j);
示例:上述匹配成功时,i=5、j=5,起始位置 = 5-5 = 0,即子串从主串下标0开始匹配。
- 匹配失败:当i遍历完整个主串(i == n),j仍未达到m,说明主串中无匹配的子串,返回-1。
4. 实现要求
4.1 函数实现
- 实现next数组生成函数:输入为子串(字符串)和子串长度,输出为next数组(整型数组);函数需严格遵循next数组的计算逻辑,确保每个下标的值正确。
- 实现KMP匹配函数:输入为主串(字符串)、子串(字符串),输出为匹配的起始位置(整数);若找到匹配,返回起始下标(从0开始);若未找到,返回-1;匹配过程需严格遵循上述KMP匹配流程,主串指针不回退。
4.2 主函数测试
- 定义主串和子串(可直接在代码中定义测试用例,也可允许用户输入主串和子串);
- 调用next数组生成函数,生成子串对应的next数组(可选择打印next数组,用于验证正确性);
- 调用KMP匹配函数,获取匹配结果,并输出:
匹配成功:打印“子串在主串的起始位置:x”(x为具体下标);
- 匹配失败:打印“未找到匹配的子串”。
至少设计3组测试用例,覆盖“完全匹配”“部分匹配”“无匹配”三种场景,验证算法正确性。
4.3 补充要求
- 算法逻辑需与上述描述完全一致,严禁使用暴力匹配逻辑替代KMP算法;
- 代码需添加清晰注释,重点标注next数组计算、KMP匹配流程的关键步骤;
- 子串长度不超过100,主串长度不超过200,确保代码可正常运行。
#include <stdio.h>
#include <string.h>
// ====================== 1. 生成 next 数组 ======================
// pattern:模式串(子串)
// next:存储最长公共前后缀长度的数组
// m:模式串长度
void getNext(char *pattern, int *next, int m) {
// 初始化:len = 最长公共前后缀的当前长度,从 0 开始
int len = 0;
// 第一个字符没有前后缀,next[0] 固定为 0
next[0] = 0;
int i = 1;
// 从第二个字符开始遍历模式串
while (i < m) {
if (pattern[i] == pattern[len]) {
// 字符相等:最长公共前后缀长度 +1
len++;
next[i] = len;
i++;
} else {
// 不相等
if (len != 0) {
// 回退到上一个可能的最长公共前后缀位置
len = next[len - 1];
} else {
// 没有公共前后缀,赋值 0
next[i] = 0;
i++;
}
}
}
}
// ====================== 2. KMP 匹配函数 ======================
// str:主串
// pattern:模式串(子串)
// 返回值:匹配成功 → 起始下标;匹配失败 → -1
int KMP(char *str, char *pattern) {
int n = strlen(str); // 主串长度
int m = strlen(pattern); // 子串长度
// 子串为空,直接返回 0(题目不用考虑)
if (m == 0) return 0;
// 主串比子串短,不可能匹配
if (n < m) return -1;
// 定义 next 数组(题目要求子串长度 ≤100)
int next[100];
// 先生成 next 数组
getNext(pattern, next, m);
int i = 0; // 主串指针(永不回退)
int j = 0; // 子串指针
while (i < n) {
if (str[i] == pattern[j]) {
// 【情况1】字符匹配:两个指针同时后移
i++;
j++;
}
// 【匹配成功】子串遍历完了
if (j == m) {
// 返回匹配起始位置 = i - j
return i - j;
}
// 【情况2】字符不匹配
else if (i < n && str[i] != pattern[j]) {
if (j != 0) {
// j 回退到最优位置,i 不动
j = next[j - 1];
} else {
// 子串第一个字符就不匹配,i 后移
i++;
}
}
}
// 循环结束都没匹配成功
return -1;
}
// ====================== 3. 测试函数(封装) ======================
void testKMP(char *str, char *pattern) {
printf("主串:%s\n", str);
printf("子串:%s\n", pattern);
int pos = KMP(str, pattern);
if (pos != -1) {
printf("匹配成功!子串在主串的起始位置:%d\n", pos);
} else {
printf("未找到匹配的子串\n");
}
printf("----------------------------------------\n");
}
// ====================== 4. 主函数(3组测试用例) ======================
int main() {
printf("========== KMP 算法测试 ==========\n\n");
// 测试用例 1:完全匹配(开头匹配)
char str1[] = "ababcabcabx";
char pattern1[] = "ababc";
testKMP(str1, pattern1);
// 测试用例 2:部分匹配(中间匹配)
char str2[] = "abcabababcxyz";
char pattern2[] = "ababc";
testKMP(str2, pattern2);
// 测试用例 3:无匹配
char str3[] = "abcdefg123456";
char pattern3[] = "ababc";
testKMP(str3, pattern3);
return 0;
}

324

被折叠的 条评论
为什么被折叠?



