C语言编程实战宝典:550例精解·涵盖基础语法·数组指针·位运算·递归·贪心·动态规划

C语言编程实战宝典:550例精解·涵盖基础语法·数组指针·位运算·递归·贪心·动态规划

本手册基于550个C语言编程实例整理而成,涵盖从入门基础到高级算法的全部知识体系,适合零基础初学者系统学习C语言编程。每个知识点配有完整可运行的代码示例和详细注释说明。


目录

  1. [第1章 C语言编程入门](#第1章 C语言编程入门)
  2. [第2章 选择语句](#第2章 选择语句)
  3. [第3章 循环控制语句](#第3章 循环控制语句)
  4. [第4章 函数](#第4章 函数)
  5. [第5章 数组](#第5章 数组)
  6. [第6章 指针](#第6章 指针)
  7. [第7章 字符串](#第7章 字符串)
  8. [第8章 结构体](#第8章 结构体)
  9. [第9章 位运算](#第9章 位运算)
  10. [第11章 递归](#第11章 递归)
  11. [第12章 栈与队列](#第12章 栈与队列)
  12. [第13章 前缀和与差分](#第13章 前缀和与差分)
  13. [第14章 贪心算法](#第14章 贪心算法)
  14. [第15章 动态规划](#第15章 动态规划)

第1章 C语言编程入门

本章涵盖C语言最基础的知识:程序结构、输入输出、变量、数据类型、运算符等35个实例。

1.1 第一个C语言程序

【实例01_01】Hello World 程序

这是每个程序员学习新语言时必写的第一个程序。

c 复制代码
#include <stdio.h>          // 包含stdio.h头文件,用于输入输出操作
int main(void)
{
    printf("Hello World!");  // 使用printf函数输出Hello World!
    
    return 0;                // 返回整数值0,表示程序正常结束
}

知识点说明:

  • #include <stdio.h>:预处理指令,引入标准输入输出头文件
  • int main(void):程序的入口函数,程序从这里开始执行
  • printf():格式化输出函数,将内容显示在屏幕上
  • return 0:表示程序正常结束

【实例01_02】输出多行文字

c 复制代码
#include <stdio.h>

int main(void)
{
    printf("我喜欢C语言!\n");
    printf("学好编程需要多写代码,实践、实践再实践!!!\n");

    return 0;
}

知识点说明:

  • \n 是转义字符,表示换行
  • 可以多次调用 printf() 输出多行内容

【实例01_03】制表符格式化输出

c 复制代码
#include <stdio.h>
int main(void)
{
    printf("学号\t姓名\t性别\t年龄\n");
    printf("001\t李四\t男\t20\n"); 
    
    return 0;
}

知识点说明:

  • \t 是制表符转义字符,用于对齐列数据
  • 常用转义字符表:
转义字符 含义
\n 换行
\t 水平制表符(Tab)
\\ 反斜杠
\" 双引号
\0 空字符

1.2 整数运算与输出格式

【实例01_04】整数加法运算

c 复制代码
#include <stdio.h>
int main(void)
{
    printf("1+2=%d\n", 1 + 2);  // 输出1+2的计算结果3
    printf("3+4=%d\n", 3 + 4);  // 输出3+4的计算结果7
    
    return 0;
}

知识点说明:

  • %d 是整数格式说明符,对应 int 类型
  • printf 的格式:printf("格式字符串", 变量1, 变量2, ...)

【实例01_05】复杂算术表达式

c 复制代码
#include <stdio.h>
int main(void)
{
    printf("(1 + 8) * 2 - 9 / 4 + 5 = %d\n", (1 + 8) * 2 - 9 / 4 + 5); // 结果为21
                 
    return 0;
}

知识点说明:

  • 整数除法 9/4 = 2(截断小数部分)
  • 运算符优先级:括号 > 乘除 > 加减

【实例01_06】平方与立方表格

c 复制代码
#include <stdio.h>
int main(void)
{
    printf("number square cube\n");
    printf("%d\t%d\t%d\n", 1, 1 * 1, 1 * 1 * 1);
    printf("%d\t%d\t%d\n", 2, 2 * 2, 2 * 2 * 2);
    printf("%d\t%d\t%d\n", 3, 3 * 3, 3 * 3 * 3);
    printf("%d\t%d\t%d\n", 4, 4 * 4, 4 * 4 * 4);
    printf("%d\t%d\t%d\n", 5, 5 * 5, 5 * 5 * 5);
    return 0;
}

运行结果:

复制代码
number square cube
1      1      1
2      4      8
3      9      27
4      16     64
5      25     125

【实例01_07】进制转换输出

c 复制代码
#include <stdio.h>
int main(void)
{
    printf("十进制整数13的十进制数是:%d\n", 13); // 十进制
    printf("十进制整数13的八进制数是:%o\n", 13); // 八进制 → 15
    printf("十进制整数13的十六进制数是:%x", 13); // 十六进制 → d
    
    return 0;    
}

格式说明符对照表:

格式符 含义 示例
%d 十进制整数 13
%o 八进制整数 15
%x 十六进制整数(小写) d
%X 十六进制整数(大写) D

1.3 浮点数与数学函数

【实例01_08】浮点数加法

c 复制代码
#include <stdio.h>
int main(void)
{
    printf("1.1+2.2=%f\n", 1.1 + 2.2);   // 输出1.1+2.2=3.300000
    printf("3.3+4.4=%f\n", 3.3 + 4.4);   // 输出3.3+4.4=7.700000
    return 0;    
}

知识点说明:

  • %f 用于输出 floatdouble 类型,默认保留6位小数
  • %.2f 可以指定保留2位小数

【实例01_09】数学函数的使用

c 复制代码
#include <stdio.h>
#include <math.h>         // 预编译指令包含math.h头文件
int main(void)
{
    printf("%f", sqrt(8) + sin(30 * 3.14159 / 180));
    return 0;    
}

常用数学函数(需包含 math.h):

函数 功能 示例
sqrt(x) 求平方根 sqrt(9.0) = 3.0
pow(x,y) 求x的y次方 pow(2,3) = 8.0
sin(x) 正弦(弧度) sin(3.14159/2) ≈ 1.0
cos(x) 余弦(弧度) cos(0) = 1.0
fabs(x) 绝对值 fabs(-3.5) = 3.5
ceil(x) 向上取整 ceil(3.2) = 4.0
floor(x) 向下取整 floor(3.8) = 3.0
log(x) 自然对数 log(2.71828) ≈ 1.0
log10(x) 以10为底的对数 log10(100) = 2.0

【实例01_10】大数计算(IPv6地址容量)

c 复制代码
#include <stdio.h>
#include <math.h>
int main(void)
{
    printf("IPv6的地址可以分配:%f年\n",
           ldexp(1.0, 128) / 1000000 / (365 * 24 * 60 * 60));
    return 0;
}

知识点说明:

  • ldexp(x, n) 计算 x × 2^n,用于处理超大数字
  • IPv6 有 2^128 个地址,约 3.4×10^38

1.4 变量与输入

【实例01_11】scanf 格式化输入

c 复制代码
#include <stdio.h>
int main(void)
{
    int a, b;                          // 定义两个整型变量
    printf("请输入两个整数,两个整数中间用空格分隔:");
    scanf("%d%d", &a, &b);            // 格式化输入函数,输入两个整数并存储到变量a和b中
    printf("%d+%d=%d", a, b, a + b);  // 输出运算结果
    return 0;
}

知识点说明:

  • scanf() 是格式化输入函数
  • &a 表示变量 a 的地址,& 是取地址运算符
  • 初学者常见错误scanf 中忘写 & 符号

【实例01_12】变量地址与 sizeof

c 复制代码
#include <stdio.h>
int main(void)
{
    int a = 10;  // 定义整型变量a并初始化为10 
    printf("变量a的地址是:%#x, 变量的值是:%d\n", &a, a);
    printf("变量a占%d个字节!", sizeof(a)); // sizeof是运算符,计算变量a所占字节数
    return 0;    
}

知识点说明:

  • %#x0x 开头的十六进制格式输出地址
  • sizeof 是运算符,返回数据类型或变量所占的字节数

1.5 整数类型详解

【实例01_13】各种整数类型

c 复制代码
#include <stdio.h>
int main(void)
{
    int iVar = 10;                   // 有符号整型变量
    signed siVar = -10;              // 等价于signed int siVar;
    unsigned uiVar = 20;             // 等价于unsigned int uiVar;
    short sVar = 10;                 // 等价于short int sVar;
    long lVar = 40;                  // 等价于long int lVar;
    unsigned long ulVar;             // 等价于unsigned long int lVar;
    long long llVar = 200;           // 等价于long long int llVar;
    unsigned long long ullVar = 100; // 等价于unsigned long long int ullVar;
    printf("int       占的字节数:%d, iVar = %d\n", sizeof(int), iVar);
    printf("short     占的字节数:%d, sVar = %hd\n", sizeof(short), sVar);
    printf("long      占的字节数:%d, lVar = %ld\n", sizeof(long), lVar);
    printf("long long 占的字节数:%d, llVar = %lld\n",
           sizeof(long long), llVar);
    return 0;
}

整数类型对照表(常见平台):

类型 字节数 取值范围 格式符
short 2 -32768 ~ 32767 %hd
int 4 -2147483648 ~ 2147483647 %d
long 4或8 视平台而定 %ld
long long 8 -9.2×10^18 ~ 9.2×10^18 %lld
unsigned int 4 0 ~ 4294967295 %u

【实例01_14】整数溢出问题

c 复制代码
#include <stdio.h>
int main(void)
{
    int x = 2147483647;      // 这是有符号整数的最大值:2^31-1
    int y = x + 1;           // y不是2147483648
    printf("x=%d, y=%d\n", x, y);
    return 0; 
}

输出结果: x=2147483647, y=-2147483648

知识点说明:

  • 整数溢出是编程中常见的错误
  • 有符号整数超过最大值会"环绕"变成负数
  • 需要更大范围时,应使用 long long 类型

1.6 实际计算问题

【实例01_15】求1到n的和

c 复制代码
#include <stdio.h>
int main(void)
{
    unsigned int s, n;      // 定义变量
    scanf("%u", &n);
    s = (n + 1) * n / 2;  // 利用求和公式计算1~n的和
    printf("s = %u\n", s); // 输出求和结果
    return 0;
}

数学原理: 1+2+3+...+n = n×(n+1)/2(高斯求和公式)


【实例01_16】秒数换算为时分秒

c 复制代码
#include <stdio.h>
int main(void)
{
    long long seconds;                          // 存储输入的秒数
    printf("输入秒数:");
    scanf("%lld", &seconds);
    printf("%lld秒 = ", seconds);
    int days = seconds / 86400;                 // 一天的秒数86400
    int hours = (seconds % 86400) / 3600;       // 一小时的秒数3600
    int minutes = (seconds % 3600) / 60;        // 一分钟的秒数60
    int remaining_seconds = seconds % 60;
    printf("%d天,%d小时, %d分钟,%d秒", days, hours, minutes, remaining_seconds);
    return 0;
}

算法思路:

  • 用整除 / 得到商
  • 用取余 % 得到余数
  • 依次求天、时、分、秒

【实例01_17】提取三位数各位数字并反转

c 复制代码
#include <stdio.h>
int main(void)
{
    int x, a, b, c; 
    scanf("%d", &x);
    a = x / 100;      // 百位 
    b = x / 10 % 10;  // 十位 
    c = x % 10;       // 个位 
    printf("%d%d%d\n", c, b, a);
    return 0; 
}

算法思路:

  • x/100 取百位
  • x/10%10 取十位(先去掉个位,再取个位)
  • x%10 取个位

1.7 自增自减运算符

【实例01_18】前缀与后缀自增

c 复制代码
#include <stdio.h> 
int main(void)
{
    int a = 7, b = 7, c, d;
    a = a + 1;  // 将a变量的值增1
    c = a++;    // 后缀方式:先返回a的值,再增1
    d = ++b;    // 前缀方式:先增1,再返回b值
    printf("a = %d, b = %d, c = %d, d = %d\n", a, b, c, d);
    return 0;
}

输出: a = 9, b = 8, c = 8, d = 8

区别说明:

  • a++(后缀):先使用a的值,再将a加1
  • ++a(前缀):先将a加1,再使用a的值

【实例01_19】交换两个变量的值

c 复制代码
#include <stdio.h> 
int main(void)
{
    int a, b, t;
    printf("请输入两个变量的值:");
    scanf("%d %d", &a, &b);
    printf("交换前a = %d, b = %d\n", a, b);
    t = a;  // a的值放入变量t中
    a = b;  // b的值放入变量a中
    b = t;  // t的值放入变量b中
    printf("交换后a = %d, b = %d\n", a, b);
    return 0;
}

算法思路: 借助临时变量t,类似"用第三个杯子倒水"的过程


【实例01_20】多变量求和

c 复制代码
#include <stdio.h> 
int main(void)
{
    int c1, c2, c3, c4, c5, c6, c7, c8;
    scanf("%d%d%d%d%d%d%d%d", &c1, &c2, &c3, &c4, &c5, &c6, &c7, &c8);
    printf("%d\n", c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8);
    return 0;
}

1.8 浮点类型与宏常量

【实例01_21】圆的面积和周长(使用宏常量)

c 复制代码
#include <stdio.h>
#define PI 3.14159  // 宏常量
int main(void)
{
    float radius, area, circumference;  // 单精度的浮点变量
    printf("请输入一个圆的半径:");
    scanf("%f", &radius);              // 输入半径
    area = PI * radius * radius;       // 计算圆的面积
    circumference = 2 * PI * radius;   // 计算圆的周长
    printf("半径为%.2f的圆的面积是:%.2f\n", radius, area);
    printf("半径为%.2f的圆的周长是:%.2f\n", radius, circumference);
    return 0;
}

知识点说明:

  • #define PI 3.14159 定义宏常量,全大写是惯例
  • float 是单精度浮点类型,double 是双精度浮点类型
  • %.2f 保留2位小数输出

浮点类型对比:

类型 字节数 精度 格式符
float 4 约6-7位有效数字 %f
double 8 约15-16位有效数字 %lf(输入) / %f(输出)

【实例01_22】双精度浮点计算

c 复制代码
#include <stdio.h>
int main(void)
{
    double x, y;               // 双精度浮点变量    
    scanf("%lf %lf", &x, &y);        
    printf("%g\n", 2 * x + 5 - y); 
    return 0;
}

知识点说明:

  • %g 格式会自动选择 %f 或科学计数法格式,去掉末尾多余的零

【实例01_23】三次多项式求值

c 复制代码
#include<stdio.h>
int main(void)
{
    double x, a, b, c, d, f;
    scanf("%lf%lf%lf%lf%lf", &x, &a, &b, &c, &d);
    f = a * x * x * x + b * x * x + c * x + d;
    printf("%.7f", f);
    return 0;
}

说明: 计算多项式 f = ax³ + bx² + cx + d 的值


1.9 字符类型

【实例01_24】字符的输入输出

c 复制代码
#include <stdio.h>
int main(void) 
{
    char c;
    printf("请输入一个字符: ");
    c = getchar();       // 从标准输入获取一个字符,并将其赋值给变量c 
    printf("您输入的字符是: ");
    putchar(c);          // 将字符c输出到标准输出流
    printf("\n该字符的ASCII值是:%d", c);
    return 0;
}

知识点说明:

  • getchar() 读取一个字符
  • putchar(c) 输出一个字符
  • 字符实际存储的是其 ASCII 整数值,可用 %d 格式输出

常见 ASCII 值:

字符 ASCII值
'0' 48
'A' 65
'a' 97
' '(空格) 32

【实例01_25】两点间中点坐标

c 复制代码
#include <stdio.h>
int main(void)
{
    double x1, y1, x2, y2;
    printf("输入第一个点的坐标:");
    scanf("(%lf,%lf)", &x1, &y1);  // 输入时形如(3.4,5.6)表示点坐标
    getchar();                       // 接收回车符
    printf("输入第二个点的坐标:");
    scanf("(%lf,%lf)", &x2, &y2);
    double x, y;
    x = (x1 + x2) / 2;
    y = (y1 + y2) / 2;
    printf("中点是:(%f,%f)", x, y);
    return 0;
}

1.10 综合应用

【实例01_26】复利计算

c 复制代码
#include <stdio.h>
#include <math.h>
int main(void)
{
    float rate, capital, deposit;
    int n;
    printf("请输入本金(元):");
    scanf("%f", &capital);
    printf("请输入年利率(%%):");
    scanf("%f", &rate);
    printf("请输入存款年限:");
    scanf("%d", &n);
    // 计算复利
    float factor = 1.0 + rate / 100;
    deposit = capital * pow(factor, n);
    // 输出结果
    printf("到期时您将获得的本利之和为:%.2f元\n", deposit);
    return 0;
}

说明: 复利公式 A = P × (1 + r)^n,其中 P 为本金,r 为年利率,n 为年数


【实例01_27】贷款余额计算

c 复制代码
#include <stdio.h>
int main(void)
{
    // 定义5个变量:贷款总额、年利率、每月还款金额、每月利率、剩余贷款
    float loan, rate, payment, interestRate, remaining;
    printf("请输入贷款总额(元):");
    scanf("%f", &loan);
    printf("请输入年利率(%%):");
    scanf("%f", &rate);
    printf("请输入每月还款金额(元):");
    scanf("%f", &payment);
    interestRate = rate / 1200;  // 计算每月利率
    // 计算前3个月还款后的余额
    remaining = loan + loan * interestRate - payment; 
    printf("第1次还款后的贷款余额为:%.2f元\n", remaining);
    remaining = remaining + remaining * interestRate - payment;
    printf("第2次还款后的贷款余额为:%.2f元\n", remaining);
    remaining = remaining + remaining * interestRate - payment;
    printf("第3次还款后的贷款余额为:%.2f元\n", remaining);
    return 0;
}

【实例01_28】字符三角形

c 复制代码
#include <stdio.h>
int main(void)
{
    char ch;                              // 定义存储1个字符的变量
    printf("请输入1个字符:");
    scanf("%c", &ch) ;
    
    printf("    %c\n", ch);              // 第1行1个字符,前面4个空格
    printf("   %c %c\n", ch, ch);        // 第2行2个字符,前面3个空格
    printf("  %c %c %c\n", ch, ch, ch);  // 第3行3个字符,前面2个空格
    printf(" %c %c %c %c\n", ch, ch, ch, ch);  // 第4行4个字符,前面1个空格
    printf("%c %c %c %c %c\n", ch, ch, ch, ch, ch); // 第5行5个字符,前面0个空格
    return 0;
}

【实例01_29】大小写字母转换

c 复制代码
#include <stdio.h> 
int main(void)
{
    char lowerch, upperch;
    printf("请输入小写字母:");
    scanf("%c", &lowerch);
    upperch = lowerch - 32;  // 将小写字母的ASCII值减32
    printf("%c对应的大写字母为:%c\n", lowerch, upperch);
    printf("%c和%c对应的ASCII码值分别为%d和%d\n", lowerch, upperch,
           lowerch, upperch);
    return 0;
}

知识点说明:

  • 小写字母 'a'~'z' 的 ASCII 码比大写字母 'A'~'Z' 大32
  • 小写转大写:upperch = lowerch - 32
  • 大写转小写:lowerch = upperch + 32

【实例01_30】凯撒密码

c 复制代码
#include <stdio.h>
int main(void)
{
    char ch;
    int n;
    scanf("%c%d", &ch, &n);
    printf("%c 替换成了 %c\n", ch, (ch - 'A' + n) % 26 + 'A');
    return 0;
}

说明: 凯撒密码是一种简单的字母替换加密,每个字母向后移动n位


1.11 布尔类型与类型转换

【实例01_31】布尔类型(_Bool)

c 复制代码
#include <stdio.h>
int main(void)
{
    _Bool b;
    printf("sizeof(_Bool) = %d\n", sizeof(_Bool));
    b = 3;          // 因为3非0,所以b的值为1
    printf("b = %d \n", b);
    b = b - 3;      // 因为b - 3表达式的值为-2,所以b的值为1
    printf("b = %d \n", b);
    b = 0;
    printf("b = %d \n", b);
    return 0;
}

知识点说明:

  • _Bool 是C99引入的布尔类型,只有0(假)和1(真)两个值
  • 任何非零值赋给 _Bool 变量,结果都是1

【实例01_32】隐式与强制类型转换

c 复制代码
#include <stdio.h>
int main(void)
{
    int a, b;
    a = 29.99 + 10.98;             // 隐式类型转换
    b = (int) 29.99 + (int) 10.99; // 强制类型转换
    printf("a = %d, b = %d", a, b);
    return 0;
}

输出: a = 40, b = 39

说明:

  • 隐式转换:29.99 + 10.98 = 40.97,赋给int变量后截断为40
  • 强制转换:(int)29.99 = 29(int)10.99 = 1029 + 10 = 39

【实例01_33】ceil 函数与类型转换

c 复制代码
#include<stdio.h>
#include<math.h> 
int main(void)
{
    int n, x, y;
    scanf("%d%d%d", &n, &x, &y);
    int num = n - ceil((double)y / x); // 强制类型转换
    printf("%d", num);
    return 0;
}

说明: (double)y / x 先将y转为double,避免整数除法丢失精度


1.12 复合赋值与逗号运算符

【实例01_34】复合赋值运算符

c 复制代码
#include <stdio.h>
int main(void)
{
    int num = 10;
    num += 5;   // 复合赋值运算符 +=,相当于 num = num + 5
    printf("num += 5 的结果为:%d\n", num);
    num -= 3;   // 复合赋值运算符 -=,相当于 num = num - 3
    printf("num -= 3 的结果为:%d\n", num);
    num *= 2;   // 复合赋值运算符 *=,相当于 num = num * 2
    printf("num *= 2 的结果为:%d\n", num);
    num /= 4;   // 复合赋值运算符 /=,相当于 num = num / 4
    printf("num /= 4 的结果为:%d\n", num);
    num %= 3;   // 复合赋值运算符 %=,相当于 num = num % 3
    printf("num %%= 3 的结果为:%d\n", num);
    return 0;
}

复合赋值运算符汇总:

运算符 等价写法
a += b a = a + b
a -= b a = a - b
a *= b a = a * b
a /= b a = a / b
a %= b a = a % b

【实例01_35】逗号运算符

c 复制代码
#include <stdio.h>
int main(void)
{
    int a = 10, b = 20, c = 30;
    a = b, c;            // 先执行a = b,再执行c(c无实际效果)
    printf("a=%d\n", a); // 因此a的值是20
    a = (b, c);          // (b,c)是一个表达式,结果是c
    printf("a=%d\n", a); // 因此a的值30
    return 0;
} 

说明: 逗号运算符从左到右依次求值,整个表达式的值是最右边操作数的值


第1章小结

通过本章35个实例,我们掌握了:

  1. 程序结构#includemain函数、return 0
  2. 输出函数printfputchar
  3. 输入函数scanfgetchar
  4. 数据类型intshortlonglong longfloatdoublechar_Bool
  5. 格式说明符%d%f%c%lf%lld
  6. 运算符:算术、赋值、自增自减、类型转换、逗号运算符
  7. 数学库sqrtpowsinceilfloor
  8. 实际问题:求和、秒换算、字符转换、复利、贷款计算

第2章 选择语句

本章包含25个实例,涵盖if-else多分支判断、三目运算符、switch-case语句等核心控制流知识。

2.1 if 语句基础

【实例02_01】求三个整数的最大值和最小值

c 复制代码
#include <stdio.h>

int main(void)
{
    int a, b, c;    // 3个变量存储输入的3个整数
    int maxV, minV; // 2个变量用于存储3个数中的最大值和最小值

    printf("请输入3个整数:\n");
    scanf("%d %d %d", &a, &b, &c);
    maxV = a;   // 假定a是最大值
    minV = a;   // 假定a是最小值
    // 之后的两个整数b和c与maxV比较,如果大则作替换
    if (b > maxV)
        maxV = b;
    if (c > maxV)
        maxV = c;
    // 之后的两个整数b和c与minV比较,如果小则作替换
    if (b < minV)
        minV = b;
    if (c < minV)
        minV = c;

    printf("最大值是:%d\n最小值是:%d\n", maxV, minV);

    return 0;
}

算法思路: 先假设第一个数是最大/最小值,然后逐一比较其他数更新


【实例02_02】求整数的绝对值

c 复制代码
#include <stdio.h>

int main(void)
{
    int x;

    printf("输入一个整数:");
    scanf("%d", &x);
    printf("%d的绝对值是:", x);
    if (x < 0)
        x = -x;  // 取相反数
    printf("%d\n", x);

    return 0;
}

if 语句语法:

c 复制代码
if (条件表达式)
    语句;
// 或
if (条件表达式)
{
    语句1;
    语句2;
}

【实例02_03】判断是否为小写字母

c 复制代码
#include <stdio.h>

int main(void) 
{
    char ch;
    printf("请输入一个字符:");
    scanf("%c", &ch);

    if (ch >= 'a' && ch <= 'z') 
    {
        printf("%c 是小写字母。\n", ch);
    }

    return 0;
}

逻辑运算符:

运算符 含义 示例
&& 逻辑与(AND) a>0 && b>0
` `
! 逻辑非(NOT) !flag

2.2 if-else 语句

【实例02_04】两个数升序排列输出

c 复制代码
#include <stdio.h>

int main(void)
{
    int a, b;

    scanf("%d %d", &a, &b);
    if (a < b)
        printf("%d %d\n", a, b);   // 输出a和b
    else
        printf("%d %d\n", b, a);   // 输出b和a

    return 0;
}

【实例02_06】三目运算符求最大值

c 复制代码
#include <stdio.h> 

int main(void)
{
    double a, b, maxV;

    printf("请输入两个实数:");
    scanf("%lf %lf", &a, &b);
    maxV = a > b ? a : b;   // max_v是a、b的最大值
    printf("两个数中较大的数为:%.2f", maxV);

    return 0;
}

三目运算符语法: 条件 ? 值1 : 值2

  • 条件为真,返回值1
  • 条件为假,返回值2

【实例02_07】对三个整数排序

c 复制代码
#include <stdio.h>

int main(void)
{
    int a, b, c, t;

    scanf("%d %d %d", &a, &b, &c);

    // 将最小的整数放在a的位置
    if (a > b)
    {
        t = a; a = b; b = t;
    }
    // 再次比较,确保a是其中最小的数
    if (a > c)
    {
        t = a; a = c; c = t;
    }
    // 比较b和c,确保b的值小于等于c的值
    if (b > c)
    {
        t = b; b = c; c = t;
    }

    printf("%d %d %d\n", a, b, c);

    return 0;
}

说明: 用三次比较交换,将三个数从小到大排列(选择排序的简化版)


【实例02_08】判断闰年

c 复制代码
#include <stdio.h>

int main(void)
{
    int year;

    printf("请输入一个年份:");
    scanf("%d", &year);

    if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
    {
        printf("Y\n"); // 是闰年
    }
    else
    {
        printf("N\n"); // 不是闰年
    }

    return 0;
}

闰年判断规则:

  • 能被4整除,但不能被100整除;或者
  • 能被400整除

【实例02_09】与7有关的数

c 复制代码
#include <stdio.h>

int main(void)
{
    int x;

    scanf("%d", &x);
    if (x % 7 == 0 || x / 10 == 7 || x % 10 == 7) // 是7的倍数或个位是7或十位是7
        printf("该数与7有关");
    else
        printf("该数与7无关");

    return 0;
}

【实例02_10】判断三角形

c 复制代码
#include<stdio.h>

int main(void)
{
    int a, b, c;

    scanf("%d %d %d", &a, &b, &c);
    if (a + b > c && a + c > b && b + c > a) // 任意两边的和大于第3边
        printf("yes\n");
    else
        printf("no\n");

    return 0;
}

三角形条件: 任意两边之和大于第三边


【实例02_11】四位数拆分计算

c 复制代码
#include <stdio.h>

int main(void)
{
    int n = -2304;
    printf("请输入一个4位整数:");
    scanf("%d", &n);
    // 拆分n为两个两位的整数a和b
    int a = n / 100;
    int b = n % 100;

    int addition = a + b;
    int subtraction = a - b;
    int multiplication = a * b;

    printf("拆分后的两个整数:a = %d,b = %d\n", a, b);
    printf("加法结果:%d\n", addition);
    printf("减法结果:%d\n", subtraction);
    printf("乘法结果:%d\n", multiplication);

    if (b != 0)
    {
        int division = a / b;
        int remainder = a % b;
        printf("除法结果:%d\n", division);
        printf("求余结果:%d\n", remainder);
    }
    else
    {
        printf("b为0了,无法进行除法和求余运算!");
    }

    return 0;
}

2.3 if-else if-else 多分支

【实例02_17】成绩等级判断

c 复制代码
#include <stdio.h>

int main(void)
{
    int score;               // 存放成绩的整型变量
    char grade;              // 存放成绩对应的等级

    printf("输入0-100成绩:");
    scanf("%d", &score);
    if (score >= 90)         // 成绩在90~100分之间的等级为A
        grade = 'A'; 
    else if (score >= 80)    // 成绩在80~89分之间的等级为B
        grade = 'B'; 
    else if (score >= 70)    // 成绩在70~79分之间的等级为C
        grade = 'C'; 
    else if (score >= 60)    // 成绩在60~69分之间的等级为D
        grade = 'D'; 
    else                     // 成绩在0~59分之间的等级为E
        grade = 'E';                              
    printf("%d对应的等级为:%c\n", score, grade);

    return 0;
}

【实例02_18】字符类型判断(使用 ctype.h)

c 复制代码
#include <stdio.h>
#include <ctype.h>

int main(void)
{
    char c;
    printf("请输入一个字符:");
    scanf("%c", &c);
    if (isdigit(c))
        printf("%c 是一个数字字符\n", c);
    else if (isalpha(c))
        printf("%c 是一个英文字符\n", c);
    else if (isspace(c))
        printf("%c 是一个空格字符\n", c);
    else
        printf("%c 是其他字符\n", c);

    return 0;
}

ctype.h 常用字符分类函数:

函数 说明
isdigit(c) 是否为数字字符(0-9)
isalpha(c) 是否为字母(a-z, A-Z)
islower(c) 是否为小写字母
isupper(c) 是否为大写字母
isspace(c) 是否为空白字符(空格、换行等)
toupper(c) 转换为大写
tolower(c) 转换为小写

【实例02_21】BMI体重指数计算

c 复制代码
#include <stdio.h>

int main(void)
{
    double h, w, t;

    printf("请输入您的身高(以m为单位):");
    scanf("%lf", &h);
    printf("请输入您的体重(以kg为单位):");
    scanf("%lf", &w);

    t = w / (h * h); // 计算BMI

    if (t < 18.5)
    {
        printf("您的BMI为%.2f,属于偏瘦。\n", t);
    }
    else if (t >= 18.5 && t < 24)
    {
        printf("您的BMI为%.2f,属于正常的体重。\n", t);
    }
    else if (t >= 24 && t < 28)
    {
        printf("您的BMI为%.2f,属于过重。\n", t);
    }
    else
    {
        printf("您的BMI为%.2f,属于肥胖。\n", t);
    }

    return 0;
}

【实例02_22】某月有多少天

c 复制代码
#include <stdio.h>

int main(void)
{
    int year, month;
    int days;

    scanf("%d %d", &year, &month);
    if (month == 4 || month == 6 || month == 9 || month == 11) // 平月
        days = 30;
    else if (month == 2) // 二月
    {
        // 判断是不是闰年
        if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
            days = 29;
        else
            days = 28;
    }
    else
        days = 31; // 大月
    printf("%d\n", days);

    return 0;
}

【实例02_20】求一元二次方程的根

c 复制代码
#include <stdio.h>
#include <math.h>

int main(void)
{
    float a, b, c, x1, x2, deta, r, i;

    scanf("%f%f%f", &a, &b, &c);
    deta = b * b - 4 * a * c;
    r = -b / (2 * a) + 0.0; // 为了避免-0
    if (deta > 0)            // 两个实根
    {
        if (a > 0)           // 因为要求x1>x2,所以又分为两种情况
        {
            x1 = r + sqrt(deta) / (2 * a);
            x2 = r - sqrt(deta) / (2 * a);
        }
        else
        {
            x1 = r - sqrt(deta) / (2 * a);
            x2 = r + sqrt(deta) / (2 * a);
        }
        printf("x1=%.5f;x2=%.5f\n", x1, x2);
    }
    else if (deta < 0) // 两个虚根
    {
        i = sqrt(-deta) / (2 * a);
        if (a < 0)
        {
            i = -i;
        }
        printf("x1=%.5f+%.5fi;x2=%.5f-%.5fi\n", r, i, r, i);
    }
    else // 相等的实根
        printf("x1=x2=%.5f\n", r);

    return 0;
}

求根公式: x = (-b ± √(b²-4ac)) / (2a),判别式Δ=b²-4ac决定根的类型


2.4 switch-case 语句

【实例02_24】判断元音辅音字母

c 复制代码
#include <stdio.h>
#include <ctype.h>

int main(void)
{
    char inputChar;
    printf("请输入一个字母:");
    scanf("%c", &inputChar);

    char uppercaseChar = toupper(inputChar); // 将输入字符转换为大写字母,方便比较

    switch (uppercaseChar)
    {
    case 'A':
    case 'E':
    case 'I':
    case 'O':
    case 'U':
        printf("%c 是元音字母\n", inputChar);
        break;
    default:
        printf("%c 不是元音字母\n", inputChar);
        break;
    }

    return 0;
}

switch 语句语法:

c 复制代码
switch (表达式)
{
case 常量1:
    语句;
    break;  // 跳出switch
case 常量2:
    语句;
    break;
default:    // 可选,处理其他情况
    语句;
    break;
}

注意: 忘写 break 会导致"穿透"(fall through),即继续执行下一个case


【实例02_25】简单计算器

c 复制代码
#include <stdio.h>

int main(void)
{
    int num1, num2; // 两个运算的整数
    char oper;      // 运算符

    scanf("%d %d %c", &num1, &num2, &oper);

    switch (oper)
    {
    case '+':
        printf("%d\n", num1 + num2);
        break;
    case '-':
        printf("%d\n", num1 - num2);
        break;
    case '*':
        printf("%d\n", num1 * num2);
        break;
    case '/':
        if (num2 == 0)
            printf("Divided by zero!\n");
        else
            printf("%d\n", num1 / num2);
        break;
    default:
        printf("Invalid operator!\n");
        break;
    }

    return 0;
}

【实例02_23】整数逆序(switch 用于数字位数)

c 复制代码
#include <stdio.h>
#include <math.h>

int main(void)
{
    int x, numDigits, ones, tens, hundreds, thousands, tenThousands;

    scanf("%d", &x);

    numDigits = ceil(log10(x + 1)); // 计算整数的位数

    ones = x % 10;                   // 个位
    tens = (x / 10) % 10;            // 十位
    hundreds = (x / 100) % 10;       // 百位
    thousands = (x / 1000) % 10;     // 千位
    tenThousands = (x / 10000) % 10; // 万位

    printf("位数:%d\n", numDigits);

    switch (numDigits)
    {
    case 1:
        printf("%d\n", ones);
        printf("%d\n", ones);
        break;
    case 2:
        printf("%d %d\n", tens, ones);
        printf("%d\n", ones * 10 + tens);
        break;
    case 3:
        printf("%d %d %d\n", hundreds, tens, ones);
        printf("%d\n", ones * 100 + tens * 10 + hundreds);
        break;
    case 4:
        printf("%d %d %d %d\n", thousands, hundreds, tens, ones);
        printf("%d\n", ones * 1000 + tens * 100 + hundreds * 10 + thousands);
        break;
    case 5:
        printf("%d %d %d %d %d\n", tenThousands, thousands, hundreds, tens, ones);
        printf("%d\n", ones * 10000 + tens * 1000 +
                           hundreds * 100 + thousands * 10 + tenThousands);
        break;
    default:
        printf("输入无效\n");
        break;
    }

    return 0;
}

2.5 综合应用

【实例02_15】温度转换(摄氏/华氏)

c 复制代码
#include <stdio.h>
#define FAC 1.8                     // 华氏温度转摄氏温度的比例因子
#define INC 32.0                    // 华氏温度转摄氏温度的常量偏移量

int main(void)
{
    float t, tc, tf;                // 读入的温度值、摄氏温度、华氏温度
    char corf;                      // 华氏温度或摄氏温度字母

    printf("Enter temperature:");
    scanf("%f %c", &t, &corf);

    if (corf == 'c' || corf == 'C')      // 摄氏转华氏
    {
        tc = t;
        tf = t * FAC + INC;
    }
    else if (corf == 'f' || corf == 'F') // 华氏转摄氏
    {
        tf = t;
        tc = (t - INC) / FAC;
    }
    else                                 // 输入错误
        tc = tf = 0.0;

    printf("The temperature is: %gC = %gF\n", tc, tf);

    return 0;
}

【实例02_16】快递邮资计算

c 复制代码
#include <stdio.h>

int main(void)
{
    int weight, y; // 邮件重量,邮资
    char isUrgent; // 是否加急

    scanf("%d %c", &weight, &isUrgent); 
    if (weight <= 1000)
        y = 8;
    else if ((weight - 1000) % 500 == 0)
        y = 8 + ((weight - 1000) / 500) * 4;
    else
        y = 8 + ((weight - 1000) / 500 + 1) * 4;
    if (isUrgent == 'y')
        y += 5; // 如果加急标,邮资加5
    printf("%d\n", y);

    return 0;
}

【实例02_19】商品折扣计算

c 复制代码
#include <stdio.h>

int main(void)
{
    int n;       // 小明要购买的商品数量
    float price; // 开心果的原始价格

    scanf("%d%f", &n, &price);

    float money = 0;
    if (n == 1)
        money = price * 0.8;
    else if (n == 2)
        money = price * 0.8 + price * 0.6;
    else if (n == 3)
        money = price * 0.8 + price * 0.6 + price * 0.55;
    else
        money = price * (0.8 + 0.6 + 0.55) + (n - 3) * price * 0.5;

    if (money <= 50) // 购物金额不大于50元加10元邮费
        money += 10;

    printf("%.2f\n", money);

    return 0;
}

第2章小结

本章掌握了C语言的三种选择控制结构:

  1. 单分支 if:只在条件成立时执行
  2. 双分支 if-else:条件成立执行一个分支,否则执行另一个
  3. 多分支 if-else if-else:多个条件依次判断
  4. 三目运算符 ?::简洁的二选一表达式
  5. switch-case:基于整数或字符值的多路分支

关键点:

  • else 与最近的未匹配 if 配对(悬挂else问题)
  • switch 中必须注意 break,否则会穿透到下一个case
  • 比较浮点数时尽量避免用 ==,应使用范围判断

第3章 循环控制语句

本章包含43个实例,涵盖 for、while、do-while 三种循环结构,以及 break、continue 语句的使用。

3.1 for 循环

【实例03_01】累加求1到n的和

c 复制代码
#include <stdio.h>
int main(void)
{
    int n, sum = 0, i;           // sum存放最后的和,相当于累加器
    
    scanf("%d", &n);
    for (i = 1; i <= n; i++)     // i的值从1变到n
        sum += i;                // 将i的值加到累加器sum中
    printf("sum = %d\n", sum);
    
    return 0;
}

for 循环语法:

c 复制代码
for (初始化; 条件判断; 更新)
{
    循环体;
}

执行过程:初始化 → 判断条件 → 执行循环体 → 更新 → 判断条件 → ...


【实例03_02】等差数列求和

c 复制代码
#include <stdio.h>
int main(void)
{
    int a, d, n, sum = 0;
    
    scanf("%d %d %d", &a, &d, &n);
    for (int i = 1; i <= n; i++)
    {
        sum += a; // 将第i项加到sum中
        a += d;   // 求第i + 1项
    }
    printf("sum = %d\n", sum);
    
    return 0;
}

【实例03_03】阶乘计算

c 复制代码
#include <stdio.h>
int main(void)
{
    int n;
    int fact = 1;
    
    scanf("%d", &n);   // 运行时注意n的值不能大于15
    for (int i = 1; i <= n; i++)
    {
        fact *= i;     // 累乘
    }
    printf("%d! = %d\n", n, fact);
    
    return 0;
}

说明: n! = 1×2×3×...×n,注意 int 最多计算 12!(超过会溢出)


【实例03_05】求n个数中的最大跨度

c 复制代码
#include <stdio.h>
int main(void)
{
    int n, maxV, minV;
    
    scanf("%d", &n);         // n个整数
    scanf("%d", &maxV);      // 读第一个整数到变量maxV中
    minV = maxV;             // 第一个数既是最大值也是最小值
    int tmp;
    for (int i = 2; i <= n; i++)
    {
        scanf("%d", &tmp);   // 读一个整数到变量tmp中
        if (tmp > maxV)      // 与最大值比
            maxV = tmp; 
        if (tmp < minV)      // 与最小值比
            minV = tmp; 
    }
    printf("%d\n", maxV - minV);  // 输出最大跨度值
    
    return 0;
}

【实例03_08】斐波那契数列(兔子问题)

c 复制代码
#include <stdio.h>
int main(void)
{
    int n;
    long long f = 1;
    
    scanf("%d", &n);
    if (n == 1)
        printf("%lld", f);
    else if (n == 2)
        printf("%lld %lld", f, f);
    else
    {
        printf("%lld %lld", f, f);
        long long f1 = 1, f2 = 1; // f1和f2分别为f的前两项
        for (int i = 3; i <= n; i++)
        {
            f = f1 + f2;  // f 为数列中前两项的和
            printf(" %lld", f);
            f1 = f2;      // 迭代
            f2 = f;       // 迭代
        }
    }
    
    return 0;
}

斐波那契数列: 1, 1, 2, 3, 5, 8, 13, 21, 34, ... 每项等于前两项之和


【实例03_09】猴子吃桃问题

c 复制代码
#include <stdio.h>
int main(void)
{
    int n, peaches = 1;    // 初始时第n天有1个桃子
    
    scanf("%d", &n);
    for (int i = n - 1; i >= 1; i--)
    {
        peaches = (peaches + 1) * 2; // peaches(i)=(peaches(i+1)+1)*2
    }
    printf("%d\n", peaches);
    
    return 0;
}

逆向推导思路: 从最后一天往前推,每天的桃子数 = (后一天的桃子数 + 1) × 2


【实例03_12】完数判断(因子和等于本身)

c 复制代码
#include <stdio.h>
int main(void)
{
    int n, sum = 0;  // sum存放1 ~ n-1间n的因子和
    
    scanf("%d", &n);
    for (int i = 1; i <= n / 2; i++)  // 不包含整数本身的因子不超过n/2
        if (n % i == 0)
            sum += i;
    if (n == sum)  // 条件成立,说明n是完数
    {
        printf("%d = 1", n);
        for (int i = 2; i <= n / 2; i++)
            if (n % i == 0)
                printf(" + %d", i);
    }
    else
        printf("Not perfect number\n");
        
    return 0;
}

完数: 因子(不含本身)之和等于本身的数,如6=1+2+3,28=1+2+4+7+14


【实例03_13】素数判断(break 的应用)

c 复制代码
#include <stdio.h>
int main(void)
{
    int n, i;
    
    scanf("%d", &n);
    for (i = 2; i < n; i++)
    {
        if (n % i == 0)  // 如果n能被i整除,说明i是n的因子
            break;
    }
    if (i == n)
        printf("Yes\n");
    else
        printf("No\n");
        
    return 0;
}

素数判断原理: 如果2到n-1之间没有一个数能整除n,则n是素数


【实例03_26】输出100以内的所有素数

c 复制代码
#include <stdio.h>

int main(void)
{
    int m, i, n = 0; // n为记录输出质数个数

    for (m = 2; m <= 100; m++)
    {
        for (i = 2; i * i <= m; i++)  // 只需判断到sqrt(m)
            if (m % i == 0)
                break;
        if (i * i > m)
        {
            n++;
            printf("%2d ", m);
            if (n % 5 == 0)
                printf("\n"); // 每5个换行
        }
    }

    return 0;
}

优化点: 只需判断 2 到 √m,若都不整除则是素数(比判断到 m-1 快很多)


3.2 while 循环

【实例03_14】while 循环求平方根累加

c 复制代码
#include <stdio.h>
#include <math.h> 
int main(void)
{
    int n;
    double sum = 0;
    
    scanf("%d", &n);
    int i = 1;          // 循环初始化在while前
    while (i <= n)      // 循环条件
    {                   
        sum += sqrt(i); // 求i的平方根并累加
        i++;            // 更新循环变量
    }
    printf("sum = %.2f", sum);
    
    return 0;
}

while 循环语法:

c 复制代码
while (条件)
{
    循环体;
}

与 for 循环的对应关系:

c 复制代码
// for循环
for (init; condition; update) { body; }

// 等价while循环
init;
while (condition) { body; update; }

【实例03_22】辗转相除法求最大公约数

c 复制代码
#include <stdio.h>

int main(void)
{
    int a, b, r; // 分别存放两个整数及它们的余数

    printf("输入两个整数:");
    scanf("%d %d", &a, &b);
    while (a % b != 0)
    {
        r = a % b;
        a = b;
        b = r;
    }
    printf("%d和%d的最大公约数是:%d", a, b, b);

    return 0;
}

辗转相除法(欧几里得算法): gcd(a,b) = gcd(b, a%b),直到余数为0


【实例03_17】逐位输出整数各位数字

c 复制代码
#include <stdio.h>
int main(void)
{
    int n;
    
    scanf("%d", &n);
    while (n != 0)
    {
        printf("%d ", n % 10); // 输出当前n的个位上的数字
        n /= 10;               // 将当前的n的个位数去掉
    }
    
    return 0;
}

3.3 do-while 循环

【实例03_18】卡普雷卡尔变换(do-while)

c 复制代码
#include <stdio.h>

int main(void) {
    int n, k = 0; // k 记录变换次数
    scanf("%d", &n);
    
    do {
        int t, maxV, minV, g, s, b;
        g = n % 10;       // 提取个位数
        s = n / 10 % 10;  // 提取十位数
        b = n / 100;      // 提取百位数
        
        if (g > s) t = g, g = s, s = t;
        if (g > b) t = g, g = b, b = t;
        if (s > b) t = s, s = b, b = t;
        
        maxV = b * 100 + s * 10 + g; // 构造最大值
        minV = g * 100 + s * 10 + b; // 构造最小值
        n = maxV - minV;
        printf("%d: %03d - %03d = %03d\n", ++k, maxV, minV, n);
    } while (n && n != 495); // 当n不为0且不等于495时继续循环
    
    return 0;
}

do-while 语法:

c 复制代码
do {
    循环体;
} while (条件);

特点: 循环体至少执行一次(先执行,后判断条件)


3.4 break 和 continue

【实例03_16】break 跳出循环

c 复制代码
#include <stdio.h>
int main(void)
{
    int nums = 12; // 客人最少的可能人数
    
    while (1)      // 无限循环
    {
        if (nums / 2 + nums / 3 + nums / 4 == 65)
        {
            break;  // 满足条件时跳出循环
        }
        nums += 12;
    }
    printf("家中共来了%d个客人。\n", nums);
    
    return 0;
}

break 和 continue 对比:

  • break:立即跳出当前整个循环
  • continue:跳过本次循环剩余部分,继续下一次循环

3.5 嵌套循环与图形输出

【实例03_32】输出左下三角星号

c 复制代码
#include <stdio.h>

int main(void)
{
    for (int row = 1; row <= 6; row++)
    {
        for (int col = 1; col <= row; col++)
        {
            printf("*");
        }
        printf("\n");
    }

    return 0;
}

输出:

复制代码
*
**
***
****
*****
******

【实例03_34】钻石图形输出

c 复制代码
#include <stdio.h>

int main(void)
{
    int n;
    char c;

    scanf("%d %c", &n, &c);

    for (int i = 1; i <= 2 * n - 1; i++) // 钻石图形的总行数 2*n-1
    {
        int row = (i - n > 0) ? 2 * n - i : i; // 钻石图形的逻辑行号

        for (int j = 1; j <= n - row; j++) // 打印空格
            printf(" ");

        for (int j = 1; j <= 2 * row - 1; j++) // 打印字符
        {
            if (j == 1 || j == 2 * row - 1 || c == 'y')
                printf("*");
            else
                printf(" ");
        }
        printf("\n");
    }

    return 0;
}

3.6 综合循环应用

【实例03_24】质因数分解

c 复制代码
#include <stdio.h>

int main(void)
{
    int n;

    scanf("%d", &n);
    printf("%d=", n);
    int start = 1;  // 标志是否为第一个因子
    int k = 2;      // 质因子从最小的2开始
    while (n != 1)
    {
        if (n % k == 0) // k是n的一个质因子
        {
            if (!start)
                printf("*");
            printf("%d", k);
            start = 0;
            n /= k;
        }
        else
            k++;
    }
    return 0;
}

例: 12 = 2 × 2 × 3


【实例03_31】e^x 泰勒级数展开

c 复制代码
#include <stdio.h>
#include <math.h>
#define esp 1E-5

int main(void)
{
    double x, tmp = 1, fact = 1, sum = 1;

    scanf("%lf", &x);

    for (int i = 1; fabs(tmp / fact) >= esp; i++)
    {
        tmp *= x;           // 更新分子
        fact *= i;          // 更新分母(阶乘)
        sum += tmp / fact;  // 累加
    }

    printf("%.4f\n", sum);

    return 0;
}

数学公式: e^x = 1 + x + x²/2! + x³/3! + ... (无穷级数)


【实例03_36】最长连续因子

c 复制代码
#include <stdio.h>
#include <math.h>

int main(void)
{
    int n;

    scanf("%d", &n);

    int maxlen = 0, start = 0;
    int rootn = sqrt(n);
    for (int i = 2; i <= rootn; i++)
    {
        if (n % i == 0)
        {
            int mul = 1, j;
            for (j = i; j <= rootn; j++)
            {
                mul *= j;
                if (n % mul != 0)
                    break;
            }
            if (j - i > maxlen)
            {
                maxlen = j - i;
                start = i;
            }
        }
    }
    if (start == 0)
        printf("%d\n%d\n", 1, n);
    else
    {
        printf("%d\n%d", maxlen, start);
        for (int i = start + 1; i < start + maxlen; i++)
            printf("*%d", i);
    }

    return 0;
}

【实例03_38】零钱兑换(三重循环穷举)

c 复制代码
#include <stdio.h>

int main(void)
{
    int x, total = 0, count = 0;

    scanf("%d", &x);

    for (int yuan5 = x / 5; yuan5 >= 1; yuan5--)
    {
        for (int yuan2 = x / 2; yuan2 >= 1; yuan2--)
        {
            for (int yuan1 = x - 1; yuan1 >= 1; yuan1--)
            {
                if (yuan1 + 2 * yuan2 + 5 * yuan5 == x)
                {
                    count++;
                    total = yuan1 + yuan2 + yuan5;
                    printf("5元:%d枚, 2元:%d枚, 1元:%d枚, 总计:%d枚\n",
                           yuan5, yuan2, yuan1, total);
                }
            }
        }
    }
    printf("共 %d 种方案", count);
    return 0;
}

【实例03_42】计算某日是当年第几天

c 复制代码
#include <stdio.h>

int main(void)
{
    int year, month, day;
    while (scanf("%d/%d/%d", &year, &month, &day) != EOF)
    {
        int sum = 0;
        for (int m = 1; m < month; m++)
        {
            int mdays;
            if (m == 2)
            {
                if (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0))
                    mdays = 29;
                else
                    mdays = 28;
            }
            else if (m == 4 || m == 6 || m == 9 || m == 11)
                mdays = 30;
            else
                mdays = 31;
            sum += mdays;
        }
        sum += day;
        printf("%d\n", sum);
    }

    return 0;
}

第3章小结

本章掌握了C语言三种循环结构:

循环类型 特点 适用场景
for 循环次数已知 固定次数的迭代
while 先判断后执行 不确定次数,条件可能一开始就为假
do-while 先执行后判断 至少执行一次的循环

常用技巧:

  • 累加用 sum += i,累乘用 product *= i
  • 求最大值:先假设第一个是最大,遍历更新
  • break 立即跳出循环,continue 跳过本次迭代
  • 嵌套循环外层控制行,内层控制列

第4章 函数

函数是C语言中最重要的代码组织方式。把相关代码封装成函数,不仅使程序结构清晰,而且实现了代码复用。本章通过29个实例,系统讲解函数的定义、声明、调用、参数传递以及变量作用域等核心知识。

4.1 函数基础概念

什么是函数?

函数就是一段有名字的代码块,完成特定任务。好比工厂里的一台专用机器:你把原材料(参数)放进去,它加工后输出产品(返回值)。

函数的语法结构:

复制代码
返回类型 函数名(参数列表)
{
    // 函数体:实现功能的代码
    return 返回值;  // 如果返回类型为void,可省略
}

函数相关的三个概念:

概念 说明 位置
函数声明(原型) 告诉编译器函数的存在和格式 main函数之前
函数定义 实现函数功能的完整代码 任意位置
函数调用 执行函数,传入实参 程序中需要的位置

4.2 基础函数实例

【实例04_01】求三个整数的最大值

这是最基础的函数示例:定义一个求两数最大值的函数,调用两次即可找出三个数中的最大值。

c 复制代码
#include <stdio.h>

int max(int a, int b);          // 函数原型:求两个整数的最大值

int main(void)
{
    int a, b, c;
    printf("请输入3个整数:");
    scanf("%d %d %d", &a, &b, &c);
    a = max(a, b);              // 函数调用:求出两个整数a和b的最大值赋值给变量a
    a = max(a, c);              // 函数调用:求出两个整数a和c的最大值赋值给变量a

    printf("最大值为:%d\n", a); // 输出最大值

    return 0;
}
int max(int a, int b)           // 函数定义
{
    return a > b ? a : b;
}

知识点说明:

  • 函数原型放在main之前,用于声明函数存在(告知编译器)
  • 函数定义可以放在main之后
  • return a > b ? a : b 使用三目运算符直接返回较大值
  • 同一个函数可以被多次调用,体现了代码复用的优势

【实例04_02】求三个浮点数的中位数

通过排序找中间值,演示多参数函数与临时变量交换技巧。

c 复制代码
#include <stdio.h>

double median(double, double, double);      // 函数声明,形参名可以省略

int main(void)
{
    double x, y, z;
    scanf("%lf %lf %lf", &x, &y, &z);
    printf("median is %.2f", median(x, y, z)); // 函数调用

    return 0;
}
double median(double x, double y, double z) // 函数定义:形参名不可省略
{ 
    double t;
    if (x > y)
    { // x与y值互换
        t = x;
        x = y;
        y = t; 
    }
    if (x > z)
    { // x与z值互换
        t = x;
        x = z;
        z = t; 
    }
    if (y > z)
    { // y与z值互换
        t = y;
        y = z;
        z = t; 
    }

    return y;   // 排序后y是中位数
} 

知识点说明:

  • 函数声明时形参名可以省略,只保留类型即可(如double median(double, double, double);
  • 函数定义时形参名不可省略,否则无法在函数体内引用参数
  • 经过三次两两比较排序后,x ≤ y ≤ z,中位数是y

【实例04_03】计算区间累加求和

演示函数定义放在main之前的写法,以及for循环在函数内的使用。

c 复制代码
#include <stdio.h>

int sum(int m, int n)  // 函数定义放在了main函数的前面
{
    int s = 0;
    for (int i = m; i <= n; i++)
        s += i;        // 累加求和

    return s;          // 返回求和的结果
}
int main(void)
{
    int m, n;

    scanf("%d %d", &m, &n);
    printf("sum = %d\n", sum(m, n)); // 函数调用

    return 0;
}

知识点说明:

  • 函数定义放在main之前时,不需要单独的函数声明(因为定义本身就包含了声明信息)
  • 函数定义放在main之后时,必须在main之前提供函数声明(原型)

【实例04_04】寻找立方和等于另一个数立方的三元组

通过嵌套循环穷举,使用函数封装立方计算逻辑,使代码更清晰。

c 复制代码
#include <stdio.h>

int cube(int x); // 函数声明

int main(void)
{
    int n;
    scanf("%d", &n);
    for (int a = 6; a <= n; a++)
        for (int b = 2; b < a; b++)
            for (int c = b; c < a; c++)
                for (int d = c; d < a; d++)
                {
                    if (cube(a) == cube(b) + cube(c) + cube(d))
                    {
                        printf("Cube = %d, Triple = (%d,%d,%d)\n", a, b, c, d);
                    }
                }

    return 0;
}
// 函数功能:计算整数x的立方值
int cube(int x) 
{
    return x * x * x;
}

知识点说明:

  • x*x*x封装为cube(x)函数,使主程序逻辑更清晰易读
  • 函数体只有一条return语句时,代码十分简洁
  • 四层嵌套循环寻找满足条件a³ = b³+c³+d³的三元组(b≤c≤d<a)

【实例04_05】统计二进制数中1的个数

使用函数封装复杂逻辑,反复调用处理多组数据。

c 复制代码
#include <stdio.h>

int countOnes(int x); // 函数声明

int main(void)
{
    int n, x;
    scanf("%d", &n);
    while (n--)        // 循环n次
    {
        scanf("%d", &x);
        printf("%d\n", countOnes(x)); // 每循环一次,调用一次countOnes函数
    }
    return 0;
}
// 函数功能:计算出一个十进制整数x在二进制数中1的个数
int countOnes(int x)
{
    int count = 0;
    while (x > 0)
    {
        if (x % 2 == 1)
        { // 判断最低位是否为1
            count++;
        }
        x = x / 2; // 去掉最低位(右移一位)
    }
    return count;
}

核心算法:

  • x % 2 == 1:判断当前最低位是否为1
  • x = x / 2:去掉最低位,相当于二进制右移一位
  • 每处理一位就检查是否为1,直到x变为0

4.3 经典数学问题函数

【实例04_06】哥德巴赫猜想验证

哥德巴赫猜想:每个大于2的偶数都能表示为两个质数之和。本实例用函数isPrime封装质数判断。

c 复制代码
#include <stdio.h>

int isPrime(int x); // 函数声明

int main(void)
{
    int n;
    scanf("%d", &n);
    int a = n / 2;    // a的值从大到小枚举
    while (1)
    {
        if (isPrime(a) + isPrime(n - a) == 2) // 都是质数相加结果为2
        {                                     // a 和 n-a 都是质数
            printf("%d %d\n", a, n - a);      // 输出结果
            break;                            // 跳出循环
        }
        a--;          // 枚举下一组 a 和 b
    }
}
// 函数功能:判断一个正整数x是否为质数,是返回1,否则返回0
int isPrime(int x)
{
    if (x <= 1)       // 因为最小的质数是2
    {
        return 0;
    }
    for (int i = 2; i * i <= x; i++)
    {
        if (x % i == 0)
        {
            return 0; // 不是质数,返回0
        }
    }
    return 1;
}

质数判断优化技巧:

  • 只需枚举到√x(即i*i <= x),大大减少判断次数
  • x有因子,则必有一个因子≤ √x
  • 调用两次isPrime,返回值之和为2时说明两个数都是质数

【实例04_07】完数判断(Perfect Number)

完数是指等于其所有真因子(不含本身)之和的数,如 6 = 1+2+3。

c 复制代码
#include <stdio.h>
#include <stdbool.h>

bool isPerfectnumber(int m); // 函数声明

int main(void)
{
    int n;
    scanf("%d", &n);
    if (n < 1 || n > 1000000) // 数据验证
        printf("Input Error!\n");
    else
    {
        for (int i = 6; i <= n; i += 2) // 完数一定是偶数,步长为2
        {
            if (isPerfectnumber(i))
            {
                printf("%d\n", i);
            }
        }
    }
    return 0;
}
// 函数功能:判断整数m是否是完数,如果是返回true,否则返回false
bool isPerfectnumber(int m)
{
    int sum = 0;
    for (int i = 1; i <= m / 2; i++)
        if (m % i == 0)
            sum += i;           // 累加所有真因子
    return m == sum;
}

知识点说明:

  • #include <stdbool.h> 引入布尔类型,可以使用 truefalse
  • bool 类型函数返回 true(非零)或 false(零)
  • 真因子最大不超过m/2,因此循环到m/2即可
  • 已知完数:6、28、496、8128......

【实例04_08】统计回文质数个数

综合使用两个函数:回文数判断 + 质数判断,实现功能组合。

c 复制代码
#include <stdio.h>

int isPalindrome(int x);
int isPrime(int x);

int main(void)
{
    int n, cnt = 0, i;
    scanf("%d", &n);
    for (i = 11; i <= n; i++)
    {                                      // 枚举出11到n之间所有整数
        if (isPalindrome(i) && isPrime(i)) // 既是回文数又是素数
            cnt++;                         // 统计个数
    }
    printf("%d\n", cnt);

    return 0;
}
// 函数功能:判断正整数x是否是回文数,是返回1,否则返回0
int isPalindrome(int x)
{
    int y = 0, z = x;  // y是x的反序数
    while (z != 0)
    {
        y = y * 10 + z % 10;  // 每次取最低位拼接到y
        z /= 10;
    }
    return x == y;             // 如果x的反序数y与x相等,那么这个整数就是回文数
}
// 函数功能:判断一个正整数x是否为质数,是返回1,否则返回0
int isPrime(int x)
{
    if (x <= 1)
        return 0;
    for (int i = 2; i * i <= x; i++)
    {
        if (x % i == 0)
            return 0;
    }
    return 1;
}

回文数判断算法:

  1. 将整数x的各位数字倒序重新组合成y
  2. 如果x == y,则x是回文数
  3. 例如:121 反转后还是 121,所以是回文数

【实例04_09】判断一个数是否与7无关

使用do-while循环逐位检查,演示bool类型返回值的使用。

c 复制代码
#include <stdio.h>
#include <stdbool.h>

bool isNoSeven(int a); // 函数声明

int main(void)
{
    int a;
    scanf("%d", &a);

    if (isNoSeven(a)) // 函数调用
        printf("%d是一个与7无关的数!", a);
    else
        printf("%d是一个与7有关的数!", a);

    return 0;
}
bool isNoSeven(int a) // 函数定义
{
    do
    {
        if (a % 7 == 0)    // 是7的倍数
            return false;
        if (a % 10 == 7)   // 个位是7
            return false;
        a /= 10;           // 去掉个位
    } while (a != 0);

    return true;
}

知识点说明:

  • 与7有关的数:能被7整除,或各位数字中含有7
  • a % 10 == 7检查个位是否为7
  • a /= 10去掉个位,依次检查十位、百位......

【实例04_10】判断丑数(Ugly Number)

丑数是只包含质因子 2、3、5 的正整数(1是特殊的丑数)。

c 复制代码
#include <stdio.h>
#include <stdbool.h>

bool isUgly(int num);  // 函数原型

int main(void)
{
    int x;
    scanf("%d", &x);
    if (isUgly(x))
        printf("%d is ugly number!\n", x);
    else
        printf("%d is not ugly number!\n", x);

    return 0;
}
// 函数功能:判断整数num是否是丑数
bool isUgly(int num)
{
    if (num <= 0)    // 丑数是正整数
        return false;
    while (1)
    {
        if (num == 1 || num == 2 || num == 3 || num == 5)
            return true;          // 1、2、3、5是丑数
        if (num % 2 == 0)
            num /= 2;             // 如果能被2整除,就除以2
        else if (num % 3 == 0)
            num /= 3;             // 如果能被3整除,就除以3
        else if (num % 5 == 0)
            num /= 5;             // 如果能被5整除,就除以5
        else
            return false;         // 不能被2、3、5整除,不是丑数
    }
}

丑数判断思路:

  • 不断将数除以2、3、5(只要能整除),如果最终等于1,则是丑数
  • 丑数举例:1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, ...

【实例04_11】用二分查找实现整数开平方

经典算法:用二分法逼近平方根,结果取整。

c 复制代码
#include <stdio.h>

int mySqrt(int x);

int main(void)
{
    int x;
    printf("Please input a number: ");
    scanf("%d", &x);
    printf("The sqrt of %d is %d\n", x, mySqrt(x));
    return 0;
}
int mySqrt(int x)
{
    int left = 1, right = x, mid;
    while (left <= right)
    {
        mid = left + (right - left) / 2;   // 避免数据溢出(不用(left+right)/2)
        if (mid == x / mid)    // 当 mid*mid = x 时,找到了结果
            return mid;

        if (mid < x / mid)     // 如果 mid² < x,结果在右侧,更新 left
            left = mid + 1;
        else                   // 如果 mid² > x,结果在左侧,更新 right
            right = mid - 1;
    }
    return right;   // 收敛时 right 是最终结果(向下取整)
}

二分查找法核心思想:

  • 每次取中间值mid,根据mid²x的大小关系缩小搜索范围
  • 时间复杂度O(log n),效率远高于逐一试探的O(√n)
  • mid = left + (right-left)/2 防止left+right整数溢出

【实例04_12】打印数字金字塔(多函数协作)

将打印金字塔拆分为多个小函数,展示函数分工协作的设计思想。

c 复制代码
#include <stdio.h>

void printSpaces(int n);
void printNumbers(int n);
void printPyramid(int n);

int main(void)
{
    int n;         // 数字金字塔的行数
    scanf("%d", &n);
    printPyramid(n); // 调用函数打印n行数字金字塔

    return 0;
}
// 函数功能:打印n行数字金字塔
void printPyramid(int n)
{
    for (int i = 1; i <= n; i++)
    {
        printSpaces(n - i); // 打印n-i个空格
        printNumbers(i);    // 打印i个i
        printf("\n");       // 换行
    }
}
// 函数功能: 打印n个空格
void printSpaces(int n)
{
    for (int i = 1; i <= n; i++)
        printf(" ");
}
// 函数功能:打印n个n
void printNumbers(int n)
{
    for (int i = 1; i <= n; i++)
        printf("%d ", n);
}

输出示例(n=4):

复制代码
      4 
   3 3 
 2 2 2 
1 1 1 1

设计思路:

  • printPyramid是主控函数,负责整体结构
  • printSpaces负责打印空格
  • printNumbers负责打印数字
  • 函数之间互相调用,各司其职

4.4 数学计算函数

【实例04_14】打印整数的每一位并求逆序数

综合运用数学函数(log10ceil)和自定义函数。

c 复制代码
#include <stdio.h>
#include <math.h>

void printEachDigit(int x);
int reverseNumber(int x);

int main(void)
{
    int x;
    scanf("%d", &x);
    int n = ceil(log10(x + 1));         // 通过数学函数计算整数位数
    printf("%d\n", n);                  // 打印整数的位数
    printEachDigit(x);                  // 打印整数的每一位
    printf("%d\n", reverseNumber(x));   // 打印逆序数

    return 0;
}
// 函数功能:打印整数x的每一位
void printEachDigit(int x)
{
    int n = ceil(log10(x + 1));
    int power = 1, digit;

    for (int i = 1; i < n; i++)  // power是10^(n-1)
        power *= 10;
    while (n--)
    {
        digit = x / power % 10;  // 计算出整数x的最高位整数值
        power /= 10;
        printf("%d ", digit);
    }
    printf("\n");
    return;                       // 因为函数的返回类型为void
}
// 函数功能:计算正整数x的逆序数
int reverseNumber(int x)
{
    int s = 0;
    while (x)
    {
        s = s * 10 + x % 10;
        x /= 10;
    }
    return s;                     // 返回正整数x的逆序数s
}

【实例04_15】计算某年某月的天数(函数嵌套调用)

days函数内部调用isLeap函数,演示函数的嵌套调用。

c 复制代码
#include <stdio.h>
#include <stdbool.h>

bool isLeap(int year);          // 函数声明
int days(int year, int month);  // 函数声明

int main(void)
{
    int year, month;
    scanf("%d %d", &year, &month);
    printf("%d\n", days(year, month)); // 函数调用

    return 0;
}
// 函数功能:判断year是否是闰年,是返回true,否则false
bool isLeap(int year)
{
    return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
}
// 函数功能:计算给定年月的天数
int days(int year, int month)
{
    int ds = 0;
    if (month == 4 || month == 6 || month == 9 || month == 11) // 平月(小月)
        ds = 30;
    else if (month == 2)        // 二月
    {
        if (isLeap(year))       // 函数调用:判断是不是闰年
            ds = 29;
        else
            ds = 28;
    }
    else                        // 大月
        ds = 31;

    return ds;
}

知识点说明:

  • 函数可以嵌套调用:days内部调用isLeap
  • 闰年判断规则:能被400整除,或者能被4整除但不能被100整除
  • 注意:函数之间的调用关系要确保被调函数已提前声明

【实例04_16】日期推算N天后的日期

在04_15的基础上,通过逐天推算求N天后的日期。

c 复制代码
#include <stdio.h>
#include <stdbool.h>

bool isLeap(int year);
int days(int year, int month);

int main(void)
{
    int year, month, day, n;
    int ds;
    scanf("%d %d %d %d", &year, &month, &day, &n);

    while (n--)
    {
        ds = days(year, month);
        if (day < ds)           // 月内,直接加1天
            day++;
        else if (month < 12)    // 非年末的当月最后一天
        {
            month++;
            day = 1;
        }
        else                    // 年末最后一天
        {
            year++;
            month = 1;
            day = 1;
        }
    }
    printf("%4d-%02d-%02d", year, month, day); // 输出日期

    return 0;
}
bool isLeap(int year)
{
    return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
}
int days(int year, int month)
{
    int ds = 0;
    if (month == 4 || month == 6 || month == 9 || month == 11)
        ds = 30;
    else if (month == 2)
    {
        if (isLeap(year))
            ds = 29;
        else
            ds = 28;
    }
    else
        ds = 31;
    return ds;
}

【实例04_17】最大公约数(辗转相除法)

欧几里得算法(辗转相除法)是求最大公约数的经典算法。

c 复制代码
#include <stdio.h>

int gcd(int m, int n);

int main(void)
{
    int A, B, d;
    scanf("%d%d%d", &A, &B, &d);
    int cnt = 0;
    for (int a = d; a <= A; a += d)
    {
        for (int b = d; b <= B; b += d)
        {
            if (gcd(a, b) == d)
                cnt++;
        }
    }
    printf("%d", cnt);
    return 0;
}
// 函数功能:计算两个整数的最大公约数
int gcd(int m, int n)
{
    int r = 0;
    // 使用辗转相除法计算最大公约数
    do
    {
        r = m % n;  // 计算余数
        m = n;      // 更新m
        n = r;      // 更新n
    } while (r);    // 余数为0时退出循环

    return m;       // 返回最大公约数
}

辗转相除法原理:

  • gcd(m, n) = gcd(n, m%n)
  • 每次将大数对小数取余,直到余数为0
  • 此时除数即为最大公约数
  • 例如:gcd(48, 18) → gcd(18, 12) → gcd(12, 6) → gcd(6, 0) = 6

【实例04_19】计算组合数 C(n,m)

利用阶乘函数计算组合数,体现函数复用。

c 复制代码
#include <stdio.h>

long long combination(int n, int m);
long long factorial(int n);

int main(void)
{
    int n, m;
    scanf("%d %d", &n, &m);
    printf("%lld\n", combination(n, m)); // 输出组合数

    return 0;
}
// 函数功能:计算组合数 C(n,m)
long long combination(int n, int m)
{
    return factorial(n) / (factorial(m) * factorial(n - m)); // C(n,m) = n! / (m! * (n-m)!)
}
// 函数功能:计算n!
long long factorial(int n)
{
    long long res = 1;  // 阶乘的初始值为1
    for (int i = 1; i <= n; i++)
        res *= i;        // 递推计算阶乘

    return res;          // 返回计算结果
}

知识点说明:

  • 组合数公式:C(n,m) = n! / (m! × (n-m)!)
  • combination函数内部调用factorial函数三次
  • 使用long long避免大数相乘时溢出

【实例04_21】泰勒级数求正弦和余弦值

用泰勒级数公式近似计算三角函数,体现数学与编程的结合。

c 复制代码
#include <stdio.h>
#include <math.h>

const double EPSILON = 1E-6;
const double PI = 3.14159;

double mysin(double x); 
double mycos(double x); 
long long factorial(int n);

int main(void)
{
    printf("sin(PI/6)=%.10f\n", sin(PI / 6));
    printf("mySin(PI/6)=%.10f\n", mysin(PI / 6));
    printf("cos(PI/6)=%.10f\n", cos(PI / 6));
    printf("mycos(PI/6)=%.10f\n", mycos(PI / 6));
    return 0;
}
// 函数功能:计算 sin(x),x是弧度值
// 泰勒公式:sin(x) = x - x³/3! + x⁵/5! - x⁷/7! + ...
double mysin(double x)
{
    double term = x, sum = 0;
    int sign = 1;   // 符号
    int i = 1;

    while (term > EPSILON)
    {
        sum += sign * term;           // 泰勒级数累加求和
        sign = -sign;                 // 符号取负(正负交替)
        i += 2;
        term = pow(x, i) / factorial(i); // x^i/i!
    }
    return sum;
}
// 函数功能:计算 cos(x),x是弧度值
// 泰勒公式:cos(x) = 1 - x²/2! + x⁴/4! - x⁶/6! + ...
double mycos(double x)
{
    double term = 1, sum = 0;
    int sign = 1;
    int i = 0;

    while (term > EPSILON)
    {
        sum += sign * term;
        sign = -sign;
        i += 2;
        term = pow(x, i) / factorial(i);
    }
    return sum;
}
// 函数功能:计算n!
long long factorial(int n)
{
    long long res = 1;
    for (int i = 1; i <= n; i++)
        res *= i;
    return res;
}

【实例04_22】计算自然常数 e 的近似值

用级数 e = 1 + 1/1! + 1/2! + 1/3! + ... 计算 e 的值。

c 复制代码
#include <stdio.h>

double eValue(int n);

int main(void)
{
    int n;
    scanf("%d", &n);
    double y = eValue(n);
    printf("%.10f", y);
    return 0;
}
// 函数功能:计算e值(精确到前n项)
double eValue(int n)
{
    double sum = 2;   // e = 1 + 1 + 1/2! + 1/3! + ...,前两项和为2
    double f = 1;     // 阶乘值:1!
    for (int i = 2; i <= n; i++)
    {
        f = f * i;    // i!(递推计算)
        sum += 1 / f; // 累加1/i!
    }
    return sum;
}

【实例04_23】四则运算表达式计算器

按优先级正确处理+-*/运算,演示复杂逻辑函数设计。

c 复制代码
#include <stdio.h>

int compute(int a, int b, char op); // 函数声明

int main(void)
{
    char op1, op2;
    int a = 0, b;
    op1 = '+';
    scanf("%d", &b);

    while (1)
    {
        scanf("%c", &op2);
        if (op2 == '=')         // 结束计算
        {
            if (op1 == '/' && b == 0) // 除数为0的处理
            {
                printf("No 0");
                break;
            }
            printf("%d\n", compute(a, b, op1));
            break;
        }
        else if (op2 == '+' || op2 == '-') // 加减优先级低
        {
            a = compute(a, b, op1);
            op1 = op2;
            scanf("%d", &b);
        }
        else if (op2 == '*' || op2 == '/') // 乘除先计算
        {
            int c;
            scanf("%d", &c);
            if (op2 == '/' && c == 0)
            {
                printf("No 0");
                break;
            }
            else
                b = compute(b, c, op2); // 先计算乘除
        }
    }
    return 0;
}
// 函数功能:根据op的不同值,执行不同运算并返回结果
int compute(int a, int b, char op)
{
    switch (op)
    {
    case '+': return a + b;
    case '-': return a - b;
    case '*': return a * b;
    case '/': return a / b;
    }
}

4.5 变量作用域

变量的作用域决定了变量在程序中可以被访问的范围。

【实例04_26】全局变量的使用

c 复制代码
#include <stdio.h>

int gV = 10;       // 定义全局变量(在所有函数外部)
void fun();

int main(void) 
{
    printf("初始全局变量的值:%d\n", gV);
    fun();         // 调用函数更新全局变量
    printf("更新全局变量后的值:%d\n", gV);
    return 0;
}
void fun() 
{
    gV = 20;       // 更新全局变量的值
}

输出结果:

复制代码
初始全局变量的值:10
更新全局变量后的值:20

【实例04_27】局部变量的作用域

c 复制代码
#include <stdio.h>

int main(void) 
{
    int x = 5;  // 外层作用域的局部变量x
    {
        int x = 10;  // 内层作用域的局部变量x,遮蔽了外层的x
        printf("内层作用域 x = %d\n", x);  // 输出10
    }
    printf("外层作用域 x = %d\n", x);  // 输出5
    return 0;
}

关键规则:

  • 内层作用域的同名变量会遮蔽(shadow)外层的变量
  • 离开内层{}后,内层变量销毁,外层变量重新可见

【实例04_28】局部变量与全局变量同名

c 复制代码
#include <stdio.h>

int x = 10;    // 全局变量x
void fun();

int main(void) 
{
    printf("全局变量 x = %d\n", x); // 访问全局变量,输出10
    fun();
    return 0;
}
void fun() 
{
    int x = 5;  // 局部变量x,遮蔽了全局变量x
    printf("局部变量 x = %d\n", x); // 访问局部变量,输出5
}

【实例04_29】静态局部变量

静态局部变量只初始化一次,在函数多次调用之间保持其值。

c 复制代码
#include <stdio.h>

void fun();

int main(void)
{
    fun(); // 第一次调用
    fun(); // 第二次调用
    return 0;
}
void fun()
{
    static int v = 0;                    // 静态局部变量,只初始化一次!
    printf("静态局部变量的值:%d\n", v); // 打印当前值
    v++;                                 // 更新值,下次调用时保留
}

输出结果:

复制代码
静态局部变量的值:0
静态局部变量的值:1

三种变量对比表:

变量类型 定义位置 生命周期 访问范围 初始值
全局变量 函数外部 程序运行期间 所有函数 自动初始化为0
局部变量 函数/块内部 函数调用期间 当前块 未定义(随机值)
静态局部变量 函数内部(带static) 程序运行期间 当前函数 自动初始化为0

4.6 第4章知识点总结

函数定义语法:

c 复制代码
返回类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, ...)
{
    // 函数体
    return 表达式;  // 返回类型为void时省略
}

关键知识点梳理:

  1. 函数声明 (原型):告诉编译器函数的存在,在main之前或头文件中声明
  2. 函数定义:实现函数功能的完整代码
  3. 函数调用函数名(实际参数列表),实参与形参类型应匹配
  4. 返回值return语句返回计算结果,void类型无需返回值
  5. 参数传递 :C语言默认值传递,函数内修改形参不影响实参

常用编程模式:

  • 将判断逻辑封装成返回boolint(0/1)的函数
  • 将重复计算封装成函数,避免代码重复
  • 函数可以嵌套调用(A调用B,B调用C)

第5章 数组

数组是存储相同类型数据的容器,是C语言中最常用的数据结构之一。本章通过46个实例,系统讲解一维数组、二维数组的定义、初始化、遍历、排序、查找及各种应用算法。

5.1 一维数组基础

数组的定义与初始化:

c 复制代码
// 定义方式1:定义同时赋初值
int a[5] = {1, 2, 3, 4, 5};

// 定义方式2:定义后逐个赋值
int b[5];
b[0] = 10; b[1] = 20; // ...

// 定义方式3:自动推断长度
int c[] = {1, 2, 3};  // 长度自动为3

// 定义方式4:部分初始化(未赋值的元素自动为0)
int d[10] = {0};       // 所有元素初始化为0

数组的基本规则:

特性 说明
下标从0开始 a[0]是第一个元素,a[n-1]是最后一个元素
连续存储 数组元素在内存中紧密排列
数组名即地址 a等价于&a[0],是首元素的地址
越界危险 访问a[n]a[-1]会引发未定义行为

【实例05_01】统计特定值出现的次数

c 复制代码
#include <stdio.h>

int main(void)
{
    int n, m;
    int a[100];    // 定义能存放100个元素的整型数组a

    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        scanf("%d", &a[i]);   // 输入n个元素
    scanf("%d", &m);
    int cnt = 0;
    for (int i = 0; i < n; i++)
        if (a[i] == m)
            cnt++;            // 统计等于m的元素个数
    printf("%d", cnt);

    return 0;
}

【实例05_02】输出数组元素的值和地址

c 复制代码
#include <stdio.h>

int main(void)
{
    int a[10];

    for (int i = 0; i < 10; i++)
        a[i] = 2 * i;               // 给数组赋值:0,2,4,6,...,18
    printf("a[0]~a[9]的值:\n");
    for (int i = 0; i < 10; i++)
    {
        printf("a[%d] = %2d  ", i, a[i]);
        if ((i + 1) % 5 == 0)
            printf("\n");           // 每5个元素换行
    }
    printf("&a[0]~&a[9]的值:\n");
    for (int i = 0; i < 10; i++)
    {
        printf("&a[%d] = %d  ", i, &a[i]); // 输出每个元素的地址
        if ((i + 1) % 5 == 0)
            printf("\n");
    }
    printf("a = %d\n", a);               // 数组名即首元素地址
    printf("sizeof(a) = %d\n", sizeof(a)); // 数组占用总字节数

    return 0;
}

知识点说明:

  • sizeof(a) = 元素个数 × 每个元素字节数(int为4字节,所以10个int = 40字节)
  • a&a[0] 的值相同,都是首元素地址

【实例05_03】统计每个元素左边比它小的个数

c 复制代码
#include <stdio.h>
#define N 100

int main(void)
{
    int arr[N], cnt[N] = {0}; // cnt[i]存放arr[i]左边比它小的整数个数
    int n;

    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        scanf("%d", &arr[i]);
    for (int i = 1; i < n; i++)   // 从第二个元素开始
        for (int j = i - 1; j >= 0; j--) // 向前找更小的元素
            if (arr[j] < arr[i])
                cnt[i]++;
    for (int i = 0; i < n; i++)
        printf("%d ", cnt[i]);

    return 0;
}

【实例05_04】利用数组存储月份天数

用数组代替复杂的if-else判断月份天数,使代码更简洁。

c 复制代码
#include <stdio.h>

int isLeap(int year);

int main(void)
{
    int y, m;
    int mdays[13] = {0, 31, 28, 31, 30, 31, 30,   // 下标0不用(留空)
                     31, 31, 30, 31, 30, 31};       // 对应1~12月天数

    scanf("%d %d", &y, &m);
    if (isLeap(y))
        mdays[2] = 29;   // 闰年2月有29天
    printf("%d\n", mdays[m]); // 直接用月份作下标访问

    return 0;
}
int isLeap(int year)
{
    return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
}

用数组查表的优势:

  • 无需复杂的多重if-else
  • 时间复杂度 O(1),直接下标访问
  • 代码更简洁,维护更方便

【实例05_05】用数组存储斐波那契数列

用数组记录所有斐波那契数,利用递推关系 fib[i] = fib[i-1] + fib[i-2]

c 复制代码
#include <stdio.h>

int main(void)
{
    long long fib[81];

    fib[1] = fib[2] = 1;        // 初始条件
    for (int i = 3; i <= 80; i++)
        fib[i] = fib[i - 1] + fib[i - 2]; // 递推公式
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        printf("%lld ", fib[i]);

    return 0;
}

与递归方法相比的优势:

  • 每个斐波那契数只计算一次(动态规划的思想)
  • 避免了递归的大量重复计算,效率极高
  • 时间复杂度 O(n),空间复杂度 O(n)

【实例05_06】统计小明能摘到的桃子数量

c 复制代码
#include <stdio.h>

int main(void)
{
    int peach[10], height, cnt = 0; // peach存放10个桃子高度

    for (int i = 0; i < 10; i++)
        scanf("%d", &peach[i]);     // 输入10个桃子高度
    scanf("%d", &height);
    for (int i = 0; i < 10; i++)
        if (height + 30 >= peach[i]) // 小明身高+臂展30cm能够到
            cnt++;
    printf("%d\n", cnt);

    return 0;
}

【实例05_07】计算班级平均年龄并统计超过平均的人数

c 复制代码
#include <stdio.h>
#define N 30

int main(void)
{
    int n, age[N], cnt = 0;
    double ave = 0;

    scanf("%d", &n);
    for (int i = 0; i < n; i++)
    {
        scanf("%d", &age[i]);
        ave += age[i];     // 累加所有年龄
    }
    ave /= n;              // 计算平均值
    for (int i = 0; i < n; i++)
        if (age[i] > ave)
            cnt++;         // 统计超过平均年龄的人数
    printf("%.2f  %d\n", ave, cnt);

    return 0;
}

【实例05_08】数组逆置

将数组元素顺序颠倒,通过"首尾交换"实现。

c 复制代码
#include <stdio.h>
const int N = 100;

int main(void)
{
    int a[N];
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        scanf("%d", &a[i]);
    for (int i = 0; i < n / 2; i++)
    {   // 距离首和尾相等的元素交换值
        int t;
        t = a[i];
        a[i] = a[n - 1 - i];
        a[n - 1 - i] = t;       // 交换 a[i] 与 a[n-1-i]
    }
    for (int i = 0; i < n; i++)
        printf("%d ", a[i]);

    return 0;
}

逆置思路:

  • 下标 i0n/2-1,与对应的 a[n-1-i] 互换
  • 只需遍历一半元素,就完成了全部交换

【实例05_09】计算两个向量的内积(点积)

c 复制代码
#include <stdio.h>
const int N = 1000;

int main(void)
{
    int a[N], b[N], n;

    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        scanf("%d", &a[i]);
    for (int i = 0; i < n; i++)
        scanf("%d", &b[i]);
    int result = 0;
    for (int i = 0; i < n; i++)
        result += a[i] * b[i];    // 内积:各分量乘积之和
    printf("%d\n", result);

    return 0;
}

5.2 数组与查找算法

【实例05_10】人民币面值兑换

用数组存储面值表,用贪心法进行兑换计算。

c 复制代码
#include <stdio.h>

int main(void)
{
    int rmb[] = {100, 50, 20, 10, 5, 1}; // 一维表存储人民币各种面值
    int n, x;

    n = sizeof(rmb) / sizeof(int); // 计算数组元素个数
    scanf("%d", &x);
    for (int i = 0; i < n; i++)
    {
        printf("%d\n", x / rmb[i]);  // 需要多少张该面值
        x %= rmb[i];                  // 减去已兑换的金额
    }

    return 0;
}

sizeof计算数组长度:

  • sizeof(rmb) = 数组总字节数
  • sizeof(int) = 单个元素字节数(4字节)
  • 二者相除即为元素个数:6个元素

【实例05_17】进制转换(十进制转R进制)

用数组存储各位数字,逆序输出实现进制转换。

c 复制代码
#include <stdio.h>

int main(void)
{
    int a[32]; // 用于存储转换后的R进制数
    int N, R;
    while (scanf("%d %d", &N, &R) != EOF)
    {
        int r, i = 0;
        do
        {
            r = N % R;    // 求余数(这一位的值)
            a[i++] = r;   // 存储余数
            N /= R;       // 更新N
        } while (N);      // 商为0时停止

        for (int j = i - 1; j >= 0; j--) // 逆序输出
            printf("%d", a[j]);
        printf("\n");
    }
    return 0;
}

进制转换原理:

  • 将N不断除以R,取余数(从低位到高位)
  • 用数组保存余数后,逆序输出(从高位到低位)
  • 例如:十进制10转二进制:10÷2=5余0,5÷2=2余1,2÷2=1余0,1÷2=0余1 → 逆序输出1010

【实例05_19】埃氏筛法求质数

经典质数筛法:用数组标记合数,剩余未标记的即为质数。

c 复制代码
#include <stdio.h>
#define N 100

int main(void)
{
    int isPrime[N + 1] = {0}; // 0表示质数(在筛子上),1表示合数(已被筛去)

    isPrime[1] = 1;                    // 1不是质数,标记为1
    for (int i = 2; i * i <= N; i++)   // 只需筛到√N
    {
        if (isPrime[i] == 1)           // 已是合数,跳过
            continue;
        for (int j = 2 * i; j <= N; j += i) // 筛去质数i的所有倍数
            isPrime[j] = 1;
    }
    int cnt = 0;
    for (int i = 2; i <= N; i++)
    {
        if (isPrime[i] == 0)           // 仍在筛子上的是质数
        {
            printf("%2d ", i);
            cnt++;
            if (cnt % 5 == 0)
                printf("\n");          // 每5个质数换行
        }
    }
    return 0;
}

埃氏筛法原理:

  1. 从2开始,每找到一个质数,就划去它的所有倍数
  2. 剩余未被划去的数就是质数
  3. 只需枚举到√N,因为N的最小质因子一定≤ √N
  4. 时间复杂度约为 O(N log log N),远优于逐一判断的 O(N√N)

5.3 数组排序算法

【实例05_31】选择排序找第k小数

选择排序思路:每次找最小值放到前面,重复k次即得第k小数。

c 复制代码
#include <stdio.h>
#define N 10001

int main(void)
{
    int n, k, arr[N];

    scanf("%d %d", &n, &k);
    for (int i = 1; i <= n; i++)
        scanf("%d", &arr[i]);

    for (int i = 1; i <= k; i++)
    {                     // 筛选第i小的数存放到arr[i]中
        int mink = i;     // 假设arr[i]是最小的
        for (int j = i + 1; j <= n; j++)
            if (arr[j] < arr[mink])
                mink = j; // 找到更小的,更新mink
        if (mink != i)
        {                 // 将第i小的数与arr[i]交换
            int tmp = arr[i];
            arr[i] = arr[mink];
            arr[mink] = tmp;
        }
    }
    printf("%d", arr[k]); // 输出第k小的数

    return 0;
}

选择排序分析:

  • 每轮从未排序部分找最小值,交换到已排序部分的末尾
  • 时间复杂度:O(n²),适合小规模数据
  • 找第k小数只需执行k轮,不必完整排序

【实例05_32】随机数组生成、排序并去重

综合应用:随机数生成 + 选择排序 + 去重输出。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

const int N = 20;

int main(void)
{
    int arr[N];

    srand(time(NULL));  // 设置随机数种子
    for (int i = 0; i < N; i++)
        arr[i] = rand() % 100 + 1; // 生成1~100的随机数

    // 选择排序
    for (int i = 0; i < N - 1; i++)
    {
        int mink = i;
        for (int j = i + 1; j < N; j++)
            if (arr[j] < arr[mink])
                mink = j;
        if (mink != i)
        {
            int tmp = arr[i];
            arr[i] = arr[mink];
            arr[mink] = tmp;
        }
    }
    // 去重
    int n = 1;
    for (int i = 1; i < N; i++)
        if (arr[i] != arr[i - 1]) // 相邻不同才保留(排序后重复元素相邻)
        {
            arr[n] = arr[i];
            n++;
        }
    printf("点名名单为:\n");
    for (int i = 0; i < n; i++)
    {
        if (i != 0) printf(" ");
        printf("%d", arr[i]);
    }
    printf("\n名单中共有%d名学生", n);

    return 0;
}

去重技巧:

  • 先排序,使相同元素相邻
  • 再遍历,只保留与前一个不同的元素
  • 这样去重只需 O(n) 时间

【实例05_33】有序数组的插入

在有序数组中插入一个新元素,保持有序。

c 复制代码
#include <stdio.h>
#define N 101

int main(void)
{
    int n, arr[N], x;

    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        scanf("%d", &arr[i]);
    scanf("%d", &x);          // 输入要插入的值

    int j = n - 1;            // j从最后一个元素开始
    while (j >= 0 && x < arr[j])
    {                         // x比arr[j]小,arr[j]后移
        arr[j + 1] = arr[j];
        j--;
    }
    arr[j + 1] = x;           // 在正确位置插入x
    for (int i = 0; i <= n; i++)
    {
        if (i != 0) printf(" ");
        printf("%d", arr[i]);
    }

    return 0;
}

【实例05_36】荷兰国旗问题(三色排序)

将只含0、1、2的数组排序,使0在前、1在中、2在后。经典的三指针算法。

c 复制代码
#include <stdio.h>

int main(void)
{
    int colors[300];
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        scanf("%d", &colors[i]);

    int i = -1, j = n, k = 0; // i指向最后一个0,j指向第一个2,k是当前处理位置
    while (k < j)
    {
        if (colors[k] == 2)    // 当前是2,交换到后面
        {
            j--;
            if (k < j)
            {
                int t = colors[k];
                colors[k] = colors[j];
                colors[j] = t;
            }
        }
        else if (colors[k] == 0) // 当前是0,交换到前面
        {
            i++;
            if (i != k)
            {
                int t = colors[k];
                colors[k] = colors[i];
                colors[i] = t;
            }
            else
                k++;
        }
        else
            k++;             // 当前是1,无需移动
    }
    for (int i = 0; i < n; i++)
        printf("%d ", colors[i]);

    return 0;
}

5.4 二维数组

二维数组可以理解为"数组的数组",常用于存储矩阵、表格等二维数据。

二维数组的定义:

c 复制代码
int a[3][4];            // 定义3行4列的整型二维数组
int b[2][3] = {{1,2,3},{4,5,6}}; // 定义并初始化

访问方式:

  • a[i][j] 表示第i行第j列的元素(从0开始)
  • 内存中按行存储(先存第0行,再存第1行......)

【实例05_37】三个班级学生年龄的统计分析

c 复制代码
#include <stdio.h>

int main(void)
{
    int n, age[3][30];   // 定义二维数组存放三个班级学生年龄
    double ave[3] = {0}; // 存放三个班级的平均年龄

    scanf("%d", &n);     // 输入每班学生人数
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < n; j++)
        {
            scanf("%d", &age[i][j]); // 第i班第j个学生的年龄
            ave[i] += age[i][j];
        }
        ave[i] /= n;     // 计算第i班平均年龄
    }
    for (int i = 0; i < 3; i++)
    {
        int cnt = 0;
        for (int j = 0; j < n; j++)
            if (age[i][j] > ave[i])
                cnt++;
        printf("%.2f  %d\n", ave[i], cnt);
    }
    return 0;
}

【实例05_39】二维数组的行交换

c 复制代码
#include <stdio.h>

int main(void)
{
    int arr[6][6], m, n;

    for (int i = 1; i < 6; i++)
        for (int j = 1; j < 6; j++)
            scanf("%d", &arr[i][j]);

    scanf("%d %d", &m, &n);
    // 交换第m行与第n行
    for (int j = 1; j < 6; j++)
    {
        int tmp;
        tmp = arr[m][j];
        arr[m][j] = arr[n][j];
        arr[n][j] = tmp;
    }
    for (int i = 1; i < 6; i++)
    {
        for (int j = 1; j < 6; j++)
            printf("%d ", arr[i][j]);
        printf("\n");
    }
    return 0;
}

【实例05_40】计算矩阵边缘元素之和

c 复制代码
#include <stdio.h>

int arr[100][100];

int main(void)
{
    int R, C, sum = 0;

    scanf("%d %d", &R, &C);
    for (int i = 0; i < R; i++)
        for (int j = 0; j < C; j++)
        {
            scanf("%d", &arr[i][j]);
            if (i == 0 || i == R - 1 || j == 0 || j == C - 1)
                sum += arr[i][j]; // 边缘元素:第一行、最后一行、第一列、最后一列
        }
    printf("%d", sum);
    return 0;
}

【实例05_42】矩阵乘法

两个矩阵相乘:A(n×m) × B(m×p) = C(n×p),其中 C[i][j] = Σ A[i][k] * B[k][j]

c 复制代码
#include <stdio.h>

int a[100][100], b[100][100], c[100][100] = {0};

int main(void)
{
    int n, m, p;

    scanf("%d %d %d", &n, &m, &p);
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            scanf("%d", &a[i][j]);         // 输入a矩阵(n×m)
    for (int i = 0; i < m; i++)
        for (int j = 0; j < p; j++)
            scanf("%d", &b[i][j]);         // 输入b矩阵(m×p)

    // 矩阵乘法:c = a * b(n×p)
    for (int i = 0; i < n; i++)
        for (int j = 0; j < p; j++)
            for (int k = 0; k < m; k++)
                c[i][j] += a[i][k] * b[k][j]; // 三重循环实现矩阵乘法

    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < p; j++)
            printf("%d ", c[i][j]);
        printf("\n");
    }
    return 0;
}

【实例05_43】杨辉三角

帕斯卡三角形(杨辉三角)的规律:每个数等于它上方两数之和。

c 复制代码
#include <stdio.h>
#define N 30

int main(void)
{
    int arr[N][N], n;

    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        arr[i][0] = arr[i][i] = 1;   // 每行首尾均为1
    for (int i = 2; i < n; i++)
        for (int j = 1; j < i; j++)
            arr[i][j] = arr[i-1][j-1] + arr[i-1][j]; // 上方两数之和
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j <= i; j++)
            printf("%2d ", arr[i][j]);
        printf("\n");
    }
    return 0;
}

杨辉三角输出(n=5):

复制代码
 1 
 1  1 
 1  2  1 
 1  3  3  1 
 1  4  6  4  1 

【实例05_44】魔方矩阵(奇数阶)

奇数阶魔方矩阵中每行、每列、每条对角线的元素之和相等。

c 复制代码
#include <stdio.h>
#define N 20

int main(void)
{
    int a[N][N] = {0};
    int row, col, n;

    scanf("%d", &n);
    if (n < 0 || n > 19 || n % 2 == 0)
        printf("请输入[1,19]区间内的一个奇数");
    else
    {
        row = 0;
        col = n / 2;      // 1的位置:第一行中间
        a[row][col] = 1;
        for (int k = 2; k <= n * n; k++)
        {
            int rowtmp = row, coltmp = col;
            row--;        // 向右上方向移动
            col++;
            if (row == -1) row = n - 1; // 行越界,回到最后一行
            if (col == n)  col = 0;     // 列越界,回到第一列
            if (a[row][col] != 0)       // 该位置已有数
            {
                row = rowtmp + 1;       // 改为放在上一个数的下方
                col = coltmp;
            }
            a[row][col] = k;
        }
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < n; j++)
                printf("%2d ", a[i][j]);
            printf("\n");
        }
    }
    return 0;
}

魔方矩阵填充规则(Siamese方法):

  1. 从第一行中间开始放1
  2. 下一个数放在当前数的右上方
  3. 若右上方越界,则绕回对面
  4. 若右上方已有数,则放在当前数的下方

【实例05_45】螺旋矩阵

按顺时针螺旋顺序填充 1 到 n² 的矩阵。

c 复制代码
#include <stdio.h>

int a[20][20];

int main(void)
{
    int n;
    scanf("%d", &n);
    int k = 1;                              // 待填充的数字
    for (int i = 0; i < (n + 1) / 2; i++) // 共(n+1)/2圈
    {
        int r, c;
        // 上边:从左到右
        for (c = i; c <= n-1-i; c++)   a[i][c] = k++;
        // 右边:从上到下
        for (r = i+1; r <= n-1-i; r++) a[r][n-1-i] = k++;
        // 下边:从右到左
        for (c = n-2-i; c >= i; c--)   a[n-1-i][c] = k++;
        // 左边:从下到上
        for (r = n-2-i; r >= i+1; r--) a[r][i] = k++;
    }
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
            printf("%3d ", a[i][j]);
        printf("\n");
    }
    return 0;
}

螺旋矩阵(n=4)输出:

复制代码
  1   2   3   4
 12  13  14   5
 11  16  15   6
 10   9   8   7

【实例05_46】蛇形矩阵

按斜线方向交替填充矩阵(右上和左下交替移动)。

c 复制代码
#include <stdio.h>

int main(void)
{
    int arr[10][10];    
    int k = 1, n;
    int row = 0, col = 0;
    int up = 1;           // 1表示右上方向,0表示左下方向

    scanf("%d", &n);
    arr[row][row] = k; 
    while (k <= n * n)
    {        
        if (up)
        {   // 向右上填充
            if (row > 0 && col < n - 1)
            { row--; col++; }    // 正常右上移动
            else if (col < n - 1)
            { col++; up = 0; }   // 已到第一行,向右移并改为左下方向
            else
            { row++; up = 0; }   // 已到最后一列,向下移并改为左下方向
        }
        else
        {   // 向左下填充
            if (col > 0 && row < n - 1)
            { row++; col--; }    // 正常左下移动
            else if (row < n - 1)
            { row++; up = 1; }   // 已到第一列,向下移并改为右上方向
            else
            { col++; up = 1; }   // 已到最后一行,向右移并改为右上方向
        }
        arr[row][col] = ++k;
    }
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
            printf("%2d ", arr[i][j]);
        printf("\n");
    }
    return 0;
}

5.5 计数排序与频率统计

【实例05_34】计数排序求数字真因子之和

用计数数组快速统计,是一种以空间换时间的算法思想。

c 复制代码
#include <stdio.h>
#define N 500000

int factorSum[N + 1] = {0}; // factorSum[i]存放i的真因子之和

int main(void)
{
    for (int i = 1; i <= N / 2; i++)
    {
        for (int j = i + i; j <= N; j += i) // 遍历i的倍数
            factorSum[j] += i;               // i是j的一个真因子
    }
    int t, n;
    scanf("%d", &t);
    while (t--)
    {
        scanf("%d", &n);
        printf("%d\n", factorSum[n]);        // 直接查表O(1)
    }
    return 0;
}

【实例05_35】计数排序统计出现次数

c 复制代码
#include <stdio.h>
#define N 100

int cnt[N] = {0};

int main(void)
{
    int n, fmax = 0, tmp;

    scanf("%d", &n);
    for (int i = 0; i < n; i++)
    {
        scanf("%d", &tmp);
        cnt[tmp]++;          // 对应计数器加1
        if (tmp > fmax)
            fmax = tmp;      // 更新最大值
    }
    for (int i = 0; i <= fmax; i++)
        printf("%d ", cnt[i]); // 输出每个整数出现的次数

    return 0;
}

5.6 第5章知识点总结

一维数组核心操作汇总:

操作 代码模式 说明
遍历 for(int i=0; i<n; i++) 处理 a[i] 逐元素访问
求最大值 max=a[0]; for(i) if(a[i]>max) max=a[i] 先假设最大,遍历更新
逆置 swap(a[i], a[n-1-i]) 首尾对称交换
查找 for(i) if(a[i]==x) break 顺序查找
插入 从后往前移动元素,找到合适位置插入 有序插入
去重 先排序,再保留不等于前一个的元素 相邻去重

二维数组核心操作:

操作 代码模式 说明
行遍历 for(i) for(j) 处理 a[i][j] 先行后列
求边缘元素 判断 i==0 或 i==R-1 或 j==0 或 j==C-1 四条边
矩阵乘法 三重循环 c[i][j] += a[i][k]*b[k][j] 经典O(n³)算法
杨辉三角 a[i][j] = a[i-1][j-1] + a[i-1][j] 递推填充

重要算法思想:

  • 埃氏筛法:用数组标记法高效求质数(时间复杂度O(N log log N))
  • 计数排序:用下标作为"值",计数数组高效统计频次(时间复杂度O(n+k))
  • 选择排序:每次选出最小值放到前面(时间复杂度O(n²))
  • 查表法:将预计算结果存入数组,查询时O(1)直接读取

第6章 指针

指针是C语言的精华,也是初学者最难掌握的知识点之一。理解了指针,才算真正掌握了C语言。本章通过40个实例,系统讲解指针的基本概念、指针与数组、指针与函数、动态内存分配等核心知识。

6.1 指针基础概念

什么是指针?

指针(Pointer)本质上是一个变量,它存储的值是内存地址。通过指针可以间接访问该地址处存储的数据。

复制代码
int a = 5;        // 普通变量a,存储值5
int *p = &a;      // 指针变量p,存储变量a的地址

指针的核心操作符:

操作符 名称 含义
& 取地址运算符 获取变量的内存地址
* 解引用运算符 通过指针访问其指向的值

【实例06_01】指针的基本使用

c 复制代码
#include <stdio.h>

int main(void)
{
    int a = 5;    // 定义整型变量a,赋值为5
    int *p = &a;  // 定义指针变量p,存储a的地址

    *p = *p + 1;  // 通过指针p间接修改a的值(a变为6)

    printf("变量p的值:%x\n", p);                      // 输出指针变量p存储的地址
    printf("变量a的地址:%x, 变量a的值:%d\n", &a, a); // 输出a的地址和值

    return 0;
}

关键点:

  • int *p 定义一个指向int类型的指针变量p
  • p = &a 将变量a的地址赋给p
  • *p = *p + 1 通过指针p间接将a的值加1
  • p&a的值相同(都是a的内存地址)

【实例06_02】未初始化指针的危险

c 复制代码
#include <stdio.h>

int main(void)
{
    double a = 5.5;
    double *p;    // 定义指针但未初始化(野指针!)
    *p = 8.8;     // 危险:向未知地址写入数据,会导致程序崩溃
    p = &a;       // 正确做法:先让p指向合法地址

    return 0;
}

警告:

  • 未初始化的指针称为野指针,其值是随机的内存地址
  • 对野指针解引用会导致程序崩溃或不可预知的错误
  • 正确做法 :定义指针后立即初始化:double *p = &a;double *p = NULL;

【实例06_04】不同类型指针的比较

c 复制代码
#include <stdio.h>

int main(void)
{
    short vshort = 12;
    int vint = 20;
    float vfloat = 3.14159;
    char vchar = 'a';
    short *pvshort = &vshort;
    int *pvint = &vint;
    float *pvfloat = &vfloat;
    char *pvchar = &vchar;

    // 输出:地址、值、指针大小、变量大小
    printf("%X:%10hd %8d %8d\n", pvshort, vshort, sizeof(pvshort), sizeof(vshort));
    printf("%X:%10d %8d %8d\n", pvint, vint, sizeof(pvint), sizeof(vint));
    printf("%X:%10f %8d %8d\n", pvfloat, vfloat, sizeof(pvfloat), sizeof(vfloat));
    printf("%X:%10c %8d %8d\n", pvchar, vchar, sizeof(pvchar), sizeof(vchar));

    return 0;
}

重要结论:

  • 所有指针变量大小相同(32位系统4字节,64位系统8字节)
  • 但指向的数据类型大小不同(short=2,int=4,float=4,char=1)
  • 指针的类型决定了解引用时读取多少字节

6.2 指针与函数

【实例06_05】用指针参数交换两个变量的值

C语言函数参数是值传递,要修改实参,必须传递指针(地址)。

c 复制代码
#include <stdio.h>

void swap(int *p1, int *p2); // 参数是指针类型

int main(void)
{
    int a, b, c;
    printf("请输入3个整数:");
    scanf("%d %d %d", &a, &b, &c);

    if (a > b) swap(&a, &b);  // 传入地址
    if (a > c) swap(&a, &c);
    if (b > c) swap(&b, &c);

    printf("3个整数由小到大排序为:");
    printf("%d %d %d\n", a, b, c);

    return 0;
}
void swap(int *p1, int *p2)
{
    int temp = *p1;  // 保存p1指向的值
    *p1 = *p2;       // 将p2的值写到p1指向的位置
    *p2 = temp;      // 将原p1的值写到p2指向的位置
}

为什么要传指针?

  • 普通参数传递时,函数内的形参是实参的副本,修改形参不影响实参
  • 传递指针后,函数内可通过*p1*p2直接操作原变量

【实例06_06】const指针的用法

c 复制代码
#include <stdio.h>

int main(void)
{
    int x = 5, y = 6;
    const int *p1 = &x;  // 指向const int的指针:不能通过p1修改值,但可以改变p1的指向
    int *const p2 = &y;  // 指向int的const指针:可以通过p2修改值,但不能改变p2的指向

    // *p1 = 7;  // 错误:不能通过p1修改所指向的值
    x++;         // 可以:通过其他方式修改x的值
    p1 = &y;     // 可以:可以修改p1的指向
    *p2 = 8;     // 可以:可以通过p2修改所指向的值
    // p2 = &x;  // 错误:不能修改p2(常量指针)

    return 0;
}

两种const指针对比:

类型 写法 能修改值 能改变指向
指向const的指针 const int *p
const指针 int *const p
const指向const的指针 const int *const p

【实例06_07】指针参数返回多个值(通过指针参数输出)

函数只能有一个返回值,但通过指针参数可以"返回"多个值。

c 复制代码
#include <stdio.h>
#include <stdbool.h>

void monthDay(int year, int yearDay, int *pMonth, int *pDay);
bool isLeap(int year);

int main()
{
    int year, yearDay, month, day;
    scanf("%d %d", &year, &yearDay);
    // pMonth和pDay是输出参数(通过指针返回计算结果)
    monthDay(year, yearDay, &month, &day);
    printf("%d年的第%d天的日期是:%4d-%02d-%02d\n", year, yearDay, year, month, day);

    return 0;
}
void monthDay(int year, int yearDay, int *pMonth, int *pDay)
{
    int daysInMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    int i;

    if (isLeap(year))
        daysInMonth[2] = 29;
    for (i = 1; i <= 12; i++)
    {
        if (yearDay <= daysInMonth[i])
        {
            *pMonth = i;        // 通过指针参数输出月份
            *pDay = yearDay;    // 通过指针参数输出日
            break;
        }
        yearDay -= daysInMonth[i];
    }
}
bool isLeap(int year)
{
    return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
}

6.3 指针与数组

【实例06_08】通过指针访问数组元素

c 复制代码
#include <stdio.h>

int main(void)
{
    int a[5] = {1, 3, 5, 7, 9};
    int *p = a;    // p指向数组首元素a[0](等价于 p = &a[0])
    
    printf("a的值是:%#X, a[0]的地址是:%#X\n", a, &a[0]); // a和&a[0]相同
    p[2] = 10;     // 等价于 a[2] = 10
    for (int i = 0; i < 5; i++)
    {
        // 用指针算术访问元素:*(p+i) 等价于 p[i] 等价于 a[i]
        printf("a[%d]的地址是:%#X a[%d]的值是:%d\n", i, p + i, i, *(p + i));
    }
    return 0;
}

指针访问数组的等价表达式:

数组下标法 指针法 含义
a[i] *(a+i)*(p+i) 第i个元素的值
&a[i] a+ip+i 第i个元素的地址
a[0] *a*p 第一个元素的值

【实例06_09】用指针逆置数组

c 复制代码
#include <stdio.h>

void swap(int *p1, int *p2);

int main(void)
{
    int a[5] = {1, 3, 5, 7, 9};
    int *p = a, *q = &a[4]; // p指向第1个,q指向最后一个

    while (p < q)   // 指针移动直到相遇
    {
        swap(p, q); // 交换p和q所指的值
        p++;        // p右移
        q--;        // q左移
    }
    for (int i = 0; i < 5; i++)
        printf("a[%d]的值是:%d\n", i, a[i]);
    return 0;
}
void swap(int *p1, int *p2)
{
    int temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

【实例06_10】指针的自增运算

c 复制代码
#include <stdio.h>

int main(void)
{
    int data[10] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 10};
    int *p1 = data, *p2 = data; // p1和p2都指向data[0]
    int *p3 = data + 3;          // p3指向data[3](值为7)

    printf("*p1 = %d, *p2 = %d, *p3 = %d\n", *p1, *p2, *p3);
    // *p1++ 先取*p1,然后p1后移(后置++)
    // *++p2  先p2前移,再取*p2(前置++)
    // (*p3)++ 取*p3后对该值加1(对p3所指的值++)
    printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n", *p1++, *++p2, (*p3)++);
    printf("*p1 = %d, *p2 = %d, *p3 = %d\n", *p1, *p2, *p3);
    return 0;
}

指针自增运算辨析:

  • *p++:先取*p,再执行p++(指针后移)
  • *++p:先执行++p(指针前移),再取*p
  • (*p)++:取*p的值,对 执行++,指针不动

6.4 动态内存分配

【实例06_25】用malloc动态分配数组

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *array;         // 定义一个指向整型的指针
    int size, sum = 0;
    printf("输入数组的大小: ");
    scanf("%d", &size);
    
    array = (int *)malloc(size * sizeof(int)); // 动态分配 size 个int的内存
    if (array == NULL)  // 检查内存是否分配成功
    {
        printf("内存分配失败!\n");
        return -1;
    }
    printf("输入%d 个数组元素:\n", size);
    for (int i = 0; i < size; i++)
    {
        scanf("%d", &array[i]);
        sum += array[i];
    }
    printf("数组中的元素是: ");
    for (int i = 0; i < size; i++)
        printf("%d ", array[i]);
    printf("\n数组的元素和是:%d\n", sum);
    
    free(array);  // 释放动态分配的内存(必须)
    return 0;
}

动态内存分配规则:

  • malloc(n) 分配n字节内存,返回void *指针
  • 必须检查返回值是否为NULL(分配失败时)
  • 使用完毕必须调用free()释放,否则会内存泄漏

6.5 第6章知识点总结

指针核心概念总结:

概念 说明
指针变量 存储内存地址的变量,类型决定指向数据的大小
取地址 & 获取变量的内存地址
解引用 * 访问指针指向位置的值
野指针 未初始化或已释放的指针,使用会导致崩溃
NULL指针 int *p = NULL 表示指针不指向任何有效地址
指针运算 p+n 表示向后偏移n个元素,p-q 表示两指针间距
数组与指针 数组名是首元素地址,a[i] 等价于 *(a+i)

第9章 位运算

位运算直接操作二进制位,是底层编程和算法优化的重要工具。虽然初学阶段不常用,但理解位运算有助于深入理解计算机原理。本章通过15个实例讲解位运算的基本操作与应用。

9.1 位运算符介绍

C语言提供了6种位运算符:

运算符 名称 说明 例子
& 按位与 两位都为1时结果为1 5&3 = 001
` ` 按位或 两位有一个1时结果为1
^ 按位异或 两位不同时结果为1 5^3 = 110
~ 按位取反 0变1,1变0 ~5
<< 左移 各位左移,低位补0 5<<1 = 1010
>> 右移 各位右移,高位补符号位 5>>1 = 0010

【实例09_01】判断奇偶数(按位与)

c 复制代码
#include <stdio.h>

int main(void)
{
    int x;
    scanf("%d", &x);
    if (x & 1 == 1)    // 最低位为1表示奇数
        printf("odd");
    else 
        printf("even");
    return 0;
}

原理:

  • 奇数的二进制表示最低位一定是1
  • x & 1 提取最低位
  • x % 2 == 1 更高效(位运算效率更高)

【实例09_02】保留低3位(按位与清零高位)

c 复制代码
#include <stdio.h>

int main(void)
{
    short a;
    scanf("%d", &a);
    a = a & 7;         // 7的二进制是 0...0111,与操作保留低3位
    printf("%d", a);
    return 0;
}

按位与的应用:

  • a & 7 等价于 a % 8(对8取模),但更高效
  • a & (n-1) 等价于 a % n(n为2的幂次)

【实例09_05】提取整数二进制的第k1到k2位

c 复制代码
#include <stdio.h>

int main(void)
{
    int x, k1, k2, res;
    scanf("%x %d %d", &x, &k1, &k2);
    res = x >> (k1 - 1);         // 将k1位移到最低位
    int k = k2 - k1 + 1, a;
    a = (1 << k) - 1;            // 构造k个1的掩码(如k=3则a=0b111=7)
    res &= a;                    // 通过掩码提取低k位
    printf("%#X %d", res, res);  // 以十六进制和十进制输出
    return 0;
}

位运算常用技巧:

  • x << n:x乘以2的n次方(左移n位)
  • x >> n:x除以2的n次方(右移n位)
  • (1 << k) - 1:生成k个1的掩码(如k=4则=0b1111=15)
  • x & mask:提取特定位

9.2 第9章知识点总结

位运算应用场景:

  • 判断奇偶:x & 1
  • 清零某位:x & ~(1<<k)(清除第k位)
  • 设置某位:x | (1<<k)(设置第k位为1)
  • 翻转某位:x ^ (1<<k)(翻转第k位)
  • 取低k位:x & ((1<<k)-1)
  • 位移运算代替乘除:x*2=x<<1x/4=x>>2

第11章 递归

递归是一种优雅的编程技巧:函数调用自身来解决问题。掌握递归的关键是找到递归出口(边界条件)递推关系。本章通过26个实例系统讲解递归的思想与应用。

11.1 递归的基本概念

递归的两要素:

  1. 递归出口(边界条件):停止递归的条件
  2. 递归体(递推关系):将大问题分解为小问题的方式

递归的执行过程:

复制代码
递推阶段:不断调用自身,问题规模变小
回归阶段:到达出口后,逐层返回结果

【实例11_01】递归计算阶乘 n!

最经典的递归示例:n! = n × (n-1)!

c 复制代码
#include <stdio.h>

int fact(int n);       // 计算 n!

int main(void)
{
    int n;
    scanf("%d", &n);
    printf("%d", fact(n));
    return 0;
}
int fact(int n)
{
    if (n == 0 || n == 1)     // 递归出口:0!和1!都等于1
        return 1;
    return n * fact(n - 1);   // 递归调用:n! = n × (n-1)!
}

递归展开过程(n=4):

复制代码
fact(4) = 4 × fact(3)
        = 4 × (3 × fact(2))
        = 4 × (3 × (2 × fact(1)))
        = 4 × (3 × (2 × 1))
        = 4 × (3 × 2)
        = 4 × 6
        = 24

【实例11_02】递归计算整数位数

c 复制代码
#include <stdio.h>

int length(int x);

int main(void)
{
    int x;
    scanf("%d", &x);
    printf("%d", length(x));    // 输出整数x的位数
    return 0;
}
int length(int x)
{
    if (x < 10)                  // 递归出口:一位数,位数为1
        return 1;
    return length(x / 10) + 1;  // 递归调用:去掉最低位,位数+1
}

递归推导(x=123):

复制代码
length(123) = length(12) + 1
            = (length(1) + 1) + 1
            = (1 + 1) + 1
            = 3

【实例11_03】Ackermann函数(深度递归)

Ackermann函数是著名的非原始递归函数,增长极快。

c 复制代码
#include <stdio.h>

int ackermann(int m, int n)
{
    if (m == 0)
        return n + 1;
    else if (n == 0)
        return ackermann(m - 1, 1);
    else
        return ackermann(m - 1, ackermann(m, n - 1));
}
int main(void)
{
    int m = 2, n = 2;
    int result = ackermann(m, n);
    printf("Ackermann(%d, %d) = %d\n", m, n, result);
    return 0;
}

【实例11_04】递归打印整数的二进制表示

利用递归先处理高位再输出低位。

c 复制代码
#include <stdio.h>

void printBinary(int x);

int main(void)
{
    int x;
    scanf("%d", &x);
    printf("整数%d的二进制形式为:", x);
    printBinary(x);
    return 0;
}
void printBinary(int x)
{
    if (x == 0 || x == 1)      // 递归出口
        printf("%d", x);
    else
    {
        printBinary(x / 2);    // 递归处理高位
        printf("%d", x % 2);   // 输出当前最低位
    }
}

递推思路:

  • 十进制转二进制时,需要从最高位输出
  • 用递归实现:先递归处理x/2(更高位),回归时再输出x%2(当前位)

【实例11_05】递归解决爬楼梯问题

爬n阶楼梯,每次可以爬1级或2级,共有多少种方法?

c 复制代码
#include <stdio.h>

int climbSteps(int n);

int main(void)
{
    int n;
    while (scanf("%d", &n) != EOF)
        printf("%d\n", climbSteps(n));
    return 0;
}
int climbSteps(int n)
{
    if (n == 1) return 1;  // 1阶:只有1种方法(走1步)
    if (n == 2) return 2;  // 2阶:2种方法(走1+1或走2)
    return climbSteps(n - 1) + climbSteps(n - 2); // 递推:最后一步走1步或走2步
}

分析:

  • 到达n阶的方法 = 从n-1阶走1步 + 从n-2阶走2步
  • 这正是斐波那契数列的递推关系!
  • n=1:1种,n=2:2种,n=3:3种,n=4:5种,n=5:8种......

注意:纯递归实现有大量重复计算,效率较低。第15章动态规划将展示如何优化。


11.2 第11章知识点总结

递归的优缺点:

特点 说明
优点 代码简洁,思路清晰,适合树、图等递归结构
缺点 有函数调用栈开销,可能栈溢出,可能重复计算

递归设计步骤:

  1. 找边界:确定什么情况下直接返回(不再递归)
  2. 找规律:当前规模与更小规模的关系
  3. 写代码:先写出口,再写递归体

常见递归问题:

  • 阶乘:f(n) = n * f(n-1),出口 f(0)=1
  • 斐波那契:f(n) = f(n-1) + f(n-2),出口 f(1)=1, f(2)=1
  • 整数位数:len(n) = len(n/10) + 1,出口 n<10
  • 爬楼梯:step(n) = step(n-1) + step(n-2),出口 step(1)=1, step(2)=2

第14章 贪心算法

贪心算法在每个决策步骤都选择当前最优解,期望最终得到全局最优解。贪心算法的关键在于证明局部最优能导致全局最优。

14.1 贪心算法核心思想

贪心策略:

  • 每步做出在当前状态下最好的选择
  • 一旦做出选择,不再回头(无后效性)
  • 适用场景:能证明局部最优可以推导全局最优的问题

【实例14_01】分发饼干(贪心+排序)

给孩子们分饼干,每个孩子有最小胃口值,饼干有大小。让尽量多的孩子满足。

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int sendCookies(int *g, int gSize, int *s, int sSize);
void input(int a[], int n);

int main(void)
{
    int m, n;
    scanf("%d %d", &m, &n);
    int *g = (int *)malloc(sizeof(int) * m);  // 孩子的胃口数组
    int *s = (int *)malloc(sizeof(int) * n);  // 饼干大小数组
    input(g, m);
    input(s, n);
    int cnt = sendCookies(g, m, s, n);
    printf("%d", cnt);
    free(g); free(s);
    return 0;
}
int compare(const void *p1, const void *p2)
{
    return *((int *)p1) - *((int *)p2);  // 升序比较函数
}
// 贪心:先满足胃口最小的孩子(用最小满足条件的饼干)
int sendCookies(int *g, int gSize, int *s, int sSize)
{
    qsort(g, gSize, sizeof(int), compare);  // 孩子胃口升序排序
    qsort(s, sSize, sizeof(int), compare);  // 饼干大小升序排序
    int cnt = 0;
    int i = 0, j = 0;                       // 双指针

    while (i < gSize && j < sSize)
    {
        if (s[j] >= g[i])  // 当前最小饼干能满足当前胃口最小的孩子
        {
            cnt++;
            i++;           // 该孩子被满足,移向下一个孩子
        }
        j++;               // 无论是否满足,都用掉该饼干
    }
    return cnt;
}

贪心思路:

  • 排序后用双指针扫描
  • 尽量用最小的饼干满足胃口最小的孩子
  • 这样能让更多孩子被满足

【实例14_02】最大周长三角形(贪心排序)

从数组中选三个数组成周长最大的三角形(需满足三角形条件)。

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int largestPerimeter(int *nums, int numsSize);
void input(int a[], int n);

int main(void)
{
    int n;
    scanf("%d", &n);
    int *nums = (int *)malloc(sizeof(int) * n);
    input(nums, n);
    int sum = largestPerimeter(nums, n);
    printf("%d", sum);
    free(nums);
    return 0;
}
int compare(const void *p1, const void *p2)
{
    return *((int *)p2) - *((int *)p1);  // 降序比较函数
}
// 贪心:降序排列后,从大到小依次尝试相邻三个数
int largestPerimeter(int *nums, int numsSize)
{
    qsort(nums, numsSize, sizeof(int), compare);  // 降序排序
    int sum = 0;
    int i = 0;
    while (i + 2 < numsSize)
    {
        if (nums[i+1] + nums[i+2] > nums[i])  // 满足三角形条件(两边之和大于第三边)
        {
            sum = nums[i] + nums[i+1] + nums[i+2];  // 第一组满足条件的即为最大
            break;
        }
        i++;
    }
    return sum;
}

【实例14_04】活动选择问题(经典贪心)

从一组活动中选出最多数量的互不冲突的活动。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#define N 20

typedef struct Action {
    int id;   // 活动序号
    int b;    // 开始时间
    int e;    // 结束时间
} Action;

int schedule(Action a[], int n, int x[]);
int compare(const void *p1, const void *p2)
{
    return (((struct Action *)p1)->e - ((struct Action *)p2)->e);
}

int main(void)
{
    Action A[N+1];
    int n;
    int x[N+1] = {0};
    int count = 0;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        A[i].id = i;
        scanf("%d %d", &A[i].b, &A[i].e);
    }
    count = schedule(A, n, x);
    printf("最多可安排 %d 个活动:\n", count);
    for (int i = 1; i <= n; i++)
        if (x[i])
            printf("第%d活动:%d %d\n", A[i].id, A[i].b, A[i].e);
}
// 贪心:按结束时间排序,每次选择结束最早且与已选活动不冲突的活动
int schedule(Action a[], int n, int x[])
{
    int cnt = 0;
    qsort(a + 1, n, sizeof(Action), compare); // 按结束时间升序排序
    int preend = 0;     // 上一个选中活动的结束时间
    for (int i = 1; i <= n; i++)
    {
        if (a[i].b >= preend)  // 活动开始时间 >= 前一个活动结束时间(不冲突)
        {
            x[i] = 1;          // 选择该活动
            cnt++;
            preend = a[i].e;   // 更新上一活动的结束时间
        }
    }
    return cnt;
}

贪心证明:

  • 按结束时间排序后,选择结束最早的活动
  • 这样为后续活动留出更多时间,能选出尽量多的活动
  • 此贪心策略可以严格证明得到最优解

【实例14_05】分数背包问题(贪心)

每件物品可以分割装入背包,求最大价值。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const int MAXN = 20;

typedef struct {
    double w;  // 重量
    double v;  // 价值
    double p;  // 单位价值 p=v/w
} Item;

double Knap(Item items[], int n, double x[], double weight);
int compare(const void *p1, const void *p2)
{
    double a = ((Item *)p1)->p;
    double b = ((Item *)p2)->p;
    if (a > b) return -1;
    else if (a < b) return 1;
    else return 0;
}

int main(void)
{
    Item items[] = {{0}, {10,20}, {20,30}, {30,66}, {40,40}, {50,60}};
    int n = sizeof(items)/sizeof(Item) - 1;
    double W = 100;
    double x[MAXN];
    for (int i = 1; i <= n; i++)
        items[i].p = items[i].v / items[i].w;  // 计算单位价值
    qsort(items+1, n, sizeof(Item), compare);   // 按单位价值降序排序
    double V = Knap(items, n, x, W);
    printf("总价值=%g\n", V);
    return 0;
}
// 贪心:优先装单位价值最高的物品,最后不够时装一部分
double Knap(Item items[], int n, double x[], double weight)
{
    double V = 0;
    memset(x, 0, (n+1)*sizeof(double));
    int i = 1;
    while (items[i].w <= weight)   // 能完整装入时全部装入
    {
        x[i] = 1;
        weight -= items[i].w;
        V += items[i].v;
        i++;
    }
    if (weight > 0)                // 不够时装一部分
    {
        x[i] = weight / items[i].w;
        V += x[i] * items[i].v;
    }
    return V;
}

14.2 第14章知识点总结

贪心算法特征:

  1. 最优子结构:问题的全局最优解包含子问题的最优解
  2. 贪心选择性:每步选择局部最优,不会影响后续选择的有效性

常见贪心问题:

问题 贪心策略
分发饼干 用最小满足条件的饼干满足胃口最小的孩子
最大周长三角形 降序排列,取最大满足条件的相邻三个数
活动选择 按结束时间排序,选结束最早且不冲突的
分数背包 按单位价值降序,优先装单位价值最高的

第15章 动态规划

动态规划(DP)是解决最优化问题的强大算法技巧,通过将问题分解为子问题,利用子问题的最优解构建原问题的最优解,避免重复计算。

15.1 动态规划的核心思想

DP三要素:

  1. 状态定义dp[i]dp[i][j] 表示什么含义
  2. 状态转移方程:大问题与小问题的关系(递推公式)
  3. 边界值:初始状态(最简单情况的答案)

DP vs 递归:

  • 递归:自顶向下,可能重复计算
  • DP(记忆化/填表):自底向上,每个子问题只算一次

【实例15_01】爬楼梯(DP版本)

将递归版的爬楼梯问题改为动态规划实现。

c 复制代码
#include <stdio.h>

int climbSteps(int n);

int main(void)
{
    int n;
    while (scanf("%d", &n) != EOF)
        printf("%d\n", climbSteps(n));
    return 0;
}
int climbSteps(int n) 
{
    int dp[31];
    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]; 
}

