49.【必备】双指针技巧与相关题目

本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~

网课链接:算法讲解050【必备】双指针技巧与相关题目_哔哩哔哩_bilibili

一.奇偶数字归位

题目:按奇偶排序数组 II

算法原理

  • 整体思路
    • 双指针策略

      • 该算法使用双指针来解决问题。一个指针(even)用于指向偶数位置,初始化为0;另一个指针(odd)用于指向奇数位置,初始化为1。
      • 通过双指针不断地交换元素,使得偶数位置为偶数,奇数位置为奇数。
    • 从数组末尾取元素调整

      • 算法每次从数组末尾取一个元素,根据这个元素的奇偶性,将其交换到合适的位置(偶数元素交换到偶数位置,奇数元素交换到奇数位置)。
  • 具体步骤
    • 初始化
      • 确定数组的长度n = nums.length
      • 初始化两个指针:odd = 1(用于指向奇数位置)和even = 0(用于指向偶数位置)。
    • 循环交换元素
      • for (int odd = 1, even = 0; odd < n && even < n;)循环中:
        • 首先检查数组末尾元素nums[n - 1]的奇偶性,使用位运算(nums[n - 1]&1)来判断,如果结果为1,则表示该元素为奇数。
        • 如果(nums[n - 1]&1) == 1(元素为奇数):
          • 调用swap函数将这个奇数元素与odd指针所指的奇数位置的元素进行交换,即swap(nums, odd, n - 1)
          • 然后将odd指针向后移动2个位置(因为奇数位置之间间隔为2),即odd += 2
        • 如果(nums[n - 1]&1)!= 1(元素为偶数):
          • 调用swap函数将这个偶数元素与even指针所指的偶数位置的元素进行交换,即swap(nums, even, n - 1)
          • 然后将even指针向后移动2个位置(因为偶数位置之间间隔为2),即even += 2
    • 最终结果
      • 当循环结束后,数组nums中的元素已经按照要求排序,即偶数位置为偶数,奇数位置为奇数,最后返回这个数组。

代码实现

// 按奇偶排序数组II
// 给定一个非负整数数组 nums。nums 中一半整数是奇数 ,一半整数是偶数
// 对数组进行排序,以便当 nums[i] 为奇数时,i也是奇数
// 当 nums[i] 为偶数时, i 也是 偶数
// 你可以返回 任何满足上述条件的数组作为答案
// 测试链接 : https://leetcode.cn/problems/sort-array-by-parity-ii/
public class Code01_SortArrayByParityII {

    // 时间复杂度O(n),额外空间复杂度O(1)
    public static int[] sortArrayByParityII(int[] nums) {
        int n = nums.length;
        for (int odd = 1, even = 0; odd < n && even < n;) {
            if ((nums[n - 1] & 1) == 1) {
                swap(nums, odd, n - 1);
                odd += 2;
            } else {
                swap(nums, even, n - 1);
                even += 2;
            }
        }
        return nums;
    }

    public static void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }

}

二.寻找重复数

题目:寻找重复数

 算法原理

  • 整体思路
    • 快慢指针的运用
      • 这个算法使用了快慢指针的技巧,类似于检测链表是否有环的方法。
      • 我们将数组中的元素视为链表中的节点,数组的下标作为指针。例如,对于数组numsnums[i]可以看作是节点i的下一个节点。
    • 确定存在环
      • 由于数组中有n + 1个整数,且数字都在[1, n]范围内,根据抽屉原理,必然存在至少一个重复的数。这种重复会导致在按照上述规则构建的“链表”中出现环。 
  • 具体步骤
    • 快慢指针相遇

      • 初始化
        • 首先,慢指针slow初始化为nums[0],快指针fast初始化为nums[nums[0]]
      • 移动指针
        • 在循环中,慢指针slow每次移动一步,即slow = nums[slow];快指针fast每次移动两步,即fast = nums[nums[fast]]
        • slowfast相遇时,此时它们在环内的某个节点相遇。这一过程类似于在一个有环的链表中,快慢指针同时出发,快指针最终会追上慢指针。
    • 找到环的入口,即重复的数

      • 指针重置
        • 当快慢指针相遇后,将快指针fast重置为0(数组的起始下标)。
      • 再次移动指针
        • 然后,慢指针slow和快指针fast以相同的速度(每次移动一步)移动。
        • 当它们再次相遇时,这个相遇的节点就是环的入口,也就是数组中的重复数。这是因为从链表头到环入口的距离和从快慢指针相遇点到环入口的距离是相等的。 

代码实现

