数组类问题总结
数组与常用的解题算法
文章目录
- 什么是数组
- 二分法
- 双指针
- 滑动窗口
- 数组基础操作
什么是数组
- 一维数组:一片连续的存储空间,下表从0开始,存储相同类型的数据,具备按照下标随机访问,时间复杂度为o(1),
- 二维数组:二维数组的存储可不是连续的存储空间,是通过一个数组记录每个一维数组的地址,然后一维数组存储数据
- 数组中的插入和删除:当在数组最后面插入或者删除时,不需要挪动其他元素,时间复杂度o(1),当在数组第k个位置时操作,则需要将k到n的数据往后或者往前挪动,时间复杂度为o(n),其实每次进行这种插入删除很耗费时间,其实可以对需要操作的数据进行标记,当空间不足或者再次查询数组是真正的进行插入或者删除,也就是标记清除算法
- 数组常见的问题,数组越界: 当数组a的长度为1时,我们访问a[1]时,就会发生数组越界,因为访问了不属于当前数组的地址,这块需要说明一下,java中对数组越界进行了判断,但是在c中并没有对这个行为进行判断,所以很多程序就通过恶心越界去访问不应该访问的内存去获取信息。
- 数组为什么是0开始的呢?有几种说法,第一种从cpu计算成本考虑,计算啊a[k]的内存地址a[k]_address = base_address + k * type_size,如果此时下标从1开始,则计算公式为a[k]_address = base_address + (k-1)*type_size,每次随机访问,对于cpu来说多了一次减法运算,这是其中一种解释;还有一种说法是历史原因,c的设计者用0开始计数数组下标,其他语言效仿c就也从0开始
二分法
- 数组作为非常常用的数据类型,常用来查询,查询时常用的算法为二分法。
- 什么事二分法:二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
- 二分法使用条件:数组有序
- 时间复杂度:o(Logn)
- 看下面这道题,主要目的为了理解二分法使用条件,二分过程中遇到数组中存在重复数据如何处理,二分法边界问题。
1 /**
2 * 二分法 首先确定不变量,既不变区间【0,nums.length -1】,左闭右闭区间
3 * @param nums
4 * @param target
5 * @return
6 */
7 public int searchInsert(int[] nums, int target) {
8 int left = 0;
9 int right = nums.length -1;
10 while(left <= right){
11 //避免溢出
12 int middle = left + ((right -left)/2);
13 if(nums[middle]<target){
14 left = middle + 1;
15 }else if(nums[middle]>target){
16 right = middle -1;
17 }else{
18 return middle;
19 }
20 }
21 return right+1;
22 }
当二分法遇到重复值时的处理办法
1二分查找重复值问题
2当我们遇到[1,2,2,2,2,2]或者[1,1,1,2]这种情况可能会出现死循环该如何解决。
3其实这种情况只会出现在二分查找的第三个判断上,既
4nums[middle] == target
5
6所以只需要对这块进行处理,既向左以及想有找到所有相同元素的下标,至于要取最左还是最右看你的需求,那么看一下代码
7
8else { //表示arr[mid] == val
9 /*思路分析
10 1.在找到mid索引值,不要马上返回
11 2.向mid索引值的左边扫描,将所有满足1000,的元素的下标, 加入到集合ArrayList
12 3.向mid索引值的右边扫描,将所有满足1000, 的元素的下标,加入到集合ArrayList
13 4.将ArrayList返回*/
14
15 //向mid左边扫描
16 int temp = mid - 1;
17 while (true) {
18 if (temp < 0 || arr[temp] != val)//没有找到就退出循环
19 break;
20 //执行到这里说明找到了,就把找到的元素添加到集合中,继续向左找
21 indexList.add(temp);
22 temp -= 1;
23 }
24 indexList.add(mid);//加入已经找到了的元素【arr[mid]==val】
25 //向mid右边扫描
26 temp = mid + 1;
27 while (true) {
28 if (temp > arr.length - 1 || arr[temp] != val)
29 break;
30 //执行到这里说明找到了,就把找到的元素添加到集合中,继续向右
31 indexList.add(temp);
32 temp += 1;
33 }
34 return indexList;
35 }
36测试用例 :int[] arr = {1, 2, 3, 5, 6, 6,6, 6, 7, 8, 9};
37target = 6;
38返回的 indexList 就是 [4,5,6,7]既为数组中为6的下标;
双指针
- 当我们遍历数组要通过俩次for循环去寻找并替换某些值时,此时时间复杂度为o(n2),可以考虑双指针(快慢指针),通过一次for实现这个功能,时间复杂度o(n)
- 看一下题目具体理解一下,如何一次for实现俩次for的工作。
1移除元素
2给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
3不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
4元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
5示例 1:
6给定 nums = [3,2,2,3], val = 3,
7函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
8你不需要考虑数组中超出新长度后面的元素。
9示例 2:
10给定 nums = [0,1,2,2,3,0,4,2], val = 2,
11函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
12注意这五个元素可为任意顺序。
13你不需要考虑数组中超出新长度后面的元素。
14说明:
15为什么返回数值是整数,但输出的答案是数组呢?
16请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
17你可以想象内部操作如下:
18// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
19int len = removeElement(nums, val);
20// 在函数里修改输入数组对于调用者是可见的。
21// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
22for (int i = 0; i < len; i++) {
23print(nums[i]);
24}
25
26
27public class RemoveElement {
28
29 /**
30 * 快慢指针
31 * @param nums
32 * @param val
33 * @return
34 */
35 public static int removeElement(int[] nums, int val) {
36 //慢指针记录置换次数
37 int slowPoint = 0;
38 //快指针遍历数组
39 for (int fastPoint = 0; fastPoint < nums.length; fastPoint++) {
40 if(val!=nums[fastPoint]){
41 nums[slowPoint++] = nums[fastPoint];
42 }
43 }
44 return slowPoint;
45 }
46}
滑动窗口
- 当遇到最小子序列这类问题,确定数组中某个范围内的和大小问题,通过不断调整这个范围或者窗口的大小,既滑动窗口。
- 时间复杂度 o(n)
- 此类问题需要注意 窗口内是什么,窗口的起始位置,窗口的结束位置,调整起止位置时注意窗口内的值符合要求。
- 看下面的题目理解注意事项和滑动
1public class MinSubArrayLen {
2 /**
3 * 209. 长度最小的子数组
4 * 给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
5 * <p>
6 * <p>
7 * <p>
8 * 示例:
9 * <p>
10 * 输入:s = 7, nums = [2,3,1,2,4,3]
11 * 输出:2
12 * 解释:子数组 [4,3] 是该条件下的长度最小的子数组。
13 * <p>
14 * <p>
15 * 进阶:
16 * <p>
17 * 如果你已经完成了 O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。
18 *
19 * @param s
20 * @param nums
21 * @return
22 */
23 public static int minSubArrayLen(int s, int[] nums) {
24 int resultLength = Integer.MAX_VALUE;
25 int start = 0;
26 int sum = 0;
27 //此处就是滑动终止的位置
28 for (int end = 0; end < nums.length; end++) {
29 sum += nums[end];
30 //此处就是滑动起始的位置
31 while (sum >= s) {
32 int len = end - start + 1;
33 resultLength = resultLength < len ? resultLength : len;
34 sum -= nums[start++];
35 }
36 }
37 if (resultLength == Integer.MAX_VALUE) {
38 return 0;
39 } else {
40
41 return resultLength;
42 }
43 }
44}
数组基础操作
- 数组的基础操作插入,如果是一个二维数组插入呢,相当于在一个平面上操作数组,下面这道题就是在二维数组插入,需要注意的就是 不变量,既边界问题。
1给定一个正整数 n,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
2示例:
3输入: 3
4输出:
5[
6[ 1, 2, 3 ],
7[ 8, 9, 4 ],
8[ 7, 6, 5 ]
9]
10
11public class GenerateMatrix {
12
13 public static void main(String[] args) {
14 int[][] res = generateMatrix(4);
15 for (int i = 0; i < res.length; i++) {
16 for (int j = 0; j < res[i].length; j++) {
17 System.out.print(res[i][j] + " ");
18 }
19 System.out.println();
20 }
21 }
22
23 public static int[][] generateMatrix(int n) {
24 int[][] res = new int[n][n];
25 //开始位置
26 int startX = 0, startY = 0;
27 //循环次数
28 int loop = n / 2;
29 //如果循环次数是奇数,那么会存在中心点,中心点是最后赋值,中心点位置
30 int mid = n / 2;
31 //每条边边界,每循环一圈就加2,因为进内圈需要加1,同时边界向内收缩1
32 int offset = 1;
33 //初始赋值
34 int count = 1;
35 //原则 左闭又开
36 // 上面 从左右导游
37 // 右边 从上到下
38 // 下面 从右到在
39 // 左边 从下到上
40 while ((loop--) > 0) {
41 int i = startX;
42 int j = startY;
43
44 // 上面 从左右导游
45 for (; j < startY + n - offset; j++) {
46 res[startX][j] = count++;
47 }
48
49 // 右边 从上到下
50 for (; i < startX + n - offset; i++) {
51 res[i][j] = count++;
52 }
53
54 // 下面 从右到在
55 for (; j > startY; j--) {
56 res[i][j] = count++;
57 }
58
59 // 左边 从下到上
60 for (; i > startX; i--) {
61 res[i][j] = count++;
62 }
63
64 startX++;
65 startY++;
66 offset += 2;
67
68 }
69
70 if (n % 2 == 1) {
71 res[mid][mid] = count;
72 }
73
74 return res;
75 }
76}
总结
- 数组的问题注意边界问题,注意不变量,
- 确定数组是否有序,是否有重复值,确定哪类问题对应的解题思路
不恋尘世浮华,不写红尘纷扰
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理