C语言数组 ------ 知识点详解
一、一维数组
1.1 一维数组的定义
语法格式:
数据类型 数组名[常量表达式];
说明:
| 项目 | 说明 |
|---|---|
| 数据类型 | 数组中每个元素的数据类型,如 int、float、char、double 等 |
| 数组名 | 遵循C语言标识符命名规则,如 arr、score、num 等 |
| 常量表达式 | 表示数组的长度(元素个数),必须是常量 或常量表达式,不能是变量 |
方括号 [] |
这是数组的标志符号,必不可少 |
关键规则:
- C语言中数组下标从0开始 ,即长度为
n的数组,下标范围是0到n-1。 - 定义时方括号内必须是整型常量 或整型常量表达式,不能使用变量(C99标准之前)。
- 编译器根据"数据类型 × 元素个数"来分配连续的内存空间。
- 数组名代表数组首元素的地址,是一个地址常量,不能被赋值。
示例:
c
int a[5]; // 定义了一个包含5个int元素的数组,下标0~4
float score[10]; // 定义了一个包含10个float元素的数组
#define N 20
char name[N]; // 用宏常量指定数组长度,合法
// int n = 10; int b[n]; // C89标准中非法,C99的VLA特性允许
内存分配原理:
假设系统为 int 类型分配4字节,则 int a[5] 在内存中分配了连续的 5×4=20 字节空间:
地址: 1000 1004 1008 1012 1016
+------+------+------+------+------+
| a[0] | a[1] | a[2] | a[3] | a[4] |
+------+------+------+------+------+
1.2 一维数组元素的引用
语法格式:
数组名[下标表达式]
说明:
- 下标可以是整型常量、整型变量或整型表达式 ,如
a[0]、a[i]、a[i+1]。 - 下标的值必须在合法范围内:
0到数组长度-1。超出范围会导致数组越界,属于未定义行为。 - 数组元素的使用方式与同类型的普通变量完全相同,可以赋值、参与运算、作为函数参数等。
常见错误:
c
int a[5];
a[5] = 10; // ❌ 错误!下标越界,a的合法下标是0~4
a[0] = 10; // ✅ 正确
a[2+1] = 20; // ✅ 正确,等价于a[3]=20
入门案例:数组元素的基本引用
c
#include <stdio.h> // 包含标准输入输出头文件
int main()
{
int a[5]; // 定义一个包含5个整型元素的数组a
int i; // 定义循环变量i
// 通过循环为数组的每个元素赋值
for (i = 0; i < 5; i++) // i从0循环到4,恰好覆盖所有下标
{
a[i] = i * 10; // 将i乘以10的结果赋给a[i]
// a[0]=0, a[1]=10, a[2]=20, a[3]=30, a[4]=40
}
// 通过循环输出数组的每个元素
for (i = 0; i < 5; i++) // 遍历数组所有元素
{
printf("a[%d] = %d\n", i, a[i]); // 输出下标和对应元素的值
}
return 0; // 程序正常结束
}
运行结果:
a[0] = 0
a[1] = 10
a[2] = 20
a[3] = 30
a[4] = 40
1.3 一维数组的初始化
初始化是在定义数组的同时给数组元素赋初值。
(1)完全初始化:给每个元素都赋初值
c
int a[5] = {10, 20, 30, 40, 50};
// a[0]=10, a[1]=20, a[2]=30, a[3]=40, a[4]=50
(2)部分初始化:只给部分元素赋初值,其余自动赋0
c
int a[5] = {10, 20};
// a[0]=10, a[1]=20, a[2]=0, a[3]=0, a[4]=0
(3)全部初始化为0
c
int a[5] = {0};
// a[0]=0, a[1]=0, a[2]=0, a[3]=0, a[4]=0
(4)省略长度:由初始化列表自动确定数组长度
c
int a[] = {1, 2, 3, 4, 5};
// 编译器自动确定数组长度为5,等价于 int a[5] = {1,2,3,4,5};
(5)指定下标初始化(C99支持的"指定初始化器")
c
int a[10] = {[0] = 1, [5] = 6, [9] = 10};
// a[0]=1, a[5]=6, a[9]=10,其余元素为0
重要注意事项:
| 场景 | 是否合法 | 说明 |
|---|---|---|
int a[5] = {1,2,3,4,5}; |
合法 | 完全初始化 |
int a[5] = {1,2,3,4,5,6}; |
❌ | 初始化值个数超过数组长度 |
int a[] = {1,2,3}; |
合法 | 自动确定长度为3 |
int a[5]; a = {1,2,3,4,5}; |
❌ | 定义之后不能用这种方式赋值,数组名是常量 |
int a[5]; a[0]=1; a[1]=2; |
合法 | 逐个元素单独赋值 |
入门案例:一维数组初始化的多种方式
c
#include <stdio.h>
int main()
{
// 方式1:完全初始化
int a[5] = {1, 2, 3, 4, 5}; // 为所有5个元素指定初值
// 方式2:部分初始化
int b[5] = {10, 20}; // 只初始化前两个元素,其余自动为0
// 方式3:全部初始化为0
int c[5] = {0}; // 5个元素全部为0
// 方式4:省略长度
int d[] = {100, 200, 300}; // 自动确定长度为3
int i; // 循环变量
printf("数组a(完全初始化):");
for (i = 0; i < 5; i++) // 遍历数组a的5个元素
printf("%d ", a[i]); // 输出每个元素的值
printf("\n"); // 输出换行
printf("数组b(部分初始化):");
for (i = 0; i < 5; i++) // 遍历数组b的5个元素
printf("%d ", b[i]); // 前两个为10和20,后三个为0
printf("\n");
printf("数组c(全部为0):");
for (i = 0; i < 5; i++) // 遍历数组c的5个元素
printf("%d ", c[i]); // 全部输出0
printf("\n");
printf("数组d(省略长度,长度为%d):", sizeof(d)/sizeof(d[0]));
// sizeof(d)是数组总字节数,sizeof(d[0])是一个元素的字节数
// 相除得到数组元素个数
for (i = 0; i < 3; i++) // 遍历数组d的3个元素
printf("%d ", d[i]); // 输出100 200 300
printf("\n");
return 0;
}
运行结果:
数组a(完全初始化):1 2 3 4 5
数组b(部分初始化):10 20 0 0 0
数组c(全部为0):0 0 0 0 0
数组d(省略长度,长度为3):100 200 300
1.4 一维数组的应用
入门案例:求一维数组中的最大值和最小值
c
#include <stdio.h>
int main()
{
int a[10] = {23, 45, 12, 67, 34, 89, 56, 78, 90, 11}; // 定义并初始化一个有10个元素的数组
int max, min; // 定义最大值和最小值变量
int i; // 循环变量
max = a[0]; // 先假设第一个元素是最大值
min = a[0]; // 先假设第一个元素是最小值
for (i = 1; i < 10; i++) // 从第二个元素开始遍历(下标从1开始)
{
if (a[i] > max) // 如果当前元素大于max
max = a[i]; // 更新max为当前元素的值
if (a[i] < min) // 如果当前元素小于min
min = a[i]; // 更新min为当前元素的值
}
printf("最大值为:%d\n", max); // 输出最大值
printf("最小值为:%d\n", min); // 输出最小值
return 0;
}
运行结果:
最大值为:90
最小值为:11
入门案例:计算数组元素的平均值
c
#include <stdio.h>
int main()
{
int score[5]; // 定义一个能存储5个成绩的数组
int i; // 循环变量
float sum = 0; // 定义累加和变量,初始化为0,使用float以存储小数
printf("请输入5个成绩:\n"); // 提示用户输入
for (i = 0; i < 5; i++) // 循环5次读取5个成绩
{
scanf("%d", &score[i]); // 读取用户输入,存入score[i]
}
for (i = 0; i < 5; i++) // 再次循环遍历数组
{
sum += score[i]; // 将每个元素累加到sum中
// 等价于 sum = sum + score[i]
}
printf("平均分为:%.2f\n", sum / 5); // 计算并输出平均值,保留两位小数
return 0;
}
运行结果(示例):
请输入5个成绩:
80 90 85 75 95
平均分为:85.00
二、二维数组
2.1 二维数组的定义
语法格式:
数据类型 数组名[行数][列数];
说明:
- 二维数组可以理解为"数组的数组",即一个一维数组,其中每个元素又是一个一维数组。
- 行数和列数都必须是整型常量或常量表达式。
- 二维数组在内存中仍然是按行连续存储的(行优先存储)。
示例:
c
int a[3][4]; // 定义一个3行4列的二维数组,共12个元素
float b[2][5]; // 定义一个2行5列的二维数组,共10个元素
内存存储方式(以 int a[3][4] 为例):
逻辑结构(3行4列的表格):
列0 列1 列2 列3
行0 | a[0][0] a[0][1] a[0][2] a[0][3]
行1 | a[1][0] a[1][1] a[1][2] a[1][3]
行2 | a[2][0] a[2][1] a[2][2] a[2][3]
内存中实际存储(按行连续排列):
a[0][0] → a[0][1] → a[0][2] → a[0][3] → a[1][0] → a[1][1] → ... → a[2][3]
2.2 二维数组元素的引用
语法格式:
数组名[行下标][列下标]
- 行下标范围:
0到行数-1 - 列下标范围:
0到列数-1
示例:
c
int a[3][4]; // 3行4列
a[0][0] = 1; // ✅ 第0行第0列
a[2][3] = 10; // ✅ 第2行第3列(最后一行最后一列)
a[3][0] = 5; // ❌ 行下标越界(最大是2)
a[0][4] = 5; // ❌ 列下标越界(最大是3)
2.3 二维数组的初始化
(1)按行完全初始化
c
int a[3][4] = {
{1, 2, 3, 4}, // 第0行
{5, 6, 7, 8}, // 第1行
{9, 10, 11, 12} // 第2行
};
(2)不分行的连续初始化(按行优先顺序依次赋值)
c
int a[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
// 等价于按行分配:第0行为1,2,3,4;第1行为5,6,7,8;第2行为9,10,11,12
(3)部分初始化
c
int a[3][4] = {{1}, {5}, {9}};
// 第0行为 1,0,0,0;第1行为 5,0,0,0;第2行为 9,0,0,0
int a[3][4] = {1, 2, 3, 4, 5};
// 按行优先填入:第0行为1,2,3,4;第1行为5,0,0,0;第2行为0,0,0,0
(4)省略第一维(行数),由编译器自动推断
c
int a[][4] = {1, 2, 3, 4, 5, 6, 7, 8};
// 12个元素 / 每行4列 = 3行 → 等价于 int a[3][4]
注意:只能省略第一维(行数),不能省略第二维(列数)。
c
int a[][4] = {...}; // ✅ 合法,列数为4
// int a[3][] = {...}; // ❌ 非法,必须指定列数
入门案例:二维数组的初始化和遍历
c
#include <stdio.h>
int main()
{
// 定义并初始化一个3行4列的二维数组
int a[3][4] = {
{1, 2, 3, 4}, // 第0行的4个元素
{5, 6, 7, 8}, // 第1行的4个元素
{9, 10, 11, 12} // 第2行的4个元素
};
int i, j; // i为行循环变量,j为列循环变量
printf("二维数组的元素为:\n");
for (i = 0; i < 3; i++) // 外层循环控制行,i从0到2
{
for (j = 0; j < 4; j++) // 内层循环控制列,j从0到3
{
printf("%4d", a[i][j]); // 输出第i行第j列的元素,%4d表示占4个字符宽度
}
printf("\n"); // 每输出一行后换行
}
return 0;
}
运行结果:
二维数组的元素为:
1 2 3 4
5 6 7 8
9 10 11 12
2.4 二维数组的应用
入门案例:矩阵转置(行变列,列变行)
c
#include <stdio.h>
int main()
{
int a[2][3] = {{1, 2, 3}, {4, 5, 6}}; // 定义2行3列的原始矩阵
int b[3][2]; // 定义3行2列的转置矩阵(行和列互换)
int i, j; // 循环变量
// 进行转置操作
for (i = 0; i < 2; i++) // 遍历原矩阵的行
{
for (j = 0; j < 3; j++) // 遍历原矩阵的列
{
b[j][i] = a[i][j]; // 将a的第i行第j列赋给b的第j行第i列
// 实现行列互换
}
}
// 输出原矩阵
printf("原矩阵a(2x3):\n");
for (i = 0; i < 2; i++) // 遍历原矩阵的行
{
for (j = 0; j < 3; j++) // 遍历原矩阵的列
{
printf("%4d", a[i][j]); // 输出原矩阵元素
}
printf("\n"); // 换行
}
// 输出转置后的矩阵
printf("转置矩阵b(3x2):\n");
for (i = 0; i < 3; i++) // 遍历转置矩阵的行
{
for (j = 0; j < 2; j++) // 遍历转置矩阵的列
{
printf("%4d", b[i][j]); // 输出转置矩阵元素
}
printf("\n"); // 换行
}
return 0;
}
运行结果:
原矩阵a(2x3):
1 2 3
4 5 6
转置矩阵b(3x2):
1 4
2 5
3 6
入门案例:求二维数组中每行的最大值
c
#include <stdio.h>
int main()
{
int a[3][4] = { // 定义3行4列的二维数组
{12, 34, 56, 23},
{45, 67, 89, 11},
{33, 55, 77, 99}
};
int i, j; // 循环变量
int max; // 用于存储每行的最大值
for (i = 0; i < 3; i++) // 外层循环:逐行遍历
{
max = a[i][0]; // 假设当前行的第一个元素是最大值
for (j = 1; j < 4; j++) // 内层循环:从当前行的第二个元素开始比较
{
if (a[i][j] > max) // 如果当前元素大于max
max = a[i][j]; // 更新max
}
printf("第%d行的最大值为:%d\n", i, max); // 输出当前行的最大值
}
return 0;
}
运行结果:
第0行的最大值为:56
第1行的最大值为:89
第2行的最大值为:99
三、字符数组
3.1 字符数组的定义
字符数组是元素类型为 char 的数组,主要用于存储字符串。
语法格式:
c
char 数组名[常量表达式];
示例:
c
char str[20]; // 定义一个能存储20个字符的字符数组
char ch[10]; // 定义一个能存储10个字符的字符数组
3.2 字符数组的初始化
(1)逐个字符初始化
c
char str[5] = {'H', 'e', 'l', 'l', 'o'};
// str[0]='H', str[1]='e', str[2]='l', str[3]='l', str[4]='o'
(2)用字符串常量初始化(推荐)
c
char str[] = "Hello";
// 等价于 char str[] = {'H', 'e', 'l', 'l', 'o', '\0'};
// 数组长度为6(5个可见字符 + 1个'\0'结束符)
(3)部分初始化
c
char str[10] = "Hello";
// str[0]='H', str[1]='e', str[2]='l', str[3]='l', str[4]='o', str[5]='\0'
// str[6]~str[9] 全部为'\0'
(4)逐字符初始化时不加'\0'的注意事项
c
char s1[] = {'H', 'i'}; // 长度为2,没有'\0',不是合法的字符串
char s2[] = {'H', 'i', '\0'}; // 长度为3,有'\0',是合法的字符串
char s3[] = "Hi"; // 长度为3,自动包含'\0',是合法的字符串
3.3 字符数组的结束标志
核心概念:'\0'(空字符,ASCII值为0)
- C语言中,字符串是以
'\0'结尾的字符数组。 '\0'是字符串的结束标志,不是字符串的内容。- 用
sizeof运算符求得的是数组占用的内存字节数 (包含'\0')。 - 用
strlen()函数求得的是字符串的有效字符个数 (不包含'\0')。
示例说明:
c
char str[] = "Hello";
// sizeof(str) = 6 (H, e, l, l, o, \0 共6个字节)
// strlen(str) = 5 (有效字符为 H, e, l, l, o 共5个)
入门案例:sizeof 与 strlen 的区别
c
#include <stdio.h>
#include <string.h> // 使用strlen函数需要包含此头文件
int main()
{
char str1[] = "Hello"; // 用字符串初始化,自动包含'\0'
char str2[] = {'H', 'e', 'l', 'l', 'o'}; // 逐字符初始化,没有'\0'
char str3[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 逐字符初始化,手动加'\0'
printf("str1: sizeof = %lu, strlen = %lu\n", sizeof(str1), strlen(str1));
// sizeof(str1)=6(含\0),strlen(str1)=5(不含\0)
printf("str2: sizeof = %lu, strlen = %lu\n", sizeof(str2), strlen(str2));
// sizeof(str2)=5(没有\0),strlen(str2)的结果不确定(找不到\0,会继续读内存)
// 这是一个危险的行为!
printf("str3: sizeof = %lu, strlen = %lu\n", sizeof(str3), strlen(str3));
// sizeof(str3)=6(含\0),strlen(str3)=5(不含\0)
return 0;
}
注意:
str2没有'\0'结束标志,strlen(str2)会继续往后读取内存直到遇到'\0'为止,结果是不确定的 ,属于未定义行为。实际编程中一定要确保字符数组以'\0'结尾。
3.4 字符数组的输入和输出
(1)使用 scanf 和 printf
c
char str[100];
scanf("%s", str); // 注意:str本身是地址,不需要加&
printf("%s", str); // 输出字符串,遇到'\0'停止
注意: scanf("%s", ...) 遇到空格、Tab或回车就会停止读入,因此不能读取包含空格的字符串。
(2)使用 gets 和 puts
c
char str[100];
gets(str); // 读取一整行,可以包含空格,遇到回车结束
puts(str); // 输出字符串并自动换行
(3)逐字符输入输出
c
char ch;
ch = getchar(); // 读取一个字符
putchar(ch); // 输出一个字符
入门案例:字符串的输入和输出
c
#include <stdio.h>
int main()
{
char str[100]; // 定义一个能存储100个字符的字符数组
printf("请输入一个单词(不含空格):");
scanf("%s", str); // 用scanf读取一个单词
printf("scanf读取到的内容:%s\n", str); // 输出结果
// 清除输入缓冲区中的换行符(重要!)
getchar(); // 吃掉scanf留在缓冲区中的回车符
printf("请输入一句话(可以含空格):");
gets(str); // 用gets读取一整行(含空格)
printf("gets读取到的内容:%s\n", str); // 输出结果
return 0;
}
运行结果(示例):
请输入一个单词(不含空格):Hello World
scanf读取到的内容:Hello
请输入一句话(可以含空格):Hello World
gets读取到的内容:Hello World
可以看到,
scanf("%s", ...)在遇到空格时就停止了,只读到了 "Hello";而gets能读取整行内容。
3.5 字符数组的应用
入门案例:统计字符串中大写字母、小写字母、数字和其他字符的个数
c
#include <stdio.h>
#include <string.h> // 使用strlen函数
int main()
{
char str[200]; // 存储输入的字符串
int upper = 0; // 大写字母计数器
int lower = 0; // 小写字母计数器
int digit = 0; // 数字计数器
int other = 0; // 其他字符计数器
int i; // 循环变量
printf("请输入一个字符串:");
gets(str); // 读取一行字符串(可以包含空格)
for (i = 0; i < strlen(str); i++) // 遍历字符串中的每个字符
{
if (str[i] >= 'A' && str[i] <= 'Z') // 如果是大写字母
upper++; // 大写计数器加1
else if (str[i] >= 'a' && str[i] <= 'z') // 如果是小写字母
lower++; // 小写计数器加1
else if (str[i] >= '0' && str[i] <= '9') // 如果是数字
digit++; // 数字计数器加1
else // 其他字符
other++; // 其他字符计数器加1
}
printf("大写字母:%d个\n", upper);
printf("小写字母:%d个\n", lower);
printf("数字:%d个\n", digit);
printf("其他字符:%d个\n", other);
return 0;
}
运行结果(示例):
请输入一个字符串:Hello World 123!
大写字母:2个
小写字母:8个
数字:3个
其他字符:3个
四、多维数组
4.1 多维数组的概念
C语言允许定义多维数组。二维以上的数组称为多维数组。最常见的是三维数组。
语法格式:
c
数据类型 数组名[维度1][维度2][维度3]...;
示例:
c
int a[2][3][4]; // 定义一个三维数组:2层×3行×4列,共24个元素
三维数组的逻辑结构:
可以想象成"一本书":
-
第1维(2):2页
-
第2维(3):每页3行
-
第3维(4):每行4列
第0页: 第1页:
列0 列1 列2 列3 列0 列1 列2 列3
行0: a[0][0][0~3] a[1][0][0~3]
行1: a[0][1][0~3] a[1][1][0~3]
行2: a[0][2][0~3] a[1][2][0~3]
多维数组在内存中仍然是线性连续存储的 ,存储顺序为:最右边的下标变化最快(即 a[0][0][0] → a[0][0][1] → a[0][0][2] → ...)。
入门案例:三维数组的初始化和遍历
c
#include <stdio.h>
int main()
{
// 定义并初始化一个2层3行4列的三维数组
int a[2][3][4] = {
{ // 第0层
{1, 2, 3, 4}, // 第0层第0行
{5, 6, 7, 8}, // 第0层第1行
{9, 10, 11, 12} // 第0层第2行
},
{ // 第1层
{13, 14, 15, 16}, // 第1层第0行
{17, 18, 19, 20}, // 第1层第1行
{21, 22, 23, 24} // 第1层第2行
}
};
int i, j, k; // 三维循环变量
// 三层循环遍历三维数组
for (i = 0; i < 2; i++) // 第1维:层(从第0层到第1层)
{
printf("第%d层:\n", i); // 输出当前层号
for (j = 0; j < 3; j++) // 第2维:行(从第0行到第2行)
{
for (k = 0; k < 4; k++) // 第3维:列(从第0列到第3列)
{
printf("%4d", a[i][j][k]); // 输出第i层第j行第k列的元素
}
printf("\n"); // 每行输出后换行
}
printf("\n"); // 每层输出后空一行
}
return 0;
}
运行结果:
第0层:
1 2 3 4
5 6 7 8
9 10 11 12
第1层:
13 14 15 16
17 18 19 20
21 22 23 24
五、数组的排序算法
排序是将一组数据按照从小到大(升序) 或从大到小(降序) 的顺序重新排列。以下是几种经典排序算法。
5.1 选择排序
基本思想:
每一轮从待排序的数据中选出最小(或最大)的元素,将其放到已排序序列的末尾。
算法步骤(以升序为例):
- 第1轮:从
a[0]~a[n-1]中找到最小值,与a[0]交换。 - 第2轮:从
a[1]~a[n-1]中找到最小值,与a[1]交换。 - 第3轮:从
a[2]~a[n-1]中找到最小值,与a[2]交换。 - ......直到只剩最后一个元素,排序完成。
图解示例(对 {64, 25, 12, 22, 11} 排序):
初始:[64, 25, 12, 22, 11]
第1轮:找到最小值11,与a[0]=64交换 → [11, 25, 12, 22, 64]
第2轮:在[25,12,22,64]中找到最小值12,与a[1]=25交换 → [11, 12, 25, 22, 64]
第3轮:在[25,22,64]中找到最小值22,与a[2]=25交换 → [11, 12, 22, 25, 64]
第4轮:在[25,64]中找到最小值25,已经就位 → [11, 12, 22, 25, 64]
排序完成!
入门案例:选择排序
c
#include <stdio.h>
int main()
{
int a[5] = {64, 25, 12, 22, 11}; // 定义并初始化待排序的数组
int i, j; // i控制轮次,j用于遍历剩余元素
int minIndex; // 记录最小值元素的下标
int temp; // 用于交换的临时变量
int n = 5; // 数组长度
// 选择排序:外层循环控制排序轮次
for (i = 0; i < n - 1; i++) // 共需n-1轮(最后一轮只剩一个元素,无需排序)
{
minIndex = i; // 假设当前位置i的元素就是最小的
// 内层循环:从i+1开始,寻找比a[minIndex]更小的元素
for (j = i + 1; j < n; j++)
{
if (a[j] < a[minIndex]) // 如果找到更小的元素
{
minIndex = j; // 更新最小值的下标
}
}
// 如果最小值不在位置i,则交换
if (minIndex != i)
{
temp = a[i]; // 将a[i]存入临时变量
a[i] = a[minIndex]; // 将最小值放到位置i
a[minIndex] = temp; // 将原来的a[i]放到最小值原来的位置
}
}
// 输出排序后的结果
printf("选择排序后的结果:");
for (i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
运行结果:
选择排序后的结果:11 12 22 25 64
时间复杂度: O(n²),空间复杂度:O(1)。
5.2 冒泡排序
基本思想:
相邻两个元素两两比较,如果前面的比后面的大(升序),就交换它们。每一轮把最大的元素"冒泡"到末尾。
算法步骤(以升序为例):
- 第1轮:从
a[0]到a[n-1],依次比较相邻元素,把最大值冒泡到最后。 - 第2轮:从
a[0]到a[n-2],把次大值冒泡到倒数第二位。 - ......直到所有元素有序。
图解示例(对 {64, 25, 12, 22, 11} 排序):
初始:[64, 25, 12, 22, 11]
第1轮:
64>25 → 交换 → [25, 64, 12, 22, 11]
64>12 → 交换 → [25, 12, 64, 22, 11]
64>22 → 交换 → [25, 12, 22, 64, 11]
64>11 → 交换 → [25, 12, 22, 11, 64] ← 64就位
第2轮:
25>12 → 交换 → [12, 25, 22, 11, 64]
25>22 → 交换 → [12, 22, 25, 11, 64]
25>11 → 交换 → [12, 22, 11, 25, 64] ← 25就位
第3轮:
12<22 → 不换 → [12, 22, 11, 25, 64]
22>11 → 交换 → [12, 11, 22, 25, 64] ← 22就位
第4轮:
12>11 → 交换 → [11, 12, 22, 25, 64] ← 排序完成
入门案例:冒泡排序
c
#include <stdio.h>
int main()
{
int a[5] = {64, 25, 12, 22, 11}; // 定义并初始化待排序数组
int i, j; // i控制轮次,j用于相邻元素比较
int temp; // 交换用的临时变量
int n = 5; // 数组长度
// 冒泡排序:外层循环控制轮次
for (i = 0; i < n - 1; i++) // 共需n-1轮
{
// 内层循环:比较相邻元素
for (j = 0; j < n - 1 - i; j++) // 每轮减少一个已排好的元素
// 第1轮比较n-1次,第2轮比较n-2次...
{
if (a[j] > a[j + 1]) // 如果前面的元素比后面的大
{
temp = a[j]; // 交换两个元素
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
// 输出排序结果
printf("冒泡排序后的结果:");
for (i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
运行结果:
冒泡排序后的结果:11 12 22 25 64
时间复杂度: O(n²),空间复杂度:O(1)。
5.3 交换排序
基本思想:
交换排序是一个广义概念,核心思想是两两比较,必要时交换。冒泡排序和快速排序都属于交换排序。此处介绍一种简单交换排序(又称"鸡尾酒排序"的简化版):
每次用当前元素和后面所有元素逐一比较,如果逆序就交换。
入门案例:简单交换排序
c
#include <stdio.h>
int main()
{
int a[5] = {64, 25, 12, 22, 11}; // 定义并初始化待排序数组
int i, j; // 循环变量
int temp; // 交换用的临时变量
int n = 5; // 数组长度
// 简单交换排序
for (i = 0; i < n - 1; i++) // 外层循环:控制起始位置
{
for (j = i + 1; j < n; j++) // 内层循环:将a[i]与后面所有元素比较
{
if (a[i] > a[j]) // 如果a[i]比后面的元素大(升序)
{
temp = a[i]; // 交换a[i]和a[j]
a[i] = a[j];
a[j] = temp;
}
}
}
printf("交换排序后的结果:");
for (i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
运行结果:
交换排序后的结果:11 12 22 25 64
时间复杂度: O(n²),空间复杂度:O(1)。
5.4 插入排序
基本思想:
将数组分为"已排序"和"未排序"两部分。每次从未排序部分取出一个元素,插入到已排序部分的正确位置。
算法步骤(以升序为例):
- 从第2个元素开始(下标1),将当前元素暂存到
temp。 - 与已排序部分从后往前逐个比较。
- 比
temp大的元素后移一位。 - 找到合适位置后,将
temp插入。
图解示例(对 {64, 25, 12, 22, 11} 排序):
初始:[64, 25, 12, 22, 11]
取25插入:25<64 → 64后移 → 插入 → [25, 64, 12, 22, 11]
取12插入:12<64 → 64后移,12<25 → 25后移 → 插入 → [12, 25, 64, 22, 11]
取22插入:22<64 → 64后移,22<25 → 25后移,22>12 → 插入 → [12, 22, 25, 64, 11]
取11插入:依次后移 → 插入 → [11, 12, 22, 25, 64]
入门案例:插入排序
c
#include <stdio.h>
int main()
{
int a[5] = {64, 25, 12, 22, 11}; // 定义并初始化待排序数组
int i, j; // i为当前待插入元素的下标,j为比较指针
int temp; // 暂存当前待插入的元素
int n = 5; // 数组长度
// 插入排序:从第2个元素(下标1)开始
for (i = 1; i < n; i++) // i从1到n-1,逐个取出待插入元素
{
temp = a[i]; // 将当前元素暂存到temp中,腾出位置
// 从已排序部分的最后一个元素开始,向前逐个比较
j = i - 1; // j指向已排序部分的最后一个元素
while (j >= 0 && a[j] > temp) // 如果j合法且a[j]比temp大
{
a[j + 1] = a[j]; // 将a[j]后移一位,为temp腾出位置
j--; // j前移,继续比较
}
a[j + 1] = temp; // 将temp插入到正确的位置
}
printf("插入排序后的结果:");
for (i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
运行结果:
插入排序后的结果:11 12 22 25 64
时间复杂度: 最好 O(n),最坏 O(n²),空间复杂度:O(1)。
5.5 折半排序(折半插入排序)
基本思想:
折半插入排序是插入排序的改进版。在寻找插入位置时,不再从后往前逐个比较,而是利用折半查找(二分查找) 来确定插入位置,从而减少比较次数。
算法步骤:
- 从第2个元素开始,将当前元素暂存到
temp。 - 在已排序部分用折半查找确定
temp应该插入的位置。 - 将该位置及之后的元素后移一位。
- 将
temp插入到确定的位置。
入门案例:折半插入排序
c
#include <stdio.h>
int main()
{
int a[6] = {64, 25, 12, 22, 11, 0}; // 数组(第0个元素先不动)
// 我们从下标1开始排序
int n = 5; // 实际待排序元素个数
int i, j; // 循环变量
int temp; // 暂存待插入元素
int low, high, mid; // 折半查找用的变量
// 从第2个元素开始,逐个插入
for (i = 2; i <= n; i++) // 用下标1~n,a[0]作为哨兵暂存
{
temp = a[i]; // 暂存当前待插入元素
// 在a[1]到a[i-1](已排序部分)中用折半查找确定插入位置
low = 1; // 已排序部分的起始位置
high = i - 1; // 已排序部分的结束位置
while (low <= high) // 折半查找
{
mid = (low + high) / 2; // 取中间位置
if (temp < a[mid]) // 如果temp小于中间元素
high = mid - 1; // 在左半部分继续查找
else // 如果temp大于等于中间元素
low = mid + 1; // 在右半部分继续查找
}
// high+1 就是插入位置,将该位置及之后的元素后移
for (j = i - 1; j >= high + 1; j--)
{
a[j + 1] = a[j]; // 元素后移一位
}
a[high + 1] = temp; // 插入temp到正确位置
}
printf("折半插入排序后的结果:");
for (i = 1; i <= n; i++) // 从下标1开始输出
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
运行结果:
折半插入排序后的结果:11 12 22 25 64
说明: 折半插入排序减少了比较次数 (利用折半查找从 O(n) 降到 O(log n)),但移动次数不变,总时间复杂度仍为 O(n²)。
5.6 排序算法的比较
| 排序算法 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
|---|---|---|---|---|---|
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
| 交换排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
| 折半插入排序 | O(n log n) | O(n²) | O(n²) | O(1) | 稳定 |
名词解释:
- 稳定排序:如果两个相等的元素在排序前后的相对顺序不变,则称该排序算法是稳定的。
- 时间复杂度:描述算法运行时间随数据规模增长的趋势。
- 空间复杂度:描述算法所需额外内存空间的大小。
选择建议:
- 数据量小(n < 50):插入排序效果好,实现简单。
- 数据量中等:冒泡排序或选择排序。
- 数据量大:应使用更高级的排序算法(如快速排序、归并排序,后续章节学习)。
六、数组应用
6.1 反转输出字符串
基本思想:
将字符串的第一个字符与最后一个字符交换,第二个与倒数第二个交换,依此类推,直到中间位置。
入门案例:反转字符串
c
#include <stdio.h>
#include <string.h> // 使用strlen函数
int main()
{
char str[100]; // 存储输入的字符串
int len; // 字符串长度
int i; // 循环变量
char temp; // 交换用的临时变量
printf("请输入一个字符串:");
gets(str); // 读取一行字符串
len = strlen(str); // 计算字符串的有效长度(不含'\0')
// 反转操作:首尾对应交换
for (i = 0; i < len / 2; i++) // 只需交换前一半
{
temp = str[i]; // 将第i个字符暂存
str[i] = str[len - 1 - i]; // 将倒数第i+1个字符放到第i个位置
str[len - 1 - i] = temp; // 将暂存的字符放到倒数第i+1个位置
}
printf("反转后的字符串:%s\n", str); // 输出反转后的结果
return 0;
}
运行结果(示例):
请输入一个字符串:Hello World
反转后的字符串:dlroW olleH
6.2 输出系统日期和时间
基本思想:
使用C标准库中 <time.h> 提供的时间函数获取系统时间,并格式化输出。
入门案例:输出当前系统日期和时间
c
#include <stdio.h>
#include <time.h> // 包含时间处理头文件
int main()
{
time_t rawtime; // 定义time_t类型的变量,用于存储时间戳
struct tm *timeinfo; // 定义tm结构体指针,用于存储分解后的时间信息
time(&rawtime); // 获取当前系统时间(时间戳形式),存入rawtime
timeinfo = localtime(&rawtime); // 将时间戳转换为本地时间的结构体
// 输出年、月、日
printf("当前日期:%d年%02d月%02d日\n",
timeinfo->tm_year + 1900, // tm_year表示从1900年起的年数,需加1900
timeinfo->tm_mon + 1, // tm_mon范围是0~11,需加1
timeinfo->tm_mday); // tm_mday是本月的日期(1~31)
// 输出时、分、秒
printf("当前时间:%02d时%02d分%02d秒\n",
timeinfo->tm_hour, // tm_hour是小时(0~23)
timeinfo->tm_min, // tm_min是分钟(0~59)
timeinfo->tm_sec); // tm_sec是秒(0~59)
// 输出星期几
printf("星期");
switch (timeinfo->tm_wday) // tm_wday是星期几(0=周日,1=周一,...,6=周六)
{
case 0: printf("日\n"); break;
case 1: printf("一\n"); break;
case 2: printf("二\n"); break;
case 3: printf("三\n"); break;
case 4: printf("四\n"); break;
case 5: printf("五\n"); break;
case 6: printf("六\n"); break;
}
return 0;
}
运行结果(示例,取决于运行时的系统时间):
当前日期:2024年01月15日
当前时间:14时30分25秒
星期一
6.3 字符串的加密和解密
基本思想:
对字符串中的每个字符进行变换,实现加密。解密是加密的逆过程。
常用方法:
- 凯撒密码:每个字符的ASCII码加上一个固定偏移量。
- 异或加密 :每个字符与一个密钥进行异或运算。异或运算的特性:
(A ^ key) ^ key = A,即加密和解密使用相同的操作。
入门案例:凯撒密码加密与解密
c
#include <stdio.h>
#include <string.h> // 使用strlen函数
int main()
{
char str[200]; // 存储原始字符串
char encrypted[200]; // 存储加密后的字符串
char decrypted[200]; // 存储解密后的字符串
int key = 3; // 加密密钥(偏移量为3)
int len; // 字符串长度
int i; // 循环变量
printf("请输入要加密的字符串:");
gets(str); // 读取原始字符串
len = strlen(str); // 获取字符串长度
// ===== 加密过程 =====
for (i = 0; i < len; i++)
{
// 对字母字符进行偏移加密
if (str[i] >= 'a' && str[i] <= 'z') // 小写字母
{
// 偏移3位,如果超过'z'则循环回'a'
encrypted[i] = (str[i] - 'a' + key) % 26 + 'a';
// 例如:'x' → (120-97+3)%26+97 = 26%26+97 = 0+97 = 97 = 'a'
}
else if (str[i] >= 'A' && str[i] <= 'Z') // 大写字母
{
encrypted[i] = (str[i] - 'A' + key) % 26 + 'A';
}
else // 非字母字符保持不变
{
encrypted[i] = str[i];
}
}
encrypted[len] = '\0'; // 添加字符串结束标志
printf("加密后的字符串:%s\n", encrypted); // 输出加密结果
// ===== 解密过程 =====
for (i = 0; i < len; i++)
{
if (encrypted[i] >= 'a' && encrypted[i] <= 'z') // 小写字母
{
// 减去偏移量,如果低于'a'则循环回'z'
decrypted[i] = (encrypted[i] - 'a' - key + 26) % 26 + 'a';
// 加26是为了防止出现负数
}
else if (encrypted[i] >= 'A' && encrypted[i] <= 'Z') // 大写字母
{
decrypted[i] = (encrypted[i] - 'A' - key + 26) % 26 + 'A';
}
else // 非字母字符保持不变
{
decrypted[i] = encrypted[i];
}
}
decrypted[len] = '\0'; // 添加字符串结束标志
printf("解密后的字符串:%s\n", decrypted); // 输出解密结果
return 0;
}
运行结果(示例):
请输入要加密的字符串:Hello World
加密后的字符串:Khoor Zruog
解密后的字符串:Hello World
入门案例:异或加密与解密
c
#include <stdio.h>
#include <string.h>
int main()
{
char str[200]; // 存储原始字符串
char result[200]; // 存储加密/解密后的字符串
char key = 'K'; // 密钥字符(可以任意指定)
int len; // 字符串长度
int i; // 循环变量
printf("请输入要加密的字符串:");
gets(str); // 读取原始字符串
len = strlen(str); // 获取字符串长度
// ===== 加密:每个字符与密钥进行异或运算 =====
for (i = 0; i < len; i++)
{
result[i] = str[i] ^ key; // 异或加密
// 例:'H'(72) ^ 'K'(75) = 11(不可见字符)
}
result[len] = '\0'; // 添加结束标志
printf("加密后的ASCII值:");
for (i = 0; i < len; i++)
{
printf("%d ", (unsigned char)result[i]); // 输出加密后的ASCII码值
}
printf("\n");
// ===== 解密:再次异或相同的密钥即可还原 =====
for (i = 0; i < len; i++)
{
result[i] = result[i] ^ key; // 再次异或,还原为原始字符
// 因为 (A ^ key) ^ key = A
}
result[len] = '\0';
printf("解密后的字符串:%s\n", result); // 输出解密结果,应与原文相同
return 0;
}
运行结果(示例):
请输入要加密的字符串:Hello
加密后的ASCII值:11 45 16 16 27
解密后的字符串:Hello
七、实践与练习
练习1:求数组中的第二大元素
c
#include <stdio.h>
int main()
{
int a[10] = {45, 12, 78, 34, 90, 56, 23, 67, 89, 11}; // 定义数组
int first, second; // first存最大值,second存第二大值
int i;
// 初始化最大值和第二大值
if (a[0] > a[1]) // 比较前两个元素
{
first = a[0]; // a[0]较大,作为最大值
second = a[1]; // a[1]较小,作为第二大值
}
else
{
first = a[1]; // a[1]较大,作为最大值
second = a[0]; // a[0]较小,作为第二大值
}
// 从第3个元素开始遍历
for (i = 2; i < 10; i++)
{
if (a[i] > first) // 如果当前元素比最大值还大
{
second = first; // 原来的最大值变成第二大
first = a[i]; // 更新最大值
}
else if (a[i] > second && a[i] != first)
// 如果当前元素比第二大值大且不等于最大值
{
second = a[i]; // 更新第二大值
}
}
printf("最大值为:%d\n", first); // 输出最大值
printf("第二大值为:%d\n", second); // 输出第二大值
return 0;
}
运行结果:
最大值为:90
第二大值为:89
练习2:用冒泡排序将字符串按ASCII码从小到大排序
c
#include <stdio.h>
#include <string.h>
int main()
{
char str[100]; // 存储输入的字符串
int len; // 字符串长度
int i, j; // 循环变量
char temp; // 交换用的临时变量
printf("请输入一个字符串:");
gets(str); // 读取字符串
len = strlen(str); // 获取长度
// 用冒泡排序对字符排序
for (i = 0; i < len - 1; i++) // 外层循环:n-1轮
{
for (j = 0; j < len - 1 - i; j++) // 内层循环:两两比较
{
if (str[j] > str[j + 1]) // 如果前一个字符的ASCII码大于后一个
{
temp = str[j]; // 交换两个字符
str[j] = str[j + 1];
str[j + 1] = temp;
}
}
}
printf("排序后的字符串:%s\n", str); // 输出排序结果
return 0;
}
运行结果(示例):
请输入一个一个字符串:HelloWorld
排序后的字符串:HWdelllloo
练习3:计算矩阵的对角线元素之和
c
#include <stdio.h>
int main()
{
int a[3][3] = { // 定义一个3x3的矩阵
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
int sum_main = 0; // 主对角线元素之和(左上到右下)
int sum_anti = 0; // 副对角线元素之和(右上到左下)
int i, j; // 循环变量
// 遍历矩阵
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
if (i == j) // 主对角线条件:行号等于列号
{
sum_main += a[i][j]; // 累加主对角线元素
}
if (i + j == 2) // 副对角线条件:行号+列号=n-1=2
{
sum_anti += a[i][j]; // 累加副对角线元素
}
}
}
// 输出矩阵
printf("矩阵为:\n");
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
printf("%4d", a[i][j]);
printf("\n");
}
printf("主对角线元素之和:%d\n", sum_main); // 1+5+9=15
printf("副对角线元素之和:%d\n", sum_anti); // 3+5+7=15
return 0;
}
运行结果:
矩阵为:
1 2 3
4 5 6
7 8 9
主对角线元素之和:15
副对角线元素之和:15
练习4:删除字符串中的指定字符
c
#include <stdio.h>
#include <string.h>
int main()
{
char str[200]; // 原始字符串
char ch; // 要删除的字符
int i, j; // 循环变量(i读指针,j写指针)
printf("请输入一个字符串:");
gets(str); // 读取原始字符串
printf("请输入要删除的字符:");
ch = getchar(); // 读取要删除的字符
j = 0; // j是"写指针",指向新字符串的当前位置
// 遍历原始字符串的每个字符
for (i = 0; str[i] != '\0'; i++) // 从头到尾遍历
{
if (str[i] != ch) // 如果当前字符不是要删除的字符
{
str[j] = str[i]; // 将该字符复制到j的位置
j++; // 写指针后移
}
// 如果是要删除的字符,则跳过(不复制,j不动)
}
str[j] = '\0'; // 在新字符串末尾添加结束标志
printf("删除后的字符串:%s\n", str); // 输出结果
return 0;
}
运行结果(示例):
请输入一个字符串:Hello World
请输入要删除的字符:l
删除后的字符串:Heo Word
八、总结
| 知识点 | 核心要点 |
|---|---|
| 一维数组定义 | 类型 数组名[常量];下标从0开始 |
| 一维数组初始化 | 完全初始化、部分初始化、省略长度、全0初始化 |
| 二维数组定义 | 类型 数组名[行][列];内存按行连续存储 |
| 二维数组初始化 | 按行初始化、连续初始化、省略行数 |
| 字符数组与字符串 | 字符串以 '\0' 结尾;sizeof 含 '\0',strlen 不含 |
| 字符串输入输出 | scanf("%s") 不读空格;gets() 读整行 |
| 排序算法 | 选择、冒泡、交换、插入、折半插入排序的原理和实现 |
| 数组应用 | 字符串反转、时间获取、字符串加密解密 |
学习建议: 数组是C语言中最重要的数据结构基础之一。建议先掌握一维数组和字符数组的基本操作,再学习二维数组和排序算法。每个知识点都要通过编写代码来加深理解,注意数组越界和字符串结束标志这两个最常见的错误。