与递归版对比:

  • 递归版:重复计算大量子问题,时间复杂度 O(2ⁿ)
  • DP版:每个子问题只算一次,时间复杂度 O(n)

【实例15_02】数塔问题(二维DP)

从数塔顶层到底层,每步只能走到下面左或右的相邻节点,求路径上数字之和的最大值。

c 复制代码
#include <stdio.h>
#define R 1010

int nums[R][R];   // nums[i][j]存放(i,j)位置的数字
int dp[R][R];     // dp[i][j]表示从(i,j)到最底层的最大路径和

int max(int a, int b) { return a >= b ? a : b; }

int dataTower(int n) 
{
    // 边界初始化:最底层的dp值等于nums值
    for (int j = 1; j <= n; j++)
        dp[n][j] = nums[n][j];
    // 状态转移:从下到上,从左到右填表
    for (int i = n-1; i >= 1; i--)       // 从倒数第二行向上
        for (int j = 1; j <= i; j++)
            dp[i][j] = nums[i][j] + max(dp[i+1][j], dp[i+1][j+1]);
    return dp[1][1];   // 顶层的答案
}

int main(void)
{
    int r;
    scanf("%d", &r);
    for (int i = 1; i <= r; i++)
        for (int j = 1; j <= i; j++)
            scanf("%d", &nums[i][j]);
    printf("%d", dataTower(r));
    return 0;
}

