1. 数组基础概念与内存模型
在C/C++算法竞赛中,数组是最基础且最重要的数据结构之一。本质上,数组是在连续内存空间中存储的相同类型元素的集合。这种连续存储特性带来了两个关键优势:一是可以通过下标直接计算出元素的内存地址(O(1)时间复杂度访问),二是对缓存机制非常友好(空间局部性原理)。
内存布局示例:
c复制int arr[5] = {10, 20, 30, 40, 50};
在32位系统中,每个int占4字节。假设数组起始地址为0x1000,则arr[2]的地址精确计算为:
0x1000 + 2*4 = 0x1008
重要提示:数组越界访问是竞赛中最常见的错误之一。C/C++不会自动检查数组边界,越界可能导致程序崩溃或更隐蔽的逻辑错误。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数组的声明与初始化技巧
2.1 静态数组声明方式
cpp复制// 完全初始化
int days[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
// 部分初始化(剩余元素自动置0)
int scores[100] = {0};
// 省略维度(编译器自动计算)
char vowels[] = {'a','e','i','o','u'};
2.2 动态数组创建方法
竞赛中更常见的动态数组实现:
cpp复制// C风格
int* arr = (int*)malloc(100 * sizeof(int));
free(arr);
// C++风格
int* arr = new int[100];
delete[] arr;
// 竞赛常用技巧:预先分配大数组
const int MAXN = 1e6 + 5;
int global_arr[MAXN]; // 避免频繁申请释放
3. 多维数组的特殊处理
3.1 二维数组的内存本质
二维数组实际上是"数组的数组",在内存中仍然线性存储。例如:
c复制int matrix[3][4] = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
访问matrix[i][j]的地址计算公式:
基地址 + i * (列数 * 元素大小) + j * 元素大小
3.2 动态二维数组创建
竞赛中处理动态二维数组的几种方式:
- 指针数组方式:
cpp复制int** mat = new int*[rows];
for(int i=0; i<rows; ++i)
mat[i] = new int[cols];
- 一维数组模拟:
cpp复制int* mat = new int[rows * cols];
// 访问mat[i][j] 等价于 mat[i*cols + j]
- vector容器方式(推荐):
cpp复制vector<vector<int>> mat(rows, vector<int>(cols));
4. 数组常用算法模板
4.1 二分查找实现
cpp复制int binarySearch(int arr[], int n, int target) {
int left = 0, right = n - 1;
while(left <= right) {
int mid = left + (right - left)/2;
if(arr[mid] == target) return mid;
if(arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
4.2 快速排序实现
cpp复制void quickSort(int arr[], int l, int r) {
if(l >= r) return;
int i = l, j = r, pivot = arr[(l+r)/2];
while(i <= j) {
while(arr[i] < pivot) i++;
while(arr[j] > pivot) j--;
if(i <= j) swap(arr[i++], arr[j--]);
}
quickSort(arr, l, j);
quickSort(arr, i, r);
}
4.3 前缀和技巧
cpp复制int prefix[100010];
void buildPrefix(int arr[], int n) {
prefix[0] = arr[0];
for(int i=1; i<n; ++i)
prefix[i] = prefix[i-1] + arr[i];
}
// 查询区间和:sum[l..r] = prefix[r] - (l>0 ? prefix[l-1] : 0)
5. 竞赛中的高频数组问题
5.1 滑动窗口问题
典型例题:求长度为k的连续子数组的最大和
cpp复制int maxSubarray(int arr[], int n, int k) {
int max_sum = 0, window_sum = 0;
for(int i=0; i<k; ++i)
window_sum += arr[i];
max_sum = window_sum;
for(int i=k; i<n; ++i) {
window_sum += arr[i] - arr[i-k];
max_sum = max(max_sum, window_sum);
}
return max_sum;
}
5.2 双指针技巧
典型例题:移除有序数组中的重复元素
cpp复制int removeDuplicates(int arr[], int n) {
if(n == 0) return 0;
int slow = 0;
for(int fast=1; fast<n; ++fast) {
if(arr[fast] != arr[slow])
arr[++slow] = arr[fast];
}
return slow + 1;
}
5.3 荷兰国旗问题
三向切分快速排序的基础:
cpp复制void dutchFlag(int arr[], int n, int mid) {
int i=0, j=0, k=n-1;
while(j <= k) {
if(arr[j] < mid) swap(arr[i++], arr[j++]);
else if(arr[j] > mid) swap(arr[j], arr[k--]);
else j++;
}
}
6. 性能优化与调试技巧
6.1 缓存友好访问模式
cpp复制// 不好的写法:列优先访问(C/C++是行优先存储)
for(int j=0; j<cols; ++j)
for(int i=0; i<rows; ++i)
sum += matrix[i][j];
// 好的写法:行优先访问
for(int i=0; i<rows; ++i)
for(int j=0; j<cols; ++j)
sum += matrix[i][j];
6.2 避免缓存抖动
当处理超大数组时:
cpp复制// 原始版本(可能触发频繁缓存失效)
for(int i=0; i<1e6; ++i)
arr[i] = process(arr[i]);
// 优化版本:分块处理
const int BLOCK = 1024;
for(int b=0; b<1e6; b+=BLOCK) {
int end = min(b+BLOCK, (int)1e6);
for(int i=b; i<end; ++i)
arr[i] = process(arr[i]);
}
6.3 调试数组问题的技巧
- 打印数组内容时限制输出范围:
cpp复制void debugPrint(int arr[], int l, int r) {
for(int i=l; i<=min(r, l+20); ++i)
cout << arr[i] << " ";
if(r > l+20) cout << "...";
cout << endl;
}
- 使用assert检查不变量:
cpp复制assert(is_sorted(arr, arr+n)); // 需要#include <cassert>
7. STL中的数组相关工具
7.1 array容器(C++11)
cpp复制#include <array>
array<int, 5> arr = {1,2,3,4,5};
// 优点:固定大小、边界检查(at()方法)、可直接赋值
7.2 vector的动态数组
cpp复制vector<int> vec(100); // 初始化100个0
vec.reserve(1000); // 预分配空间
vec.push_back(42); // 自动扩容
7.3 实用算法函数
cpp复制// 排序
sort(arr, arr+n);
// 反转
reverse(arr, arr+n);
// 填充
fill(arr, arr+n, 0);
// 查找极值
auto it = max_element(arr, arr+n);
8. 数组与指针的深入理解
8.1 数组名的特殊性质
cpp复制int arr[5];
cout << sizeof(arr); // 输出20(假设int为4字节)
int* ptr = arr;
cout << sizeof(ptr); // 输出指针大小(通常4或8字节)
8.2 数组退化为指针
函数参数中的数组声明:
cpp复制void func(int arr[]) { // 实际等价于 int* arr
// sizeof(arr) 将是指针大小
}
8.3 指针算术运算
cpp复制int arr[5] = {10,20,30,40,50};
int* p = arr + 2; // 指向arr[2]
cout << p[-1]; // 输出20(等价于*(p-1))
9. 实战案例分析
9.1 力扣经典题:两数之和
cpp复制vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> seen;
for(int i=0; i<nums.size(); ++i) {
int complement = target - nums[i];
if(seen.count(complement))
return {seen[complement], i};
seen[nums[i]] = i;
}
return {};
}
9.2 最大子数组和问题
Kadane算法实现:
cpp复制int maxSubArray(vector<int>& nums) {
int max_sum = INT_MIN, current = 0;
for(int num : nums) {
current = max(num, current + num);
max_sum = max(max_sum, current);
}
return max_sum;
}
9.3 合并有序数组
cpp复制void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int i = m-1, j = n-1, k = m+n-1;
while(j >= 0) {
nums1[k--] = (i >=0 && nums1[i] > nums2[j]) ? nums1[i--] : nums2[j--];
}
}
10. 进阶技巧与优化
10.1 位运算压缩数组
当数组元素取值有限时:
cpp复制// 用int的每一位表示bool数组
const int BIT = 32;
int compact[1000/BIT + 1];
void setBit(int pos) {
compact[pos/BIT] |= (1 << (pos%BIT));
}
bool getBit(int pos) {
return compact[pos/BIT] & (1 << (pos%BIT));
}
10.2 内存对齐优化
cpp复制// 保证数组起始地址是16字节对齐
alignas(16) int aligned_arr[1024];
10.3 SIMD指令优化
使用SSE指令处理数组:
cpp复制#include <emmintrin.h>
void simdAdd(float* a, float* b, float* c, int n) {
for(int i=0; i<n; i+=4) {
__m128 va = _mm_load_ps(a+i);
__m128 vb = _mm_load_ps(b+i);
__m128 vc = _mm_add_ps(va, vb);
_mm_store_ps(c+i, vc);
}
}
在实际竞赛中,数组操作的速度往往直接影响程序的整体性能。我曾在一个在线判题系统中,通过将二维数组访问模式从列优先改为行优先,使程序运行时间从TLE(超过时间限制)优化到了500ms以内。这种性能差异在算法竞赛中常常是决定胜负的关键。
