双指针技巧

通俗解释

双指针技巧是一种在数组、链表等线性数据结构中非常实用的算法策略。它通过使用两个指针在数据结构上按一定规则移动,来高效地解决各种问题。

双指针技巧就像是我们在处理一排物品(比如数组、链表里的元素)时,同时用两只手去操作。每只手对应一个指针,它们可以在这排物品上按照一定规则移动,通过两只手的配合来高效地完成各种任务,比如找特定的元素组合、判断是否存在某种情况等。下面详细介绍三种常见的双指针类型。

(一) 同向双指针的通俗理解(滑动窗口)

同向双指针是双指针技巧中的一种,就像是两个人在同一条道路上朝着相同的方向前进。这两个人可以代表两个指针,他们在处理数组、链表或者字符串等线性数据结构时,按照一定的规则在数据序列上移动,从而高效地解决各种问题。

适用场景

同向双指针常用于处理需要在一个序列中找出满足特定条件的连续子序列、对序列进行去重或者合并等问题。比如,在一个数组中找出和为特定值的连续子数组,或者在一个有序数组中去除重复的元素。

操作方式与原理

1. 初始化

开始时,两个指针(通常称为左指针和右指针)都位于序列的起始位置。就好像两个人站在道路的起点,准备一起出发。

2. 右指针移动

右指针会先开始移动,它就像一个 “探索者”,不断地向前走,去探索序列中的元素。每移动一步,右指针会把新遇到的元素纳入到当前考虑的范围内。

3. 判断条件

在右指针移动的过程中,我们会根据具体的问题条件来判断当前由左右指针所界定的子序列是否满足要求。这就好比两个人一边走,一边检查他们走过的这段路是否符合某种标准。

4. 左指针移动

如果当前子序列满足条件,我们可能会尝试让左指针也向前移动,缩小子序列的范围,看看是否还能满足条件。左指针的移动就像是后面的人向前走,尝试减少走过的路程但仍然满足标准。如果不满足条件,右指针会继续向前移动,扩大子序列的范围。

5. 重复步骤 3 和 4

不断重复上述判断和移动指针的过程,直到右指针到达序列的末尾。

实际例子辅助理解

去除有序数组中的重复元素

假设有一个有序数组 [1, 1, 2, 2, 2, 3, 4, 4, 5],我们要使用同向双指针去除其中的重复元素。

  • 初始化:左指针 left 和右指针 right 都指向数组的第一个元素 1
  • 右指针移动:右指针 right 开始向右移动,它会依次遇到元素。当它遇到第一个 2 时,发现和左指针指向的 1 不同。
  • 更新数组和移动左指针:此时,我们把左指针 left 向右移动一位,然后将右指针 right 指向的 2 放到左指针 left 现在指向的位置,也就是让数组变为 [1, 2, 2, 2, 2, 3, 4, 4, 5]
  • 继续移动右指针:右指针继续向右移动,遇到下一个 2,发现和左指针指向的 2 相同,右指针继续移动。当遇到 3 时,又和左指针指向的 2 不同,再次将左指针右移一位并更新数组。
  • 重复操作:不断重复这个过程,直到右指针到达数组末尾。最后,左指针之前的元素就是去重后的数组 [1, 2, 3, 4, 5]
找出数组中和为特定值的连续子数组

假设有数组 [1, 2, 3, 4, 5],要找出和为 7 的连续子数组。

  • 初始化:左指针 left 和右指针 right 都指向数组的第一个元素 1,同时记录当前子数组的和 sum = 1
  • 右指针移动:右指针 right 向右移动一位,指向 2,此时 sum = 1 + 2 = 3,小于目标值 7,右指针继续移动。
  • 继续移动右指针:右指针指向 3sum = 3 + 3 = 6,还是小于 7,继续移动。当右指针指向 4 时,sum = 6 + 4 = 10,大于 7
  • 移动左指针:此时让左指针 left 向右移动一位,指向 2,同时更新 sum = 10 - 1 = 9,仍然大于 7,左指针继续移动。当左指针指向 3 时,sum = 9 - 2 = 7,找到了满足条件的连续子数组 [3, 4]

通过这些例子可以看出,同向双指针通过巧妙地移动左右指针,避免了很多不必要的计算,从而提高了算法的效率。

(二) 快慢双指针的通俗理解

快慢双指针也是双指针技巧里很实用的一种,就好像在一个跑道上有两个运动员,一个跑得慢,一个跑得快,他们同时朝着相同方向奔跑。在算法里,这两个 “运动员” 就是两个指针,在处理数组、链表等线性数据结构时,以不同的速度移动,来解决各种各样的问题。

适用场景

快慢双指针常用于链表相关问题,比如判断链表是否有环、寻找链表的中间节点、找到链表倒数第 k 个节点等。当然,在一些数组问题中也能发挥作用。

操作方式与原理

1. 初始化

一开始,快慢两个指针都位于数据结构的起始位置。这就好比两个运动员在跑道的起点同时做好起跑准备。

2. 指针移动

快指针每次移动的 “步数” 比慢指针多。通常情况下,慢指针每次移动一步,而快指针每次移动两步。想象一下,在跑道上快的运动员一步能跨得更远,所以前进速度更快。

3. 利用速度差解决问题

由于快慢指针的移动速度不同,它们之间会逐渐拉开距离,我们就利用这个速度差来达到解决问题的目的。在移动过程中,根据具体问题的要求,观察快慢指针的位置关系或者它们所指向元素的特性。

