目录
- 未来还会逐渐补充细节。
万字解析:从 C 语言到初阶数据结构
前言
C语言是一门经典而强大的编程语言,因其高效性和底层控制能力,成为初学者和专业开发者的首选。它不仅是操作系统(如Linux)和嵌入式系统的基石,也是算法竞赛的热门语言。本文面向零基础学习者,带你从C语言的基本语法到初阶数据结构,再到算法竞赛案例和实战项目,逐步构建编程思维。
为什么选择C语言?
- 高效性:C语言贴近硬件,执行速度快,适合高性能场景。
- 广泛应用:从操作系统到游戏开发,C语言无处不在。
- 学习基础:掌握C语言后,学习C++、Java等语言会更轻松。
学习路线:本文从C语言基础(变量、流程控制、函数)开始,逐步引入数组、指针、结构体等进阶内容,再到数据结构(链表、栈、队列),最后通过算法竞赛案例和实战项目巩固知识。每节包含详细文字讲解、规范代码和注释,确保初学者易懂。
适合人群:
- 零基础学习者,想快速入门编程。
- 算法竞赛新手,准备参加ACM、LeetCode等。
- 计算机科班大一学生,夯实基础。
第一章:C 语言初识与环境搭建
C 语言的历史与影响
C语言诞生于1972年,由贝尔实验室的Dennis Ritchie开发,用于编写Unix操作系统。它的设计理念“简单而强大”影响了C++、Java等现代语言。C语言的代码执行效率高,广泛应用于操作系统、嵌入式设备和游戏开发。学习C语言就像掌握了一把“万能钥匙”,为你打开编程世界的大门。
开发工具介绍
开始编程前,需要搭建开发环境。推荐以下工具:
- Dev-C++:简单易用的集成开发环境(IDE),适合初学者。
- VS Code:轻量级编辑器,配合GCC编译器,适合进阶用户。
- GCC:命令行编译器,适合Linux/Mac用户,Windows可通过MinGW安装。
环境搭建步骤(以Windows为例):
- 下载并安装Dev-C++或VS Code+MinGW。
- 配置环境变量,确保
gcc
命令可用。 - 测试环境:编写并运行一个简单程序。
第一个程序 Hello World
让我们编写第一个C程序,输出“Hello, World!”:
#include <stdio.h>
int main() {
// 输出欢迎信息
printf("Hello, World!\n");
return 0;
}
代码解析:
#include <stdio.h>
:引入标准输入输出库,提供printf
函数。int main()
:程序的入口函数,所有C程序从这里开始执行。printf("Hello, World!\n")
:打印字符串,\n
表示换行。return 0
:表示程序正常结束,返回0给操作系统。
练习建议:修改程序,输出你的名字或一句个性化的问候语。尝试用printf
输出多行文本,观察换行符的效果。
第二章:变量、数据类型与运算符
基本数据类型
变量是程序中存储数据的容器,C语言支持以下基本数据类型:
int
:整数,如42
或-10
。float
:单精度浮点数,如3.14
。double
:双精度浮点数,精度更高,适合大范围计算。char
:单个字符,如'A'
或'1'
。
常量与变量命名规范
- 常量:使用
const
关键字定义不可修改的值,如const int MAX = 100;
。 - 变量命名:遵循清晰、可读的原则:
- 使用有意义的名字,如
studentAge
或total_score
。 - 避免使用C语言关键字(如
int
、if
)。 - 不要以数字开头,如
1score
是非法的。
- 使用有意义的名字,如
运算符与表达式
C语言支持多种运算符:
- 算术运算:
+
(加)、-
(减)、*
(乘)、/
(除)、%
(取模)。 - 比较运算:
>
、<
、==
、!=
、>=
、<=
。 - 逻辑运算:
&&
(与)、||
(或)、!
(非)。
趣味小练习:BMI 计算器
让我们编写一个程序,计算用户的BMI(体重/身高²)并判断健康状况:
#include <stdio.h>
int main() {
float weight, height, bmi;
// 获取用户输入
printf("请输入体重(kg):");
scanf("%f", &weight);
printf("请输入身高(m):");
scanf("%f", &height);
// 计算BMI
bmi = weight / (height * height);
// 输出结果并判断健康状况
printf("你的BMI是:%.2f\n", bmi);
if (bmi < 18.5) {
printf("偏瘦,建议均衡饮食。\n");
} else if (bmi < 24) {
printf("正常,保持健康生活!\n");
} else {
printf("偏胖,建议适量运动。\n");
}
return 0;
}
代码解析:
float
:用于存储小数,确保计算精度。scanf("%f", &weight)
:读取用户输入,&
表示变量的内存地址。%.2f
:控制输出格式,保留两位小数。- 条件语句:根据BMI范围(<18.5偏瘦,18.5-24正常,>24偏胖)输出建议。
练习建议:添加输入验证,确保体重和身高为正数。尝试输出更详细的健康建议。
第三章:输入输出与格式化
printf 输出格式详解
printf
是C语言的核心输出函数,支持格式化输出:
%d
:整数。%f
:浮点数(默认6位小数)。%c
:字符。%s
:字符串。- 控制精度:如
%.2f
表示保留2位小数。
scanf 输入用法与常见问题
scanf
用于读取用户输入,常见注意事项:
- 使用
&
获取变量地址(字符串除外)。 - 换行符可能导致输入错误,建议在格式字符串前加空格,如
" %c"
。 - 检查输入是否成功,避免程序崩溃。
小项目:自我介绍程序
编写一个程序,让用户输入姓名、年龄和身高,格式化输出一段自我介绍:
#include <stdio.h>
int main() {
char name[50];
int age;
float height;
// 获取用户输入
printf("请输入你的名字:");
scanf("%s", name);
printf("请输入你的年龄:");
scanf("%d", &age);
printf("请输入你的身高(m):");
scanf("%f", &height);
// 格式化输出自我介绍
printf("\n--- 自我介绍 ---\n");
printf("大家好,我的名字是%s,今年%d岁,身高%.2f米。\n", name, age, height);
return 0;
}
代码解析:
char name[50]
:定义字符数组存储名字,最多49个字符(留1位给\0
)。scanf("%s", name)
:字符串输入无需&
,因为数组名是地址。- 格式化输出:通过
printf
拼接成完整句子,增强可读性。
练习建议:添加兴趣爱好输入,输出更丰富的介绍。尝试限制名字长度,避免溢出。
第四章:流程控制
if / else 条件判断
条件语句根据条件执行不同代码块,类比生活中的“如果…那么…”:
#include <stdio.h>
int main() {
int score;
printf("请输入成绩(0-100):");
scanf("%d", &score);
// 判断成绩等级
if (score >= 90) {
printf("优秀\n");
} else if (score >= 60) {
printf("及格\n");
} else {
printf("不及格\n");
}
return 0;
}
代码解析:
if-else
:根据条件选择执行路径。- 输入验证:实际应用中应检查
score
是否在0-100范围内。
switch 语句
switch
适合多分支选择,简化代码:
#include <stdio.h>
int main() {
int choice;
printf("请选择(1-3):");
scanf("%d", &choice);
// 根据选项执行
switch (choice) {
case 1:
printf("你选择了选项1:开始游戏\n");
break;
case 2:
printf("你选择了选项2:查看设置\n");
break;
case 3:
printf("你选择了选项3:退出\n");
break;
default:
printf("无效选项,请输入1-3\n");
}
return 0;
}
代码解析:
break
:防止“穿透”到下一个case
。default
:处理无效输入。
循环结构
C语言支持三种循环:
for
:适合已知循环次数。while
:适合条件驱动循环。do-while
:至少执行一次循环体。
小练习:乘法口诀表
打印九九乘法表,练习嵌套循环:
#include <stdio.h>
int main() {
// 外层循环控制行
for (int i = 1; i <= 9; i++) {
// 内层循环控制列
for (int j = 1; j <= i; j++) {
printf("%d * %d = %-2d ", j, i, i * j);
}
printf("\n");
}
return 0;
}
代码解析:
- 嵌套循环:外层控制行(1-9),内层控制列(1到i)。
%-2d
:左对齐,占2位宽度,确保输出整齐。- 结果形如:
1*1=1 2*2=4 ...
。
练习建议:修改程序,打印完整的9x9表格(包括重复项)。尝试用while
循环重写。
算法竞赛案例:求1到N的和
竞赛中常考简单数学问题,如计算1到N的和。以下展示暴力法和公式法:
#include <stdio.h>
int main() {
int n;
printf("请输入正整数N:");
scanf("%d", &n);
// 暴力法:循环累加
long long sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
printf("暴力法:1到%d的和是%lld\n", n, sum);
// 公式法:(n * (n + 1)) / 2
sum = (long long)n * (n + 1) / 2;
printf("公式法:1到%d的和是%lld\n", n, sum);
return 0;
}
代码解析:
- 暴力法:通过循环累加,时间复杂度O(n),适合理解过程。
- 公式法:使用数学公式
(n * (n + 1)) / 2
,时间复杂度O(1),效率更高。 long long
:防止大数溢出,适合竞赛中处理大范围输入。
练习建议:添加输入验证,确保N为正整数。尝试计算1到N的平方和。
第五章:函数与模块化思维
为什么要用函数
函数将代码分解成可重用的模块,优点包括:
- 复用性:相同逻辑只需写一次。
- 可读性:清晰的函数名让代码更易懂。
- 维护性:修改某功能只需调整对应函数。
函数声明与定义
函数由声明(原型)和定义组成。以下是求两数最大值的函数:
#include <stdio.h>
// 函数声明
int max(int a, int b);
int main() {
int x, y;
printf("请输入两个整数:");
scanf("%d %d", &x, &y);
printf("较大值是:%d\n", max(x, y));
return 0;
}
// 函数定义
int max(int a, int b) {
return a > b ? a : b; // 三目运算符,简洁返回较大值
}
代码解析:
- 声明:告诉编译器函数的签名(返回值类型、参数)。
- 定义:实现函数的具体逻辑。
- 三目运算符
?:
:等价于if-else
,但更简洁。
变量作用域
- 局部变量:定义在函数内,仅函数内可见。
- 全局变量:定义在函数外,全局可访问,但慎用以免逻辑混乱。
- static变量:在函数内保持值不变,多次调用保留上次结果。
小项目:简易计算器
实现一个支持加、减、乘、除的计算器,封装运算为函数:
#include <stdio.h>
// 函数声明
double add(double a, double b);
double subtract(double a, double b);
double multiply(double a, double b);
double divide(double a, double b);
int main() {
double num1, num2;
char op;
// 获取用户输入
printf("请输入第一个数字:");
scanf("%lf", &num1);
printf("请输入运算符(+,-,*,/):");
scanf(" %c", &op); // 注意空格,避免换行符干扰
printf("请输入第二个数字:");
scanf("%lf", &num2);
// 根据运算符调用对应函数
switch (op) {
case '+':
printf("结果:%.2f\n", add(num1, num2));
break;
case '-':
printf("结果:%.2f\n", subtract(num1, num2));
break;
case '*':
printf("结果:%.2f\n", multiply(num1, num2));
break;
case '/':
if (num2 != 0) {
printf("结果:%.2f\n", divide(num1, num2));
} else {
printf("错误:除数不能为0\n");
}
break;
default:
printf("无效运算符\n");
}
return 0;
}
// 函数定义
double add(double a, double b) { return a + b; }
double subtract(double a, double b) { return a - b; }
double multiply(double a, double b) { return a * b; }
double divide(double a, double b) { return a / b; }
代码解析:
- 每个运算独立为函数,体现模块化思想。
double
:支持小数运算,提高精度。- 错误处理:检查除数是否为0,避免程序崩溃。
scanf(" %c", &op)
:空格吸收换行符,确保输入正确。
练习建议:添加幂运算函数(如pow(a, b)
)。实现连续计算功能(循环输入)。
第六章:数组与字符串
一维数组
数组是存储相同类型元素的连续内存空间。以下是输入并打印数组的示例:
#include <stdio.h>
#define MAX_SIZE 100 // 定义最大数组大小
int main() {
int arr[MAX_SIZE], n;
// 输入数组元素
printf("请输入数组元素个数:");
scanf("%d", &n);
printf("请输入%d个整数:\n", n);
for (int i = 0; i < n; i++) {
scanf("%d", &arr[i]);
}
// 打印数组
printf("数组内容:");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
代码解析:
#define MAX_SIZE 100
:定义常量,避免硬编码。- 动态输入:用户指定数组大小
n
,增强灵活性。 - 循环遍历:输入和输出均使用
for
循环。
二维数组
二维数组可看作矩阵,适合存储表格数据:
#include <stdio.h>
int main() {
int matrix[3][3];
// 输入3x3矩阵
printf("请输入3x3矩阵:\n");
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
scanf("%d", &matrix[i][j]);
}
}
// 打印矩阵
printf("矩阵内容:\n");
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", matrix[i][j]);
}
printf("\n");
}
return 0;
}
代码解析:
matrix[3][3]
:定义3x3二维数组。- 嵌套循环:外层控制行,内层控制列。
- 可视化输出:每行末尾换行,保持格式清晰。
字符串与字符数组
字符串是字符数组,以空字符\0
结尾。以下是基本字符串操作:
#include <stdio.h>
#include <string.h>
int main() {
char str[100];
printf("请输入一个字符串:");
scanf("%s", str); // 注意:scanf只读取不含空格的字符串
// 使用字符串函数
printf("字符串长度:%lu\n", strlen(str));
printf("字符串:%s\n", str);
return 0;
}
代码解析:
char str[100]
:分配足够空间存储字符串。strlen
:计算字符串长度(不含\0
)。- 注意:
scanf
不适合读取含空格的字符串,后续会介绍fgets
。
小项目:单词统计程序
编写程序,统计用户输入文本中的单词数:
#include <stdio.h>
#include <string.h>
int main() {
char text[1000];
int wordCount = 0;
int inWord = 0;
// 获取整行输入
printf("请输入一段文字(以回车结束):\n");
fgets(text, sizeof(text), stdin);
// 统计单词
for (int i = 0; text[i] != '\0'; i++) {
if (text[i] == ' ' || text[i] == '\n') {
inWord = 0; // 遇到空格或换行,单词结束
} else if (inWord == 0) {
inWord = 1; // 遇到非空格,单词开始
wordCount++;
}
}
printf("单词数:%d\n", wordCount);
return 0;
}
代码解析:
fgets
:读取整行输入,包括空格,适合处理文本。- 单词计数逻辑:通过空格或换行分割,记录单词开始。
inWord
:标记是否在单词内部,避免重复计数。
练习建议:统计每个单词的长度,输出最长单词。尝试忽略标点符号。
第七章:指针初探
指针是什么
指针存储变量的内存地址,类比“藏宝图”,指向数据的存储位置。指针是C语言的核心特性,理解它对后续数据结构学习至关重要。
取地址与解引用
#include <stdio.h>
int main() {
int a = 10;
int *p = &a; // 取地址,p存储a的地址
// 打印变量、地址和通过指针访问的值
printf("a的值:%d\n", a);
printf("a的地址:%p\n", (void*)&a);
printf("通过指针访问a:%d\n", *p); // 解引用
// 修改a的值
*p = 20;
printf("修改后a的值:%d\n", a);
return 0;
}
代码解析:
&a
:取变量a
的地址。*p
:解引用,访问指针指向的内存值。- 指针修改:通过
*p = 20
直接改变a
的值。
指针与数组
数组名本质是指向数组首元素的指针:
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5};
int *p = arr; // 数组名是首地址
// 使用指针遍历数组
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i));
}
printf("\n");
return 0;
}
代码解析:
arr
等价于&arr[0]
,指向数组第一个元素。*(p + i)
:访问第i个元素,等价于arr[i]
。- 指针算术:
p + i
自动跳跃i
个元素大小。
竞赛案例:交换两个变量
指针在竞赛中常用于高效操作,如交换变量:
#include <stdio.h>
// 使用指针交换两个变量
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 5, y = 10;
printf("交换前:x=%d, y=%d\n", x, y);
swap(&x, &y);
printf("交换后:x=%d, y=%d\n", x, y);
return 0;
}
代码解析:
- 指针参数:
int *a
接收变量地址。 - 解引用:
*a
和*b
直接修改原变量。 - 临时变量:
temp
保存交换过程中的值。
练习建议:用指针实现数组逆序(如[1,2,3]
变为[3,2,1]
)。尝试不用临时变量交换两个数(使用异或运算)。
第八章:结构体与文件操作
结构体定义与使用
结构体(struct
)将多个相关数据组合在一起,如学生的姓名和成绩:
#include <stdio.h>
struct Student {
char name[50];
int score;
};
int main() {
struct Student s1;
// 输入学生信息
printf("请输入学生姓名和成绩:");
scanf("%s %d", s1.name, &s1.score);
// 输出信息
printf("学生:%s, 成绩:%d\n", s1.name, s1.score);
return 0;
}
代码解析:
struct Student
:定义结构体,包含姓名和成绩字段。- 访问成员:使用
.
操作符,如s1.name
。 - 结构体适合组织复杂数据,类似现实中的“档案”。
结构体数组
存储多个学生信息:
#include <stdio.h>
struct Student {
char name[50];
int score;
};
int main() {
struct Student students[3];
// 输入学生信息
for (int i = 0; i < 3; i++) {
printf("请输入第%d个学生姓名和成绩:", i + 1);
scanf("%s %d", students[i].name, &students[i].score);
}
// 输出学生列表
printf("\n学生列表:\n");
for (int i = 0; i < 3; i++) {
printf("%s: %d\n", students[i].name, students[i].score);
}
return 0;
}
代码解析:
students[3]
:定义结构体数组,存储3个学生。- 循环输入输出:批量处理数据,提高效率。
文件读写
C语言支持文件操作,使用fopen
、fscanf
和fprintf
:
#include <stdio.h>
struct Student {
char name[50];
int score;
};
int main() {
struct Student s;
FILE *fp;
// 写入文件
fp = fopen("students.txt", "w");
printf("请输入学生姓名和成绩:");
scanf("%s %d", s.name, &s.score);
fprintf(fp, "%s %d\n", s.name, s.score);
fclose(fp);
// 读取文件
fp = fopen("students.txt", "r");
fscanf(fp, "%s %d", s.name, &s.score);
printf("从文件读取:%s %d\n", s.name, s.score);
fclose(fp);
return 0;
}
代码解析:
fopen("students.txt", "w")
:以写模式打开文件。fprintf
:格式化写入,类似printf
。fscanf
:从文件读取数据。fclose
:关闭文件,释放资源。
小项目:成绩管理系统
实现一个支持添加、查询和文件存储的成绩管理系统:
#include <stdio.h>
#include <string.h>
#define MAX_STUDENTS 100
struct Student {
char name[50];
int score;
};
int main() {
struct Student students[MAX_STUDENTS];
int count = 0;
FILE *fp;
// 菜单循环
while (1) {
printf("\n1. 添加学生\n2. 查询成绩\n3. 保存到文件\n4. 从文件读取\n5. 退出\n");
printf("请选择:");
int choice;
scanf("%d", &choice);
if (choice == 5) break;
if (choice == 1) {
if (count < MAX_STUDENTS) {
printf("请输入学生姓名和成绩:");
scanf("%s %d", students[count].name, &students[count].score);
count++;
printf("添加成功!\n");
} else {
printf("存储已满!\n");
}
} else if (choice == 2) {
char name[50];
printf("请输入学生姓名:");
scanf("%s", name);
int found = 0;
for (int i = 0; i < count; i++) {
if (strcmp(students[i].name, name) == 0) {
printf("%s的成绩:%d\n", name, students[i].score);
found = 1;
break;
}
}
if (!found) printf("未找到学生!\n");
} else if (choice == 3) {
fp = fopen("students.txt", "w");
for (int i = 0; i < count; i++) {
fprintf(fp, "%s %d\n", students[i].name, students[i].score);
}
fclose(fp);
printf("保存成功!\n");
} else if (choice == 4) {
fp = fopen("students.txt", "r");
count = 0;
while (fscanf(fp, "%s %d", students[count].name, &students[count].score) != EOF) {
count++;
}
fclose(fp);
printf("读取成功!\n");
} else {
printf("无效选项!\n");
}
}
return 0;
}
项目解析:
- 功能:支持添加学生、查询成绩、文件存储和读取。
- 数据结构:使用结构体数组存储学生信息。
- 文件操作:实现数据持久化,重启程序数据不丢失。
- 交互:菜单式界面,易于操作。
练习建议:增加删除学生功能,按成绩排序输出。尝试限制姓名长度,防止溢出。
第九章:初阶数据结构
数据结构简介
数据结构是组织和管理数据的方式,直接影响程序效率。数组适合静态数据,数据结构如链表、栈、队列适合动态场景。学习数据结构能帮助你解决复杂问题,如算法竞赛中的高效查询和排序。
链表(Linked List)
链表由节点组成,每个节点包含数据和指向下一节点的指针。以下是单向链表的实现:
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data; // 存储数据
struct Node* next; // 指向下一节点
};
// 在头部插入节点
struct Node* insertAtHead(struct Node* head, int value) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = value;
newNode->next = head;
return newNode;
}
// 打印链表
void printList(struct Node* head) {
struct Node* current = head;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}
int main() {
struct Node* head = NULL;
// 插入节点
head = insertAtHead(head, 3);
head = insertAtHead(head, 2);
head = insertAtHead(head, 1);
// 打印链表
printList(head);
// 释放内存
struct Node* current = head;
while (current != NULL) {
struct Node* temp = current;
current = current->next;
free(temp);
}
return 0;
}
代码解析:
- 节点定义:
struct Node
包含数据和指针。 - 动态内存:
malloc
分配内存,free
释放,防止内存泄漏。 - 插入操作:头部插入时间复杂度O(1),适合动态数据。
练习建议:实现尾部插入和删除节点功能。尝试反转链表。
栈(Stack)
栈是“后进先出”(LIFO)的数据结构,类似盘子堆叠。以下是数组实现的栈:
#include <stdio.h>
#define MAX_SIZE 100
struct Stack {
int items[MAX_SIZE];
int top; // 栈顶索引
};
// 初始化栈
void init(struct Stack* s) {
s->top = -1;
}
// 压栈
void push(struct Stack* s, int value) {
if (s->top < MAX_SIZE - 1) {
s->items[++s->top] = value;
} else {
printf("栈溢出!\n");
}
}
// 出栈
int pop(struct Stack* s) {
if (s->top >= 0) {
return s->items[s->top--];
}
printf("栈空!\n");
return -1;
}
int main() {
struct Stack s;
init(&s);
// 测试栈操作
push(&s, 1);
push(&s, 2);
printf("出栈:%d\n", pop(&s));
printf("出栈:%d\n", pop(&s));
return 0;
}
代码解析:
- LIFO:后压入的元素先弹出。
- 数组实现:
top
记录栈顶位置,push
和pop
操作简单。 - 边界检查:防止栈溢出或空栈弹出。
算法案例:括号匹配
栈常用于检查括号序列是否有效(如()
、{}
、[]
):
#include <stdio.h>
#include <string.h>
#define MAX_SIZE 100
struct Stack {
char items[MAX_SIZE];
int top;
};
// 初始化栈
void init(struct Stack* s) {
s->top = -1;
}
// 压栈
void push(struct Stack* s, char c) {
if (s->top < MAX_SIZE - 1) {
s->items[++s->top] = c;
}
}
// 出栈
char pop(struct Stack* s) {
if (s->top >= 0) {
return s->items[s->top--];
}
return '\0';
}
// 检查括号是否匹配
int isValid(char* s) {
struct Stack stack;
init(&stack);
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == '(' || s[i] == '{' || s[i] == '[') {
push(&stack, s[i]); // 左括号入栈
} else {
char top = pop(&stack); // 右括号与栈顶比较
if ((s[i] == ')' && top != '(') ||
(s[i] == '}' && top != '{') ||
(s[i] == ']' && top != '[')) {
return 0; // 不匹配
}
}
}
return stack.top == -1; // 栈空表示匹配
}
int main() {
char str[100];
printf("请输入括号序列:");
scanf("%s", str);
printf(isValid(str) ? "有效\n" : "无效\n");
return 0;
}
代码解析:
- 栈应用:左括号入栈,右括号与栈顶匹配。
- 逻辑:栈空且所有括号匹配则有效。
- 竞赛技巧:时间复杂度O(n),空间复杂度O(n)。
练习建议:扩展支持多类型括号(如<>
)。尝试统计无效括号的位置。
队列(Queue)
队列是“先进先出”(FIFO)的数据结构,类似排队。以下是数组实现的队列:
#include <stdio.h>
#define MAX_SIZE 100
struct Queue {
int items[MAX_SIZE];
int front, rear; // 队首和队尾
};
// 初始化队列
void init(struct Queue* q) {
q->front = 0;
q->rear = -1;
}
// 入队
void enqueue(struct Queue* q, int value) {
if (q->rear < MAX_SIZE - 1) {
q->items[++q->rear] = value;
} else {
printf("队列已满!\n");
}
}
// 出队
int dequeue(struct Queue* q) {
if (q->front <= q->rear) {
return q->items[q->front++];
}
printf("队列为空!\n");
return -1;
}
int main() {
struct Queue q;
init(&q);
// 测试队列操作
enqueue(&q, 1);
enqueue(&q, 2);
printf("出队:%d\n", dequeue(&q));
printf("出队:%d\n", dequeue(&q));
return 0;
}
代码解析:
- FIFO:先入队的元素先出队。
- 数组实现:
front
和rear
跟踪队首和队尾。 - 边界检查:防止队列溢出或空队列出队。
练手项目:排队叫号系统
模拟银行排队叫号系统:
#include <stdio.h>
#define MAX_SIZE 100
struct Queue {
int items[MAX_SIZE];
int front, rear;
};
// 初始化队列
void init(struct Queue* q) {
q->front = 0;
q->rear = -1;
}
// 入队
void enqueue(struct Queue* q, int value) {
if (q->rear < MAX_SIZE - 1) {
q->items[++q->rear] = value;
} else {
printf("队列已满!\n");
}
}
// 出队
int dequeue(struct Queue* q) {
if (q->front <= q->rear) {
return q->items[q->front++];
}
printf("队列为空!\n");
return -1;
}
int main() {
struct Queue q;
init(&q);
int number = 1; // 号码从1开始
// 菜单循环
while (1) {
printf("\n1. 取号\n2. 叫号\n3. 退出\n");
printf("请选择:");
int choice;
scanf("%d", &choice);
if (choice == 3) break;
if (choice == 1) {
enqueue(&q, number);
printf("你的号码是:%d\n", number++);
} else if (choice == 2) {
int num = dequeue(&q);
if (num != -1) {
printf("请%d号客户办理业务\n", num);
}
} else {
printf("无效选项!\n");
}
}
return 0;
}
项目解析:
- 功能:模拟取号和叫号,队列确保先到先服务。
- 交互:菜单式操作,适合初学者练习。
- 扩展性:可添加显示当前队列状态的功能。
练习建议:实现循环队列,解决数组空间浪费问题。添加显示等待人数功能。
第十章:算法竞赛小案例
排序
排序是竞赛常见问题,以下是冒泡排序:
#include <stdio.h>
void bubbleSort(int arr[], int n) {
// 外层控制轮数
for (int i = 0; i < n - 1; i++) {
// 内层比较相邻元素
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main() {
int arr[] = {5, 3, 8, 1, 2};
int n = 5;
bubbleSort(arr, n);
printf("排序后:");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
代码解析:
- 冒泡排序:相邻元素比较交换,逐步将最大值“冒泡”到一端。
- 时间复杂度:O(n²),适合小规模数据。
- 空间复杂度:O(1),仅需常数额外空间。
搜索
二分查找适用于有序数组:
#include <stdio.h>
int binarySearch(int arr[], int n, int target) {
int left = 0, right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出
if (arr[mid] == target) return mid;
if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1; // 未找到
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int n = 5, target;
printf("请输入要查找的数字:");
scanf("%d", &target);
int result = binarySearch(arr, n, target);
if (result != -1) {
printf("找到在位置%d\n", result);
} else {
printf("未找到\n");
}
return 0;
}
代码解析:
- 二分查找:每次折半搜索,时间复杂度O(log n)。
- 前提:数组必须有序。
- 技巧:
mid = left + (right - left) / 2
防止整数溢出。
小项目:通讯录搜索
实现一个支持排序和二分查找的通讯录:
#include <stdio.h>
#include <string.h>
#define MAX_CONTACTS 100
struct Contact {
char name[50];
char phone[20];
};
// 按姓名排序
void bubbleSort(struct Contact arr[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (strcmp(arr[j].name, arr[j + 1].name) > 0) {
struct Contact temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// 二分查找姓名
int binarySearch(struct Contact arr[], int n, char* target) {
int left = 0, right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
int cmp = strcmp(arr[mid].name, target);
if (cmp == 0) return mid;
if (cmp < 0) left = mid + 1;
else right = mid - 1;
}
return -1;
}
int main() {
struct Contact contacts[MAX_CONTACTS];
int count = 0;
// 输入联系人
printf("请输入联系人数量:");
scanf("%d", &count);
for (int i = 0; i < count; i++) {
printf("请输入第%d个联系人姓名和电话:", i + 1);
scanf("%s %s", contacts[i].name, contacts[i].phone);
}
// 排序
bubbleSort(contacts, count);
// 打印排序后的通讯录
printf("\n通讯录:\n");
for (int i = 0; i < count; i++) {
printf("%s: %s\n", contacts[i].name, contacts[i].phone);
}
// 查找
char target[50];
printf("\n请输入要查找的姓名:");
scanf("%s", target);
int result = binarySearch(contacts, count, target);
if (result != -1) {
printf("找到:%s, 电话:%s\n", contacts[result].name, contacts[result].phone);
} else {
printf("未找到\n");
}
return 0;
}
项目解析:
- 排序:按姓名排序,便于二分查找。
- 查找:二分查找提高查询效率,时间复杂度O(log n)。
- 结构体:存储姓名和电话,模拟真实通讯录。
练习建议:增加添加和删除联系人功能。尝试用顺序查找比较效率。
简单动态规划:台阶问题
问题:有n级台阶,每次可走1或2步,问有多少种走法。
#include <stdio.h>
int climbStairs(int n) {
if (n <= 2) return n;
int dp[100];
dp[1] = 1; // 1级台阶:1种走法
dp[2] = 2; // 2级台阶:2种走法
for (int i = 3; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2]; // 当前台阶 = 前一级 + 前两级
}
return dp[n];
}
int main() {
int n;
printf("请输入台阶数:");
scanf("%d", &n);
printf("走法数量:%d\n", climbStairs(n));
return 0;
}
代码解析:
- 动态规划:用数组
dp
记录子问题解,避免重复计算。 - 递推公式:
dp[i] = dp[i-1] + dp[i-2]
,类似斐波那契数列。 - 时间复杂度:O(n),空间复杂度O(n)。
练习建议:优化空间复杂度到O(1),仅用两个变量存储。尝试扩展到每次走1、2或3步。
第十一章:初学者实战项目推荐
猜数字游戏
用户猜测系统生成的随机数,练习循环和条件:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
srand(time(NULL)); // 初始化随机种子
int target = rand() % 100 + 1; // 1-100的随机数
int guess, attempts = 0;
printf("猜数字游戏(1-100)!\n");
do {
printf("请输入你的猜测:");
scanf("%d", &guess);
attempts++;
if (guess > target) {
printf("太大了!\n");
} else if (guess < target) {
printf("太小了!\n");
} else {
printf("恭喜你,猜对了!用了%d次。\n", attempts);
}
} while (guess != target);
return 0;
}
项目解析:
- 随机数:
rand()
生成,srand(time(NULL))
确保每次不同。 - 循环:
do-while
适合至少执行一次的场景。 - 交互:提供提示,增强用户体验。
练习建议:限制猜测次数,添加重玩功能。
简易计算器升级版
在第五章计算器基础上,添加菜单和文件存储:
#include <stdio.h>
#include <string.h>
double add(double a, double b) { return a + b; }
double subtract(double a, double b) { return a - b; }
double multiply(double a, double b) { return a * b; }
double divide(double a, double b) { return a / b; }
int main() {
FILE *fp;
double num1, num2, result;
char op;
while (1) {
printf("\n1. 计算\n2. 查看历史\n3. 退出\n");
printf("请选择:");
int choice;
scanf("%d", &choice);
if (choice == 3) break;
if (choice == 1) {
printf("请输入第一个数字:");
scanf("%lf", &num1);
printf("请输入运算符(+,-,*,/):");
scanf(" %c", &op);
printf("请输入第二个数字:");
scanf("%lf", &num2);
fp = fopen("calc_history.txt", "a");
switch (op) {
case '+':
result = add(num1, num2);
break;
case '-':
result = subtract(num1, num2);
break;
case '*':
result = multiply(num1, num2);
break;
case '/':
if (num2 != 0) {
result = divide(num1, num2);
} else {
printf("错误:除数不能为0\n");
fclose(fp);
continue;
}
break;
default:
printf("无效运算符\n");
fclose(fp);
continue;
}
printf("结果:%.2f\n", result);
fprintf(fp, "%.2f %c %.2f = %.2f\n", num1, op, num2, result);
fclose(fp);
} else if (choice == 2) {
fp = fopen("calc_history.txt", "r");
if (fp == NULL) {
printf("暂无历史记录\n");
continue;
}
char line[100];
printf("\n计算历史:\n");
while (fgets(line, sizeof(line), fp)) {
printf("%s", line);
}
fclose(fp);
} else {
printf("无效选项\n");
}
}
return 0;
}
项目解析:
- 菜单:支持计算和查看历史,增强交互性。
- 文件存储:记录每次计算结果,实现持久化。
- 错误处理:处理除零和无效输入。
练习建议:添加清除历史功能。支持三角函数运算(如sin、cos)。
迷你图书馆管理系统
管理图书信息,支持添加、查询和文件存储:
#include <stdio.h>
#include <string.h>
#define MAX_BOOKS 100
struct Book {
char title[100];
char author[50];
int id;
};
int main() {
struct Book books[MAX_BOOKS];
int count = 0;
FILE *fp;
while (1) {
printf("\n1. 添加图书\n2. 查询图书\n3. 保存到文件\n4. 从文件读取\n5. 退出\n");
printf("请选择:");
int choice;
scanf("%d", &choice);
if (choice == 5) break;
if (choice == 1) {
if (count < MAX_BOOKS) {
printf("请输入图书ID、标题和作者:");
scanf("%d %s %s", &books[count].id, books[count].title, books[count].author);
count++;
printf("添加成功!\n");
} else {
printf("库已满!\n");
}
} else if (choice == 2) {
int id;
printf("请输入图书ID:");
scanf("%d", &id);
int found = 0;
for (int i = 0; i < count; i++) {
if (books[i].id == id) {
printf("图书:%s, 作者:%s, ID:%d\n", books[i].title, books[i].author, books[i].id);
found = 1;
break;
}
}
if (!found) printf("未找到图书!\n");
} else if (choice == 3) {
fp = fopen("library.txt", "w");
for (int i = 0; i < count; i++) {
fprintf(fp, "%d %s %s\n", books[i].id, books[i].title, books[i].author);
}
fclose(fp);
printf("保存成功!\n");
} else if (choice == 4) {
fp = fopen("library.txt", "r");
count = 0;
while (fscanf(fp, "%d %s %s", &books[count].id, books[count].title, books[count].author) != EOF) {
count++;
}
fclose(fp);
printf("读取成功!\n");
} else {
printf("无效选项!\n");
}
}
return 0;
}
项目解析:
- 功能:支持图书添加、查询和文件存储。
- 数据结构:结构体数组存储图书信息。
- 文件操作:实现数据持久化,模拟小型数据库。
练习建议:增加删除图书功能。按标题或作者排序。
结语
学习回顾
本文从C语言基础(变量、流程控制、函数)到进阶(数组、指针、结构体),再到初阶数据结构(链表、栈、队列),通过详细讲解和代码示例,带你逐步掌握编程核心知识。算法竞赛案例和实战项目帮助你将理论应用于实践,培养解决实际问题的能力。
下一步建议
- 进阶指针:学习指针与动态内存分配(如
malloc
、free
)。 - C标准库:深入了解
string.h
、math.h
等库函数。 - 算法优化:练习快速排序、图算法等进阶内容。
- 平台实践:在LeetCode、HackerRank上刷题,参与开源项目。
鼓励
编程是一门需要耐心和实践的技能。每天写几行代码,尝试新项目,调试错误,你会逐渐从新手成长为高手。坚持下去,编程的世界将为你敞开无限可能!