一、数组
好久没写算法题,之前喜欢按着习惯选择刷题,很早以前就听说代码随想录,今天跟着代码随想录再过一遍算法
1.1二分查找
-
常见疑问
- middle一定是在[left, right]这个范围内
- 标准代码不会越界,因为在else if中出现越界后,下一次循环就不会通过
-
左闭右闭区间
-
代码示例
public class BinarySearch<T> { public int BinarySearch(int[] arr, int target) { int left = 0, right = arr.length - 1, middle = 0; while (left <= right) { middle = (left + right) / 2; if (arr[middle] == target) { return middle; } else if(arr[middle] > target) { right = middle - 1; } else if(arr[middle] < target) { left = middle + 1; } } return -1; } public static void main(String[] args) { BinarySearch<Integer> binarySearch = new BinarySearch<>(); int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9}; System.out.println(binarySearch.BinarySearch(arr, 5)); } }
-
为什么是
left <= right
,举一个极端例子[2, 2],这个时候如果是没有=
,则无法返回值 -
为什么是
middle - 1 middle + 1
,因为在arr[middle] > target
和arr[middle] < target
的时候,已经判断过arr[middle]已不在范围内了,直接排除即可
-
-
左闭右开区间
-
代码示例
public class BinarySearch<T> { public int BinarySearch(int[] arr, int target) { int left = 0, right = arr.length - 1, middle = 0; while (left < right) { middle = (left + right) / 2; if (arr[middle] == target) { return middle; } else if(arr[middle] > target) { right = middle; } else if(arr[middle] < target) { left = middle + 1; } } return -1; } public static void main(String[] args) { BinarySearch<Integer> binarySearch = new BinarySearch<>(); int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9}; System.out.println(binarySearch.BinarySearch(arr, 5)); } }
- 为什么是
left < right
,因为在极端情况下,比如不断缩减或一开始数组就是这样[2, 2),这明显错误,所以不存在相等 - 为什么是
middle
,因为是开区间,开区间本身就不包括在内 - 左开右闭同理,不赘诉
-
1.2双指针
1.2.1 快慢指针:拥有快慢指针,快指针先行,慢指针跟随条件移动
-
常用来数组元素的删除
-
代码示例
public int removeElement(int[] nums, int val) { int fast = 0, slow = 0; for (; fast < nums.length; ++fast) { if (nums[fast] != val) { nums[slow] = nums[fast]; slow++; } } return slow; }
-
实战
-
力扣27
暴力
class Solution { public int removeElement(int[] nums, int val) { int n = nums.length; for(int i = 0; i < n; ++i) { if (nums[i] == val) { for (int j = i; j < n - 1; ++j) { nums[j] = nums[j + 1]; } n -= 1; i -= 1; } } return n; } }
快慢指针
class Solution { public int removeElement(int[] nums, int val) { int n = nums.length; int fast = 0; int slow = 0; while (fast < n) { if (nums[fast] != val) { nums[slow] = nums[fast]; slow++; } fast++; } return slow; } }
- 在只有一个要移除的元素时和暴力的区别不大,只有在多个需要去除的元素才能体现出优势,但这里算是初步体现了快慢双指针的思想,让一个快指针去遍历每一个元素并判断每一个元素是否等于要删除的元素,慢指针代表索引号,这样避免了找到一次元素就要遍历一次的尴尬处境。这里体现了双指针思想,拥有两个自由的判断点,通过两种指针的交替配合从而避免暴力双层循环(暴力往往只有一个自由的判断点,外层循环的遍历),下面几题变种也体现了这种思想。
-
-
1.2.2 左右指针:两个指针相互操作,知道相撞
-
常用来比较大小并排序,实现O(n)时间复杂度
-
代码示例
public int[] sortedSquares(int[] nums) { int left = 0; int n = nums.length; int right = n - 1; int[] res = new int[n]; while (right >= left) { if (nums[left] * nums[left] > nums[right] * nums[right]) { res[n - 1] = nums[left] * nums[left++]; } else { res[n - 1] = nums[right] * nums[right--]; } n -= 1; } return res; }
-
这里为什么是
right >= left
和二分查找类似,因为都是左闭右闭的场景 -
实战
-
力扣977
public int[] sortedSquares(int[] nums) { int left = 0; int n = nums.length; int right = n - 1; int[] res = new int[n]; while (right >= left) { if (nums[left] * nums[left] > nums[right] * nums[right]) { res[n - 1] = nums[left] * nums[left++]; } else { res[n - 1] = nums[right] * nums[right--]; } n -= 1; } return res; }
- 左指针和右指针都往中间靠,如果右指针比左指针大,则右指针–,如果左指针比右指针大,则左指针++,知道右指针小于左指针
-
-
1.2.3 滑动窗口
-
用来计算哪一段区间最符合要求
-
实战
-
力扣209
暴力(失败,超出时间限制)
class Solution { public int minSubArrayLen(int target, int[] nums) { int res = Integer.MAX_VALUE; int n = nums.length - 1; for (int i = 0; i <= n; ++i) { int add = 0; for (int j = i; j <= n; ++j) { add += nums[j]; if (add >= target) { res = Math.min(j - i + 1, res); break; } } } return res == Integer.MAX_VALUE ? 0 : res; } }
滑动窗口
public int minSubArrayLen(int target, int[] nums) { int left = 0, right = 0; int n = nums.length; int result = Integer.MAX_VALUE; int subLength = 0; int sum = 0; for (; right < n; ++right) { sum += nums[right]; while (sum >= target) { subLength = right - left + 1; result = Math.min(subLength, result); sum -= nums[left++]; } } return result == Integer.MAX_VALUE ? 0 : result; }
-
-
若使用头指针,则和暴力没什么区别,所以选择尾部。滑动窗口一层循环则可直接计算,尾指针先移动,满足条件时,尾指针不动,头指针+1,再判断,如果通过,则更新result。否则继续移动尾指针,直到尾指针到数组尾部结束。
-
1.3模拟
-
对于难以应用算法的问题,可以通过模拟问题中的思路进行代码编写
-
实战
-
力扣54
class Solution { public List<Integer> spiralOrder(int[][] matrix) { List<Integer> res = new ArrayList<Integer>(); int u = 0; int d = matrix.length - 1; int l = 0; int r = matrix[0].length - 1; while(true) { for (int i = l; i <= r; ++i) { res.add(matrix[u][i]); } if (++u > d) break; for (int i = u; i <= d; ++i) { res.add(matrix[i][r]); } if (--r < l) break; for (int i = r; i >= l; --i) { res.add(matrix[d][i]); } if (--d < u) break; for (int i = d; i >= u; --i) { res.add(matrix[i][l]); } if (++l > r) break; } return res; } }
-
这是很常见的模拟之一,与力扣59互补,拿到这种题有常见的思路
-
大多数这种题目,都是给予条件,根据条件进行不断的计算。所以我们需要确定 1.循环中的计算 2.循环条件 3.终止条件
-
上述写法是比较快的,有些地方是较优解的
-
我比较喜欢使用while循环,因为while循环往往可以表示确定次数的循环和不确定次数的循环,for循环是确定次数的。但for循环也有一些优势,简洁明了,很容易判断循环条件
-
拿与力扣59举例,那题是一个正方形,所以只会出现一种特殊情况就是层数为奇数的时候,需要手动添加垂直水平中间数组中的值,而这里行列是不一样的,所以终止的情况有多种,上下左右都有可能,所以直接在while循环中添加判断是比较复杂的
-
这里,我再说个结论,中间的循环写法很大程度依靠终止条件的写法。力扣59强调每一个方向使用相同开闭区间的数组。这里是每一个变都是闭区间,是因为59题是依靠着一个实时的变量来控制数字在数组中的走向,这里判断条件不好写绕圈次数,因为上面说过终止条件判断较多,所以不太好靠一个实时变量来控制走向。由于终止条件很多,所以这里引用上下左右边界,就很好的解决了循环内写法的问题
-
这种模拟的方法效率更高,大家在写模拟的时候,可以先将思路列举好,起始条件、终止条件、遍历写法,然后再一步一步优化
-
-
-
-
-
总结
-
双指针技术的魅力就在于它通过一个主循环来控制遍历,同时在循环内进行值的计算和条件判断,从而避免了不必要的嵌套循环。这种方式不仅提高了效率,还使得代码更加简洁和易于理解。所以写双指针的时候需要在一层循环就进行计算,
-
以下的写法就是为了写双指针而写,实际上就是暴力,没有做到第一层循环就计算,力扣209
public int minSubArrayLen(int target, int[] nums) { int start = 0, end = 0; int n = nums.length; int res = Integer.MAX_VALUE; while (end < n) { int add = 0; int i = start; // 计算窗口内的和 for (; i <= end; i++) { add += nums[i]; if (add >= target) { res = Math.min(end - start + 1, res); break; // 找到满足条件的和后,退出循环 } } // 如果找到满足条件的和,移动 start if (add >= target) { start++; } else { end++; // 否则,移动 end } } return res == Integer.MAX_VALUE ? 0 : res; // 如果没有找到,返回 0 }
-
-