万字解析:从 C 语言到初阶数据结构

  • 未来还会逐渐补充细节。

万字解析:从 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为例):

  1. 下载并安装Dev-C++或VS Code+MinGW。
  2. 配置环境变量,确保gcc命令可用。
  3. 测试环境:编写并运行一个简单程序。

第一个程序 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;
  • 变量命名:遵循清晰、可读的原则:
    • 使用有意义的名字,如studentAgetotal_score
    • 避免使用C语言关键字(如intif)。
    • 不要以数字开头,如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语言支持文件操作,使用fopenfscanffprintf

#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记录栈顶位置,pushpop操作简单。
  • 边界检查:防止栈溢出或空栈弹出。

算法案例:括号匹配

栈常用于检查括号序列是否有效(如(){}[]):

#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:先入队的元素先出队。
  • 数组实现frontrear跟踪队首和队尾。
  • 边界检查:防止队列溢出或空队列出队。

练手项目:排队叫号系统

模拟银行排队叫号系统:

#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语言基础(变量、流程控制、函数)到进阶(数组、指针、结构体),再到初阶数据结构(链表、栈、队列),通过详细讲解和代码示例,带你逐步掌握编程核心知识。算法竞赛案例和实战项目帮助你将理论应用于实践,培养解决实际问题的能力。

下一步建议

  • 进阶指针:学习指针与动态内存分配(如mallocfree)。
  • C标准库:深入了解string.hmath.h等库函数。
  • 算法优化:练习快速排序、图算法等进阶内容。
  • 平台实践:在LeetCode、HackerRank上刷题,参与开源项目。

鼓励

编程是一门需要耐心和实践的技能。每天写几行代码,尝试新项目,调试错误,你会逐渐从新手成长为高手。坚持下去,编程的世界将为你敞开无限可能!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值