// 寻找重复数
// 给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n)
// 可知至少存在一个重复的整数。
// 假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
// 你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
// 测试链接 : https://leetcode.cn/problems/find-the-duplicate-number/
public class Code02_FindTheDuplicateNumber {

    // 时间复杂度O(n),额外空间复杂度O(1)
    public static int findDuplicate(int[] nums) {
        if (nums == null || nums.length < 2) {
            return -1;
        }
        int slow = nums[0];
        int fast = nums[nums[0]];
        while (slow != fast) {
            slow = nums[slow];
            fast = nums[nums[fast]];
        }
        // 相遇了,快指针回开头
        fast = 0;
        while (slow != fast) {
            fast = nums[fast];
            slow = nums[slow];
        }
        return slow;
    }

}

三.接雨水

题目:接雨水

算法原理

一、辅助数组解法(trap1)的原理
  • 计算左右最大高度数组
    • 左最大高度数组(lmax)
      • 首先初始化lmax[0]=nums[0]
      • 然后通过一个循环for (int i = 1; i < n; i++),计算从左到右每个位置i左侧的最大值。对于每个ilmax[i]lmax[i - 1]nums[i]中的较大值。这意味着lmax数组中的每个元素lmax[i]都表示从0i这个区间内的最大高度。
    • 右最大高度数组(rmax)
      • 首先初始化rmax[n - 1]=nums[n - 1]
      • 然后通过一个逆序循环for (int i = n - 2; i >= 0; i--),计算从右到左每个位置i右侧的最大值。对于每个irmax[i]rmax[i + 1]nums[i]中的较大值。这样rmax数组中的每个元素rmax[i]都表示从in - 1这个区间内的最大高度。
  • 计算接水量
    • 对于每个位置i1 <= i <= n - 2),该位置能够接住的水量取决于其左侧最大高度lmax[i - 1]和右侧最大高度rmax[i + 1]中的较小值减去自身高度nums[i]
    • 如果这个差值大于0,则表示可以接住水,将其累加到总的接水量ans中。即ans += Math.max(0, Math.min(lmax[i - 1], rmax[i + 1]) - nums[i]);
二、双指针解法(trap2)的原理
  • 初始化
    • 定义左指针l = 1(从数组的第二个元素开始),右指针r = nums.length - 2(从数组的倒数第二个元素开始)。
    • 初始化lmax=nums[0],表示当前左指针左侧的最大高度;rmax=nums[nums.length - 1],表示当前右指针右侧的最大高度。
    • 初始化接水量ans = 0
  • 移动指针并计算接水量
    • while (l <= r)循环中:
      • 如果lmax <= rmax
        • 对于左指针l位置,能接住的水量为lmax - nums[l](如果这个值大于0),将其累加到ans中。
        • 然后更新lmaxlmaxnums[l]中的较大值,并且左指针l向右移动一位(l++)。
      • 否则(即lmax>rmax):
        • 对于右指针r位置,能接住的水量为rmax - nums[r](如果这个值大于0),将其累加到ans中。
        • 然后更新rmaxrmaxnums[r]中的较大值,并且右指针r向左移动一位(r--)。
    • 这个过程不断更新左右两侧的最大高度,并且根据较小的那一侧最大高度来计算当前指针位置能够接住的水量,直到左右指针相遇。

代码实现

// 接雨水
// 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水
// 测试链接 : https://leetcode.cn/problems/trapping-rain-water/
public class Code03_TrappingRainWater {

    // 辅助数组的解法(不是最优解)
    // 时间复杂度O(n),额外空间复杂度O(n)
    // 提交时改名为trap
    public static int trap1(int[] nums) {
        int n = nums.length;
        int[] lmax = new int[n];
        int[] rmax = new int[n];
        lmax[0] = nums[0];
        // 0~i范围上的最大值,记录在lmax[i]
        for (int i = 1; i < n; i++) {
            lmax[i] = Math.max(lmax[i - 1], nums[i]);
        }
        rmax[n - 1] = nums[n - 1];
        // i~n-1范围上的最大值,记录在rmax[i]
        for (int i = n - 2; i >= 0; i--) {
            rmax[i] = Math.max(rmax[i + 1], nums[i]);
        }
        int ans = 0;
        //   x              x
        //   0 1 2 3...n-2 n-1
        for (int i = 1; i < n - 1; i++) {
            ans += Math.max(0, Math.min(lmax[i - 1], rmax[i + 1]) - nums[i]);
        }
        return ans;
    }

