二分查找
704.二分查找
https://leetcode-cn.com/problems/binary-search/
这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件
写二分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
二分法第一种写法
第一种写法,我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要非常重要)。
区间的定义这就决定了二分法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:
- while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1 ```java
// 二分法第一种写法 public int search1(int[] nums, int target) {
// 左闭右开int L = 0;int R = nums.length;int mid = 0;while (L < R) {mid = (L + R) >> 1;if (nums[mid] > target) {R = mid;} else if (nums[mid] < target) {L = mid + 1;} else {return mid;}}return -1;
}
**二分法第二种写法**<br />如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。<br />有如下两点:- while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的- if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]```java// 二分法第二种写法public int search2(int[] nums, int target) {// 左闭右闭int L = 0;int R = nums.length - 1;int mid = 0;while (L <= R) {mid = (L + R) >> 1;if (nums[mid] > target) {R = mid - 1;} else if (nums[mid] < target) {L = mid + 1;} else {return mid;}}return -1;}
35.搜索插入位置
https://leetcode-cn.com/problems/search-insert-position/
public int searchInsert(int[] nums, int target) {//左闭右开int L = 0;int R = nums.length;int mid = 0;while (L < R) {mid = (L + R) >> 1;if (nums[mid] > target) {R = mid;} else if (nums[mid] < target) {L = mid + 1;} else {return mid;}}return L;}
34.在排序数组中查找元素的第一个和最后一个位置
https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/
把所有情况都讨论一下。
寻找target在数组里的左右边界,有如下三种情况:
- 情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}
- 情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
- 情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
这三种情况都考虑到,说明就想的很清楚了。
接下来,在去寻找左边界,和右边界了。
采用二分法来去寻找左右边界,为了让代码清晰,分别写两个二分来寻找左边界和右边界。
public int[] searchRange(int[] nums, int target) {int[] res = new int[]{-1, -1};res[0] = findLeftBoard(nums, target);res[1] = findRightBoard(nums, target);return res;}// 找左边界private int findLeftBoard(int[] nums, int target) {//左闭右开int L = 0;int R = nums.length;int mid = 0;int location = -1;while (L < R) {mid = (L + R) >> 1;if (nums[mid] > target) {R = mid;} else if (nums[mid] < target) {L = mid + 1;} else { // ==R = mid;location = mid;}}return location;}// 找右边界private int findRightBoard(int[] nums, int target) {//左闭右开int L = 0;int R = nums.length;int mid = 0;int location = -1;while (L < R) {mid = (L + R) >> 1;if (nums[mid] > target) {R = mid;} else if (nums[mid] < target) {L = mid + 1;} else { // ==L = mid + 1;location = mid;}}return location;}
69.Sqrt(x)
https://leetcode-cn.com/problems/sqrtx/
比如Sqrt(104),
104的开方肯定比104小,
所以就从1 ~ 104这个范围二分,
每次找到mid, 然后让mid的平方与104比较,
- 若mid的平方 > 104, 说明找大了, 再去左边二分
- 若mid的平方 < 104, 说明找小了,再去右边二分
- 若mid的平方 = 104,说明找到了一个暂定的结果,记录,但是我们还要看能不能再找一个离104更近的,所以再去右边二分
所以第二和第三种情况是可以合并的
public int mySqrt(int x) {if (x == 0) return 0;long L = 1;long R = x;long ans = 1;while (L < R) {long mid = (L + R) >> 1;if (mid * mid > x) {R = mid;} else if (mid * mid <= x) {L = mid + 1;ans = mid;}}return (int)ans;}
367. 有效的完全平方数
https://leetcode-cn.com/problems/valid-perfect-square/
public boolean isPerfectSquare(int num) {if (num == 1) {return true;}long L = 1;long R = num;while (L < R) {long mid = (L + R) >> 1;if (mid * mid < num) {L = mid + 1;} else if (mid * mid > num) {R = mid;} else {return true;}}return false;}
移除元素
数组移除元素用的比较多的就是双指针 | 快慢指针
27. 移除元素
https://leetcode-cn.com/problems/remove-element/
public int removeElement(int[] nums, int val) {int last = nums.length - 1;int i = 0;while (i < last) {if (nums[i] == val) {swap(nums, i, last--);}else {i++;}}return last;}private void swap(int[] nums, int i, int j) {int tmp = nums[i];nums[i] = nums[j];nums[j] = tmp;}
26. 删除有序数组中的重复项
https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/
快慢指针
public int removeDuplicates(int[] nums) {int slow = 0;int fast;for (fast = 1; fast < nums.length; fast++) {if (nums[fast] != nums[slow]) {slow++;swap(nums, slow, fast);}}return slow + 1;}private void swap(int[] nums, int i, int j) {int tmp = nums[i];nums[i] = nums[j];nums[j] = tmp;}
283. 移动零
**_https://leetcode-cn.com/problems/move-zeroes/
//双指针// 两个原则// 1) fast位置是0, fast++// 2) fast位置不是0. fast位置跟slow的下一个数交换, fast++, slow++public void moveZeroes(int[] nums) {int slow = -1;int fast;for (fast = 0; fast < nums.length; fast++) {if (nums[fast] != 0) {swap(nums, ++slow, fast);}}}private void swap(int[] nums, int i, int j) {int tmp = nums[i];nums[i] = nums[j];nums[j] = tmp;}
844. 比较含退格的字符串
https://leetcode-cn.com/problems/backspace-string-compare/
双指针解法
public boolean backspaceCompare(String s, String t) {char[] S = s.toCharArray();char[] T = t.toCharArray();int i = s.length() - 1;int j = t.length() - 1;int Iskip = 0;int Jskip = 0;while (i >= 0 || j >= 0) {while (i >= 0) {if (S[i] == '#') {Iskip++;i--;} else if (Iskip > 0) {Iskip--;i--;} else {break;}}while (j >= 0) {if (T[j] == '#') {Jskip++;j--;} else if (Jskip > 0) {Jskip--;j--;} else {break;}}if (i >= 0 && j >= 0) {if (S[i] != T[j]) {return false;}} else {if (i >= 0 || j >= 0) {return false;}}i--;j--;}return true;}
栈解法
// 栈解法public boolean backspaceCompare(String s, String t) {char[] S = s.toCharArray();char[] T = t.toCharArray();return process(S).equals(process(T));}public String process(char[] str) {Stack<Character> stack = new Stack<>();for (char c : str) {if (c == '#') {if (!stack.isEmpty()) {stack.pop();}} else {stack.push(c);}}return stack.toString();}
有序数组的平方
https://leetcode-cn.com/problems/squares-of-a-sorted-array/
注意利用题目中是有序数组这一特点若原数组全为非负数, 那么平方后,数组仍是升序的
- 若原数组全为负数, 那么平方后,数组是降序的
- 若原数组既有负数又有正数,那么平方后,数组是先降序后升序的
双指针解法
public int[] sortedSquares(int[] nums) {int N = nums.length;int[] ans = new int[N];int pos = N - 1;for (int i = 0, j = N - 1; i <= j; ) {if (nums[i] * nums[i] < nums[j] * nums[j]) {ans[pos] = nums[j] * nums[j];j--;} else {ans[pos] = nums[i] * nums[i];i++;}pos--;}return ans;}
长度最小/大的子数组
209. 长度最小的子数组
滑动窗口解法
public int minSubArrayLen(int target, int[] nums) {int L = 0;int sum = 0;int len = Integer.MAX_VALUE;for (int R = 0; R < nums.length; R++) {sum += nums[R];while (sum >= target) {len = Math.min(len, R - L + 1);sum -= nums[L++];}}return len == Integer.MAX_VALUE ? 0 : len;}
904. 水果成篮
滑动窗口解法
public int totalFruit(int[] fruits) {int L = 0;HashMap<Integer, Integer> map = new HashMap<>();int ans = 0;for (int R = 0; R < fruits.length; R++) {map.put(fruits[R], map.containsKey(fruits[R]) ? map.get(fruits[R]) + 1 : 1);while (map.size() >= 3) {map.put(fruits[L], map.get(fruits[L]) - 1);if (map.get(fruits[L]) == 0) {map.remove(fruits[L]);}L++;}ans = Math.max(ans, R - L + 1);}return ans;}
76. 最小覆盖子串
https://leetcode-cn.com/problems/minimum-window-substring/
滑动窗口 + 欠债表欠债表

