【双指针算法】

本文详细介绍了双指针算法的概念,包括快慢指针、左右指针的应用,并通过实例讲解如何使用双指针解决LeetCode上的“移动零”、“复写零”和“快乐数”问题。展示了双指针在数组和链表操作中的高效性。
摘要由CSDN通过智能技术生成
博主头像

🌠个人主页 : @赶路人- -
🌌个人格言 :

要努力成为梧桐,让喜鹊在这里栖息。
要努力成为大海,让百川在这里聚积。

Never give up, Never lose the opportunity to succeed.

前言

欢迎来到我的算法栏目!
从今天开始,我将分享各种有关算法的知识和技巧,希望能帮助大家提升算法设计和分析的能力。
如果你也对算法感兴趣,欢迎加入我的算法栏目。让我们一起开始这个令人兴奋的学习之旅吧!
↖(▔^▔)↗

双指针算法是什么

双指针算法(Two Pointers Algorithm)通过使用两个指针,分别从数组或链表的头部和尾部开始向中间移动,常可以用来解决数组或链表问题中的多种问题。其中包括:判断是否存在满足某条件的两个数、寻找满足某条件的连续子序列、判断一个字符串是否为回文串以及将一个数组或链表按照某种方式重新排序等。双指针算法的时间复杂度通常为 O(n),因此在处理大规模数据时,使用双指针算法可以有效提高算法的效率。根据指针移动的方式,双指针算法可以分为三类:快慢指针、左右指针和对撞指针。不同类型的双指针算法适用于不同类型的问题,使用时需要根据问题的具体情况来选择合适的指针移动方式。

  1. **快慢指针(Fast and Slow Pointers)**通常用于链表问题中,如判断链表是否有环或找到链表的中间节点等。通过使用两个指针,一个快指针和一个慢指针,快指针每次移动2步,慢指每次移动1步,当快指到达链表尾部时,慢指针就到达了链表中间位置。
  2. **左右指针(Left and Right Pointers)**通常用于数组问题中,如在有序数组中查找目标元素等。通过使用两个指针,一个左指针和一个右指针,左指针从数组头部开始向右移动,右指针从数组尾部开始向左移动,根据具体问题来移动指针,最终得出结果。
  3. **对撞指针(Two Pointers Approach)**通常用于有序数组或链表问题中,如判断回文字符串或找到两个数的平方和等。通过使用两个指针,一个指向数组或链表的头部,另一个指向尾部,根据具体问题来移动指针,最终得出结果。

不同类型的双指针算法适用于不同类型的问题,使用时需要根据问题的具体情况来选择合适的指针移动方式。

1.移动零

题目来源LeetCode : https://leetcode.cn/problems/move-zeroes/submissions/497790642/

1.1题目解析

题目描述: 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]

1.2解题思路

两个指针:
current(最近) destination(目的地)
cur : 从左向右扫描数组, 遍历数组 .
dest : 已处理的区间内 , 非零元素的最后一个位置 .

在cur移动一次处理一个位置 , 即cur左边为处理过的 , 右边为待处理的 .
在这里插入图片描述
在cur处理过的区间 , 用dest分为非零元素和零元素 , 即dest左边为非零元素右边为零元素 .
在这里插入图片描述
由此在处理的过程中 , 可以用两个指针分成三个区间
[0 , dest] 非零元素 , [dest+1 , cur - 1 ] 零元素 , [cur , n - 1]待处理 .

这里可能有小土豆有疑问了 , 第二个区间右边为啥是 cur - 1 呢 , 为啥不是cur , 没事我必给你讲懂 . 首先回到cur指针的含义上 , cur是从左向右扫描的 , 因此cur的左边表示处理过的 , 那么处理过的最右边不就是cur - 1 了吗 . 然后待扫描的就是[cur , n - 1] .

所以当cur移动到n 位置时即处理完毕 .

当cur到了n位置时那么 [ cur , n - 1]位置是不是已经没有待处理元素.

在这里插入图片描述

那么具体怎么处理呢 下面我以一个例子来讲 .
初始化 :

初始化指针

cur初始化为0 , 应该是比较好理解的 , cur从左往右扫描嘛 , 那就从第一个元素开始 . 那么dest为什么初始化为 - 1 呢 . 区间[0 ,dest]不是表示非零元素吗 , 那么一开始还没有非零元素 , 那么就可以初始化为 - 1

首先cur移动 , 那么就会有两种情况cur的位置元素是 0 和非 0 , 当cur位置是 0 时为了保证前面区间符合条件cur向右移动一位即可(移动完区间[dest , cur - 1]都是 0元素) , 当元素是非 0 时 那么这个元素就要移动到区间 [0,dest] 里面 , 即移动到dest + 1指向的位置 , 因为区间元素多了一个即dest要 dest++ .