状态定义: dp[i][j] = 从位置(i,j)到最底层能取得的最大路径和
转移方程: dp[i][j] = nums[i][j] + max(dp[i+1][j], dp[i+1][j+1])
填表顺序: 从最后一行(底层)向上依次计算


【实例15_03】最小路径和(网格DP)

在n×n网格中从左上角到右下角,只能向右或向下走,求路径上数字之和的最小值。

c 复制代码
#include <stdio.h>
#define N 105

int nums[N][N]; // 网格中的费用
int dp[N][N];   // dp[i][j]表示从左上角到(i,j)的最小费用

int min(int a, int b) { return a <= b ? a : b; }

int minPass(int n)
{
    dp[0][0] = nums[0][0];
    for (int i = 1; i < n; i++)
    {
        dp[0][i] = dp[0][i-1] + nums[0][i]; // 第一行只能从左边来
        dp[i][0] = dp[i-1][0] + nums[i][0]; // 第一列只能从上面来
    }
    for (int i = 1; i < n; i++)
        for (int j = 1; j < n; j++)
            dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + nums[i][j]; // 从上或从左来
    return dp[n-1][n-1];
}

int main(void)
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            scanf("%d", &nums[i][j]);
    printf("%d", minPass(n));
    return 0;
}

【实例15_04】最短路径(多阶段DP)

