C语言编程实战宝典:550例精解·涵盖基础语法·数组指针·位运算·递归·贪心·动态规划
本手册基于550个C语言编程实例整理而成,涵盖从入门基础到高级算法的全部知识体系,适合零基础初学者系统学习C语言编程。每个知识点配有完整可运行的代码示例和详细注释说明。
目录
- [第1章 C语言编程入门](#第1章 C语言编程入门)
- [第2章 选择语句](#第2章 选择语句)
- [第3章 循环控制语句](#第3章 循环控制语句)
- [第4章 函数](#第4章 函数)
- [第5章 数组](#第5章 数组)
- [第6章 指针](#第6章 指针)
- [第7章 字符串](#第7章 字符串)
- [第8章 结构体](#第8章 结构体)
- [第9章 位运算](#第9章 位运算)
- [第11章 递归](#第11章 递归)
- [第12章 栈与队列](#第12章 栈与队列)
- [第13章 前缀和与差分](#第13章 前缀和与差分)
- [第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用于输出float或double类型,默认保留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;
}
知识点说明:
%#x以0x开头的十六进制格式输出地址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 = 10,29 + 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个实例,我们掌握了:
- 程序结构 :
#include、main函数、return 0 - 输出函数 :
printf、putchar - 输入函数 :
scanf、getchar - 数据类型 :
int、short、long、long long、float、double、char、_Bool - 格式说明符 :
%d、%f、%c、%lf、%lld等 - 运算符:算术、赋值、自增自减、类型转换、逗号运算符
- 数学库 :
sqrt、pow、sin、ceil、floor等 - 实际问题:求和、秒换算、字符转换、复利、贷款计算
第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语言的三种选择控制结构:
- 单分支 if:只在条件成立时执行
- 双分支 if-else:条件成立执行一个分支,否则执行另一个
- 多分支 if-else if-else:多个条件依次判断
- 三目运算符
?::简洁的二选一表达式 - 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:判断当前最低位是否为1x = 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>引入布尔类型,可以使用true和falsebool类型函数返回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;
}
回文数判断算法:
- 将整数
x的各位数字倒序重新组合成y - 如果
x == y,则x是回文数 - 例如:
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检查个位是否为7a /= 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】打印整数的每一位并求逆序数
综合运用数学函数(log10、ceil)和自定义函数。
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时省略
}
关键知识点梳理:
- 函数声明 (原型):告诉编译器函数的存在,在
main之前或头文件中声明 - 函数定义:实现函数功能的完整代码
- 函数调用 :
函数名(实际参数列表),实参与形参类型应匹配 - 返回值 :
return语句返回计算结果,void类型无需返回值 - 参数传递 :C语言默认值传递,函数内修改形参不影响实参
常用编程模式:
- 将判断逻辑封装成返回
bool或int(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;
}
逆置思路:
- 下标
i从0到n/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;
}
埃氏筛法原理:
- 从2开始,每找到一个质数,就划去它的所有倍数
- 剩余未被划去的数就是质数
- 只需枚举到
√N,因为N的最小质因子一定≤ √N - 时间复杂度约为 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
- 下一个数放在当前数的右上方
- 若右上方越界,则绕回对面
- 若右上方已有数,则放在当前数的下方
【实例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类型的指针变量pp = &a将变量a的地址赋给p*p = *p + 1通过指针p间接将a的值加1p和&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+i 或 p+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<<1,x/4=x>>2
第11章 递归
递归是一种优雅的编程技巧:函数调用自身来解决问题。掌握递归的关键是找到递归出口(边界条件)和递推关系。本章通过26个实例系统讲解递归的思想与应用。
11.1 递归的基本概念
递归的两要素:
- 递归出口(边界条件):停止递归的条件
- 递归体(递推关系):将大问题分解为小问题的方式
递归的执行过程:
递推阶段:不断调用自身,问题规模变小
回归阶段:到达出口后,逐层返回结果
【实例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章知识点总结
递归的优缺点:
| 特点 | 说明 |
|---|---|
| 优点 | 代码简洁,思路清晰,适合树、图等递归结构 |
| 缺点 | 有函数调用栈开销,可能栈溢出,可能重复计算 |
递归设计步骤:
- 找边界:确定什么情况下直接返回(不再递归)
- 找规律:当前规模与更小规模的关系
- 写代码:先写出口,再写递归体
常见递归问题:
- 阶乘:
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章知识点总结
贪心算法特征:
- 最优子结构:问题的全局最优解包含子问题的最优解
- 贪心选择性:每步选择局部最优,不会影响后续选择的有效性
常见贪心问题:
| 问题 | 贪心策略 |
|---|---|
| 分发饼干 | 用最小满足条件的饼干满足胃口最小的孩子 |
| 最大周长三角形 | 降序排列,取最大满足条件的相邻三个数 |
| 活动选择 | 按结束时间排序,选结束最早且不冲突的 |
| 分数背包 | 按单位价值降序,优先装单位价值最高的 |
第15章 动态规划
动态规划(DP)是解决最优化问题的强大算法技巧,通过将问题分解为子问题,利用子问题的最优解构建原问题的最优解,避免重复计算。
15.1 动态规划的核心思想
DP三要素:
- 状态定义 :
dp[i]或dp[i][j]表示什么含义 - 状态转移方程:大问题与小问题的关系(递推公式)
- 边界值:初始状态(最简单情况的答案)
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章 | 动态规划 | 状态定义、转移方程、填表法 |
编程学习建议
- 多动手实践:理解代码后必须自己敲一遍,再尝试修改
- 看懂再写:不要死记硬背代码,理解原理后自然能写出来
- 由浅入深:先掌握基础语法,再学算法与数据结构
- 调试能力:学会使用调试器(gdb/IDE断点)定位问题
- 多做练习:编程能力来自大量练习,本手册每个实例都是好的练习题
编程路上,代码即是最好的老师。每一个报错都是进步的阶梯,坚持练习,必有收获!