算法通过村 | 第三关 | 双指针的讲解与应用(白银篇)

啥是双指针?

先来说说啥是双指针

        双指针法是一种常用的算法技巧,它使用两个指针在数组或链表中按照特定的条件进行遍历、搜索或操作。

在双指针法中,一般会设置两个指针,通常称为快指针和慢指针。快指针和慢指针的初始位置可以相同,也可以有一定间隔,根据问题的要求决定。然后,根据特定的条件,快指针和慢指针开始遍历、移动或交互,直到满足某个条件为止。

以下是双指针法的一些常见应用场景:

1. 数组或链表的遍历:可以使用两个指针指向不同的位置,分别遍历数组或链表的不同部分。


2. 链表中的环检测:设置快指针和慢指针,快指针一次移动两步,慢指针一次移动一步,如果存在环,快指针最终会追上慢指针。


3. 两数之和:在有序数组中查找两个数,使它们的和等于给定的目标值,可以使用双指针,一个指向数组的最左端,一个指向数组的最右端,根据两个指针指向的数的和与目标值的比较结果,调整指针的位置。


4. 反转数组或链表:可以使用双指针,一个指向起始位置,一个指向末尾位置,交换对应位置的元素,然后移动指针,继续交换,直到指针相遇。


5. 快慢指针:在解决一些特定问题时,可以使用快指针和慢指针来跟踪数组或链表中的某些特定元素或位置。

        双指针法的优势在于可以在一次遍历的过程中解决问题,时间复杂度通常为O(n),其中n是数组或链表的长度。它是一种节省空间的解题方法,因为只使用了常数级别的额外空间。

需要注意的是,在使用双指针法解决问题时,要仔细考虑指针的初始位置、移动的条件和边界条件,以确保算法的正确性和高效性。

         再通俗一点讲就是,双指针其实是两个变量,他可能并不是一个真正的指针,但在处理上面所说的几种情况上,是非常简单好用的。具体的例子我们就在下面的题目中来了解吧!


删除元素

移除元素

27. 移除元素 - 力扣(LeetCode)

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

输入: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],也会被视作正确答案。
 思路

        有的hxd可能说了,多余的元素,直接删掉不就行了。但是我们要知道数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。

        在删除的时候,从删除位置开始的所有元素都要向前移动,所以这题的关键是如果有很为val的元素的时候,要怎么避免元素反复向前移动!双指针是一个不错的选择~

暴力解法

再此之前先来看一下啊暴力解法,即用两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。当外层遇到 val 时,内层循环开始移动数组元素,知道遍历完成!

代码如下:

class Solution {
    public int removeElement(int[] nums, int val) {
        int size = nums.length;
        for (int i=0;i<size;i++)
        if(nums[i] == val){
            for(int j=i+1;j<size ;j++){
                nums[j-1] = nums[j];
            }
            i--;
            size--;
        }
        return size;
    }
}

// 时间复杂度O(n^2)  空间复杂度O(1)
双指针解法1(快慢指针)

便捷之处为,通过一个for循环完成两个for循环的工作,大大节省了时间。

对于此题:

  • 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
  • 慢指针:指向更新 新数组下标的位置

 开始时两个指针都在0处,具体代码如下:

class Solution {
    public int removeElement(int[] nums, int val) {
        // 快慢指针
        int slow = 0;
        for(int fast=0;fast < nums.length; fast++){
            if(nums[fast] != val){
                nums[slow] = nums[fast];
                slow++;
            }
        }
        return slow;
    }
}
// 时间复杂度:O(n)
// 空间复杂度:O(1)
双指针解法2(相对双指针)

因为本题说了,元素的顺序可以改变,我们才可以使用相对双指针这个方法,因为它是将右侧不为val的值来替换左侧为val的值。具体代码如下:

 public int removeElement(int[] nums, int val) {
        int left = 0;
        int right = nums.length - 1;
        while(right >= 0 && nums[right] == val) right--; 
        //将right移到从右数第一个值不为val的位置
        while(left <= right) {
            if(nums[left] == val) { //left位置的元素需要移除
                //将right位置的元素移到left(覆盖),right位置移除
                nums[left] = nums[right];
                right--;
            }
            left++;
            while(right >= 0 && nums[right] == val) right--;
        }
        return left;
    }

删除数组中的重复项

26. 删除有序数组中的重复项 - 力扣(LeetCode)

给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:

  • 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
  • 返回 k 。
nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2,并且原数组 nums 的前两个元素被修改为 1, 2 不需要考虑数组中超出新长度后面的元素。

 怎么样我的老伙计们,会不会使用双指针了。嗯?不会,哦,我的老伙计,你一定是在跟我开玩笑吧~doge,好吧,我也不咋会。。。。

解法

代码如下:

public int removeDuplicates(int[] nums){
    int slow = 0;
    int n = nums.length;
    if(nums == null || n ==0){
        return 0;
    }
    for(int fast = 1;fast < n;fast++){
        if(nums[slow] != nums[fast]){
            slow ++;
            nums[slow] = nums[fast];
        }
    }
    return slow + 1;
}

也可以这样:

 int slow = 1;
    int n = nums.length;
    for(int fast = 0;fast < n;fast++){
        if(nums[fast] != nums[slow-1]){
            nums[slow] = nums[fast];
            slow++;
        }
    }
    return slow;

再来个拓展,同样也是删除,只不过是又添加了一些限定条件

删除重复项||

80. 删除有序数组中的重复项 II - 力扣(LeetCode)