常见问题实例分析

1. 判断链表是否有环

想象有一个环形跑道(代表有环的链表)和一个直线跑道(代表无环的链表)。让快慢两个运动员在跑道上跑步。

  • 初始化:快慢指针都指向链表的头节点,就像两个运动员站在跑道起点。
  • 移动指针:慢指针每次移动一个节点,快指针每次移动两个节点。
  • 判断结果
    • 如果链表没有环,就像直线跑道,快指针会先到达跑道的终点(链表的末尾)。
    • 如果链表有环,就像环形跑道,快指针在跑了几圈之后,一定会追上慢指针,也就是快慢指针会相遇。所以,当快慢指针相遇时,就说明链表有环;如果快指针到达了链表末尾,就说明链表没有环。
  • 另外当我们已经利用快慢指针判断出链表有环后,接下来要找到链表的第一个入环节点。这里可以借助快慢指针相遇的位置以及链表的特性来解决问题。
    • 从链表头节点到入环点的距离 a,等于从相遇点绕环 k - 1 圈再加上从相遇点到入环点的距离 c。所以,当快慢指针相遇后,我们让一个指针从链表头节点开始,另一个指针从相遇点开始,两个指针都以每次移动一步的速度前进,它们最终会在入环点相遇。
2. 寻找链表的中间节点

还是以跑道为例,假设跑道长度固定(代表链表长度固定),我们要找到跑道的中间位置。

  • 初始化:快慢指针都从链表的头节点出发。
  • 移动指针:慢指针一步一步地走,快指针一次走两步。
  • 确定中间节点:当快指针到达链表的末尾时,因为快指针的速度是慢指针的两倍,所以慢指针刚好走到链表的中间位置。此时慢指针所指向的节点就是链表的中间节点。
3. 找到链表倒数第 k 个节点

我们可以先让快指针在跑道上提前跑 k 步,然后快慢指针再同时开始正常移动,且快指针速度还是比慢指针快。

  • 初始化:快慢指针都指向链表头节点,先让快指针向前移动 k 步。这就好比快运动员先提前跑了 k 步。
  • 移动指针:之后快慢指针同时移动,快指针每次移动两步,慢指针每次移动一步。
  • 确定倒数第 k 个节点:当快指针到达链表末尾时,慢指针所在的位置就是链表的倒数第 k 个节点。因为快指针提前跑了 k 步,在相同时间内快慢指针移动,快指针到终点时,慢指针就处于倒数第 k 个位置。

总结

快慢双指针通过不同的移动速度产生的速度差,巧妙地解决了很多与位置、循环相关的问题,避免了一些复杂的遍历和计算,提高了算法效率。在实际应用中,关键是要根据具体问题确定好快慢指针的初始位置、移动规则以及判断条件。

(三)从两头往中间的双指针(对撞指针)

从两头往中间的双指针,也叫对撞指针,就像是两个人分别站在一条道路的两端,然后同时朝着道路中间走去。在算法里,这两个人代表两个指针,它们分别指向数据结构(如数组、字符串)的起始位置和结束位置,然后根据特定的规则向中间移动,以此来高效地解决问题。

适用场景

这种双指针技巧常用于处理有序的数据结构,例如有序数组或有序链表。常见的应用场景包括:

  • 寻找两个数之和等于目标值。
  • 反转数组或字符串。
  • 验证回文串等。

操作步骤

1. 初始化

将一个指针(左指针)指向数据结构的起始位置,另一个指针(右指针)指向数据结构的结束位置。就像两个人站在道路的两端做好出发准备。

2. 移动指针

根据具体问题的条件,移动左右指针。通常有以下几种情况:

  • 如果满足特定条件,可能同时移动两个指针。
  • 如果不满足条件,可能只移动左指针或右指针。
3. 判断终止条件

当左右指针相遇或者越过彼此时,停止操作。这就好比两个人在道路中间相遇或者已经走过了对方。

实例分析

1. 两数之和 II - 输入有序数组

问题描述:给定一个已按照升序排列的有序数组 numbers,找到两个数使得它们相加之和等于目标数 target。返回这两个数的下标(下标从 1 开始)。

操作过程

  • 初始化:左指针 left 指向数组的第一个元素,右指针 right 指向数组的最后一个元素。
  • 移动指针:计算左右指针所指元素的和 sum
    • 如果 sum 等于 target,则找到了满足条件的两个数,返回它们的下标。
    • 如果 sum 小于 target,说明和不够大,将左指针右移一位,让和增大。
    • 如果 sum 大于 target,说明和太大了,将右指针左移一位,让和减小。
  • 终止条件:当左右指针相遇时,说明没有找到满足条件的两个数。
2. 反转字符串

问题描述:编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。

操作过程

  • 初始化:左指针 left 指向字符数组的第一个字符,右指针 right 指向字符数组的最后一个字符。
  • 移动指针:交换左右指针所指的字符,然后将左指针右移一位,右指针左移一位。
  • 终止条件:当左右指针相遇或者越过彼此时,停止交换,字符串反转完成。

相应题目链接

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

287. 寻找重复数 - 力扣(LeetCode)

42. 接雨水 - 力扣(LeetCode)

881. 救生艇 - 力扣(LeetCode)

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

475. 供暖器 - 力扣(LeetCode)

41. 缺失的第一个正数 - 力扣(LeetCode)

你可能感兴趣的:(算法)