在有向图中从城市1到城市n的最短路径。

c 复制代码
#include <stdio.h>
#define N 105

int nums[N][N]; // 城市之间的交通图
int dp[N];      // dp[i]表示从城市i到终点的最少费用
int post[N];    // post[i]表示城市i的下一站

int minFee(int n)
{
    for (int i = 1; i <= n; i++)
    {
        dp[i] = 32767;  // 初始化为极大值
        post[i] = -1;
    }
    dp[n] = 0;          // 终点到终点费用为0
    for (int i = n-1; i >= 1; i--)  // 逆向计算(从终点倒推)
        for (int j = i+1; j <= n; j++)
            if (nums[i][j] != 0 && nums[i][j] + dp[j] < dp[i])
            {
                dp[i] = nums[i][j] + dp[j];
                post[i] = j;  // 记录路径
            }
    return dp[1];
}

int main(void)
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            scanf("%d", &nums[i][j]);
    printf("%d\n", minFee(n));
    // 输出路径
    for (int i = 1; i != -1; i = post[i])
    {
        if (i != 1) printf("->");
        printf("%d", i);
    }
    return 0;
}

【实例15_05】不同路径数(网格路径计数)

在m×n网格中从左上角到右下角,只能向右或向下走,共有多少种不同路径?

c 复制代码
#include <stdio.h>
#define N 101

