1.前置知识与隐性知识
前置知识
数组是存放在连续内存空间上的相同类型数据的集合
因为要保证内存地址的连续性,删除和增添元素的时候会移动其他元素的地址,为了保持数组的不变性,很多语言并没有提供直接删除数组元素的方式
数组下标为什么从零开始 访问元素时需要知道元素对应的内存地址,数组指向的内存的地址为首元素的地址 即 array[0] 所以第n个元素是
array[n] = array[0] + size * n如果是从1开始计算,那么第n和元素是array[n] = array[0] + size * (n - 1)从1开始编号比从0开始编号每次获取内存地址都多了一次 减法运算,也就多了一次cpu指令的运行。这也是数组从0下标开始访问一个原因。
优点
可以通过下标更快的查找元素,遍历数组的时候非常快,cpu会有缓存,cpu读取缓存只能读取连续内存的内容,数组的连续性符合cpu缓存的指令原理
缺点
- 为了保证连续性,增删会产生大量的元素移动工作
- 要求内存连续,如果不存在的话在一些语言中会触发垃圾回收
- 数组扩容很多都是通过一个容量更大的数组来存放,只是对于使用者来说是透明的
- 数组可能会出现越界,导致内存地址异常
数组板块涉及到的算法思想
- 双指针
- 滑动窗口(双指针扩展)
- 区间划分
- 前缀和
隐性知识
(一)底层机制认知
-
内存连续性带来的缓存优化
- 行优先遍历效率高于列优先(空间局部性原理)
- 预分配空间策略减少内存碎片(动态数组倍增扩容)
-
双指针法的变体应用
- 快慢指针:标记有效区间(移除元素)
- 首尾指针:处理有序特征(有序数组平方)
- 滑动窗口:动态调整区间(最小子数组)
-
边界条件处理模式
- 循环不变量原则(二分查找区间定义)
- 整数溢出预防(mid=left+(right-left)/2)
(二)高阶技巧
-
前缀和与哈希表结合
- 快速计算子数组和(和为K的子数组)
- 二维前缀和预处理(矩阵区域极值问题)
-
时空权衡策略
- 暴力解法→双指针优化(时间复杂度从O(n²)→O(n))
- 空间换时间(多数元素问题使用哈希表计数)
-
问题转换技巧
- 求和转求差(两数之和问题)
- 极值问题转区间收敛(长度最小子数组)
(三)思维模式
-
模式迁移能力
- 数组双指针→链表操作(快慢指针找中点)
- 螺旋矩阵填充→图像处理路径规划
-
测试用例设计
- 全0数组测试边界(多数元素问题)
- 连续重复值验证算法鲁棒性(移除元素)
2.算法详解
704二分查找
解决二分查找有3种解法,左闭右开/左闭右闭/递归(待补充)
我的初次尝试解法
import java.util.ArrayList;
import java.util.List;
public class leetcode704 {
/**
* 没有一把过的原因 nums[mid] < target 写错成了 nums[mid] > target
* @param nums
* @param target
* @return
*/
public static int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int mid = (left + right) / 2;
if(nums[left] > target || nums[right] < target){
return -1;
}
while ( left<=right) {
if (nums[mid] == target) {
return mid;
} else if (nums[mid] > target) {
//遍历左侧
right = mid - 1;
mid = (left + right) / 2;
} else if (nums[mid] < target) {
//遍历右侧
left = mid + 1;
mid = (left + right) / 2;
}
}
return -1;
}
public static void main(String[] args) {
int[] nums = new int[]{-1,0,3,5,9,12};
List<Integer> integers = new ArrayList<>();
integers.add(-1);
integers.add(0);
integers.add(3);
integers.add(5);
integers.add(9);
integers.add(12);
int search = search(nums, 3);
System.out.println(search);
}
}
27移除元素
两种解法:暴力破解(待补充)/双指针法
双指针解法
public class leetcode27 {
public static int removeElement(int[] nums, int val) {
int slowIndex = 0;
//数组的元素如何移除,要是空的
for(int fastIndex = 0; fastIndex < nums.length; fastIndex++){
if(nums[fastIndex] != val){
nums[slowIndex] =nums[fastIndex];
slowIndex++;
}
}
return slowIndex;
}
public static void main(String[] args) {
int[] nums = new int[]{3,2,2,3};
int[] nums2 = new int[]{0,1,2,2,3,0,4,2};
int val = 2;
int i = removeElement(nums2, val);
System.out.println(i);
for (int i1 = 0; i1 < nums2.length; i1++) {
System.out.print(nums2[i1]+" ");
}
}
}
977有序数组的平方
题目描述
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。 请你设计时间复杂度为 O(n) 的算法解决本问题
示例 1:
输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100] 排序后,数组变为 [0,1,9,16,100]
直接排序法(比较偷懒)
public int[] sortedSquares(int[] nums) {
int[] ans = new int[nums.length];
for (int i = 0; i < nums.length; ++i) {
ans[i] = nums[i] * nums[i];
}
Arrays.sort(ans);
return ans;
}
双指针法
public class leetcode977 {
public static int[] sortedSquares(int[] nums) {
//用Math求平方 3的值
int[] nums_out = new int[nums.length];
int right = nums.length - 1;
int left = 0;
//用Math求平方 left的值
int i = left;
while (i<nums.length){
if (getPowInt(nums[left])> getPowInt(nums[right])) {
nums_out[nums.length - 1 - i] = getPowInt(nums[left]);
left++;
} else {
nums_out[nums.length - 1 - i] = getPowInt(nums[right]);
right--;
}
i++;
}
return nums_out;
}
public static int getPowInt(int value){
return (int)Math.pow(value,2);
}
public static void main(String[] args) {
int[] nums = new int[]{-4, -1, 0, 3, 10};
int[] ints = sortedSquares(nums);
for (int i = 0; i < ints.length; i++) {
System.out.println(ints[i]);
}
}
}
3.算法听想变用
听(反学习)
专家视角:看到内存布局与缓存机制的关系
新手误区:仅关注语法不思考物理存储
想(参考答案思维)
- 写代码的时候需要思考所有的已知条件是否使用到了,也就是最开始就有梳理好已知条件的个数,然后解题的时候对比自己使用到了哪里,如果没用到的也能解出,要么是方法不够优秀或者解法不对
- 数组一般是大小提前确定的,也就是左右边界固定,那么操作的时候就容易联想从后往前,左右开工的双指针
- 数组可以对应数学里的区间概念,因此解题时把数据结构翻译成对应的数学概念能更快找到解题线索(计算机编程与数学概念映射关系的梳理)
解题核心原则(AI生成)
-
条件穷举与验证
- 列出题目所有显性/隐性条件(数组特性、边界约束、数据范围等)
- 每写一行代码前确认:当前使用的条件是否覆盖所有必要信息
- 解法完成后验证:未使用的条件可能是优化提示或错误标志
例:旋转数组搜索题必须同时使用有序性和旋转点特性
-
数据结构映射
数组物理结构 → 数学区间概念 | 数组表示 | 数学区间 | 长度计算 | 有效索引 | |---------------|----------|------------|------------------| | `arr[0..n-1]` | [0, n-1] | n | 0 ≤ i ≤ n-1 | | 左闭右闭 | [L, R] | R-L+1 | L ≤ i ≤ R | | 左闭右开 | [L, R) | R-L | L ≤ i < R |
变(深层迁移)
模式提炼的四个层次(以数组为例)
-
物理结构模式
- 案例:数组内存连续性 → 迁移到缓存行预取机制
- 思考:当处理矩阵乘法时,如何通过分块访问(blocking)提升缓存命中率?
-
逻辑操作模式
- 案例:双指针快慢指针 → 迁移到链表环检测
- 思考:在解决"删除排序链表重复元素(82题)"时,如何调整快慢指针的移动策略?
-
数学抽象模式
- 案例:数组下标映射 → 迁移到哈希函数设计
- 思考:当处理"两数之和(1题)"时,如何将数学互补关系转化为哈希表键值对?
-
系统思维模式
- 案例:滑动窗口时间复杂度控制 → 迁移到实时流数据处理
- 思考:在解决"最长无重复子串(3题)"时,如何将该模式应用于实时日志分析场景?
深层迁移的思考框架
1. 模式解耦(Decouple)
- 关键问题:当前模式的核心约束条件是什么?
- 案例:二分查找的核心是"有序性+上下界",迁移到非数值场景时需保留这两个条件
2. 模式重构(Refactor)
- 执行步骤:
① 参数替换:将数组索引替换为时间戳(如日程安排问题)
② 维度扩展:将一维指针升级为二维坐标(如岛屿问题)
③ 条件松弛:允许部分有序性缺失(如旋转数组搜索)
3. 模式验证(Validate)
- 验证矩阵:
| 原场景 | 新场景 | 模式匹配度 | 需调整点 |
|----------------|----------------|------------|-------------------|
| 数组去重 | 流数据去重 | 70% | 增加LRU淘汰机制 |
| 双指针求和 | 三数之和 | 85% | 增加第三指针约束 |
用(聚焦+模式化)
聚焦-->不变&经典
不管数组的算法如何改变条件,关于数组数据结构的性质是不变的,例如大小固定不能删除只能覆盖
系统化训练建议:
- 每道题用表格列出所有条件及其使用位置
- 手动模拟2个元素/3个元素的边界案例
- 比较不同区间写法的转换关系([L,R] ⇄ [L,R))
- 总结个人易错点形成自查清单
模式化-->代码模板
二分查找模板
// 1. 左闭右闭 [L,R]
int L = 0, R = arr.length - 1;
while (L <= R) {
int mid = L + (R - L) / 2;
if (arr[mid] == target) return mid; // 精确命中
else if (arr[mid] < target) L = mid + 1; // 砍左
else R = mid - 1; // 砍右
}
return -1; // 未找到
// 2. 左闭右开 [L,R)
int L = 0, R = arr.length;
while (L < R) {
int mid = L + (R - L) / 2;
if (arr[mid] >= target) R = mid; // 右开:保留 mid 但不包含
else L = mid + 1; // 左闭:排除 mid
}
return L;
遇到有序数组问题时自动触发
if (problem.contains("sorted array") || input.isSorted()) {
// 第一反应选项:
1. 二分查找(时间复杂度O(logn))
2. 双指针逼近(时间复杂度O(n))
3. 哈希表辅助(空间换时间O(n))
// 决策树:
if (需要找特定值) → 选项1
else if (需要找组合) → 选项2
else → 选项3
}
额外补充
晚上刷到吴军老师的计算之魂的介绍,其中有3个计算机的核心思想 递归/递推,分治(归并排序),平衡(tradeoff|安全/性能/成本)例如CPU多级缓存存储的平衡
数组算法解题钥匙串
暴力枚举 → 哈希表优化 → 双指针法 → 滑动窗口
↑↓
时间复杂度:O(n²)→O(n)
←→
空间复杂度:O(1)→O(n)
参考链接
notes.kamacoder.com/questions/5…
AI对话梳理思路 chat.deepseek.com/a/chat/s/66… monica.cn/home/chat/%…