15. 三数之和 (为0)
下标不能是重复的,必定右 i<l<r
1、先对数组排序(从小到大)
2、外层 i 遍历
-
- 如果 nums[i] > 0 ,整个 nums[] 后面的必定无法有三元组为0(排过序了,后面的 nums[l] nums[r] 都会大于0)。break。
- 如果 nums[i] = nums[i-1] 即当前这个值已经在前一个算过了,这个的答案是包含于前一个的,不用搞了。continue。
3、对于 nums[i] 后面的那段,用左右指针,l = i+1; r = len-1 往中间
三数之和 = nums[i] + nums[l] + nums[r]
-
- 如果三数之和小于0,那么左指针向右,右指针不动,使和变大
- 如果三数之和大于0,那么左指针不动,右指针向左,使和变小
- 如果三数之和等于0
- while nums[l] = nums[l+1] l++ 左指针和它右边的一样了,那它右边的可以直接替换掉它,不然会重复
- while nums[r] = nums[r-1] r-- 右指针和它左边的一样了,那它左边的可以直接替换掉它,不然会重复
- 添加结果,同时这个结果已经有了,那么继续往下走,l++; r--
class Solution { public List<List<Integer>> threeSum(int[] nums) { List<List<Integer>> result = new ArrayList(); // 先从小到大排序,注意对数组的排序用 Arrays.sort Arrays.sort(nums); int len = nums.length; // 为了确保不重复 i<r<l for (int i=0;i<len;i++) { // 已经排过序了,nums[i]>0 必定无法有三元组(后面的 j,k 都会大于 0) if (nums[i]>0) { break; } // 对于 -4 -1 -1 0 1 2, 第一个 -1 就已经把 -1,-1,2 和 -1,0,1 两个答案搞出来了 // 后面那个 -1 不用再继续搞了,必定被包含在前一个 -1 的答案集中,且答案集比前一个 -1 的答案集小 if (i != 0 && nums[i] == nums[i-1]) { continue; } // 对于 i 右边这段左右指针进行操作 // 左指针 int l = i+1; // 右指针 int r = len-1; while(l<r) { int sum = nums[i] + nums[l] + nums[r]; if (sum == 0) { // 左指针和它右边的一样了,那它右边的可以直接替换掉它。不替换的话会重复 // 注意加限制 l<r while (l<r && nums[l] == nums[l+1]) { l++; } // 右指针和它左边的一样了,那它左边的可以直接替换掉它。不替换的话会重复 // 注意加限制 l<r while (l<r && nums[r] == nums[r-1]) { r--; } result.add(Arrays.asList(nums[i], nums[l], nums[r])); // 添加完结果,别忘了继续移动左右执政 l++; r--; } // 左指针向右,右指针不动。可使和变大 else if (sum<0) { l++; } // 左指针不动,右指针向左,可使和变小 else if (sum>0) { r--; } } } return result; } }
16. 最接近的三数之和 (最接近 target)
与上面的三数之和(为0)类似,只是设置一个 minGap,记录三数之和和 target 的最小差值
每次有更小的出现,就去更新这个 minGap ,同时更新此时 minGap 对应的三数之和 sum4MinGap
class Solution { public int threeSumClosest(int[] nums, int target) { Arrays.sort(nums); // 注意这里取最大值用的是 Integer.MAX_VALUE int minGap=Integer.MAX_VALUE; // 注意题目里要求的返回是最小差值时的三数之和,不是最小差值 int sum4MinGap = 0; for (int i=0;i<nums.length;i++) { // 不重复算, 同样的数字之前已经算过了的话 if (i!=0 && nums[i] == nums[i-1]) { continue; } // 对于 nums[i] 后面的 i+1 到 len-1这段 int leftIndex = i+1; int rightIndex = nums.length-1; while(leftIndex < rightIndex) { int sum = nums[i] + nums[leftIndex] + nums[rightIndex]; int curGap = Math.abs(target - sum); if (sum < target) { // 偏小,左指针向右移,使和变大 leftIndex++; // 记录最小差值 if (curGap < minGap) { minGap = curGap; sum4MinGap = sum; } } else if (sum>target) { // 偏大,右指针向左移,使和变小 rightIndex--; // 记录最小差值 if (curGap < minGap) { minGap = curGap; sum4MinGap = sum; } } else if (sum == target) { return sum; } } } return sum4MinGap; } }
最接近的两数之和
先排序。使用对向双指针的方式,两个指针分别从头尾开始向中间走,若指针指向的两个值的和大于目标值,则将右指针往左一步;若两个值的和小于或等于目标值,则右指针往左一步,在循环过程中进行打擂台,比较当前的差值和记录的最小值,直到数组遍历结束。
167. 两数之和 II - 输入有序数组
class Solution { public int[] twoSum(int[] numbers, int target) { int l=0; int r=numbers.length-1; int[] result = new int[2]; while (l<r) { int sum = numbers[l] + numbers[r]; // l 向右移动使自己变大使和变大 if (sum < target) { l++; } // r 向左移动使自己变小使和变小 else if (sum > target) { r--; } // sum == target else { // +1 是因为题目里规定下标从1开始 result[0]=l+1; result[1]=r+1; while (l<r && numbers[l+1]==numbers[l]) { l++; } while (l<r && numbers[r-1]==numbers[r]) { r--; } // 规定只有一个答案,那先 Break。如果有多个答案,那还可以继续找 break; } } return result; } }
18. 四数之和
确定一个数后,剩下的用三数之和的解法即可。
即外层循环 i=0;i<len-3;i++ , 内层循环 j=i+1;j<len-2;j++, 然后左指针: leftIndex=j+1, rightIndex=len-1 ,后面就是常规的 while(leftIndex<rightIndex)......
注意这样是 i < j < leftIndex < rightIndex 的
为什么 i<len-3 ,因为要留三个位置给后面的 j、leftIndex、rightIndex
为什么 j<len-2,因为要留两个位置给后面的 leftIndex、rightIndex
注意求四数之和与 target 比较的时候,sum 的类型最好为 long ,因为有用例可能取值非常大,四数加起来会超过整数范围
【附加剪枝】,减少循环次数【紧急情况下不是必须的】
1、i>0 && nums[i]==nums[i-1] 前面那个i已经算过了,这一次可以 continue
2、j>i+1 && nums[j]==nums[j-1] j是从 j+1 开始的,即 i 的后半段。在这一段里如果前面那个 j 已经算过了,这一次可以 continue
3、(long) nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target break
第一个数确定后,与后面最小的三个数加起来还大于 target
【为什么这里 break 而不 continue 】
因为 对于同样的 i , 后续的循环继续对其它三个下标右移的话, 只可能比target更大, 所以可以跳过这个 i
因为 对于同样的 i+1 i+2 i+3 组合,i 没有左移变小的空间了(前面的 i 已经看过), 所以可以跳出整个循环
4、(long) nums[i] + nums[len-1] + nums[len-2] + nums[len-3] < target continue
第一个数确定后,与后面最大的三个数加起来还小于 target
【为什么这里 continue 而不 break】
因为 对于同样的 i , 后续的循环继续对其它三个下标左移的话, 只可能比target更小,所以要跳过这个 i
因为 对于同样的 len-1 len-2 len-3 组合,i 继续右移增大的话还可能反超 target, 所以不能跳出整个循环
。。。。。。
j 的内层循环与 3、4 同理
class Solution { public List<List<Integer>> fourSum(int[] nums, int target) { List<List<Integer>> res = new ArrayList(); Arrays.sort(nums); int len = nums.length; // 要留有三个位置给后面待确定的三个元素,因此 i<len-3 for (int i=0;i<len-3;i++) { // 确定的第一个数是重复元素,不重复算,跳过 if (i>0 && nums[i] == nums[i-1]) { continue; } // 第一个数确定后,与后面最小的三个数加起来还大于 target // 【有可能是非常大的数字,超过 int 范畴,因此要转为 (long)】 // 【为什么这里 break 而不 continue】 // 因为 对于同样的 i , 后续的循环继续对其它三个下标右移的话, 只可能比target更大,所以可以跳过这个 i // 因为 对于同样的 i+1 i+2 i+3 组合,i 没有左移变小的空间了(前面的 i 已经看过), 所以可以跳出整个循环 if ((long) nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target) { break; } // 第一个数确定后,与后面最大的三个数加起来还小于 target(这三个数加起来可能会超过 int 的范畴) // 【为什么这里 continue 而不 break】 // 因为 对于同样的 i , 后续的循环继续对其它三个下标左移的话, 只可能比target更小,所以要跳过这个 i // 因为 对于同样的 len-1 len-2 len-3 组合,i 继续右移增大的话还可能反超 target, 所以不能跳出整个循环 if ((long) nums[i] + nums[len-1] + nums[len-2] + nums[len-3] < target) { continue; } // 【j=i+1】开始 下面就是熟悉通用的三数之和 // 要留有两个位置给后面待确定的两个元素,因此 j<len-2 for (int j=i+1;j<len-2;j++) { // 确定了 i 后,让后面的 j 不重复 if (j>i+1 && nums[j] == nums[j-1]) { continue; } if ((long) nums[i] + nums[j] + nums[j+1] + nums[j+2] > target) { break; } if ((long) nums[i] + nums[j] + nums[len-1] + nums[len-2] < target) { continue; } // 下面就是熟悉通用的三数之和 int leftIndex = j+1; int rightIndex = len-1; while (leftIndex < rightIndex) { long sum = nums[i] + nums[j] + nums[leftIndex] + nums[rightIndex]; if (sum < target) { leftIndex++; } else if (sum > target) { rightIndex--; } else if (sum == target) { res.add(Arrays.asList(nums[i], nums[j], nums[leftIndex], nums[rightIndex])); // 因为是排序过的。左指针去重【这里就保证了四元组不重复】 // 注意条件限制 leftIndex<rightIndex while(leftIndex<rightIndex && nums[leftIndex] == nums[leftIndex+1]) leftIndex++; // 因为是排序过的。右指针去重【这里就保证了四元组不重复】 // 注意条件限制 leftIndex<rightIndex while(leftIndex<rightIndex && nums[rightIndex] == nums[rightIndex-1]) rightIndex--; // 【去重之后还要继续往下走】 leftIndex ++; rightIndex --; } } } } return res; } }
283. 移动零
我的解法:
- 左指针一直指的是最后结果的右边界,也就是说最后一定是 左指针以左的元素里没有 0 值,左指针以右的元素都是0值
- 如果左指针位置为 0,那么右指针一直向右直到找到一个不为 0 的值与左指针交换。left++ right++
- 如果左指针位置不为0,那么 left++ right++
更好的解法:右指针去寻找所有不为0的,左边视原数组为无物,从头挨个开始放
- 每当右指针处不为0 nums[right] != 0,就将它放到左指针的位置 nums[left] = nums[right] left++ right++
- 每当右指针为 0,就将只有 right++
- 最后 0~左指针 就是结果,把 左指针~末尾 全部置为0 即可
class Solution { public void moveZeroes(int[] nums) { int left = 0; int right = 0; while(left<nums.length && right<nums.length) { if (nums[right] != 0) { nums[left] = nums[right];
left++;
right++; } else { right++; } } for (int i=left;i<nums.length;i++) { nums[i] = 0; } } public void moveZeroes2(int[] nums) { int left = 0; int right = 0; while(left<nums.length && right<nums.length) { if (nums[left] == 0) { // 如果后面都是 0,那么 right 一直右移也找不到非 0 的就会移出边界 // 所以要在前面加上判断条件 right<nums.length while(right<nums.length && nums[right] == 0) { right++; } // 提前 break,不再 swap if (right == nums.length) { break; } swap(nums, left, right); left++; right++; } else { left++; right++; } } } private void swap(int[] nums, int index1, int index2) { int tmp = nums[index1]; nums[index1] = nums[index2]; nums[index2] = tmp; } }
26. 删除有序数组中的重复项
就是将不重复的元素复制到数组的左边
双指针:l r
如果 nums[l] == nums[r] 那么 r 继续向右寻找不与 nums[l] 相等 的元素
如果 nums[l] != nums[r] 那么 把这个不相等的 nums[r] 复制到 l 的右边。同时 l 和 r 同时向右 移动
class Solution { public int removeDuplicates(int[] nums) { // 左指针 int left=0; // 右指针负责找不重复的,不重复的话把不重复的依次复制到左指针的右边(占据重复元素的位置) int right=0; while(right < nums.length) { // index2 右指针找到的都是重复的,那么继续向右找 if (nums[right] == nums[left]) { right++; } // 找到不重复的复制到左指针的右边 else { // 考虑 1,2,3,4 如果左右指针挨着,说明它们之间本来就没有重复的,不用多赋值一遍 if (right - left>1) { nums[left+1] = nums[right]; } left++; right++; } } return left + 1; } }
56. 合并区间
输入:intervals = [[1,3],[2,6],[8,10],[15,18]] 输出:[[1,6],[8,10],[15,18]]
先按左边界排序,然后左右指针遍历新数组。左指针:上一个区间; 右指针:当前区间
- 如果当前区间左边界小于等于上一个区间的右边界,则说明是重叠边界,更新当前区间的左右边界,将当前区间的左右边界扩展为重叠区间的最大;
- 如果当前区间的左边界大于上一个区间的右边界,则将上一个区间放入结果数组。
遍历完毕,再将数组最后一个区间放入结果数组。
class Solution { public int[][] merge(int[][] intervals) { // 注意这里怎么根据起始元素排序 Arrays.sort(intervals, (e1, e2)->e1[0]-e2[0]); // 结果数组长度先设为和 intervels 一样 int[][] res = new int[intervals.length][2]; // 左指针 int l = 0; // 右指针 int r = 1; // 结果数组现在复制到哪儿了 int resNowIndex = 0; // l<r 为了使得最后一个元素能被复制,判断条件是 l while(l < intervals.length) { // 重叠区间:右指针区间的起始小于等于左指针区间的结束 [1,"3"]["2",6][8,10] if (r < intervals.length && intervals[r][0] <= intervals[l][1]) { // 将右指针的区间更新为融合了左边界之后的最大 [1,3][2,6][8,10]-->[1,3][1,6][8,10] if (intervals[r][1] >= intervals[l][1]) { intervals[r][0] = intervals[l][0]; } // 注意 [1,4][2,3] 要合并成 [1,4] else if (intervals[r][1] < intervals[l][1]) { intervals[r][0] = intervals[l][0]; intervals[r][1] = intervals[l][1]; } } else { // 不是重叠区间,就将上一个结果放到答案中,将 [1,6] 放到答案中 res[resNowIndex][0] = intervals[l][0]; res[resNowIndex][1] = intervals[l][1]; resNowIndex++; } r++; l++; } // 注意怎么去掉数组多出来的0 return Arrays.copyOf(res, resNowIndex); } }
31. 下一个排列
以求 12385764
的下一个排列为例:
首先从后向前查找第一个相邻升序的元素对 (i,j)
。这里 i=4
,j=5
,对应的值为 5
,7
:
然后在 [j,end)
从后向前查找第一个大于 A[i]
的值 A[k]
。这里 A[i]
是 5
,故 A[k]
是 6
:
将 A[i]
与 A[k]
交换。这里交换 5
、6
:
这时 [j,end)
必然是降序,逆置 [j,end)
,使其升序。这里逆置 [7,5,4]
:
因此,12385764
的下一个排列就是 12386457
。
最后再可视化地对比一下这两个相邻的排列(橙色是蓝色的下一个排列):
class Solution { public void nextPermutation(int[] nums) { int len = nums.length; int i=len-2; int j=len-1; // i,j 是从后往前查找的第一个升序对 while(i>=0) { if (nums[i]<nums[j]) { break; } i--; j--; } // 全部降序 是最大的情况 654321 的下一个重新变成 123456 if (i == -1) { Arrays.sort(nums); return; } // nums[i] nums[j] 第一个升序对。现在找 k,是从后往前 [j,end) 中最后一个大于 nums[i] 的 int k=len-1; while (k>j) { if (nums[k] > nums[i]) { break; } k--; } //查找区间包括 j, k 有可能等于j // 交换 nums[i] 与 nums[k] swap(nums, i, k); // 此时 [j,end) 【必然为降序,反转使其升序】。以后面尽可能大,靠近前面nums[i] 抬上去的数 for (int l=j,r=len-1;l<r;l++,r--) { // 逆序即 [j,end) :首尾两两交换,直到中间 swap(nums, l, r); } return; } private void swap(int[] nums, int a, int b) { int temp = nums[a]; nums[a] = nums[b]; nums[b] = temp; } }
27. 移除元素
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
这个题解里面有动画说明
class Solution { public int removeElement(int[] nums, int val) { // 慢指针用来指向当前结果集的最右端 int slowIndex = 0; // 快指针用来逐个遍历 int fastIndex = 0; // 快指针每一步都走 for (fastIndex = 0; fastIndex < nums.length; fastIndex++) { // 如果 nums[fastIndex] == val ,那么这个位置是要被移除的,不能放进结果集里,slowIndex 不动 if(nums[fastIndex] == val) { } // 如果 nums[fastIndex] != val ,那么这个位置可以被放进结果集里 else { nums[slowIndex] = nums[fastIndex]; slowIndex++; } } return slowIndex; } }
88. 合并两个有序数组
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
nums1 的长度为 m+n,有效长度只有m,后面的 n 是填充的0
不用开辟一个新数组来存放合并结果,只需 从后往前 遍历nums1 和 nums2,选出大的放在 nums1 的末尾 (官方题解有证明这样永远不会占掉 nums1 前面的)
注意写法(对于一个遍历完了,一个遍历没完的情况,不用写那么多判断分支):
- 循环条件是 p1>-1 || p2>-1 ,是 nums1 和 nums2 都没遍历完
- 如果 nums1 遍历完了 nums2 没遍历完,就是在循环里判断 p1==-1
- 如果 nums2 遍历完了 nums2 没遍历完,就是在循环里判断 p2==-1
class Solution { public void merge(int[] nums1, int m, int[] nums2, int n) { int p1 = m-1; int p2 = n-1; // 双指针,从尾到头遍历,将nums1和nums2中较大的放到 nums1 后面空出来的位置0 int tail = m+n-1; while(p1>-1 || p2>-1) { // nums1 已经遍历完了(p1==-1),nums2 还没完(p2>0) if (p1 == -1) { nums1[tail--] = nums2[p2--]; } // nums2 已经遍历完了(p2==-1),nums1 还没完(p1>0) else if (p2 == -1) { nums1[tail--] = nums1[p1--]; } // 下面三种都是 nums1 和 nums2 都没完的情况 else if (nums1[p1]>nums2[p2]) { nums1[tail--] = nums1[p1--]; } else if (nums1[p1]<nums2[p2]) { nums1[tail--] = nums2[p2--]; } else if (nums1[p1]==nums2[p2]) { nums1[tail--] = nums1[p1--]; nums1[tail--] = nums2[p2--]; } } }
75. 颜色分类
官方题解 解法三
class Solution { public void sortColors(int[] nums) { // 0 元素的下一个位置。整个数组的前面,向后增长 int p0=0; // 2 元素的下一个位置。整个数组的后面,向前增长 int p2=nums.length-1; // i 走到 p2 即 2 元素的起始位置,应该终止 for (int i=0;i<=p2;i++) { // 因为 p2 位置可能也是2,因此被错误地换到 i 之后。还要再放到末尾去 // 0 2 1 2 如 i是第一个2 p2是第二个2 i和p2交换后 p2--在1地位置,现在还要把 i 位置的2再放到末尾去 while(i<=p2 && nums[i] == 2) { swap(nums, i, p2); // 从后往前 p2--; } if (nums[i]==0) { swap(nums, i, p0); p0++; } } } public void swap(int[] nums, int a, int b) { int tmp = nums[a]; nums[a] = nums[b]; nums[b] = tmp; } }
11. 盛最多水的容器
在初始时,左右指针分别指向数组的左右两端
此时我们需要移动一个指针。移动哪一个呢?直觉告诉我们,应该移动对应数字较小的那个指针。这是因为,由于容纳的水量是由
两个指针指向的数字中较小值 ∗ 指针之间的距离
决定的。如果我们移动数字较大的那个指针,那么前者「两个指针指向的数字中较小值」不会增加,后者「指针之间的距离」会减小,那么这个乘积会减小。因此,我们移动数字较大的那个指针是不合理的。因此,我们移动数字较小的那个指针。
class Solution { public int maxArea(int[] height) { int left=0; int right=height.length-1; int max = 0; while (left<right) { int curCap = Math.min(height[left], height[right]) * (right - left); if (curCap>max) { max = curCap; } if (height[right] < height[left]) { right--; } else { left++; } } return max; } }
125. 验证回文串
class Solution { public boolean isPalindrome(String s) { int l=0; int r=s.length()-1; while (l<r) { // 右移到第一个有效的 while (l<r && !isValid(s.charAt(l))) { l++; } // 左移到第一个有效的 while (l<r && !isValid(s.charAt(r))) { r--; } // l==r 例如 aca。后面的例如 aa if (l == r || (l+1 == r && isEquals(s.charAt(l), s.charAt(r)))) { return true; } if (l<r && isEquals(s.charAt(l), s.charAt(r))) { l++; r--; } else { return false; } } if (l==r) { return true; } else { return false; } } // 判断是否为字母或数字 注意是 >= <= 而不是 > < private boolean isValid(char c) { if ((c>='A' && c<='Z') || (c>='a' && c<='z') || (c>='0' && c<='9')) { return true; } else { return false; } } // 判断大小写字母是否相等 private boolean isEquals(char a, char b) { if (a>='A' && a<='Z') { a = (char) (a - ('A'-'a')); } if (b>='A' && b<='Z') { b = (char) (b - ('A'-'a')); } return a==b; } }
392. 判断子序列
为短串中的每一个字符,依次在长串中寻找。i=0 j=0
如果相等,两个都移动;如果不相等,只长串中的那个移动,去为短串当前字符继续寻找相等的
class Solution { public boolean isSubsequence(String s, String t) { int i=0; int j=0; while (i<s.length() && j<t.length()) { // 如果相等,都移动一步 if (s.charAt(i) == t.charAt(j)) { i++; j++; } // 如果不相等,长的那个移动一步去为"子序列"寻找下一个数字 else { j++; } } // 如果 "子序列" 走完了,说明是子序列 return i==s.length(); } }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器