/二分查找/
704. 二分查找
初次提交
2023 年 3 月 13 日
public int search(int[] nums, int target) {
int start = 0;
int end = nums.length - 1;
int half;
do {
if (nums[start] == target) return start;
if (nums[end] == target) return end;
half = (start + end) / 2;
if (nums[half] == target) return half;
if (nums[half] > target) end = half;
else start = half;
} while (start + 1 != end && start != end);
return -1;
}
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:41.8 MB, 在所有 Java 提交中击败了92.21%的用户
- 整数 int 类型的除法是 [截断]处理的。
start + 1 != end && start != end
的判断条件我自己都没搞清楚,稀里糊涂地写的(但居然是对的)- 对二分查找法的思路理解不透彻,逻辑写的比较混乱
优化思路
- 首先判断 target 是否在此数组大小区间内,不在的话直接返回 -1;
- 当中间数与 target 不相等时,我们采用的赋值方式是 left = mid + 1 或者 right = mid - 1,这也就意味着最终 left 是可能等于 right 的,而此时仍需判断与 target 是否相等;
- 如果一直没找到 target,这个 [left, right] 区间会慢慢缩小为 left == right,而此时如果还是没找到 target,则一定会发生 left > right,导致循环结束,最终返回 -1;
public int search(int[] nums, int target) {
if (target < nums[0] || target > nums[nums.length - 1]) {
return -1;
}
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = (left + right)/2;
if (nums[mid] == target) return mid;
else if (nums[mid] < target) left = mid + 1;
else if (nums[mid] > target) right = mid - 1;
}
return -1;
}
35. 搜索插入位置:二分查找
初次提交
2023 年 3 月 13 日
public int searchInsert(int[] nums, int target) {
if (target < nums[0]) return 0;
if (target > nums[nums.length - 1]) return nums.length;
int left = 0;
int right = nums.length - 1;
int mid = 0;
while(left <= right) {
mid = (left + right) / 2;
if (nums[mid] == target) return mid;
else if (nums[mid] > target) right = mid - 1;
else left = mid + 1;
}
// 说明最后动的是 right,也就是说 nums[mid] > target
if (mid == left) return mid;
else return mid + 1;
}
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:40.8 MB, 在所有 Java 提交中击败了87.02%的用户
这道题是二分查找法的一个变体,只要牢牢记住:二分查找法没找到 target 的情况下,出循环之前时:left == mid == right,也就是说只需要判断最后是 left 还是 right 改变导致循环结束,从而也就能知道 nums[mid] 和 target 的大小关系。
34. 在排序数组中查找元素的第一个和最后一个位置
初次提交
2023 年 3 月 14 日
public int[] searchRange(int[] nums, int target) {
int[] range = new int[]{-1, -1};
if (nums.length == 0) return range;
if (target < nums[0] || target > nums[nums.length - 1]) return range;
int left = 0;
int right = nums.length - 1;
int mid;
// 找右界
while (left <= right) {
mid = (left + right) / 2;
if (nums[mid] == target) {
if (mid + 1 < nums.length && nums[mid + 1] == target) {
left = mid + 1;
} else {
range[1] = mid;
break;
}
} else if (nums[mid] > target) right = mid - 1;
else left = mid + 1;
}
if (range[1] == -1) return range;
left = 0;
right = nums.length - 1;
// 找左界
while (left <= right) {
mid = (left + right) / 2;
if (nums[mid] == target) {
if (mid - 1 >= 0 && nums[mid - 1] == target) {
right = mid - 1;
} else {
range[0] = mid;
break;
}
} else if (nums[mid] > target) right = mid - 1;
else left = mid + 1;
}
return range;
}
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:44.6 MB, 在所有 Java 提交中击败了61.49%的用户
失误点:判断 nums[mid + 1] 和 nums[mid - 1] 是需要有个前提条件:mid + 1 < nums.length 以及 mid - 1 >= 0,否则会出现数组越界。
而这两种特殊情况所对应的 mid 实际已经无需再进行判断(因为已经处于了数组的边界)
69. x 的平方根
初次提交
2023 年 3 月 14 日
public int mySqrt(int x) {
if (x == 0 || x == 1) return x;
for (int i = 0; i <= x / 2; i++) {
if ((long) i * i <= x && (long) (i + 1) * (i + 1) > x) return i;
}
return 0;
}
执行用时:67 ms, 在所有 Java 提交中击败了5.05%的用户
内存消耗:39 MB, 在所有 Java 提交中击败了26.45%的用户
优化思路
使用二分查找,所有数的算术平方根都在 [0, x] 这个区间内,所以一定可以找到。
还需要注意的是,mid * mid 是有可能会越界的,即超出 MAX_INT 的值,因此需要转为 long
;
public int mySqrt(int x) {
int left = 0;
int right = x;
int mid;
while (left <= right) {
mid = (left + right) / 2;
if ((long) mid * mid <= x && (long) (mid + 1) * (mid + 1) > x) return mid;
if ((long) mid * mid < x) left = mid + 1;
else right = mid - 1;
}
return 0;
}
执行用时:1 ms, 在所有 Java 提交中击败了94.71%的用户
内存消耗:38.5 MB, 在所有 Java 提交中击败了90.87%的用户
367. 有效的完全平方数
初次提交
2023 年 3 月 14 日
public boolean isPerfectSquare(int num) {
int left = 0;
int right = num;
int mid;
while (left <= right) {
mid = (left + right) / 2;
if ((long)mid * mid == num) return true;
else if ((long)mid * mid > num) right = mid - 1;
else left = mid + 1;
}
return false;
}
执行耗时:0 ms,击败了 100.00% 的 Java 用户
内存消耗:38.2 MB,击败了 60.17% 的 Java 用户
虽然题目告诉 1 <= num <= 2^31 - 1,但 left 仍指定为 0,是因为防止 left + right 越界
要牢记,此二分法是在 [left, right] 闭区间里找目标!
/数组元素移除/
27. 移除元素
初次提交
2023 年 3 月 13 日
public int removeElement(int[] nums, int val) {
int maxVal = 50;
int ans = nums.length;
for (int i = 0; i < nums.length; i ++) {
if (nums[i] == val) {
nums[i] = maxVal + 1;
ans --;
}
}
for (int i = nums.length - 1; i > 0; i --) {
int max = i;
for(int j = i - 1;j >= 0; j --) {
if (nums[j] > nums[max]) max = j;
}
if (max != i) {
int temp = nums[max];
nums[max] = nums[i];
nums[i] = temp;
}
}
return ans;
}
执行用时:1 ms, 在所有 Java 提交中击败了3.01%的用户
内存消耗:39.8 MB, 在所有 Java 提交中击败了90.16%的用户
题目说 0 <= nums[i] <=50
,因此将所要移除的元素值设置为 51,然后再进行一次排序,但这样做用时很不理想。
优化思路:快慢双指针
- 快指针的作用是:循环整个数组,并找到所有和目标元素不相等的元素。
- 慢指针的作用是:作为最终目标数组的指针。(可以假设在慢指针看来,它是看不到原数组元素的,它只能看到最终数组元素)
public int removeElement(int[] nums, int val) {
// 快慢指针
int slowIndex = -1;
for (int fastIndex = 0; fastIndex < nums.length; fastIndex ++) {
// 当快指针找到和目标元素不相等的元素时,慢指针才往后挪个位置并把该元素放入其中
if (nums[fastIndex] != val)
nums[++ slowIndex] = nums[fastIndex];
}
return slowIndex + 1;
}
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:39.8 MB, 在所有 Java 提交中击败了90.16%的用户
26. 删除排序数组中的重复项
初次提交
2023 年 3 月 13 日
public int removeDuplicates(int[] nums) {
int slowIndex = -1;
for(int fastIndex = 0; fastIndex < nums.length; fastIndex ++) {
while(fastIndex < nums.length - 1) {
if(nums[fastIndex + 1] == nums[fastIndex]) fastIndex ++;
else break;
}
nums[++ slowIndex] = nums[fastIndex];
}
return slowIndex + 1;
}
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:43.2 MB, 在所有 Java 提交中击败了33.86%的用户
完成这道题的时候利用的是快慢双指针
注意题目中描述的:这是个升序数组
优化思路
更好地利用到题目描述的:这是个升序数组。那么慢指针对应的数组当然也是升序数组。
public int removeDuplicates(int[] nums) {
int slowIndex = 0;
for(int fastIndex = 1; fastIndex < nums.length; fastIndex ++) {
if (nums[fastIndex] != nums[slowIndex]) nums[++ slowIndex] = nums[fastIndex];
}
return slowIndex + 1;
}
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:42.8 MB, 在所有 Java 提交中击败了89.20%的用户
283. 移动零
初次提交
2023 年 3 月 14 日
public void moveZeroes(int[] nums) {
int lowIndex = -1;
for (int fastIndex = 0; fastIndex < nums.length; fastIndex ++) {
if (nums[fastIndex] != 0) nums[++ lowIndex] = nums[fastIndex];
}
while (lowIndex + 1 < nums.length) {
nums[++ lowIndex] = 0;
}
}
执行耗时:1 ms,击败了 100.00% 的 Java 用户
内存消耗:42.8 MB,击败了 57.93% 的 Java 用户
844. 比较含退格的字符串
初次提交
2023 年 3 月 14 日
public boolean backspaceCompare(String s, String t) {
s = backSpace(s);
t = backSpace(t);
return s.equals(t);
}
public String backSpace(String s) {
char[] ss = s.toCharArray();
int slowIndex = -1;
for (int fastIndex = 0; fastIndex < ss.length; fastIndex ++) {
if (ss[fastIndex] != '#') ss[++ slowIndex] = ss[fastIndex];
else if(slowIndex > -1) slowIndex --;
}
if (slowIndex == -1) return "";
else return String.valueOf(ss).substring(0, slowIndex + 1);
}
执行耗时:0 ms,击败了 100.00% 的 Java 用户
内存消耗:39.8 MB,击败了 34.48% 的 Java 用户
- 如果想方便地遍历字符串的每个字符的话,最好将其转为
char[]
。最后使用String.valueOf()
转回来。 .substring(start, end)
是截取[start, end)
,因此 slowIndex 需要加 1 来进行截取
/有序数组的平方/
977. 有序数组的平方
初次提交
2023 年 3 月 14 日
public int[] sortedSquares(int[] nums) {
for (int i = 0; i < nums.length; i++) {
nums[i] = nums[i] * nums[i];
}
for (int i = 0; i < nums.length; i++) {
int min = nums[i];
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] < min) {
int temp = nums[j];
nums[j] = min;
min = temp;
}
}
nums[i] = min;
}
return nums;
}
执行耗时:441 ms,击败了 5.21% 的 Java 用户
内存消耗:43.9 MB,击败了 5.06% 的 Java 用户
很笨的暴力解法…
优化思路:左右双指针
由于原数组是非降序排列的,那么平方后的最大值只会出现在原数组的最左或者最右,因此此题的思路是:
- 定义一个和原数组长度相同的数组,并指向其最后一个元素位置;
- 定义左右双指针,每次找到最大值就可以对指针进行挪动。
- 循环结束的条件是:左右指针相遇,此时是最后一个需要处理的元素,处理完毕后左右指针错开,循环结束。
public int[] sortedSquares(int[] nums) {
int[] ans = new int[nums.length];
int left = 0;
int right = nums.length - 1;
int index = right;
while (left <= right) {
if (nums[left] * nums[left] < nums[right] * nums[right]) {
ans[index --] = nums[right] * nums[right];
right --;
} else {
ans[index --] = nums[left] * nums[left];
left ++;
}
}
return ans;
}
执行耗时:1 ms,击败了 100.00% 的 Java 用户
内存消耗:43.4 MB,击败了 25.40% 的 Java 用户
/长度最小子数组/
209. 长度最小的子数组
初次提交
2023 年 3 月 14 日
public int minSubArrayLen(int target, int[] nums) {
int min = Integer.MAX_VALUE;
for (int left = 0; left < nums.length; left ++) {
int sum = 0;
for (int right = left; right < nums.length; right ++) {
sum += nums[right];
if (sum >= target) {
if (right - left + 1 < min)
min = right - left + 1;
break;
}
}
}
return min == Integer.MAX_VALUE? 0 : min;
}
提交失败:Time Limit Exceeded
只想到了暴力求解,然后超时了。
优化思路:双指针之滑动窗口
- 窗口:满足和 ≥ target 的长度最小的连续子数组
- 窗口的结束位置如何移动:如果当前窗口值 sum 小于 target,则不断移动 right 指针,直到大于 target
- 窗口的起始位置如何移动:如果当前窗口值 sum 已经大于 target 了,left 指针不断向前移动,直到窗口值再次小于 target 值。(此过程中,需要记录下窗口长度并与 min 做对比)
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int left = 0;
int sum = 0;
int min = Integer.MAX_VALUE;
for (int right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) {
min = Math.min(min, right - left + 1);
sum -= nums[left++];
}
}
return min == Integer.MAX_VALUE ? 0 : min;
}
}
执行耗时:1 ms,击败了 100.00% 的 Java 用户
内存消耗:48.7 MB,击败了 57.67% 的 Java 用户
不要以为 for 里放一个 while 就以为是 O(n^2) , 主要是看每一个元素被操作的次数,每个元素在滑动窗口进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是 O(n)。
904. 水果成篮
初次提交
2023 年 3 月 15 日
public int totalFruit(int[] fruits) {
int left = 0;
int max = 1;
int[] fruitType = new int[2];
fruitType[0] = fruits[left];
boolean type = false;// 表示还未找到第二种类型的水果
for (int right = 1; right < fruits.length; right++) {
if (!type && fruits[right] != fruitType[0]) { // 找到第二类水果了
fruitType[1] = fruits[right];
type = true;
max = right - left + 1 > max ? right - left + 1: max;
} else if (type && fruits[right] != fruitType[0] && fruits[right] != fruitType[1]) {
// 出现第三种类型的水果了
// 将第二种水果类型更换为新碰到的水果类型
fruitType[1] = fruits[right];
// 将第一种水果类型更换为上个窗口遇到的最后一种水果
fruitType[0] = fruits[right - 1];
// 开始右移窗口左指针
left = right - 1;
while (left > 0) {
if (fruits[left - 1] == fruitType[0]) left--;
else break;
}
} else max = right - left + 1 > max ? right - left + 1: max;
}
return max;
}
执行耗时:5 ms,击败了 96.73% 的 Java 用户
内存消耗:50.4 MB,击败了 68.02% 的 Java 用户
这道题看似很复杂,其实质上就是:找某个数组的只包含最多两种数字的最长子数组。
因为没有具体的判断条件,所以每次右指针右移过后都需要判断窗口长度与 max 值的大小
76. 最小覆盖子串
初次提交
/螺旋矩阵/
59. 螺旋矩阵 II
初次提交
2023 年 3 月 15 日
public int[][] generateMatrix(int n) {
int[][] ans = new int[n][n];
int row = 0;
int col = 0;
int direction = 1;
int topBorder = -1, rightBorder = n, bottomBorder = n, leftBorder = -1;//4 个边界
for (int i = 1; i <= n * n; i++) {
ans[row][col] = i;
// 根据方向来改变行列
switch (direction) {
case 1: // 从左到右,列增加
if(col + 1 == rightBorder) { // 如果已经到达右边界,改变方向
row ++; // 这里可以放心地增加行,因为如果右边是右边界,下面不可能会是下边界,否则循环就结束了
topBorder ++;//上边界下移
direction = 2;//方向改变
} else col ++;
break;
case 2: // 从上到下,行增加
if(row + 1 == bottomBorder) {
col --;
rightBorder --;
direction = 3;
} else row ++;
break;
case 3: // 从右到左,列减少
if(col - 1 == leftBorder) {
row --;
bottomBorder --;
direction = 4;
} else col --;
break;
case 4: // 从下到上,行减少
if(row - 1 == topBorder) {
col ++;
leftBorder ++;
direction = 1;
} else row --;
break;
}
}
return ans;
}
执行耗时:0 ms,击败了 100.00% 的 Java 用户
内存消耗:39.3 MB,击败了 86.48% 的 Java 用户
没有技巧,全是体力活。
54. 螺旋矩阵
初次提交
2023 年 3 月 15 日
public List<Integer> spiralOrder(int[][] matrix) {
int direction = 1;
int m = matrix.length;
int n = matrix[0].length;
int row = 0, col = 0;
int topBorder = -1, rightBorder = n, bottomBorder = m, leftBorder = -1;
List<Integer> ans = new ArrayList<>();
for (int i = 0; i < m * n; i++) {
ans.add(Integer.valueOf(matrix[row][col]));
switch(direction) {
case 1: // 从左往右
if (col + 1 == rightBorder) {
row ++;
direction = 2;
topBorder ++;
} else col ++;
break;
case 2: // 从上往下
if (row + 1 == bottomBorder) {
col --;
direction = 3;
rightBorder --;
} else row ++;
break;
case 3: // 从右往左
if (col - 1 == leftBorder) {
row --;
direction = 4;
bottomBorder --;
} else col --;
break;
case 4: // 从下往上
if (row - 1 == topBorder) {
col ++;
direction = 1;
leftBorder ++;
} else row --;
break;
}
}
return ans;
}
执行耗时:0 ms,击败了 100.00% 的 Java 用户
内存消耗:39.4 MB,击败了 78.87% 的 Java 用户