给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]
解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2,3 。 不需要考虑数组中超出新长度后面的元素。

         都有出现两次的了,那离出现3次,5次的还远吗,我们不放提炼出一个公式来,当然这个公式值对应该题的场景下,老老实实的使用双指针还是很赞的。

通用模板

        举个板栗,有这样一个数组[1,1,1,2,2,2,2,3,4],首先让前两位保留就得到了1,1  ,然后对后面的继续进行遍历,能够流下来的前提是当前位置与前面k个元素不同,这样我们就跳过了多余的元素,以此类推就可以得到如下代码:

public removeDuplicate(int[] nums){
    return process(nums,2);
}
int process(int[] nums,int k){
    int u  = 0;
    for(int x : nums){
        if(u < k || nums[u-k] != x) nums[u++] = x;
    }
    return u;
}

使用 foreach 循环遍历数组 nums 中的每个元素 x。在循环体中,首先判断 u 是否小于 k,如果是,则表示当前元素是一个新的不重复元素,将其放在数组中第 u 个位置,并将 u 向后移动一位。

如果 u 大于等于 k,则需要判断当前元素 x 是否与前 k 个元素相等。如果相等,则说明当前元素是一个重复元素,需要跳过,不将其放入新数组中。如果不相等,则说明当前元素是一个新的不重复元素,将其放在数组中第 u 个位置,并将 u 向后移动一位。

         肿么样,是不是简单又好记,如果实在想不起来双指针就用这个解决也会让人眼前一亮!

再来看看双指针解法吧~

双指针解法
public int removeDuplicates(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    
    int j = 0; // j 表示当前已经去重后的数组的最后一个位置
    int count = 1; // 当前元素的出现次数,初始化为 1
    
    for (int i = 1; i < nums.length; i++) {
        if (nums[i] == nums[j]) {
            count++;
        } else {
            count = 1;
        }
        
        if (count <= 2) {
            j++;
            nums[j] = nums[i];
        }
    }
    
    return j + 1;
}

 元素奇偶排序

按奇偶排序数组

905. 按奇偶排序数组 - 力扣(LeetCode)

给你一个整数数组 nums,将 nums 中的的所有偶数元素移动到数组的前面,后跟所有奇数元素。

返回满足此条件的 任一数组 作为答案。

示例 1:

输入:nums = [3,1,2,4]
输出:[2,4,3,1]
解释:[4,2,3,1]、[2,4,1,3] 和 [4,2,1,3] 也会被视作正确答案。

 从题中可以了解到我们只需要关注奇偶问题,不用管顺序问题,把不就好办了,heihei。莽夫(暴力)解法这里就不在介绍了,前面也有示例,我们要讲的是双指针法。

前面我们也说了一种名为 “相对双指针” 解法,它非常适合本题,大概得思路如下:

        定义两个指针,一个指向数组的头部 left,一个指向数组的尾部 right。然后从头开始遍历数组,如果遇到奇数,则将该元素与 right 指向的元素进行交换,并将 right 向前移动一位;如果遇到偶数,则将 left 向后移动一位。重复这个过程,直到left指向的元素的索引大于等于right指向的元素的索引为止。这样就可以将奇数移到数组的前面,偶数移到数组的后面,并且保持奇数和偶数的相对顺序不变。

一句话就是,l 为奇数,l r 交换且 r 向前移动一位,为偶数,向后移动一位。

代码如下:

public int[] sortArrayByParity(int[] nums) {
        int left  = 0;
        int right = nums.length - 1;

        while(left < right){
            if(nums[left] % 2 >nums[right] % 2){
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
            }
            if(nums[left] % 2 == 0) left ++;
            if(nums[right] % 2 == 1) right--;
            }
        return nums;
    }

如果要是考虑数组中奇偶各部分的顺序的话,可以进行如下调整,创建一个辅助数组,分别比较左边和右边的,left 比较左边,right比较右边。具体的代码就不展示了。

题目链接:调整数组顺序使奇数位于偶数前面(一)_牛客题霸_牛客网 (nowcoder.com)

数组轮转

189. 轮转数组 - 力扣(LeetCode)

 快捷解法:先把整个数组都翻转,之后根据k将数组分成两组,最好将两个再次翻转就得到了结果。代码如下:

public void rotate(int[] nums,int k){
    int len = nums.length;
    // 轮转的次数判断
    k %= len;
    reverse(nums,0,len - 1);
    reverse(nums,0,k - 1);
    reverse(nums,k,len - 1);
}

public void reverse(int[] nums,int start,int end){
    while(start < end){
        int tmp = nums[start];
        nums[start] = nums[end];
        nums[end] = tmp;
        start += 1;
        end -= 1;
    }
}

数组区间问题

228. 汇总区间 - 力扣(LeetCode)

给出一个简单的双指针解法,各位hxdm自己看吧

public static List<String> summaryRanges(int[] nums){
    List<String> res = new ArrayList<>();
    int len = nums.length;
    int slow = 0;
    for(int fast = 0;fast < len;fast++){
        if(fast + 1 == len || nums[fast] + 1 != nums[fast + 1]){
            StringBuilder sb = new StringBuilder();
            sb.append(nums[slow]);
            if(slow != fast){
                sb.append("->").append(nums[fast]);
            }
            res.add(sb.toString());
            slow = fast + 1;
        }
    }
    return res;
} 

 


OK到此为止已经学习了多种关于数组的题目,接下来还会继续分享~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

計贰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值