Java 算法解析 - 双指针

本文选取八道算法题目,对题目进行详解,对算法原理进行图文并茂的讲解,最终附上完整答案!

目录

[283 移动零](#283 移动零)

题目描述

算法原理

完整代码

[1089 复写零](#1089 复写零)

题目描述

算法原理

完整代码

[202 快乐数](#202 快乐数)

题目描述

算法原理

完整代码

[11 盛水最多的容器](#11 盛水最多的容器)

题目描述

算法原理

完整代码

[611. 有效三角形的个数](#611. 有效三角形的个数)

题目描述

算法原理

完整代码

[179. 和为 s 的两个数字](#179. 和为 s 的两个数字)

题目描述

算法原理

完整代码

15.三数之和

题目描述

算法原理

完整代码

[18 四数之和](#18 四数之和)

题目描述

算法原理

完整代码

完!


283 移动零

283. 移动零 - 力扣(LeetCode)

题目描述

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

即:将最终要把 nums 操作为:非零元素,都在零元素之前,且非零元素的相对顺序不变。且只能在这个数组本身中进行操作。

示例 1:

输入:nums = [0, 1, 0, 3, 12]

输出:[1, 3, 12, 0, 0]

示例 2:

输入 nums = [0]

输出:[0]

算法原理

可以将这道题目抽象为一种,数组分两块的题型,即,将数组的内容,分为左右两部分

当遇到这种数组划分,数组分块的时候,我们可以考虑双指针算法来解决问题(注意:这里的双指针,指的不是 C 中的地址指针,我们这里是利用数组的下标来充当指针)

dest 和 cur 两个指针,将整个数组分成了三个区间

0, dest\]:非零区间 \[dest + 1, cur - 1\]:0 \[cur, n - 1\]:待处理 ![](https://i-blog.csdnimg.cn/direct/dc62dcf1f9604a3fb1db54efc4701ae9.png) cur 指针向后移动,直到 cur 到了 n - 1 位置,即处理完所有数据后:也就可以返回数组了\~ ![](https://i-blog.csdnimg.cn/direct/dc168288678d45b9990db096065257bd.png) 回到我们的题目上:举出具体的栗子:\[ 0, 1, 0, 3, 12

dest 指针的位置,是已处理区间内,非零元素的最后一个位置,cur 最开始指向第一个位置,一个元素都没有处理,且也没有非零元素,所以 dest 的位置可以先置为 -1

cur 遇到 0 元素,直接向后移动一个元素,dest 应该指向的是已处理区间中的最后一个非零元素,dest 不变

当 cur 遇到非 0 元素,则根据题意,要让这个非零元素,加入到前面的非 0 区域。此时,我们可以让 dest 后移一个位置,指向 0 元素,然后交换非 0(1) 和 0 的位置。

cur 再 ++ 即可~

接下来的流程如上面一样,cur 遇到 0 元素,则直接向后 ++,当遇到 非 0 元素时,dest 指针 ++ 指向后面的 0 元素,然后交换 cur 指向的非 0 和 dest 指向的 0。直到 cur 遍历完整个数组~

完整代码

时间复杂度为 O(n) 空间复杂度为 O(1)

java 复制代码
class Solution {
    public void moveZeroes(int[] nums) {
        int cur = 0, dest = -1, n = nums.length;
        for (cur = 0; cur < n; cur++) {
            // cur找到非0元素,进行交换
            if (nums[cur] != 0) {
                int temp = nums[++dest]; // 注意是前置++,dest先++找到非0元素
                nums[dest] = nums[cur];
                nums[cur] = temp;
            }
        }
    }
}

1089 复写零

1089. 复写零 - 力扣(LeetCode)

题目描述

有一个长度固定的整数数组 arr,需将该数组中出现的每个 0 都复写一遍,并将其余的元素向右平移。

注意:不要再超过数组长度的位置写入元素,对输入的数组就地进行修改。

示例 1:

输入:arr = [1, 0, 2, 3, 0, 4, 5, 0]

输出:[1, 0, 0, 2, 3, 0, 0, 4]

示例 2:

输入:arr = [1, 2, 3]

输出:[1, 2, 3]

算法原理

根据题目描述,如果没有下面的就地修改的注意事项,我们很容易想到再开辟一个数组,用双指针来进行异地操作:

开辟一个新的数组,cur 用来扫描元素,dest 用来指向最终的位置,然后 cur 进行判断

cur 指向非 0,dest 直接将元素添加进入新的数组,cur 指向 0,dest 向右添加两个 0,直到 dest 超出数组的长度,cur 结束判断

但由于题目要求是就地 操作,我们可以将上面的异地双指针操作 ,优化为,双指针的就地操作

如果 dest 和 cur 都从数组最左侧开始遍历,因为题目要求的是复写 0,就会导致元素覆盖:

当 cur 指向 0 时候,dest 要向右复写两个 0,导致元素 2 被覆盖,当 cur 再向右移动,就找不到 2 了

可以从右向左移动 cur 和 dest,cur 最初指向最后一个复写的数据(4)的位置

这样从右向左复写,就可以实现任务

那么问题来了,如何找到最后一个复写的数据的位置呢? ==》 双指针算法

双指针算法中,dest 的位置,我们在上一题已经反复强调了,应该是已处理区域内的非 0 区间内的最后一个位置。最开始不知道这最后一个位置在哪里,所以先定义为 -1

cur 向后遍历数组,来决定 dest 向后一步还是两步

按照这样的步骤,cur 向后遍历,直到 dest 到结束位置,此时,cur 指向的就是最后一个复写的元素,此时,dest 指向了要开始抄写的位置:

补充:这里还会存在一种特例情况发生,如下:

如果数据为上面这样,当我们按照双指针算法向后寻找的时候,最终的结果是 cur 的位置正确,依然能找到最后一个复写的元素,但是 dest 的位置已经越界访问了~

即,这里需要处理一下,当最后一个 cur 为 0 的时候,dest 可能出现的越界情况

当 cur 指向的最后一个复写的元素为 0 时候,向左复写,dest 要将自己的位置,和 dest - 1 的位置,都复写为 0

但 dest 此时已经越界,指向的是 n 位置,我们仅仅需要 n - 1 位置变为0,然后让 cur--,dest -= 2 即可,就相当于,在这里提前处理了一位复写 0 的操作

总结步骤:

  1. 先用双指针算法,找到最后一个复写的元素

1.1 先判断 cur 位置的值

1.2 决定 dest 向后移动一步还是两步

1.3 判断 dest 是否到结束位置

1.4 cur++

处理一下边界情况:当最后一个 cur 为 0 的时候,dest 可能出现越界情况,此时仅需让 n - 1位置变为 0,然后 cur--,dest -= 2 即可~

  1. 从后向前,完成复写操作

完整代码

java 复制代码
class Solution {
    public void duplicateZeros(int[] arr) {
        int cur = 0, dest = -1, n = arr.length;
        // 1. 先找到最后一个需要复写的数
        while (cur < n) {
            if (arr[cur] == 0) {
                dest += 2;
            } else {
                dest++;
            }
            if (dest >= n - 1) break;
            cur++;
        }
        
        // 2. 处理一下边界情况
        if (n == dest) {
            arr[n - 1] = 0;
            cur--;
            dest -= 2;
        }
         
         // 3. 从后向前进行复写操作
         while (cur >= 0) {
            if (arr[cur] != 0) {
                arr[dest--] = arr[cur--];
            } else {
                arr[dest--] = 0;
                arr[dest--] = 0;
                cur--;
            }
         }
    }
}

202 快乐数

202. 快乐数 - 力扣(LeetCode)

题目描述

编写一个算法来判断一个数 n 是否是快乐数

快乐数定义:

对于一个正整数,每一次将该数替换为它每个位置上数字的平方和

然后重复这个过程直到这个数变为 1,也有可能是无限循环,但始终变不到 1

如果这个过程结果为 1,那么这个数就是快乐数

如果 n 是快乐数就返回 true,不是则返回 false

示例 1:

输入:n = 19

输出:true

解释:n

19 -> 1 * 1 + 9 * 9 = 82

82 -> 8 * 8 + 2 * 2 = 68

68 -> 6 * 6 + 8 * 8 = 100

100 -> 1 * 1 + 0 * 0 + 0 * 0 = 1

示例 2:

输入:n = 2

输出:false

2 -> 4 -> 16 -> 37 -> 58 -> 89 -> 145 -> 42 -> 20 -> 4 -> 16(数字 16 重复,则会陷入循环,且最终结果肯定不会是 1)

为了方便叙述,我们将题目中" 对于一个正整数,每一次将该数替换为它每个位置上数字的平方和"这个操作,称为 x 操作。

题目告诉我们,当我们不断重复 x 操作的时候,计算结果一定会"死循环",死的方式有两种:

情况 1:一直在 1 中死循环 1 -> 1 -> 1 -> 1 -> 1

情况 2:在历史的数据中死循环,但始终变不到 1

由于上述两种情况只会出现一种,因此,只要我们能确定,循环是在情况 1 出现,还是在情况 2 出现,就一定能得到结果。

算法原理

由题目描述理解,我们发现,这道题关键是要判断链表是否有环,我们已经由题目得知,所给的数据一定可以成环,重点研究的就是,成环是情况 1 还是情况 2。

解法:快慢双指针

  1. 定义快慢指针

  2. 慢指针每次向后移动一步,快指针每次向后移动两步

  3. 判断相遇时候的值即可

这里补充一个鸽巢原理, 来说明一下,为什么一定会成环。

总结:

由题目分析和鸽巢原理,我们可知,当重复执行 x 操作的时候,数据一定会陷入到一个循环之中。而快慢指针,有一个特性,就是在一个圆圈中,快指针,总是会追上慢指针的,也就是说,他们总会在一个位置上相遇。如果相遇的位置是 1,那么这个数就是快乐数,如果相遇位置不是 1,那么就不是快乐数。

补充:如果求数 n 每个位置上的数字的平方和:

  1. 把 n 的每一位都提取出来

1.1 int t = n % 10 // 提取一位

1.2 n /= 10 // 干掉一位

直到 n 的值变为 0

  1. 提取每一位的时候,用一个变量 sum来记录这一位的平方与之前提取位数的平方和

sum += t * t

完整代码

java 复制代码
class Solution {

    public int bitSum(int n) {

        int sum = 0;
        while (n != 0) {
            int t = n % 10;
            sum += t * t;
            n /= 10;
        }
        return sum;
    }
    public boolean isHappy(int n) {
        int slow = n, fast = bitSum(n);
        while (slow != fast) {
            slow = bitSum(slow);
            fast = bitSum(bitSum(fast));
        }
        return slow == 1;
    }
}

11 盛水最多的容器

11. 盛最多水的容器 - 力扣(LeetCode)

题目描述

给定一个长度为 n 的整数数组 height,有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i])。

找出其中的两条线,使得他们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

示例 1:

输入:1,8,6,2,5,4,8,3,7

输出:49

解释:图中垂直线为数组[1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49(7 * 7)

示例 2:

输入:height = [1,1]

输出:1

注意:题目要求的是容器能够容纳水的体积,即底部的长度 * 高的长度。在示例 1 中

两个标红数据之间的长度为 8 - 1 = 7 高度为短的那一边 7 ==》 7 * 7 = 49

算法原理

解法一:暴力枚举,即分别求取每两个的数据之间的体积,然后比较~

解法二:利用单调性,使用双指针来解决问题

首先我们先理解单调性:

如上图,left 指向高度 1,right 指向高度 7,容积是 8 * 1 = 8

如果移动 right(较高的柱子),容积只会更小或者不变。

原因:容器的容积由两个因素决定:两根柱子之间的高度(底边长),两根柱子中较矮的那个高度(高度)

两根柱子的距离一定会缩小(指针向内)。

right 移动后,如果遇到的是比 right 还高的柱子,高度受较矮的柱子影响,不会变化,底边长减小,容积减小。

如果遇到的是比 right 还低的柱子,容积肯定也会减小

如果遇到的是和 right 一样高的柱子,因为底边长减小了,所以容积也一定会减小

因此,我们可以移动较矮的柱子,移动较矮的柱子,虽然底边长还是会缩小,但是较矮的柱子,可能遇到更高的柱子,从而让矮的高度增加,最终使得整体容积增大

核心思路:

  1. 初始化两个指针,分别指向数组的左右两端

  2. 计算当前两个指针所能形成的容积

2.1 容积 = 底边长(right - left)* 高度(较矮柱子的高度)

  1. 保留最大值

  2. 移动指针:为了获得更大的容积,由单调性可得,应该移动较矮的柱子

  3. 重复 2 - 4 直到两个指针相遇

时间复杂度为 O(n),只有一个循环,指针移动的总次数不会超过 n 次

空河复杂度为O(1)

完整代码

java 复制代码
class Solution {
    public int maxArea(int[] height) {
        int left = 0, right = height.length - 1, ret = 0, h = 0, v = 0;
        while (left < right) {
            h = Math.min(height[left], height[right]);
            v = (right - left) * h;
            ret = Math.max(v,ret);
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }
        return ret;
    }
}

611. 有效三角形的个数

611. 有效三角形的个数 - 力扣(LeetCode)

题目描述

给定一个包含非负整数的数组 nums,返回其中可以组成三角形三条边的三元组个数

示例 1:

输入: nums = 2, 2,3,4

输出:3

解释:有效的组合是:2,3,4(使用第一个 2);2,3,4(使用第二个 2);2,2,3

示例 2:

输入:nums = 4,2,3,4

输出:4

解释:4,2,3;4,2,4;4,3,4;2,3,4

算法原理

构成三角形:任意两边之和大于第三边

但我们可以进行小小优化,即先对整个数组排序,然后三条边中的较小的两条边之和大于第三条边即可~

解法一:暴力枚举

三层 for 循环,枚举出所有的三元组 ==》 超时~

解法二:利用单调性,使用双指针算法来解决问题

可以先固定一个最长边,然后在比这条边小的有序数组中,找到一个二元组,使得这个二元组之和大于这个最长边(因为有序数组,且我们最开始先固定了一个最长边,所以二元组都比最长边短),这样就找到一组三角形。由于数组是有序的,我们遍历数组即可~

举例:

nums = 2, 2,3,4,5,9,10

可以先对整个数组进行排序,最长边就是数组最后面的数据。固定 c 为 10,left 指向 2,right 指向 10

第一种情况:left + right > c ,则 left 之后的元素,都比 left 大,与 right 相加,都比 c 大,都可以与 right,c 组成三角形,加上 left ,个数共有 right - left 个,然后 right--

第二种情况:left + right <= c,right 之前的元素,都比 right 小,与 left 相加,仍然都比 c 小,无法与 left,c 组成三角形,只能 left++。

核心思路:

  1. 数组排序

  2. 固定住最大的数

  3. 在最大的数的左区间中,使用双指针算法,快速统计出符合要求的三元组个数

时间复杂度为 O(n^2)排序占 O(n log n)双重循环(外层 n 次,内层总共 n 次)

空间复杂度 O(n log n),主要来自排序操作需要的栈空间

完整代码

java 复制代码
class Solution {
    public int triangleNumber(int[] nums) {
        // 1. 排序
        Arrays.sort(nums);
    
        // 2. 利用双指针解决问题
        int ret = 0;
        int n = nums.length;
        
        for (int i = n - 1; i >= 2; i--) { // 先固定最大的数
            // 利用双指针快速统计出符合要求的三元组的个数
            int left = 0, right = i - 1;
            while (left < right) {
                if (nums[left] + nums[right] > nums[i]) {
                    ret += right - left;
                    right--;
                } else {
                    left++;
                }
            }
        }
        return ret;
    }
}

179. 和为 s 的两个数字

LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)

题目描述

输入一个递增排序的数组,和一个数字 s,在数组中查找两个数,使得它们的和正好是 s。如果有多对数组的和为 s,则输出任意一对即可。

示例 1:

输入:nums = 2,7,11,15, target = 9

输出:2,7 或者 7,2

算法原理

解法一:暴力枚举 O(n^2)

两层 for 循环嵌套,循环出所有组合,看和是否为 target,是的话直接返回

解法二:利用单调性,使用双指针算法解决问题

举例:2,7,11,15,19,21 t = 30

left 指向 2,right 指向 21,记 left + right 为 sum

会有三种情况:

1: sum < t

此时,left 指向的元素 2,left 与 right 中间的元素 7,11,15,19,left 指向的元素 2 与最大的 right 指向的 21,相加之和都不满足 t,则 left 与中间的元素相加之和,更无法满足 t,所以可以放心的将 left 指向的元素 2 删除 ==》 left++

  1. sum > t

此时,right 指向的元素 21,left 与 right 中间的元素 15,19,right 指向的元素 21 与最小的 left 指向的 11,相加之和都大于 t,则 right 与中间的元素相加之和,更大于 t,所以可以将 right 指向的元素 21 删除 ==》 right--

  1. sum == t:直接返回结果即可~ 注意这里返回的是 nums[left] 和 nums[right]

时间复杂度:O(n)

空间复杂度:O(1)

完整代码

java 复制代码
class Solution {
    public int[] twoSum(int[] price, int target) {
        int left = 0, right = price.length - 1;
        while (left < right) {
            int sum = price[left] + price[right];
            if (sum < target) {
                left++;
            } else if (sum > target) {
                right--;
            } else {
                return new int[]{price[left], price[right]};
            }
        }
        return new int[]{0};
    }
}

15.三数之和

15. 三数之和 - 力扣(LeetCode)

题目描述

一个整数数组 nuns,判断是否存在三元组 nums[i] nums[j] nums[k] 满足 i j k 三者互不相同,同时满足,nums[i] nums[j] nums[k] == 0。返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组

示例 1:

输入:nums = -1,0,1,2,-1,-4

输出:-1,-1,2; -1,0,1

注意:输出的顺序和三元组的顺序并不重要

示例 2:

输入:nums = 0,1,1

输出:[ ]

解释:唯一可能的三元组的和不为0

算法原理

解法 1:排序 + 暴力枚举 + set 去重

解法 2:排序 + 双指针

  1. 对数组进行排序

  2. 固定一个数 a。此处有一个优化:此处固定的 a 必须是小于等于 0 的数。当 a 大于 0 的时候, -a 就是小于 0 的数,在后面的区间,都是大于 0 的数,不可能有和为小于 0 的数了

  3. 在该数 a 后面的区间内,利用双指针算法,快速找到两个和为 -a 的数据

但这道题,还需要处理不重 不漏 这两个细节问题:

  1. 不重:当找到一种结果后,left 和 right 两个指针,要跳过重复元素

当使用完一次双指针算法后,i 也需要跳过重复元素

  1. 不漏:找到一种结果之后,不要停,缩小区间,继续寻找

时间复杂度:O(n^2)

空间复杂度:O(1)

完整代码

java 复制代码
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {

        // 1. 排序
        Arrays.sort(nums);

        // 2. 利用双指针解决问题
        List<List<Integer>> ret = new ArrayList<>();

        int n = nums.length;
        for (int i = 0; i < n; ) { // 固定 a

            if (nums[i] > 0) break; // 小优化

            int taregt = -nums[i];
            int left = i + 1;
            int right = nums.length - 1;
            while (left < right) {
                int sum = nums[left] + nums[right];
                if (sum < taregt) {
                    left++;
                } else if (sum > taregt) {
                    right--;
                } else {
                    ret.add(new ArrayList<Integer>(Arrays.asList(nums[i], nums[left], nums[right])));
                    right--; left++; // 缩小区间继续寻找

                    // 去重 left right 且防止越界
                    while(left < right && nums[left] == nums[left - 1]) {
                        left++;
                    }
                    while (left < right && nums[right] == nums[right + 1]) {
                        right--;
                    }
                }
            }
            // 对 i 去重
            i++;
            while (i < n && nums[i] == nums[i - 1]) {
                i++; // 去重之后,i 已经指向了我们的元素 a 则在 for 循环中不需要最后 i++ 了~
            }
        }
        return ret;
    }
}

18 四数之和

18. 四数之和 - 力扣(LeetCode)

题目描述

有一个由 n 个整数组成的数组 nums,和一个目标值 target。请找出并返回满足下述条件,且不重复的四元组:nums[a] nums[b] nums[c] nums[d]

0 <= a b c d <= n

a b c d 互不相同

nums[a] + nums[b] + nums[c] + nums[d] == target

示例 1:

输入:nums = 1,0,-1,0,-2,2 target = 0

输出:-2,-1,1,2; -2,0,0,2; -1,0,0,1

示例 2 :

输入:nums = 2,2,2,2 ,2 target = 8

输出:2,2,2,2

算法原理

解法:排序 + 双指针

  1. 依次固定一个数 a

  2. 在 a 后面的区间内,利用"三数之和",找到三个数,使得这三个数的和为 target - a

2.1 依次固定一个数 b

2.2 在 b 后面的区间内,利用双指针,找到两个数,使得这两个数的和为 target - a - b

在这个问题中,也需要处理 不重 和 不漏问题

完整代码

java 复制代码
class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> ret = new ArrayList<>();

        // 1. 排序
        Arrays.sort(nums);

        // 2. 利用双指针解决问题
        int n = nums.length;
        for (int i = 0; i < n; ) { // 固定数 a
            // 3. 在 a 后面的区间,利用三数之和,找到三个数,
            for (int j = i + 1; j < n; ) {  // 固定数 b
                // 在 b 后面的区间,利用"双指针" 找到两个数
                // 使这两个数字的和等于 target - a - b
                int left = j + 1, right = n - 1;
                long aim = (long)target - nums[i] - nums[j];
                while (left < right) {
                    int sum = nums[left] + nums[right];
                    if (sum > aim) {
                        right--;
                    } else if (sum < aim) {
                        left++;
                    } else {
                        ret.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                        left++; right--;
                        // 去重 1
                        while (left < right && nums[left] == nums[left - 1]) left++;
                        while (left < right && nums[right] == nums[right + 1]) right--;
                    }
                }
                // 去重 j
                j++;
                while (j < n && nums[j] == nums[j - 1]) j++;
            }
            // 去重 i
            i++;
            while(i < n && nums[i] == nums[i - 1]) i++;
        }
        return ret;
    }
}

完!

相关推荐
火山锅22 分钟前
🚀 Spring Boot枚举转换新突破:编译时处理+零配置,彻底告别手写转换代码
java·架构
overFitBrain23 分钟前
数据结构-5(二叉树)
开发语言·数据结构·python
孟柯coding25 分钟前
常见排序算法
数据结构·算法·排序算法
秋千码途27 分钟前
小架构step系列25:错误码
java·架构
Point30 分钟前
[LeetCode] 最长连续序列
前端·javascript·算法
rookiesx34 分钟前
安装本地python文件到site-packages
开发语言·前端·python
是阿建吖!37 分钟前
【优选算法】链表
数据结构·算法·链表
kev_gogo38 分钟前
关于回归决策树CART生成算法中的最优化算法详解
算法·决策树·回归
m0_687399841 小时前
Ubuntu22 上,用C++ gSoap 创建一个简单的webservice
开发语言·c++
屁股割了还要学1 小时前
【C语言进阶】一篇文章教会你文件的读写
c语言·开发语言·数据结构·c++·学习·青少年编程