int uniquePaths(int m, int n)
{
    int dp[N][N];
    for (int j = 0; j < n; j++)
        dp[0][j] = 1;  // 第一行都是1(只能从左往右)
    for (int i = 0; i < m; i++)
        dp[i][0] = 1;  // 第一列都是1(只能从上往下)
    for (int i = 1; i < m; i++)
        for (int j = 1; j < n; j++)
            dp[i][j] = dp[i-1][j] + dp[i][j-1]; // 从上方来 + 从左方来
    return dp[m-1][n-1];
}

int main(void)
{
    int m, n;
    scanf("%d %d", &m, &n);
    printf("%d", uniquePaths(m, n));
    return 0;
}

15.2 第15章知识点总结

动态规划三步法:

复制代码
Step 1: 定义状态
        dp[i] = 子问题i的最优解
        
Step 2: 找转移方程
        dp[i] = f(dp[i-1], dp[i-2], ...)
        
Step 3: 确定边界值
        dp[0], dp[1] = 已知的初始值

常见动态规划问题模式:

问题类型 状态 转移方程
爬楼梯 dp[i] = i阶楼梯的走法数 dp[i] = dp[i-1] + dp[i-2]
数塔 dp[i][j] = 从(i,j)到底层最大和 dp[i][j] = nums[i][j] + max(dp[i+1][j], dp[i+1][j+1])
最小路径和 dp[i][j] = 左上角到(i,j)最小和 dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + cost[i][j]
路径数 dp[i][j] = 到(i,j)的路径数 dp[i][j] = dp[i-1][j] + dp[i][j-1]