    // 双指针的解法(最优解)
    // 时间复杂度O(n),额外空间复杂度O(1)
    // 提交时改名为trap
    public static int trap2(int[] nums) {
        int l = 1, r = nums.length - 2, lmax = nums[0], rmax = nums[nums.length - 1];
        int ans = 0;
        while (l <= r) {
            if (lmax <= rmax) {
                ans += Math.max(0, lmax - nums[l]);
                lmax = Math.max(lmax, nums[l++]);
            } else {
                ans += Math.max(0, rmax - nums[r]);
                rmax = Math.max(rmax, nums[r--]);
            }
        }
        return ans;
    }

}

四.救生艇

题目:救生艇

算法原理

  • 整体思路
    • 排序的目的
      • 首先对数组people进行排序,这是为了方便后续的配对操作。通过排序,可以使得体重较轻的人在数组的左边,体重较重的人在数组的右边。
    • 双指针的运用
      • 算法使用双指针l(左指针,初始指向数组的第一个元素)和r(右指针,初始指向数组的最后一个元素)来遍历数组。
      • 每次尝试将最轻的人和最重的人放在一艘船上(如果他们的体重之和不超过limit),如果不行,则最重的人单独一艘船。
  • 具体步骤
    • 初始化
      • people数组排序后,初始化变量:
        • ans = 0,用于记录所需的船的数量。
        • l = 0,左指针指向数组的最左边。
        • r = people.length - 1,右指针指向数组的最右边。
        • sum = 0,用于临时存储两人的体重之和。
    • 循环配对或单独安排
      • while (l <= r)循环中:
        • 首先计算当前指针所指两人的体重之和(当l == r时,只有一个人,就取这个人的体重),即sum = l == r? people[l] : people[l]+people[r];
        • 如果sum > limit,说明最轻的人和最重的人不能放在同一艘船上,此时最重的人(右指针指向的人)单独一艘船,所以r--
        • 如果sum <= limit,说明最轻的人和最重的人可以放在同一艘船上,此时l++(最轻的人已经安排),r--(最重的人已经安排)。
        • 无论哪种情况,都需要一艘船,所以ans++
    • 最终结果

      • 当循环结束后(即所有人都被安排到船上),ans的值就是承载所有人所需的最小船数。

代码实现

import java.util.Arrays;

// 救生艇
// 给定数组 people
// people[i]表示第 i 个人的体重 ,船的数量不限,每艘船可以承载的最大重量为 limit
// 每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit
// 返回 承载所有人所需的最小船数
// 测试链接 : https://leetcode.cn/problems/boats-to-save-people/
public class Code04_BoatsToSavePeople {

    // 时间复杂度O(n * logn),因为有排序,额外空间复杂度O(1)
    public static int numRescueBoats(int[] people, int limit) {
        Arrays.sort(people);
        int ans = 0;
        int l = 0;
        int r = people.length - 1;
        int sum = 0;
        while (l <= r) {
            sum = l == r ? people[l] : people[l] + people[r];
            if (sum > limit) {
                r--;
            } else {
                l++;
                r--;
            }
            ans++;
        }
        return ans;
    }

}

五.盛最多水的容器

题目:盛最多水的容器

算法原理

  • 整体思路
    • 双指针法的运用
      • 采用双指针法,一个指针l指向数组的开头(最左边的垂线),另一个指针r指向数组的末尾(最右边的垂线)。
      • 通过移动指针来调整容器的宽度,并根据指针所指元素的高度来计算容器的面积,不断寻找最大面积。
  • 具体步骤
    • 初始化
      • 初始化变量ans = 0,用于存储最大的盛水面积。
      • 设定双指针l = 0(指向数组的第一个元素)和r = height.length - 1(指向数组的最后一个元素)。
    • 循环计算面积并调整指针
      • for (int l = 0, r = height.length - 1; l < r;)循环中:
        • 首先计算当前指针lr所构成容器的面积,计算公式为Math.min(height[l], height[r])*(r - l)。这里Math.min(height[l], height[r])表示容器的高度(取两条垂线中较矮的高度),(r - l)表示容器的宽度。然后将这个面积与当前的最大面积ans进行比较,通过ans = Math.max(ans, Math.min(height[l], height[r])*(r - l));更新最大面积。
        • 接着根据指针所指元素的高度来调整指针:
          • 如果height[l] <= height[r],说明左边的垂线较矮,将左指针l向右移动一位(l++),这样做是因为如果保持右指针不变,移动左指针可能会找到更高的垂线从而增大面积。
          • 如果height[l]>height[r],说明右边的垂线较矮,将右指针r向左移动一位(r--),同理,这样做可能会找到更高的垂线从而增大面积。
    • 最终结果

      • 当循环结束(即l不再小于r)时,ans的值就是能够盛最多水的容器的面积。

