1. 数组基础概念与核心操作
数组是计算机科学中最基础且最重要的数据结构之一,几乎所有编程语言都原生支持数组类型。从内存角度看,数组是一块连续的内存空间,存储相同类型的元素集合。这种连续存储特性带来了两个关键特性:一是可以通过索引快速访问任意元素(时间复杂度O(1)),二是大小通常在创建时就固定(静态数组)。
在实际工程中,我们最常遇到的数组操作可以归纳为四大类:
- 遍历:按顺序访问每个元素
- 查找:在数组中定位特定元素
- 插入:在指定位置添加新元素
- 删除:移除指定位置的元素
新手常见误区:认为数组大小可以动态调整。实际上,大多数语言中的基础数组是固定大小的,动态扩容需要创建新数组并拷贝元素,这也是ArrayList等动态数组结构的核心实现原理。
2. 二分查找算法深度解析
二分查找是数组查找操作中最经典的算法,时间复杂度为O(log n),比线性查找的O(n)效率更高。但使用二分查找有个重要前提:数组必须是有序的(升序或降序)。
2.1 二分查找的两种实现方式
左闭右闭区间写法([left, right]):
python复制def binary_search(nums, target):
left, right = 0, len(nums) - 1
while left <= right: # 注意等号
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1 # 明确排除mid
else:
right = mid - 1 # 明确排除mid
return -1
左闭右开区间写法([left, right)):
python复制def binary_search(nums, target):
left, right = 0, len(nums)
while left < right: # 不加等号
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid # 保持右开
return -1
2.2 边界条件与易错点
- 循环条件:左闭右闭用
<=,左闭右开用< - 中间值计算:推荐
mid = left + (right - left) // 2写法,避免整数溢出 - 边界更新:明确排除已检查的mid位置
- 返回值:未找到时的处理要统一
实战经验:在ACM竞赛中,二分查找的变种问题(如查找第一个大于等于target的元素)出现频率极高。建议掌握查找左边界和右边界的模板代码。
3. 双指针技巧在数组操作中的应用
双指针是处理数组问题的高效技巧,特别适合解决元素移除、去重、合并等问题。其核心思想是使用两个指针协同遍历数组,减少不必要的操作。
3.1 移除指定元素
LeetCode经典问题:原地移除数组中所有等于val的元素,返回新长度。双指针解法:
python复制def removeElement(nums, val):
slow = 0
for fast in range(len(nums)):
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
return slow
这个解法的时间复杂度是O(n),空间复杂度O(1),因为只使用了常数个额外空间。slow指针指向下一个待填充位置,fast指针寻找非val元素。
3.2 有序数组去重
对于已排序数组,使用双指针可以高效去重:
python复制def removeDuplicates(nums):
if not nums:
return 0
slow = 1
for fast in range(1, len(nums)):
if nums[fast] != nums[fast-1]:
nums[slow] = nums[fast]
slow += 1
return slow
3.3 双指针的进阶应用
- 滑动窗口:解决子数组/子串问题
- 快慢指针:检测循环、找中点等
- 对撞指针:两数之和、反转数组等
性能对比:在处理大规模数据时,双指针算法通常比暴力解法快100倍以上。例如在10^6量级的数组中移除元素,双指针只需几毫秒,而暴力解法可能需要数秒。
4. 数组排序与平方问题
4.1 有序数组的平方
给定非递减整数数组(可能含负数),返回每个元素平方后的新数组,要求也按非递减排序。直接平方后排序的解法时间复杂度是O(nlogn),使用双指针可以达到O(n):
python复制def sortedSquares(nums):
n = len(nums)
result = [0] * n
left, right = 0, n - 1
pos = n - 1
while left <= right:
if abs(nums[left]) > abs(nums[right]):
result[pos] = nums[left] ** 2
left += 1
else:
result[pos] = nums[right] ** 2
right -= 1
pos -= 1
return result
这个解法利用了原数组已排序的特性,比较两端的绝对值大小,将较大的平方值从后往前填入结果数组。
4.2 常见排序算法对比
| 算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|
| 冒泡排序 | O(n^2) | O(1) | 稳定 | 小规模数据 |
| 选择排序 | O(n^2) | O(1) | 不稳定 | 交换成本高的场景 |
| 插入排序 | O(n^2) | O(1) | 稳定 | 基本有序的小数据 |
| 快速排序 | O(nlogn) | O(logn) | 不稳定 | 通用排序 |
| 归并排序 | O(nlogn) | O(n) | 稳定 | 链表排序、外部排序 |
| 堆排序 | O(nlogn) | O(1) | 不稳定 | 需要原地排序 |
| 计数排序 | O(n+k) | O(k) | 稳定 | 整数小范围数据 |
| 基数排序 | O(d(n+k)) | O(n+k) | 稳定 | 多关键字排序 |
5. 数组的进阶应用与性能优化
5.1 树状数组(Fenwick Tree)
树状数组是一种高效处理前缀和查询与单点更新的数据结构,时间复杂度均为O(logn)。典型应用场景包括:
- 动态计算前缀和
- 解决逆序对问题
- 处理区间更新、单点查询问题
基础实现:
python复制class FenwickTree:
def __init__(self, size):
self.n = size
self.tree = [0] * (self.n + 1)
def update(self, index, delta):
while index <= self.n:
self.tree[index] += delta
index += index & -index
def query(self, index):
res = 0
while index > 0:
res += self.tree[index]
index -= index & -index
return res
5.2 动态数组的实现原理
以Python的list为例,动态数组的扩容策略通常是:
- 初始分配较小容量(如4个元素)
- 当空间不足时,按当前大小的倍数扩容(通常是2倍)
- 扩容时需要分配新内存并拷贝所有元素
这种策略使得动态数组的均摊时间复杂度为O(1)。关键代码模拟:
python复制class DynamicArray:
def __init__(self):
self._capacity = 4
self._size = 0
self._data = [None] * self._capacity
def append(self, value):
if self._size == self._capacity:
self._resize(2 * self._capacity)
self._data[self._size] = value
self._size += 1
def _resize(self, new_capacity):
new_data = [None] * new_capacity
for i in range(self._size):
new_data[i] = self._data[i]
self._data = new_data
self._capacity = new_capacity
5.3 多维数组的内存布局
以C语言中的二维数组为例,内存是按行优先顺序连续存储的。对于int arr[3][4]:
- 总大小 = 3行 × 4列 × sizeof(int)
- 元素arr[i][j]的地址 = 基地址 + (i×4 + j)×sizeof(int)
这种布局对缓存友好,因为访问相邻行元素时,缓存命中率高。但在处理列优先访问时性能会下降。
6. 数组问题的调试技巧与性能分析
6.1 常见错误排查
- 数组越界:总是检查索引是否在[0, len-1]范围内
- 空指针:处理数组参数前检查是否为None/null
- 边界条件:特别注意空数组、单元素数组等特殊情况
- 值传递与引用传递:某些语言中数组是引用类型,函数内修改会影响原数组
6.2 性能分析工具
Python示例(使用cProfile):
python复制import cProfile
import random
def test_performance():
arr = [random.randint(0, 1000) for _ in range(100000)]
# 测试排序性能
arr.sort()
cProfile.run('test_performance()')
C++示例(使用Google Benchmark):
cpp复制#include <benchmark/benchmark.h>
#include <vector>
#include <algorithm>
static void BM_ArraySort(benchmark::State& state) {
std::vector<int> v(state.range(0));
for (auto _ : state) {
state.PauseTiming();
std::generate(v.begin(), v.end(), []{ return rand() % 1000; });
state.ResumeTiming();
std::sort(v.begin(), v.end());
}
}
BENCHMARK(BM_ArraySort)->Range(8, 8<<10);
BENCHMARK_MAIN();
6.3 算法优化实例
问题:给定数组,找出所有和为target的连续子数组。
暴力解法(O(n^2)):
python复制def findSubarrays(nums, target):
res = []
for i in range(len(nums)):
current_sum = 0
for j in range(i, len(nums)):
current_sum += nums[j]
if current_sum == target:
res.append(nums[i:j+1])
return res
优化解法(使用前缀和+哈希表,O(n)):
python复制def findSubarrays(nums, target):
res = []
prefix_sum = {0: [-1]}
current_sum = 0
for i in range(len(nums)):
current_sum += nums[i]
if (current_sum - target) in prefix_sum:
for start in prefix_sum[current_sum - target]:
res.append(nums[start+1:i+1])
if current_sum not in prefix_sum:
prefix_sum[current_sum] = []
prefix_sum[current_sum].append(i)
return res