当cur位置元素为 0 时 , 区间无需处理 cur++即可 .
在这里插入图片描述
当cur位置元素非零时 , 首先dest++ (dest指向非零区间的最后一位元素 , 此时非零区间要多加一位元素) ,
然后交换cur元素和dest元素 , 最后cur++(cur在向后移动) .
在这里插入图片描述

总结
cur从左往右遍历过程中
1. 遇到 0 元素 : cur++;
2. 遇到非 0 元素 :
dest++;
swap(dest , cur);
cur++;

相关知识点 : 双指针的思想是快速排序里面最核心的 , 仅需把第一个区间改为 <=tmp , 第二个区间为 >tmp 即可 .

1.3代码实现

java代码示例

    public void moveZeroes(int[] nums) {
        for(int cur = 0,dest = -1;cur<nums.length;cur++){
            if(nums[cur] != 0){
                dest++;
                int tmp = nums[cur];
                nums[cur] = nums[dest];
                nums[dest] = tmp;
            }
        }
    }

2.复写零

题目来源LeetCode : https://leetcode.cn/problems/duplicate-zeros/solutions/1604450/fu-xie-ling-by-leetcode-solution-7ael/

2.1题目解析

题目描述 : 给你一个长度固定的整数数组 arr ,请你将该数组中出现的每个零都复写一遍,并将其余的元素向右平移。
注意:请不要在超过该数组长度的位置写入元素。请对输入的数组 就地 进行上述修改,不要从函数返回任何东西。

示例 1:
输入: arr = [1,0,2,3,0,4,5,0]
输出:[ 1,0,0,2,3,0,0,4]
解释: 调用函数后,输入的数组将被修改为:[1,0,0,2,3,0,0,4]
示例 2:
输入: arr = [1,2,3]
输出:[1,2,3]
解释: 调用函数后,输入的数组将被修改为:[1,2,3]

2.2解题思路

cur指针 : 移动 , 遍历数组 .
dest指针 : 执行复写操作 .

解法:先更据"异地"操作 , 然后化为双指针下的"就地"操作 . 在新建一个数组时:

博主头像

当cur位置为非零则复写一次 , 即 arr[++dest] = arr[cur++] ; 当cur位置为零时则复写两次 ,
即arr[++dest] = arr[cur] ;arr[++dest] = arr[cur++] ;
在这里插入图片描述
但是本题在进行"就地"操作时 , 会发生dest指针走的比cur , 这就会发生cur指针还没有遍历到的位置就已经被复写了 . (这句话比较绕 , 请看下面例子)
在这里插入图片描述
这个时候2的位置还没有复写就已经被覆盖了

哎 ! 所以这道题双指针不可以用了吗? 可以写在双指针文章这里欸.哈哈哈
不是的 , 既然咱们从左到右不可以 , 那么就可以试试从右到左 .

初始化dest指向最后一位数即0 , cur指向最一位复写的数在这个例子中就是4啦 .
在这里插入图片描述
这样下去就会发现 , 是可以实现复写的 ,
**那么 , 为什么呢 , 为什么从左到右不可以 , 从右到左又可以了呢? **
可以发现在从左到右复写时 , dest指针会跑到了cur指针的前面去了 , 这样就会发生某个数还没扫描到就已经被复写操作覆盖了 , 改变了原来题目的复写内容 . 而从右到左则不会因为我们已经确定了cur指向最后一个需要复写的数 , 这样dest指针就不会跑到cur指针的前面去 , 这样就不会改变需要复写的内容 .

所以这道题的解题步骤是
1.找到最后一位需要复写的位置 .
2.从右向左复写 .

第2步其实就是上面模拟的从右到左的过程 , 所以下面就不在讲解 , 相信经过第一题大家已经可以轻松的搞定 , 下面我们就开始第1步步骤如下

1.1判断cur位置 的值 .
1.2决定dest向右移动一步还是两步
(cur位置值为非零移动一步 零则移动一步)
1.3判断一下dest是否已经到结束位置
1.4 cur++ ;

初始化:

cur = 0 ;
dest = -1 ;
//为什么dest要等于-1 呢 ,因为当dest到达数组最后一位时就可以停止 , 此时cur就指向了需要复写的最后一位数 

注意! 有特殊情况 , 请看例子 .
在这里插入图片描述

例子中可以看到cur指针没有问题 , 指向了最后一位需要复写的数 , 但是dest指针因为最后一位需要复写的数是0最后移动两步导致越界了 .
所以我们要处理一下这个边界情况 .
在发生dest越界的情况 , 一定是因为最后一位是0 . 所以只要最后一位复写一次0 , 然后cur向左移动一步 , dest向左移动两步即可.
以下是处理代码 .

