文章

双指针技术

双指针技术

一、背景

双指针技术是一种常用的算法设计技巧,广泛应用于数组、链表和字符串等数据结构中。它通过使用两个指针同时遍历数据结构,从而实现高效的解决方案。双指针技术通常分为两种类型:快慢指针和左右指针。

二、双指针的类型

  1. 快慢指针:这种方法使用两个指针,一个指针(快指针)以较快的速度移动,另一个指针(慢指针)以较慢的速度移动。常用于检测链表中的环寻找链表的中间节点等问题。
  2. 左右指针:这种方法使用两个指针分别从数据结构的两端开始向中间移动。常用于排序数组的两数之和、反转字符串等问题。

三、双指针的应用示例

示例1:反转字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ReverseString {
    public void reverseString(char[] s) {
        int left = 0;
        int right = s.length - 1;
        while (left < right) {
            char temp = s[left];
            s[left] = s[right];
            s[right] = temp;
            left++;
            right--;
        }
    }
}

示例2:两数之和 II - 输入有序数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TwoSum {
    public int[] twoSum(int[] numbers, int target) {
        int left = 0;
        int right = numbers.length - 1;
        while (left < right) {
            int sum = numbers[left] + numbers[right];
            if (sum == target) {
                return new int[]{left + 1, right + 1};
            } else if (sum < target) {
                left++;
            } else {
                right--;
            }
        }
        return new int[]{-1, -1}; // No solution found
    }
}

示例3:环形链表检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LinkedListCycle {
    public boolean hasCycle(ListNode head) {
        if (head == null) return false;
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                return true; // Cycle detected
            }
        }
        return false; // No cycle
    }
}

四、LeetCode例题

解决双指针的问题,最重要的是要双指针的移动策略,理解题意后,确定两个指针的初始位置和移动条件。以下是一些经典的LeetCode双指针题目:

例1.15. 三数之和

1.题目描述 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。 2.解题思路

  • 排序数组:首先对数组进行排序,这样可以方便地使用双指针法。
  • 固定一个数:遍历排序后的数组,固定一个数 nums[i],然后使用双指针法在剩余的部分寻找两个数,使得它们的和为 -nums[i]。
  • 双指针法:设置两个指针 left 和 right,分别指向固定数之后的部分的开始和结束位置,根据当前三个数的和调整指针位置。

3.算法正确性证明

通过排序数组,可以确保在使用双指针法时,能够有效地跳过重复的元素,从而避免重复的三元组。 固定一个数后,使用双指针法可以在 O(n) 的时间内找到所有满足条件的组合。

4.代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ThreeSum {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0; i < nums.length - 2; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) continue; // Skip duplicates
            int left = i + 1;
            int right = nums.length - 1;
            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];
                if (sum == 0) {
                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    while (left < right && nums[left] == nums[left + 1]) left++; // Skip duplicates
                    while (left < right && nums[right] == nums[right - 1]) right--; // Skip duplicates
                    left++;
                    right--;
                } else if (sum < 0) {
                    left++;
                } else {
                    right--;
                }
            }
        }
        return result;
    }
}

例2.160. 相交链表

这个题目不是传统意义上的双指针问题,需要通过敏锐的数学思维发现其中的双指针技巧。

1.题目描述 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。 2.解题思路

  • 暴力解法:使用哈希表存储一个链表的节点,然后遍历另一个链表,检查每个节点是否在哈希表中存在。时间复杂度为 O(m+n),空间复杂度为 O(m) 或 O(n)。

  • 双指针法:使用双指针分别遍历两个链表,当一个指针到达链表末尾时,跳转到另一个链表的头节点。这样两个指针最终会在相交节点相遇,或者都到达 null。

3.算法正确性证明

假设链表 A 的长度为 a,链表 B 的长度为 b,相交部分的长度为 c。那么当指针 A 遍历完链表 A 后,跳转到链表 B 的头节点,此时它已经走了 a 步;

同理,指针 B 遍历完链表 B 后,跳转到链表 A 的头节点,它已经走了 b 步。

接下来,两个指针将分别走 c 步到达相交节点。

因此,当两个指针相遇时,它们已经走了 a + b 步。

4.代码实现

1
2
3
4
5
6
7
8
9
10
11
12
public class IntersectionOfTwoLinkedLists {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) return null;
        ListNode pointerA = headA;
        ListNode pointerB = headB;
        while (pointerA != pointerB) {
            pointerA = (pointerA == null) ? headB : pointerA.next;
            pointerB = (pointerB == null) ? headA : pointerB.next;
        }
        return pointerA; // or pointerB
    }
}
本文由作者按照 CC BY 4.0 进行授权