第六章 数组
关键考点总览
跳转到:关键考点总览- 数组的定义格式(元素个数必须是常量表达式)
- 数组的初始化方式(完全初始化、部分初始化、省略大小)
- 数组下标从0开始,范围0至n-1
- 数组在内存中连续存储,下标超界问题
- 顺序查找与二分查找算法
- 选择排序与冒泡排序算法
- 时间复杂度分析:O(N) vs O(log₂N)
- 多维数组的定义、内存排列(行优先/后下标先变)
- 字符串的表示(字符数组 + '\0')
- 字符串处理函数(cstring库)
详细知识点
跳转到:详细知识点6.1 一维数组
跳转到:6.1 一维数组定义/语法
跳转到:定义/语法-
数组定义格式:
类型 数组名[元素个数];示例:
int array[5];[来源:第6章 p7,数组的定义] -
定义数组时元素个数必须是常量表达式,不能用变量:
int n = 5;int array[n]; // ❌ 标准 C++ 不支持变长数组(VLA);少数编译器扩展可能允许,但不具可移植性const int NumOfElement = 5; // ✅ 编译期常量(需用常量表达式初始化)int array[NumOfElement]; // ✅ 正确:数组长度必须是常量表达式[来源:第6章 p7,数组的定义]
-
数组元素引用方式:通过数组名和下标引用,如
array[2]- 下标可为任意整型表达式(整数常量、整型变量、整型算术表达式) [来源:第6章 p7,数组的定义]
规则/规范
跳转到:规则/规范-
数组下标从0开始,当数组大小为n时,下标范围为 0 至 n-1 [来源:第6章 p7,数组的定义]
-
下标从0开始的原因(戴克斯特拉主张):
- 子序列 a[i..j) 包含的元素个数就是 j-i(更优雅)
- 第i个元素的位置是 基地址 + i×元素大小(即从基地址偏移 i 个元素),避免偏移量计算中的减法
- [i, i) 自然表示空序列 [来源:第6章 p8,为什么下标应该从0开始]
-
只能对具体元素操作,不能对数组整体操作:
int arr[5];cin >> arr; // ❌ 错误:不能直接输入整个数组for (int i = 0; i < 5; i++)cin >> arr[i]; // ✅ 正确:逐个元素输入arr = {1,2,3,4,5}; // ❌ 错误:内置数组不可整体赋值(只能在定义时用{}初始化)
// ✅ 正确做法:在定义时初始化:int arr2[5] = {1,2,3,4,5};
[来源:第6章 p11,数组的操作]
#### 初始化
- **完全初始化**:```cppfloat x[5] = {-1.1, 0.2, 33.0, 4.4, 5.05};[来源:第6章 p10,初始化]
-
部分初始化:初始化表长度短于数组元素数目时,剩余元素被初始化为0:
float x[5] = {-1.1, 0.2, 33.0};// 等价于 float x[5] = {-1.1, 0.2, 33.0, 0, 0};[来源:第6章 p10,初始化]
-
省略数组大小:带初始化的数组可以不定义规模,编译器根据初值个数决定大小:
int a[] = {1,2,3,4,5}; // 默认数组大小为5[来源:第6章 p10,初始化]
内存模型
跳转到:内存模型-
数组在内存中连续存储:定义数组就是定义了一块连续的内存空间,空间大小 = 元素个数 × 每个元素所占空间大小 [来源:第6章 p12,数组存在于内存中]
-
示例:
char string[4]占用4个连续字节(地址如99, 100, 101, 102)short scores[3]占用3×sizeof(short)个连续字节(sizeof(short)至少为2,常见为2) [来源:第6章 p12,数组存在于内存中]
易错点与辨析
跳转到:易错点与辨析-
⚠️ 数组下标越界(越界访问/缓冲区溢出):
- 对内置数组(原生数组),C/C++语言层面不做运行时下标越界检查
- 如定义
int array[10],合法下标范围是0至9;访问array[10]属于未定义行为(UB),可能“看起来没事”,也可能崩溃或破坏数据 - 超界访问可能改写其他变量的数据,造成程序甚至系统崩溃
- 语言本身不提供自动越界检查;编译器/IDE 有时能对“明显的常量越界”给出告警,但一般无法保证发现所有越界。可借助静态分析、AddressSanitizer(ASan)/UBSan 等工具辅助排查 [来源:第6章 p13,数组下标超界问题]
-
⚠️ 更常见的越界来自输入/拷贝写入(如
cin >> char数组、strcpy/strcat等),可能导致缓冲区溢出;编译器通常不会在运行时阻止 [来源:第6章 p13,Notes]
6.2 查找和排序
跳转到:6.2 查找和排序算法1:顺序(线性)查找
跳转到:算法1:顺序(线性)查找-
适用条件:无需有序(任意序列都可用;对有序序列还可加入“提前停止”优化)
-
算法描述:从头到尾扫描序列,逐个比较每个元素的值是否与目标值相同;找到则返回序号并结束,否则直到序列结束返回-1表示未找到 [来源:第6章 p17,算法1:顺序(线性)查找]
-
代码模式:
int k, x;int array[] = {2, 3, 1, 7, 5, 8, 9, 0, 4, 6};cout << "请输入要查找的元素值:"; cin >> x;for (k = 0; k < 10; ++k)if (x == array[k]) {cout << k;break;}if (k == 10) cout << "-1";[来源:第6章 p17,顺序查找代码]
算法2:二分查找
跳转到:算法2:二分查找-
适用条件:序列已排序
-
算法描述:每次拿出序列中间的元素与目标值比较:
- 如果相同则找到
- 如果目标值大于中间元素,则取序列后一半重复
- 如果目标值小于中间元素,则取序列前一半重复
- 直到找到或没有可再分的序列为止 [来源:第6章 p18,算法2:二分查找]
-
代码模式:
int lh, rh, mid, x;int array[] = {0,1,2,3,4,5,6,7,8,9};cout << "请输入要查找的数据:"; cin >> x;lh = 0, rh = 9;while (lh <= rh) {mid = lh + (rh - lh) / 2; // 更稳妥:避免(lh+rh)在大规模下标时溢出if (x == array[mid]) {cout << x << "的位置是:" << mid << endl;break;}if (x < array[mid])rh = mid - 1;elselh = mid + 1;}if (lh > rh) cout << "没有找到" << endl;[来源:第6章 p19,二分查找代码]
-
⚠️ 易错点:二分查找中
mid+1和mid-1不能省略,否则可能陷入死循环- 例如:在
{0,1,2,3,4,6,7,8,9,10,11}中查找5会陷入死循环 [来源:第6章 p19,Notes]
- 例如:在
选择排序法
跳转到:选择排序法-
算法描述:
- 从未排序部分找到最小元素
- 与未排序部分的第一个元素交换
- 重复直到全部排序完成 [来源:第6章 p21,选择排序法]
-
代码模式:
int array[] = {2, 5, 1, 9, 10, 0, 4, 8, 7, 6};int lh, minh, k, tmp, n = 10;for (lh = 0; lh < n-1; lh++) {minh = lh;for (k = lh; k < n; ++k)if (array[k] < array[minh]) minh = k;tmp = array[lh];array[lh] = array[minh];array[minh] = tmp;}[来源:第6章 p22,选择排序代码]
-
循环结构:两重循环
- 外层循环:
lh从0到n-2(控制已排序边界) - 内层循环:
k从lh到n-1(寻找最小元素) [来源:第6章 p21-22]
- 外层循环:
冒泡排序法
跳转到:冒泡排序法-
算法描述:
- 对序列从头扫描,比较相邻两个元素
- 如果大的在前小的在后,就交换(冒泡过程)
- 一趟扫描后最大元素到最后位置
- 重复以上过程 [来源:第6章 p23,气泡排序法]
-
代码模式(含优化):
int a[] = {0, 3, 5, 1, 8, 7, 9, 4, 2, 10, 6};int i, j, tmp, n = 11;bool flag;for (i = n-1; i > 0; i--) { // i表示本趟比较的上界(未排序区间末尾下标),最大元素会“冒”到 i 位置flag = true;for (j = 0; j < i; ++j) // j遍历待冒泡序列if (a[j] > a[j+1]) {tmp = a[j];a[j] = a[j+1];a[j+1] = tmp;flag = false;}if (flag) break; // 一趟冒泡中没有发生交换,排序结束}[来源:第6章 p25,冒泡排序代码]
-
优化点:使用
flag标志,若一趟冒泡中没有发生交换,说明已排序完成,可提前结束 [来源:第6章 p25,Notes]
时间复杂度分析
跳转到:时间复杂度分析-
算法效率衡量标准:
- 时间复杂度:程序执行时间与问题规模之间的关系
- 空间复杂度:程序运行所占用的存储空间与问题规模之间的关系 [来源:第6章 p26,哪个算法更好]
-
线性搜索时间复杂度:
- 最好情况:1步(第一个元素就找到)
- 最坏情况:N步
- 平均情况:(N+1)/2次
- 最坏时间复杂度:O(N) [来源:第6章 p27,时间复杂度分析]
-
二分搜索时间复杂度:
- 最好情况:1步(中间位置找到)
- 最坏情况:log₂N步(不断二分直到不可再分)
- 平均情况:log₂N级别
- 最坏时间复杂度:O(log₂N) [来源:第6章 p27,时间复杂度分析]
-
效率对比实例:搜索10⁶个元素的列表,二分搜索只需20步,而线性搜索需要10⁶步 [来源:第6章 p28,不同时间复杂度算法的效率]
-
为什么是log₂N:每次二分至少筛选掉剩余元素的一半,经历log₂N次二分就能找到答案(N/2^m,所以 m = log₂N) [来源:第6章 p27,Notes]
6.3 多维数组
跳转到:6.3 多维数组定义/语法
跳转到:定义/语法- 多维数组定义:数组的每一个元素又是数组的数组
- 二维数组定义格式:
示例:类型 数组名[常量表达式1][常量表达式2];[来源:第6章 p38,多维数组]int a[4][5];// 可以理解为定义了一维数组a[4],每个元素又是一维数组int[5]// a[0], a[1]等就是数组名,其元素就是a[0][0], a[0][1], a[1][0]等
内存排列(重要)
跳转到:内存排列(重要)-
二维数组在内存中按一维连续存放,采用**行优先(后下标先变)**的方式:
a[0][0], a[0][1], a[0][2], a[0][3], a[0][4],a[1][0], a[1][1], ...[来源:第6章 p39,二维数组的表示]
-
规则:最后一位下标先改变,进行一轮之后,倒数第二个下标加一,依次轮替 [来源:第6章 p39,Notes]
初始化
跳转到:初始化-
按一维形式初始化(按存储顺序):
int a[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};[来源:第6章 p40,多维数组的初始化]
-
用花括号分行初始化:
int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};[来源:第6章 p40,多维数组的初始化]
-
部分初始化(其他元素默认为0):
int a[3][4] = {1, 2, 3, 4, 5}; // 按一维顺序,后面补0int a[3][4] = {{1, 2}, {3, 4}, {5}}; // 每行部分初始化// 结果:第0行[1,2,0,0],第1行[3,4,0,0],第2行[5,0,0,0][来源:第6章 p40,多维数组的初始化]
代码模式:矩阵乘法
跳转到:代码模式:矩阵乘法- 矩阵乘法核心代码:
[来源:第6章 p42,矩阵乘法代码]for (i = 0; i < NumOfRowA; ++i)for (j = 0; j < NumOfColB; ++j) {c[i][j] = 0;for (k = 0; k < NumOfColA; ++k)c[i][j] += a[i][k] * b[k][j];}
6.4 字符串
跳转到:6.4 字符串定义/语法
跳转到:定义/语法-
字符串:由一系列字符组成的一维数组
-
字符串常量:用一对双引号括起来,如
"Hello!" -
字符串变量:用字符类型的数组表示和存储 [来源:第6章 p44,字符串]
-
字符串初始化的三种常见方式(C 风格字符串):
char ch1[] = "Hello!"; // 编译器自动补 '\0',数组长度为 7char ch2[7] = "Hello!"; // 显式给出长度(需包含 '\0')char ch3[] = {'H','e','l','l','o','!','\0'}; // 字符列表(需手动写 '\0')[来源:第6章 p44,字符串]
-
空字符串:不包含任何字符的字符串
"",占用1个字节(存储'\0') [来源:第6章 p44,字符串]
规则/规范
跳转到:规则/规范-
字符串以
'\0'结尾:字符串除了存放指定数据,还要默认存一位空字符'\0',所以尺寸是元素个数+1 [来源:第6章 p44,字符串] -
⚠️ 注意
'a'和"a"的区别:'a'是一个单字符,占1个字节"a"是一个字符串,占2个字节('a' + '\0') [来源:第6章 p44,字符串]
字符串的输入输出
跳转到:字符串的输入输出-
用
cin >>输入:cin >> ch;- ⚠️ 无法输入包含空白字符的字符串(空格会被当作输入结束)
- ⚠️ cin不检查输入是否越界 [来源:第6章 p45,字符串的输入输出]
-
用
cin.getline()输入任意字符串:cin.getline(ch, 数组长度, 结束标记);// 输入到达"数组长度-1"或遇到标记字符时结束// 结束标记可以缺省,此时回车是默认结束标记[来源:第6章 p45,字符串的输入输出]
-
输出字符串:
cout << ch; // 可以直接用数组名输出,一般数组不行[来源:第6章 p45,字符串的输入输出]
字符串处理函数(cstring库)
跳转到:字符串处理函数(cstring库)| 函数 | 功能 |
|---|---|
char *strcpy(dst, src) | 拷贝字符串src到dst,包括'\0',返回dst |
char *strncpy(dst, src, n) | 至多从src拷贝n个字符到dst;若src长度≥n则不会自动补'\0';src不足n时其余补0 |
char *strcat(dst, src) | 将src接到dst尾,返回dst |
char *strncat(dst, src, n) | 从src至多取n个字符接到dst尾,并补'\0' |
size_t strlen(s) | 返回s的长度(不含'\0') |
int strcmp(s1, s2) | 比较s1和s2:s1>s2返回正数,相等返回0,s1<s2返回负数 |
int strncmp(s1, s2, n) | 如strcmp,但至多比较n个字符 |
char *strchr(s, ch) | 返回s中第一次出现ch的地址,不存在返回NULL |
char *strrchr(s, ch) | 返回s中最后出现ch的地址,不存在返回NULL |
char *strstr(s1, s2) | 返回s1中第一次出现s2的地址,不存在返回NULL |
[来源:第6章 p46,字符串处理函数]
-
头文件:
#include <cstring>[来源:第6章 p46,字符串处理函数] -
⚠️ 注意:
strlen()和sizeof()不同:strlen()返回字符串长度(不含'\0')sizeof()返回数组占用的字节数 [来源:第6章 p46,Notes]
6.5 例题结论
跳转到:6.5 例题结论【执行结果分析】字母频率统计
跳转到:【执行结果分析】字母频率统计- 问题:输入一串小写字母,统计每个字母出现的次数
- 关键技巧:利用字符运算
ch - 'a'将字母映射到数组下标0-25 - 代码要点:
[来源:第6章 p14-15,字母频率统计]int count[26] = {0};char ch;ch = cin.get();while (ch >= 'a' && ch <= 'z') {++count[ch - 'a'];ch = cin.get();}
【执行结果分析】单词统计
跳转到:【执行结果分析】单词统计- 问题:输入一行文字,统计有多少个单词(单词之间以空格分开)
- 算法:当发现当前字符为非空格而前一字符是空格时,找到一个新单词
- 代码要点:
[来源:第6章 p47-48,单词统计]char sentence[80], prev = ' ';int i, num = 0;cin.getline(sentence, 80);for (i = 0; sentence[i] != '\0'; ++i) {if (prev == ' ' && sentence[i] != ' ') ++num;prev = sentence[i];}
例题思路提示
跳转到:例题思路提示-
埃拉托斯特尼筛法求素数:要得到n以内的全部素数,必须把不大于√n的所有素数的倍数剔除
- 原理:任何非质数都会有一个因数不大于它的算术平方根(因为是一大一小配对相乘) [来源:第6章 p50,例题Notes]
-
ISBN校验:
- 旧版(10位 ISBN-10):设数字为 d1..d10(d10 可能为 X 表示10),满足 (10·d1 + 9·d2 + ... + 1·d10) mod 11 = 0
- 新版(13位 ISBN-13):前12位按权 1,3,1,3,... 加权求和为 S,校验位 d13 = (10 - (S mod 10)) mod 10(等价于 (S + d13) mod 10 = 0) [来源:第6章 p51,例题]
扩展知识(了解即可)
跳转到:扩展知识(了解即可)P vs NP问题
跳转到:P vs NP问题- 多项式时间:存在常数k,使得算法最坏运行时间在O(n^k)以内
- P问题:能在多项式时间内被解决的决策问题
- NP问题:能在多项式时间内被验证的决策问题
- P ⊆ NP,但 P=NP? 是未解决的千禧年难题之一 [来源:第6章 p32,P vs NP问题]
深度学习与NP问题
跳转到:深度学习与NP问题- 深度学习为NP难问题提供实用的启发式或近似解法
- 优势:推理速度快、泛化能力强、能处理高维输入
- 局限:无法解决P vs NP理论问题、非精确解法、训练成本高 [来源:第6章 p35-36,深度学习和NP问题]