all是欠款总数量


只要减减后,不小于0,就是有效还款,all也要减减,
若小于0则是无效还款,all不变




第一次all变成0的时刻,记录窗口长度,此时窗口的含义是如果子串必须以0开头,至少多长才能包含所有的字符,
后面的先别管,L开始缩
以1开头,记录窗口长度,然后L继续缩
欠款了, 让R往右继续
。。。
。。
。。
补充:coding细节, 窗口设置成左闭右开的,初始值就很容易设置, [0, 0) 就表示这个窗口一开始没数
public static String minWindow(String s, String t) {if (s.length() < t.length()) {return "";}char[] str = s.toCharArray();char[] target = t.toCharArray();int[] map = new int[256];int all = target.length;int L = 0;int R = 0;int minLen = -1;int ansL = -1;int ansR = -1;for (char c : target) {map[c]++;}while (R != str.length) {map[str[R]]--;if (map[str[R]] >= 0) { // 有效还款all--;}if (all == 0) {while (map[str[L]] < 0) {map[str[L++]]++;}if (minLen == -1 || minLen > R - L + 1) {minLen = R - L + 1;ansL = L;ansR = R;}all++;map[str[L++]]++;}R++;}return minLen == -1 ? "" : s.substring(ansL, ansR + 1);}
螺旋矩阵
59. 螺旋矩阵Ⅱ
https://leetcode-cn.com/problems/spiral-matrix-ii/
这是正方形的矩阵
public int[][] generateMatrix(int n) {int[][] res = new int[n][n];int leftRow = 0;int leftCol = 0;int rightRow = n - 1;int rightCol = n - 1;int count = 1;while (leftRow <= rightRow && leftCol <= rightCol) {if (leftRow == rightRow) { // 正方形矩阵只用额外判断这里即可for (int i = leftCol; i <= rightCol; i++) {res[leftRow][i] = count++;}}else {for (int i = leftCol; i < rightCol; i++) {res[leftRow][i] = count++;}for (int i = leftRow; i < rightRow; i++) {res[i][rightCol] = count++;}for (int i = rightCol; i > leftCol ; i--) {res[rightRow][i] = count++;}for (int i = rightRow;i > leftRow ; i--) {res[i][leftCol] = count++;}}leftRow++;leftCol++;rightRow--;rightCol--;}return res;}
54. 螺旋矩阵
https://leetcode-cn.com/problems/spiral-matrix/
public List<Integer> spiralOrder(int[][] matrix) {int leftRow = 0;int leftCol = 0;int rightRow = matrix.length - 1;int rightCol = matrix[0].length - 1;List<Integer> list = new ArrayList<>();while (leftRow <= rightRow && leftCol <= rightCol) {if (leftRow == rightRow ) { // 正方形矩阵额外判断的地方for (int i = leftCol; i <= rightCol; i++) {list.add(matrix[leftRow][i]);}} else if (leftCol == rightCol) { // 还有可能是长方形矩阵for (int i = leftRow; i <= rightRow; i++) {list.add(matrix[i][leftCol]);}} else {for (int i = leftCol; i < rightCol; i++) {list.add(matrix[leftRow][i]);}for (int i = leftRow; i < rightRow; i++) {list.add(matrix[i][rightCol]);}for (int i = rightCol; i > leftCol; i--) {list.add(matrix[rightRow][i]);}for (int i = rightRow; i > leftRow; i--) {list.add(matrix[i][leftCol]);}}leftRow++;leftCol++;rightRow--;rightCol--;}return list;}
剑指Offer29. 顺时针打印矩阵
https://leetcode-cn.com/problems/shun-shi-zhen-da-yin-ju-zhen-lcof/
public int[] spiralOrder(int[][] matrix) {if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {return new int[0];}int leftRow = 0;int leftCol = 0;int rightRow = matrix.length - 1;int rightCol = matrix[0].length - 1;int[] ans = new int[matrix.length * matrix[0].length];int index = 0;while (leftRow <= rightRow && leftCol <= rightCol) {if (leftRow == rightRow) {for (int i = leftCol; i <= rightCol; i++) {ans[index++] = matrix[leftRow][i];}} else if (leftCol == rightCol) {for (int i = leftRow; i <= rightRow; i++) {ans[index++] = matrix[i][leftCol];}} else {for (int i = leftCol; i < rightCol; i++) {ans[index++] = matrix[leftRow][i];}for (int i = leftRow; i < rightRow; i++) {ans[index++] = matrix[i][rightCol];}for (int i = rightCol; i > leftCol; i--) {ans[index++] = matrix[rightRow][i];}for (int i = rightRow; i > leftRow; i--) {ans[index++] = matrix[i][leftCol];}}leftRow++;leftCol++;rightRow--;rightCol--;}return ans;}
总结
在面试中,数组是必考的基础数据结构。
其实数据的题目在思想上一般比较简单的,但是如果想高效,并不容易。
我们之前一共讲解了四道经典数组题目,每一道题目都代表一个类型,一种思想。二分法
循环不变量原则,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。
双指针法
双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
暴力解法时间复杂度:$O(n^2)$
- 双指针时间复杂度:$O(n)$
滑动窗口
主要要理解滑动窗口如何移动 窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。
滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将$O(n^2)$的暴力解法降为$O(n)$。
如果没有接触过这一类的方法,很难想到类似的解题思路,滑动窗口方法还是很巧妙的。
模拟行为
模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,十分考察大家对代码的掌控能力。
在这道题目中,我们再一次介绍到了循环不变量原则,其实这也是写程序中的重要原则。
相信大家又遇到过这种情况: 感觉题目的边界调节超多,一波接着一波的判断,找边界,踩了东墙补西墙,好不容易运行通过了,代码写的十分冗余,毫无章法,其实真正解决题目的代码都是简洁的,或者有原则性的,大家可以在这道题目中体会到这一点。