代码实现

import java.util.Arrays;

// 供暖器
// 冬季已经来临。 你的任务是设计一个有固定加热半径的供暖器向所有房屋供暖。
// 在加热器的加热半径范围内的每个房屋都可以获得供暖。
// 现在,给出位于一条水平线上的房屋 houses 和供暖器 heaters 的位置
// 请你找出并返回可以覆盖所有房屋的最小加热半径。
// 说明:所有供暖器都遵循你的半径标准,加热的半径也一样。
// 测试链接 : https://leetcode.cn/problems/heaters/
public class Code06_Heaters {

    // 时间复杂度O(n * logn),因为有排序,额外空间复杂度O(1)
    public static int findRadius(int[] houses, int[] heaters) {
        Arrays.sort(houses);
        Arrays.sort(heaters);
        int ans = 0;
        for (int i = 0, j = 0; i < houses.length; i++) {
            // i号房屋
            // j号供暖器
            while (!best(houses, heaters, i, j)) {
                j++;
            }
            ans = Math.max(ans, Math.abs(heaters[j] - houses[i]));
        }
        return ans;
    }

    // 这个函数含义:
    // 当前的地点houses[i]由heaters[j]来供暖是最优的吗?
    // 当前的地点houses[i]由heaters[j]来供暖,产生的半径是a
    // 当前的地点houses[i]由heaters[j + 1]来供暖,产生的半径是b
    // 如果a < b, 说明是最优,供暖不应该跳下一个位置
    // 如果a >= b, 说明不是最优,应该跳下一个位置
    public static boolean best(int[] houses, int[] heaters, int i, int j) {
        return j == heaters.length - 1
                ||
                Math.abs(heaters[j] - houses[i]) < Math.abs(heaters[j + 1] - houses[i]);
    }

}

六.供暖器

题目:供暖器

算法原理

  • 整体思路
    • 排序的重要性
      • 首先对房屋位置数组houses和供暖器位置数组heaters进行排序。排序后的数组便于我们进行后续的距离比较和最小加热半径的计算。
    • 遍历房屋寻找最小加热半径
      • 通过嵌套的循环结构,遍历每一个房屋,对于每个房屋,找到能够为其供暖的最近的供暖器,然后计算该房屋到这个供暖器的距离。最后取所有房屋到供暖器距离中的最大值作为最小加热半径。
  • 具体步骤
    • 初始化与排序
      • 初始化变量ans = 0,用于存储最终的最小加热半径。
      • houses数组和heaters数组分别进行排序,这使得我们在后续计算距离时能够按照顺序进行高效的查找。
    • 遍历房屋(外层循环)
      • 使用for (int i = 0, j = 0; i < houses.length; i++)循环遍历每个房屋。其中i表示房屋的索引,j表示供暖器的索引,初始时都为0。
      • 对于每个房屋houses[i],我们需要找到能够为其供暖的最近的供暖器。
    • 寻找最近供暖器(内层循环)
      • 在内部的while (!best(houses, heaters, i, j))循环中,通过不断调整供暖器的索引j,来找到为houses[i]供暖的最优供暖器。
      • 函数best的作用是判断当前房屋houses[i]由当前供暖器heaters[j]供暖是否是最优的。如果j是最后一个供暖器(j == heaters.length - 1),那么当前供暖器就是最优的;否则,比较当前供暖器heaters[j]到房屋houses[i]的距离Math.abs(heaters[j] - houses[i])和下一个供暖器heaters[j + 1]到房屋houses[i]的距离Math.abs(heaters[j + 1] - houses[i]),如果前者小于后者,那么当前供暖器就是最优的,否则不是最优的,需要继续寻找(j++)。
    • 计算并更新最小加热半径
      • 当找到为houses[i]供暖的最优供暖器heaters[j]后,计算房屋houses[i]到供暖器heaters[j]的距离Math.abs(heaters[j] - houses[i]),并通过ans = Math.max(ans, Math.abs(heaters[j] - houses[i]))更新最小加热半径ans。取所有房屋到供暖器距离中的最大值,这样就可以确保所有房屋都能被供暖。
    • 最终结果
      • 当所有房屋都被遍历完后,ans的值就是可以覆盖所有房屋的最小加热半径。

代码实现

