双指针
双指针场景
例题
原地修改数组
数据有序,一个指针(slow)用来维护结果集,一个指针(fast)来遍历
26. 删除有序数组中的重复项
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
输入:nums = [1,1,2]
输出:2, nums = [1,2]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 3 * 104
-104 <= nums[i] <= 104
nums 已按升序排列
思路:
因为是有序数组去重复元素,我们应该想到用两个同向的双指针 fast和slow,slow用来指向放置不重复元素的位置,fast指向当前遍历到的位置。
当fast指向的位置和前一个元素不相同时,就可以将此数复制到slow指向位置,slow后移。
class Solution {
public int removeDuplicates(int[] nums) {
// 不重复元素插入的位置
int slow = 1;
// 遍历到的位置
int fast = 1;
int n = nums.length;
if (n == 0) {
return 0;
}
while(fast<n){
// fast和前一个元素不同,则移到slow处
if(nums[fast]!=nums[slow-1]){
nums[slow] = nums[fast];
slow++;
}
// fast和前一个相同, 则继续向后走 即忽略
fast++;
}
return slow;
}
}
80. 删除有序数组中的重复项(删除k个重复元素)
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]
解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3。 不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:nums = [0,0,1,1,1,1,2,3,3]
输出:7, nums = [0,0,1,1,2,3,3]
解释:函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3。不需要考虑数组中超出新长度后面的元素
。
提示:
1 <= nums.length <= 3 * 10⁴
-10⁴ <= nums[i] <= 10⁴
nums 已按升序排列
思路:
i是保留的数组的位置,j是遍历nums数组的指针,判断当前j是否能保留就是看nums[j]和nums[i-k]是否相等
public int removeDuplicates(int[] nums) {
int n = nums.length;
if (n<=2){
return n;
}
int i = 2;
for (int j = 2;j<n;j++){
if (nums[j]!=nums[i-2]){
nums[i] = nums[j];
i++;
}
}
return i;
}
83.删除排序链表中的重复元素
给定一个已排序的链表的头
head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
示例 1:
输入:head = [1,1,2]
输出:[1,2]
示例 2:
输入:head = [1,1,2,3,3]
输出:[1,2,3]
public ListNode deleteDuplicates(ListNode head) {
if (head==null){
return null;
}
ListNode slow = head;
ListNode fast = head;
while (fast!=null){
if (slow.val!=fast.val){
slow.next = fast;
slow = slow.next;
}
fast = fast.next;
}
slow.next = null;
return head;
}
27.移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
public int removeElement(int[] nums, int val) {
int n = nums.length;
int slow = 0;
int fast = 0;
while (fast<n){
if (nums[fast]!=val){
nums[slow++] = nums[fast];
}
fast++;
}
return slow;
}
求和
这种求和问题 就是先排序,然后通过左右指针(left=0,right=n-1)移动 得到解
注意的就是去重复值
求和问题模板:团灭nSum问题
- 模板是两数之和去重复值,后面n数之和就是固定一个值调用n-1数之和
- 三数之和其实就是遍历nums[i],求两个数的和是target - nums[i],即在两数之和的基础上加了个for循环
- 四数之和 就是固定一个值,求三数之和
public int[] twoSum(int[] nums, int target) {
// 1、没排好序的先排序
Arrays.sort(nums);
// 2、左右指针查找target
int n = nums.length;
int left = 0, right = n-1;
// 答案:所有符合条件的元素值
List<List<Integer>> ans = new ArrayList<>();
while (left<right){
int sum = nums[left] + nums[right];
int leftValue = nums[left];
int rightValue = nums[right];
if (sum==target){
List<Integer> curAns = new ArrayList<>();
curAns.add(nums[left]);
curAns.add(nums[right]);
ans.add(curAns);
// 去除重复值
while (left<right && nums[left]==leftValue){
left++;
}
while (left<right && nums[right]==rightValue){
right--;
}
}else if(sum>target){
// 去除重复值
while (left<right && nums[right]==rightValue){
right--;
}
}else{
// 去除重复值
while (left<right && nums[left]==leftValue){
left++;
}
}
}
return ans;
}
剑指 Offer 57. 和为s的两个数字
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
示例 2:
输入:nums = [10,26,30,31,47,60], target = 40
输出:[10,30] 或者 [30,10]
限制:
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^6
思路: 注意做这种求和的 为了防止溢出应该用减的方式nums[l] == target-nums[r] 而不是加的方式
class Solution {
public int[] twoSum(int[] nums, int target) {
int l = 0,r = nums.length-1;
while(l<r){
if(nums[l]==target-nums[r]){
return new int[]{nums[l],nums[r]};
}
else if(nums[l]<target-nums[r]){
l++;
}
else{
r--;
}
}
return new int[0];
}
}
15. 三数之和
15. 三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
示例 2:
输入:nums = []
输出:[]
示例 3:
输入:nums = [0]
输出:[]
提示:
0 <= nums.length <= 3000
-105 <= nums[i] <= 105
思路:
先排序 ,然后就可以用双指针
这个题要注意的是去重, 由于我们的数组是排好序的,所以我们可以在指针移动的时候判断和下一个值是否相等
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> ans = new ArrayList<>();
int n = nums.length;
for (int i = 0; i < n; i++) {
int left = i+1;
int right = n-1;
while(left<right){
// 三数之和
int sum = nums[i]+nums[left]+nums[right];
// 左指针和右指针对应的值
int lValue = nums[left];
int rValue = nums[right];
if(sum==0){
ArrayList<Integer> lst = new ArrayList<>();
lst.add(nums[i]);
lst.add(nums[left]);
lst.add(nums[right]);
ans.add(lst);
// 去重
while(left<right && lValue==nums[left]){
left++;
}
while(left<right && rValue==nums[right]){
right--;
}
}
else if(sum>0){
// 去重
while(left<right && rValue==nums[right]){
right--;
}
}
else{
// 去重
while(left<right && lValue==nums[left]){
left++;
}
}
}
while(i<n-1 && nums[i]==nums[i+1]){
i++;
}
}
return ans;
}
16. 最接近的三数之和
给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。
示例:
输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。
提示:
3 <= nums.length <= 10^3
-10^3 <= nums[i] <= 10^3
-10^4 <= target <= 10^4
思路:
同样是求三数之和,只是变成了最接近的,所以就算一下ans和sum 减target
得到最接近的
注意这个也是有重复值的,也要注意去重
public int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);
int n = nums.length;
int ans = 10000; // 注意不能写成MAX.Integer 因为这样-负的target会越界
for(int i = 0;i<n;i++){
int left =i+1;
int right = n-1;
while(left<right){
int sum = nums[i]+nums[left]+nums[right];
int leftValue = nums[left];
int rightValue = nums[right];
if(Math.abs(ans-target)>Math.abs(sum-target)){
ans = sum;
}
if(sum==target){
return ans;
}
else if(sum>target){
while(right>left && nums[right]==rightValue){
right--;
}
}
else{
while(right>left && nums[left]==leftValue){
left++;
}
}
}
// 重复元素
while(i<n-1 && nums[i]==nums[i+1]){
i++;
}
}
return ans;
}
18. 四数之和
在三数之和的基础上加一层for循环
注意新加了溢出判断 所以在相加判断前要将nums[i]强转成long
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
List<List<Integer>> ans = new ArrayList<>();
int n = nums.length;
// 固定一个值
for (int i =0;i<n;i++){
// 求三数之和 为nums[a]+nums[b]+nums[c] == target - nums[i]
List<List<Integer>> threeAnsList = threeSum(nums,i+1,target - nums[i]);
for (List<Integer> threeAns:threeAnsList){
threeAns.add(nums[i]);
ans.add(threeAns);
}
// 去重复值
while (i<n-1 && nums[i]==nums[i+1]){
i++;
}
}
return ans;
}
public List<List<Integer>> threeSum(int[] nums, int start, int target) {
List<List<Integer>> res = new ArrayList<>();
int n = nums.length;
for (int i = start; i < n; i++) {
// -nums[i] == nums[j]+nums[k] 求两数之和是-nums[i]
int left=i+1,right = n-1;
while (left<right){
int leftValue = nums[left];
int rightValue = nums[right];
if ((long)nums[left]+nums[right]+nums[i]==target){
// 答案
List<Integer> curAns = new ArrayList<>();
curAns.add(nums[i]);
curAns.add(leftValue);
curAns.add(rightValue);
res.add(curAns);
// 去重
while (left<right && nums[left]==leftValue){
left++;
}
while (left<right && nums[right]==rightValue){
right--;
}
} else if((long)nums[left]+nums[right]+nums[i]>target){
// 去重
while (left<right && nums[right]==rightValue){
right--;
}
}else {
// 去重
while (left<right && nums[left]==leftValue){
left++;
}
}
}
// 去重
while (i<n-1 && nums[i]==nums[i+1]){
i++;
}
}
return res;
}
}
用左右指针维护子数组
这部分题目都是通过left和right来维护一个子数组,可行解有很多,下一个解可以在上一个可行解的基础上继续移动
209. 长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
思路:
通过left和right维护子数组的左右边界,没有达到条件右指针右移,达到条件后,左指针右移收缩
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int n = nums.length;
// left和right就是当前子数组的边界
int ans = Integer.MAX_VALUE;
int left = 0;
int right = 0;
// 当前窗口内的和
int sum = nums[left];
while(left<=right & right<n){
// 没有达到条件,窗口右移
if(sum<target){
right++;
if(right<=n-1)
sum+=nums[right];
}else{ // 找到可行解
ans = Math.min(ans,right-left+1);
// 窗口左边界右移 缩小窗口
sum-=nums[left];
left++;
}
}
return ans==Integer.MAX_VALUE?0:ans;
}
}
题解的:
class Solution {
public int minSubArrayLen(int s, int[] nums) {
int n = nums.length;
if (n == 0) {
return 0;
}
int ans = Integer.MAX_VALUE;
int start = 0, end = 0;
int sum = 0;
while (end < n) {
sum += nums[end];
while (sum >= s) {
ans = Math.min(ans, end - start + 1);
sum -= nums[start];
start++;
}
end++;
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
}
713. 乘积小于K的子数组*
给定一个正整数数组 nums。
找出该数组内乘积小于 k 的连续的子数组的个数。
示例 1:
输入: nums = [10,5,2,6], k = 100
输出: 8
解释: 8个乘积小于100的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于100的子数组。
说明:
0 < nums.length <= 50000
0 < nums[i] < 1000
0 <= k < 10^6
思路:
自己做的时候没有想到这样
遍历right,找到满足的左边界。这样下一个right就可以用前一个right
public int numSubarrayProductLessThanK(int[] nums, int k) {
if(k<=1){
return 0;
}
int left = 0;
int mul = 1;
int ans = 0;
int n = nums.length;
// 以right 为右边界,找到最小的left
for (int right=0; right<n;right++){
mul*=nums[right];
// 向右移动left,得到满足条件的left
while(mul>=k){
mul /= nums[left++];
}
// 得到以当前right为右边界的 子数组的个数
ans += right-left+1;
}
return ans;
}
845.数组中的最长山脉
我们把数组 A 中符合下列属性的任意连续子数组 B 称为 “山脉”:
B.length >= 3
存在 0 < i < B.length - 1 使得 B[0] < B[1] < … B[i-1] < B[i] > B[i+1] > … > B[B.length - 1]
(注意:B 可以是 A 的任意子数组,包括整个数组 A。)
给出一个整数数组 A,返回最长 “山脉” 的长度。
如果不含有 “山脉” 则返回 0。
示例 1:
输入:[2,1,4,7,3,2,5]
输出:5
解释:最长的 “山脉” 是 [1,4,7,3,2],长度为 5。
示例 2:
输入:[2,2,2]
输出:0
解释:不含 “山脉”。
提示:
0 <= A.length <= 10000
0 <= A[i] <= 10000
思路:从右向左,先找单调递增,找到后再找单调递减
public int longestMountain(int[] arr) {
int n = arr.length;
// 当前窗口的右边界
int right =n-1;
int ans = 0;
while(right>=0){
int cur = right;
while(cur>0 && arr[cur-1]>arr[cur]){
cur--;
}
// 已找到山脉右半部分
if(cur<right){
int mid = cur;
// 找山脉的左半部分
while(cur>0 && arr[cur-1]<arr[cur]){
cur--;
}
// 找到可行解
if(cur<mid){
ans = Math.max(ans,right-cur+1);
}
right=cur;
}
else{ // 以此right为右边界的没有可行解
right--;
}
}
return ans;
}
两个数组
对于两个有序数组,一人一个指针 进行比较
88. 合并两个有序数组
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
提示:
nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-109 <= nums1[i], nums2[i] <= 109
i: 从后往前遍历nums1,作为当前要填充的位置
l1,l2 分别指向各自的数组, 谁大就填谁
对于l2<0,说明l2已经都移到了nums1中 直接可以返回;当l1<0,要把nums2全部移过来。
public void merge(int[] nums1, int m, int[] nums2, int n) {
int l2 = n-1;
int l1 = m-1;
// 从后往前遍历,i指向当前要插入的位置
for (int i = m+n-1; i >= 0; i--) {
// nums2已经全部填到nums1中
if(l2<0){
return ;
}
// 谁大谁填进去
if((l1>=0 && nums2[l2]>nums1[l1])||l1<0){
nums1[i] = nums2[l2];
l2--;
}else{
nums1[i] = nums1[l1];
l1--;
}
}
}
349. 两个数组的交集
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
说明:
输出结果中的每个元素一定是唯一的。
我们可以不考虑输出结果的顺序。
思路:
由于不考虑输出结果的顺序,所以就把两个数组都排序,然后用分别用l1,l2对应两个数组 进行比较。 此外还要当前指针和前一个指针是否相同进行去重。
public int[] intersection(int[] nums1, int[] nums2) {
int n1 = nums1.length;
int n2 = nums2.length;
Arrays.sort(nums1);
Arrays.sort(nums2);
List<Integer> list = new ArrayList<>();
int l1=0,l2=0;
while(l1<n1 && l2<n2){
if(nums1[l1]==nums2[l2]){
list.add(nums1[l1]);
l1++;
l2++;
}
else if(nums1[l1]>nums2[l2]){
l2++;
}
else{
l1++;
}
while(l1>0 && l1<n1 &&nums1[l1]==nums1[l1-1]){
l1++;
}
while(l2>0 && l2<n2 &&nums2[l2]==nums2[l2-1]){
l2++;
}
}
int n = list.size();
int[] ans = new int[n];
for (int i = 0; i < n; i++) {
ans[i] = list.get(i);
}
return ans;
}
题解的思路:
去重部分不一样,就是在往ans 添加的时候判断是否重复,否则就不添加
public int[] intersection(int[] nums1, int[] nums2) {
int n1 = nums1.length;
int n2 = nums2.length;
Arrays.sort(nums1);
Arrays.sort(nums2);
int[] ans = new int[n1+n2];
int l1=0,l2=0,l=0;
while(l1<n1 && l2<n2){
if(nums1[l1]==nums2[l2]){
// 保证元素唯一性
if(l==0 || nums1[l1]!=ans[l-1]){
ans[l++] = nums1[l1];
}
l1++;
l2++;
}
else if(nums1[l1]>nums2[l2]){
l2++;
}
else{
l1++;
}
}
return Arrays.copyOfRange(ans,0,l);
}
922. 按奇偶排序数组 II
给定一个非负整数数组 A, A 中一半整数是奇数,一半整数是偶数。
对数组进行排序,以便当 A[i] 为奇数时,i 也是奇数;当 A[i] 为偶数时, i 也是偶数。
你可以返回任何满足上述条件的数组作为答案。
示例:
输入:[4,2,5,7]
输出:[4,5,2,7]
解释:[4,7,2,5],[2,5,4,7],[2,7,4,5] 也会被接受。
提示:
2 <= A.length <= 20000
A.length % 2 == 0
0 <= A[i] <= 1000
思路:
方法一:
用一个新数组,奇偶两个指针指向即将插入的位置
public int[] sortArrayByParityII(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
int j = 1,o = 0;
for (int i = 0; i < n; i++) {
if(nums[i]%2!=0){
ans[j] = nums[i];
j+=2;
}
else{
ans[o] = nums[i];
o+=2;
}
}
return ans;
}
方法二:
原地,用奇偶两个指针,从前往后遍历偶数指针,如果当前数为奇数,则开始遍历奇数,直到奇数位置为偶数,就交换
public int[] sortArrayByParityII(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
int j = 1;
for (int i = 0; i < n; i=i+2) {
// 是奇数就交换
if(nums[i]%2==1){
// 找到放置的奇数的位置
while(nums[j]%2==1){
j+=2;
}
swap(nums,i,j);
}
}
return nums;
}
public void swap(int[] nums, int i, int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}