arr[ n-1 ]  = 0 ;
cur  -=  1 ;
dest  -= 2 ;

至于第2步从右向左复写0 , 相信看过前面之后难度不大 ,以下直接给过程.
2.1判断cur位置的值
2.2
cur值为0 , arr[dest–] = 0;arr[dest] = 0;
cur值不为0 , arr[dest–] = arr[cur]
2.3 cur–;dest–;

2.3代码实现

java代码示例

public void duplicateZeros(int[] arr) {
       int cur = 0,dest = -1,n = arr.length;
       //找最后一位需要复写的数
        while(cur < n){
            if(arr[cur] == 0)
                dest += 2;
            else
                dest ++;
            if(dest >= n-1) break;
            cur ++;
        }
       //处理边界情况
       if(dest == n){
           arr[n - 1] = 0;
           cur -= 1; dest -= 2;
       }
        //从右向左完成复写操作
        while(cur >= 0){
            if(arr[cur] == 0){
                arr[dest--] = 0;
                arr[dest--] = 0;
                cur--;
            }
            else
                arr[dest--] = arr[cur--];
        }
    }

3.快乐数

题目来源LeetCode : https://leetcode.cn/problems/happy-number/description/

3.1题目解析

题目描述 : 编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。

题目示例
题目解释
示例一
在这里插入图片描述

示例二
在这里插入图片描述

如示例二图所示 在到达20的时候 又回到了之前写过的4 , 即后面会一直在这个环里进行死循环 .
如果这个数陷入了死循环 , 即不是快乐数 ,返回false.

3.2 解题思路

其实两种情况可以抽象成一种情况 , 即这个数在经过操作后 , 一定会进入到一个环里面 , 只不过是快乐数时那个环里面的数全部都是1 ,
在这里插入图片描述
相信学过数据结构的土豆 , 做过一道判断链表是否有环的题目 , 和这道很类似 , 即那道是判断是否有环 , 这里是判断环里的数字是否是1 .
在环形链表的题目中 , 我们可以用快慢双指针的办法来解决 , 那么这道题目可能也可以 , 下面我们来试一下 .

解法: 快慢双指针
(双指针是一种思想 , 并不一定要是指针来实现 ,在数组中可以用下标来表示 , 甚至可以直接用一个数来充当指针)
1. 定义快慢双指针 .
2. 慢指针每次向后移动一步 , 快指针每次向后移动两步 .
3. 判断相遇时候的值是否为

3.3代码实现

java代码示例

public int bitSum(int n)//返回n每一位数上的平方和
{
    int sum = 0;
    while(n != 0)
    {
        int t = n % 10; // 取n的最后位数
        sum += t * t; // 将最后位数的平方加到sum上
        n /= 10; // 将n除以10,去掉最后位数
    }
    return sum ;
}

public boolean isHappy(int n) {
    int slow = n, fast = bitSum(n); //slow为线中的第一个数 , fast为第二个数
    while(slow != fast){ // 当slow和fast相等时,退出循环(找到了一个重复数)
        slow = bitSum(slow); // slow更新数线中的后一个数
        fast = bitSum(bitSum(fast)); // fast更新为数线中的后两位数
    }
    return slow == 1; // 判断这个重复数是否是1
}

3.4拓展知识(不重要)

以下内容仅为扩展 , 对于数学证明兴趣不大的同学可以直接跳过 ,没有影响 .
在快乐数的定义中第二句话是"然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。".
这道题目在开始的时候是没有这句话的 , 如果没有这句话 ,大家可以细想一下是不是题目还会有第三种情况 ,即如果这个数没有成环是一条直线下去呢 , 如果需要考虑这种情况的话那么这道题目的难度会直接飙升 .
下面我们来证明一下为什么一定会有环 .
首先要用到的是 鸽巢原理(抽屉原理) : n个巢 , n+1个鸽子 ,那么至少有一个巢里面的鸽子数大于1 .
在本题中数据的最大范围是int的 即 2 ^ 31-1
231-1= 2147483648≈2.1 ✖109 << 9999999999(10个9)
9999999999经过 f 操作后(题目中每个位置上的数字的平方和) 等于9^2 ✖10 = 810
因为2.1 ✖109 << 9999999999所以题目中输入的n经过一次f变化后一定会落在区间[ 1, 810 ]中的 , 并且以后的每次变化也落在[ 1, 810 ]区间中 .
区间[1 , 810]里面一共有810个数 , 那么当n经过811次f变化后一定会出现重复的(原因:鸽巢原理) , 所以后面一定会出现环 .
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值