文章目录
- 普通数组理论基础
-
- [1. 数组的基本概念](#1. 数组的基本概念)
-
- [1.1 基本术语](#1.1 基本术语)
- [1.2 数组的特点](#1.2 数组的特点)
- [2. 数组的基本操作](#2. 数组的基本操作)
-
- [2.1 数组的遍历](#2.1 数组的遍历)
- [2.2 数组的修改](#2.2 数组的修改)
- [3. 前缀和(Prefix Sum)](#3. 前缀和(Prefix Sum))
-
- [3.1 前缀和的基本概念](#3.1 前缀和的基本概念)
- [3.2 前缀和的构造](#3.2 前缀和的构造)
- [3.3 区间和查询](#3.3 区间和查询)
- [4. 矩阵(二维数组)操作](#4. 矩阵(二维数组)操作)
-
- [4.1 矩阵的基本概念](#4.1 矩阵的基本概念)
- [4.2 矩阵的遍历](#4.2 矩阵的遍历)
- [4.3 螺旋矩阵](#4.3 螺旋矩阵)
- [4.4 矩阵的统计操作](#4.4 矩阵的统计操作)
- [5. 数组操作的时间复杂度](#5. 数组操作的时间复杂度)
- [6. 何时使用数组技巧](#6. 何时使用数组技巧)
-
- [6.1 使用场景](#6.1 使用场景)
- [6.2 判断标准](#6.2 判断标准)
- [7. 数组操作的优缺点](#7. 数组操作的优缺点)
-
- [7.1 优点](#7.1 优点)
- [7.2 缺点](#7.2 缺点)
- [8. 常见题型总结](#8. 常见题型总结)
-
- [8.1 前缀和类](#8.1 前缀和类)
- [8.2 矩阵类](#8.2 矩阵类)
- [8.3 数组基本操作类](#8.3 数组基本操作类)
- [9. 总结](#9. 总结)
普通数组理论基础
1. 数组的基本概念
**数组(Array)**是一种线性数据结构,由相同类型的元素按一定顺序排列而成,通过索引访问元素。
1.1 基本术语
- 索引(Index):数组中元素的位置,通常从0开始
- 元素(Element):数组中存储的数据
- 长度(Length):数组中元素的数量
- 一维数组:线性排列的数组
- 二维数组(矩阵):按行和列排列的数组
1.2 数组的特点
- 连续内存:数组元素在内存中连续存储
- 随机访问:通过索引可以在O(1)时间内访问任意元素
- 固定大小:数组大小在创建时确定(C++中vector是动态数组)
- 元素覆盖:数组元素不能直接删除,只能覆盖
示例:
一维数组:[1, 2, 3, 4, 5]
索引: 0 1 2 3 4
二维数组(矩阵):
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
行索引:0, 1, 2
列索引:0, 1, 2
2. 数组的基本操作
2.1 数组的遍历
一维数组遍历:
cpp
// 方式1:索引遍历
for(int i = 0; i < nums.size(); i++) {
// 访问 nums[i]
}
// 方式2:范围遍历
for(int num : nums) {
// 访问 num
}
二维数组遍历:
cpp
// 遍历二维数组
for(int i = 0; i < matrix.size(); i++) {
for(int j = 0; j < matrix[i].size(); j++) {
// 访问 matrix[i][j]
}
}
2.2 数组的修改
重要特性:
- 数组元素不能直接删除,只能覆盖
- 删除操作需要将后续元素前移
示例:
cpp
// 删除索引为index的元素
void removeElement(int arr[], int &size, int index) {
if(index < 0 || index >= size) {
return; // 索引无效
}
// 将后续元素前移
for(int i = index; i < size - 1; i++) {
arr[i] = arr[i + 1];
}
size--; // 减少数组大小
}
3. 前缀和(Prefix Sum)
3.1 前缀和的基本概念
前缀和是数组中前i个元素的和,用于快速计算区间和。
定义:
prefix[i] = arr[0] + arr[1] + ... + arr[i]- 区间
[a, b]的和 =prefix[b] - prefix[a-1](a > 0) - 区间
[0, b]的和 =prefix[b]
示例:
原数组:[1, 2, 3, 4, 5]
前缀和:[1, 3, 6, 10, 15]
计算区间[1, 3]的和:
prefix[3] - prefix[0] = 10 - 1 = 9
验证:2 + 3 + 4 = 9 ✓
3.2 前缀和的构造
核心思路:
- 遍历数组,累加元素
- 将累加结果存入前缀和数组
模板代码:
cpp
// 构造前缀和数组
vector<int> prefix(n);
int presum = 0;
for(int i = 0; i < n; i++) {
presum += arr[i];
prefix[i] = presum;
}
3.3 区间和查询
适用场景:多次查询数组的区间和
核心思路:
- 使用前缀和数组快速计算区间和
- 时间复杂度:O(1)(查询),O(n)(预处理)
模板代码:
cpp
// LeetCode 58. 区间和
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, a, b;
cin >> n;
vector<int> vec(n); // 输入数组
vector<int> p(n); // 前缀和数组
int presum = 0;
// 构造前缀和数组
for(int i = 0; i < n; i++) {
scanf("%d", &vec[i]);
presum += vec[i];
p[i] = presum;
}
// 查询区间和
while(~scanf("%d%d", &a, &b)) {
int sum;
if(a == 0) {
sum = p[b]; // 区间[0, b]的和
} else {
sum = p[b] - p[a - 1]; // 区间[a, b]的和
}
printf("%d\n", sum);
}
return 0;
}
关键点:
- 前缀和数组:
p[i]表示前i+1个元素的和 - 区间和计算:
[a, b]的和 =p[b] - p[a-1] - 边界处理:当a=0时,直接使用
p[b] - 时间复杂度:预处理O(n),查询O(1)
4. 矩阵(二维数组)操作
4.1 矩阵的基本概念
**矩阵(Matrix)**是二维数组,由行和列组成。
基本操作:
- 访问元素:
matrix[i][j](第i行第j列) - 行数:
matrix.size() - 列数:
matrix[0].size()
4.2 矩阵的遍历
按行遍历:
cpp
for(int i = 0; i < matrix.size(); i++) {
for(int j = 0; j < matrix[i].size(); j++) {
// 访问 matrix[i][j]
}
}
按列遍历:
cpp
for(int j = 0; j < matrix[0].size(); j++) {
for(int i = 0; i < matrix.size(); i++) {
// 访问 matrix[i][j]
}
}
4.3 螺旋矩阵
适用场景:按螺旋顺序遍历或生成矩阵
核心思路:
- 使用循环不变量:每次操作的方法一致(左闭右开)
- 按圈遍历:从外圈到内圈
- 处理边界:单行或单列的特殊情况
模板1:螺旋矩阵(遍历)
适用场景:按螺旋顺序遍历矩阵并输出元素
模板代码:
cpp
// LeetCode 54. 螺旋矩阵
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> ans;
int m = matrix.size();
int n = matrix[0].size();
int startx = 0, starty = 0; // 起始位置
int offset = 1; // 每圈的偏移量
int loop = min(m, n) / 2; // 循环圈数
// 处理单行或单列
if(m == 1) {
return matrix[0];
}
if(n == 1) {
for(int k = 0; k < m; k++) {
ans.push_back(matrix[k][0]);
}
return ans;
}
// 按圈遍历(左闭右开)
while(loop--) {
int i = startx, j = starty;
// 第一行:从左到右
for(; j < n - offset; j++) {
ans.push_back(matrix[i][j]);
}
// 最后一列:从上到下
for(; i < m - offset; i++) {
ans.push_back(matrix[i][j]);
}
// 最后一行:从右到左
for(; j > starty; j--) {
ans.push_back(matrix[i][j]);
}
// 第一列:从下到上
for(; i > startx; i--) {
ans.push_back(matrix[i][j]);
}
startx++;
starty++;
offset++;
}
// 处理剩余的单行或单列
if(min(m, n) % 2 == 1) {
if(m <= n) {
// 剩余单行
for(int jj = starty; jj < n - offset + 1; jj++) {
ans.push_back(matrix[startx][jj]);
}
} else {
// 剩余单列
for(int ii = startx; ii < m - offset + 1; ii++) {
ans.push_back(matrix[ii][starty]);
}
}
}
return ans;
}
};
关键点:
- 循环不变量:左闭右开
- 四个方向:右→下→左→上
- 边界处理:单行或单列的特殊情况
- 时间复杂度:O(m × n),空间复杂度:O(1)
模板2:螺旋矩阵II(生成)
适用场景:生成一个n×n的螺旋矩阵
模板代码:
cpp
// LeetCode 59. 螺旋矩阵II
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> vec(n, vector<int>(n));
int startx = 0, starty = 0; // 起始位置
int offset = 1; // 每圈的偏移量
int count = 1; // 填充的数字
int cir = n / 2; // 循环圈数
int mid = n / 2; // 中间位置
// 按圈填充(左闭右开)
while(cir--) {
int i = startx, j = starty;
// 第一行:从左到右
for(; j < n - offset; j++) {
vec[i][j] = count++;
}
// 最后一列:从上到下
for(; i < n - offset; i++) {
vec[i][j] = count++;
}
// 最后一行:从右到左
for(; j > startx; j--) {
vec[i][j] = count++;
}
// 第一列:从下到上
for(; i > starty; i--) {
vec[i][j] = count++;
}
startx++;
starty++;
offset++;
}
// 处理中间元素(n为奇数时)
if(n % 2) {
vec[mid][mid] = count;
}
return vec;
}
};
关键点:
- 循环不变量:左闭右开
- 四个方向填充:右→下→左→上
- 中间元素:n为奇数时需要单独处理
- 时间复杂度:O(n²),空间复杂度:O(n²)
4.4 矩阵的统计操作
适用场景:统计矩阵的行和、列和等
模板代码:
cpp
// 统计矩阵的行和与列和
vector<int> horizontal(n, 0); // 每行的和
vector<int> vertical(m, 0); // 每列的和
// 统计行和
for(int i = 0; i < n; i++) {
for(int j = 0; j < m; j++) {
horizontal[i] += matrix[i][j];
}
}
// 统计列和
for(int j = 0; j < m; j++) {
for(int i = 0; i < n; i++) {
vertical[j] += matrix[i][j];
}
}
5. 数组操作的时间复杂度
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 访问元素 | O(1) | O(1) | 通过索引直接访问 |
| 遍历数组 | O(n) | O(1) | 需要访问所有元素 |
| 查找元素 | O(n) | O(1) | 线性查找 |
| 插入元素 | O(n) | O(1) | 需要移动后续元素 |
| 删除元素 | O(n) | O(1) | 需要移动后续元素 |
| 前缀和构造 | O(n) | O(n) | 需要额外空间存储前缀和 |
| 区间和查询 | O(1) | O(1) | 使用前缀和数组 |
| 矩阵遍历 | O(m × n) | O(1) | m为行数,n为列数 |
| 螺旋矩阵 | O(m × n) | O(1) | 需要遍历所有元素 |
注意:
- 数组的随机访问是O(1)
- 插入和删除操作需要移动元素,时间复杂度为O(n)
- 前缀和可以优化区间和查询到O(1)
6. 何时使用数组技巧
6.1 使用场景
-
区间和问题
- 多次查询数组的区间和
- 使用前缀和优化查询效率
-
矩阵遍历问题
- 螺旋矩阵遍历
- 矩阵的按行、按列遍历
- 矩阵的统计操作
-
数组基本操作
- 数组的遍历
- 数组元素的修改
- 数组的查找
-
二维数组问题
- 矩阵的生成
- 矩阵的旋转、转置
- 矩阵的统计和计算
6.2 判断标准
当遇到以下情况时,考虑使用数组技巧:
- 需要多次查询区间和 → 使用前缀和
- 需要按特殊顺序遍历矩阵 → 使用螺旋矩阵技巧
- 需要统计矩阵的行和、列和 → 使用矩阵统计
- 需要快速访问元素 → 利用数组的随机访问特性
示例:
cpp
// 问题:多次查询数组的区间和
// 暴力解法:每次查询O(n)
for(int i = a; i <= b; i++) {
sum += arr[i];
}
// 前缀和解法:预处理O(n),查询O(1)
vector<int> prefix(n);
// 构造前缀和数组
for(int i = 0; i < n; i++) {
prefix[i] = (i == 0 ? arr[i] : prefix[i-1] + arr[i]);
}
// 查询区间和
int sum = prefix[b] - (a > 0 ? prefix[a-1] : 0);
7. 数组操作的优缺点
7.1 优点
- 随机访问:通过索引可以在O(1)时间内访问任意元素
- 内存连续:数组元素在内存中连续存储,访问效率高
- 实现简单:数组操作逻辑清晰,易于实现
- 前缀和优化:可以将区间和查询优化到O(1)
7.2 缺点
- 固定大小:数组大小在创建时确定(C++中vector是动态数组)
- 插入删除慢:插入和删除操作需要移动元素,时间复杂度为O(n)
- 空间限制:需要连续的内存空间
- 无法直接删除:数组元素不能直接删除,只能覆盖
8. 常见题型总结
8.1 前缀和类
-
区间和查询
- 使用前缀和数组快速计算区间和
- 时间复杂度:预处理O(n),查询O(1)
-
子数组和问题
- 结合前缀和和哈希表
- 可以解决子数组和等于k的问题
8.2 矩阵类
-
螺旋矩阵
- 按螺旋顺序遍历或生成矩阵
- 使用循环不变量(左闭右开)
-
矩阵统计
- 统计矩阵的行和、列和
- 矩阵的旋转、转置
-
矩阵遍历
- 按行遍历、按列遍历
- 对角线遍历等特殊遍历方式
8.3 数组基本操作类
-
数组遍历
- 一维数组遍历
- 二维数组遍历
-
数组修改
- 元素的覆盖
- 元素的移动
9. 总结
普通数组是基础的数据结构,掌握数组的基本操作和常用技巧对于解决算法问题至关重要。
核心要点:
- 数组特性:连续内存、随机访问、元素覆盖
- 前缀和:用于优化区间和查询,预处理O(n),查询O(1)
- 矩阵操作:螺旋矩阵、矩阵统计、矩阵遍历
- 时间复杂度:访问O(1),遍历O(n),插入删除O(n)
- 空间复杂度:通常为O(1)或O(n)
使用建议:
- 区间和问题优先考虑前缀和
- 矩阵遍历问题考虑螺旋矩阵技巧
- 注意数组的边界条件
- 利用数组的随机访问特性
- 合理使用循环不变量
常见题型总结:
- 前缀和类:区间和查询、子数组和问题
- 矩阵类:螺旋矩阵、矩阵统计、矩阵遍历
- 数组基本操作:数组遍历、数组修改