贪心 vs 动态规划:

特点 贪心 动态规划
思路 每步选局部最优 所有子问题都求解
效率 通常更快 较慢,但保证最优
适用 贪心选择性成立时 存在重叠子问题时

总结

本手册已系统讲解了C语言编程的核心知识体系,从基础入门到算法进阶:

知识体系回顾

章节 核心主题 关键技能
第1章 C语言入门 程序结构、输入输出、变量、运算符
第2章 选择语句 if-else、switch-case、条件判断
第3章 循环控制 for、while、do-while、break、continue
第4章 函数 函数定义、调用、参数传递、作用域
第5章 数组 一维/二维数组、排序、查找、矩阵
第6章 指针 指针基础、指针与数组/函数、动态内存
第9章 位运算 按位与/或/异或、移位操作
第11章 递归 递归思想、递归设计、尾递归
第14章 贪心算法 贪心策略、排序+贪心
第15章 动态规划 状态定义、转移方程、填表法

编程学习建议

  1. 多动手实践:理解代码后必须自己敲一遍,再尝试修改
  2. 看懂再写:不要死记硬背代码,理解原理后自然能写出来
  3. 由浅入深:先掌握基础语法,再学算法与数据结构
  4. 调试能力:学会使用调试器(gdb/IDE断点)定位问题
  5. 多做练习:编程能力来自大量练习,本手册每个实例都是好的练习题