public static int findRadius(int[] houses, int[] heaters) {
        Arrays.sort(houses);
        Arrays.sort(heaters);
        int ans = 0;
        for (int i = 0, j = 0; i < houses.length; i++) {
            // i号房屋
            // j号供暖器
            while (!best(houses, heaters, i, j)) {
                j++;
            }
            ans = Math.max(ans, Math.abs(heaters[j] - houses[i]));
        }
        return ans;
    }

    // 这个函数含义:
    // 当前的地点houses[i]由heaters[j]来供暖是最优的吗?
    // 当前的地点houses[i]由heaters[j]来供暖,产生的半径是a
    // 当前的地点houses[i]由heaters[j + 1]来供暖,产生的半径是b
    // 如果a < b, 说明是最优,供暖不应该跳下一个位置
    // 如果a >= b, 说明不是最优,应该跳下一个位置
    public static boolean best(int[] houses, int[] heaters, int i, int j) {
        return j == heaters.length - 1
                ||
                Math.abs(heaters[j] - houses[i]) < Math.abs(heaters[j + 1] - houses[i]);
    }

七.缺失的第一个正数

题目:缺失的第一个正数

算法原理

  • 整体思路
    • 区域划分与目标
      • 算法将数组划分为不同的区域。目标是让数组中索引为(i)的位置存放值(i + 1)。
      • 有一个左边区域(由指针(l)界定),在这个区域左边,是已经满足(arr[i]=i + 1)的部分;有一个右边区域(由指针(r)界定),是所谓的“垃圾区”。
    • 通过交换调整元素位置
      • 算法通过不断地交换元素,将合适的元素移动到合适的位置,使得数组逐渐接近目标状态,最后确定第一个缺失的正整数。
  • 具体步骤
    • 初始化
      • 初始化两个指针:(l = 0),它左边的区域是已经处理好的部分(即索引(i)处的值为(i+1));(r = arr.length),它界定了右边的“垃圾区”。
    • 循环处理元素(while (l < r))
      • 情况一:如果(arr[l]=l + 1),这意味着当前位置(l)已经满足要求,直接将指针(l)向右移动一位((l++))。
      • 情况二:如果(arr[l]\leq l),这说明(arr[l])的值不应该出现在当前位置(因为按照目标,索引(l)处应该是(l + 1),而(arr[l]\leq l)不符合要求);如果(arr[l]>r),表示(arr[l])的值超出了我们目前期望的范围(我们期望(1)到(r)之间的值合理分布);如果(arr[arr[l]-1]=arr[l]),说明有重复的值,这三种情况都表明(arr[l])是一个“坏”元素,将其与(r - 1)位置的元素交换,并且将(r)减(1)(即把这个元素扔到“垃圾区”)。
      • 情况三:如果不满足前面两种情况,那么(arr[l])的值应该放在(arr[l]-1)的位置,所以将(arr[l])与(arr[arr[l]-1])交换,这样可以将(arr[l])放到更合适的位置。
    • 确定结果
      • 当循环结束后,(l)左边的部分是满足要求的,而第一个缺失的正整数就是(l + 1)。

代码实现

// 缺失的第一个正数
// 给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
// 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
// 测试链接 : https://leetcode.cn/problems/first-missing-positive/
public class Code07_FirstMissingPositive {

    // 时间复杂度O(n),额外空间复杂度O(1)
    public static int firstMissingPositive(int[] arr) {
        // l的左边,都是做到i位置上放着i+1的区域
        // 永远盯着l位置的数字看,看能不能扩充(l++)
        int l = 0;
        // [r....]垃圾区
        // 最好的状况下,认为1~r是可以收集全的,每个数字收集1个,不能有垃圾
        // 有垃圾呢?预期就会变差(r--)
        int r = arr.length;
        while (l < r) {
            if (arr[l] == l + 1) {
                l++;
            } else if (arr[l] <= l || arr[l] > r || arr[arr[l] - 1] == arr[l]) {
                swap(arr, l, --r);
            } else {
                swap(arr, l, arr[l] - 1);
            }
        }
        return l + 1;
    }

    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

}

八.总结

设置两个指针的技巧,其实这种说法很宽泛:

1)有时候所谓的双指针技巧,就单纯是代码过程用双指针的形式表达出来而已,没有单调性(贪心)方面的考虑。

2)有时候的双指针技巧包含单调性(贪心)方面的考虑,牵扯到可能性的取舍。这对分析能力的要求会变高。其实是先有的思考和优化,然后代码变成了双指针的形式

3)所以,双指针这个“皮”不重要,分析题目单调性(贪心)方面的特征,这个能力才重要。

常见的双指针类型:

1)同向双指针

2)快慢双指针

3)从两头往中间的双指针

4)其他

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值