编程路上,代码即是最好的老师。每一个报错都是进步的阶梯,坚持练习,必有收获!

相关推荐
wuminyu18 分钟前
专家视角看Lambda表达式的原理解析
java·linux·c语言·jvm·c++
modelmd30 分钟前
研究C语言的hello world输出
c语言·开发语言·chrome
12.=0.1 小时前
【stm32_7】定时器的原理与应用、基本定时器、通用定时器、PWM、模拟脉冲信号的宽度、利用PWM控制外设、逻辑分析仪的使用
c语言·stm32·单片机·嵌入式硬件
khalil10201 小时前
代码随想录算法训练营Day-38动态规划06 | 322. 零钱兑换、279.完全平方数、139.单词拆分、多重背包、总结
数据结构·c++·算法·leetcode·动态规划
jimy11 小时前
C语言历史版本和gnu扩展版本
c语言·算法·gnu
70asunflower2 小时前
堆与栈:C 语言内存管理的核心概念
c语言·开发语言
我不是懒洋洋2 小时前
【数据结构】二叉树OJ(单值二叉树、检查两棵树是否相同、对称二叉树、二叉树的前序遍历、另一颗树的子树)
c语言·数据结构·c++·经验分享·算法·leetcode·visual studio
爱编码的小八嘎2 小时前
C语言完美演绎9-8
c语言
wljy12 小时前
每日一题(2026.4.29) 猫猫与数学
c语言·c++·算法·蓝桥杯·stl·牛客
Rabitebla3 小时前
【C++】string 类:原理、踩坑与对象语义
linux·c语言·数据结构·c++·算法·github·学习方法