双指针算法深度解析

双指针算法简介

双指针是一种常用的算法技巧,它通过使用两个指针在数据结构上进行操作,通常用来优化时间复杂度,解决数组或链表中的一些特定问题。双指针技巧一般用于“有序”数据结构(如排序后的数组或链表),但也可以扩展到一些特殊的无序问题。

双指针的基本思想

双指针算法通过两个指针同时遍历数组或链表,指针之间根据特定规则相互移动,从而达到解决问题的目的。双指针的经典应用场景包括:

  1. 寻找两数之和(例如给定一个有序数组,查找是否存在两个数之和等于目标值)
  2. 反转字符串或数组
  3. 滑动窗口问题(例如求最大子数组和、最小子数组长度等)
  4. 合并两个有序数组

双指针有两种常见的移动方式:

  • 快慢指针:一个指针每次移动两步,另一个指针每次移动一步,用于检测环形链表、寻找中间节点等问题。
  • 左右指针:两个指针分别从序列的两端开始,向中间逼近,常用于有序数组、字符串等的处理。

1.经典问题:两数之和(Two Sum)

测试链接:https://leetcode.cn/problems/two-sum/description/

假设给定一个有序数组,要求找到两个数的和等于目标值。我们可以用双指针技巧高效地解决这个问题。

问题描述:

给定一个有序数组nums和一个目标值target,请你在数组中找出两个数,使得它们的和等于目标值,并返回它们的数组下标。

思路:
  1. 初始化两个指针:一个指向数组的开头(left),另一个指向数组的末尾(right)。
  2. 计算当前指针所指元素的和:
    • 如果和等于目标值,返回这两个指针所指的元素的下标。
    • 如果和小于目标值,说明需要更大的值,将left指针向右移动。
    • 如果和大于目标值,说明需要更小的值,将right指针向左移动。
  3. 重复这个过程,直到两个指针相遇。
Java代码示例:
public class TwoSum {
    public static int[] twoSum(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        
        while (left < right) {
            int sum = nums[left] + nums[right];
            
            if (sum == target) {
                return new int[] { left, right }; // 找到目标值,返回下标
            } else if (sum < target) {
                left++; // 需要更大的值,移动左指针
            } else {
                right--; // 需要更小的值,移动右指针
            }
        }
        
        throw new IllegalArgumentException("No solution found");
    }

    public static void main(String[] args) {
        int[] nums = {2, 7, 11, 15};
        int target = 9;
        int[] result = twoSum(nums, target);
        
        System.out.println("Indices: [" + result[0] + ", " + result[1] + "]");
    }
}
代码解释:
  1. left指针从数组的开始位置出发,right指针从数组的末尾出发。
  2. while (left < right) 确保两个指针没有交叉。
  3. 根据sumtarget的比较结果,决定移动哪一个指针。若sum == target,则返回当前指针位置的下标。
时间复杂度:

时间复杂度为O(n),其中n是数组的长度。由于每个元素最多只被访问一次,因此时间复杂度为线性时间。

空间复杂度:

空间复杂度为O(1),只用了常数空间。

其他应用场景

除了“两数之和”问题,双指针还可以解决许多其他问题。以下是一些常见的应用场景:

  1. 反转数组或字符串:双指针从两端向中间靠拢交换元素。
  2. 无重复元素的两数之和:在无重复的情况下,可以使用双指针找到目标值的两数之和。
  3. 滑动窗口:利用双指针来调整窗口大小,解决如最长子串、最小子数组等问题。
  4. 合并两个有序数组:通过双指针同时遍历两个数组,按顺序合并成一个新的有序数组。
2.反转字符串代码示例:
public class ReverseString {
    public static void reverse(char[] s) {
        int left = 0, right = s.length - 1;
        
        while (left < right) {
            char temp = s[left];
            s[left] = s[right];
            s[right] = temp;
            left++;
            right--;
        }
    }

    public static void main(String[] args) {
        char[] s = {'h', 'e', 'l', 'l', 'o'};
        reverse(s);
        System.out.println(s); // 输出: "olleh"
    }
}

测试链接:https://leetcode.cn/problems/reverse-string/description/


经典问题复盘:三数之和

测试链接:https://leetcode.cn/problems/3sum/description/

问题描述:

给定一个包含n个整数的数组nums,判断nums中是否存在三个元素a, b, c,使得a + b + c = 0。请找出所有满足条件的三元组。

思路:
  1. 首先,将数组排序,排序后的数组使得能够通过双指针技术有效查找符合条件的三元组。
  2. 遍历数组,固定第一个元素nums[i],然后通过双指针在剩余的部分寻找符合条件的两个数。
  3. 若当前三元组和为0,保存结果,并移动指针跳过重复元素。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ThreeSum {
    public static 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;
            
            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++;
                    while (left < right && nums[right] == nums[right - 1]) right--;
                    left++;
                    right--;
                } else if (sum < 0) {
                    left++;
                } else {
                    right--;
                }
            }
        }
        return result;
    }

    public static void main(String[] args) {
        int[] nums = {-1, 0, 1, 2, -1, -4};
        List<List<Integer>> result = threeSum(nums);
        
        for (List<Integer> triplet : result) {
            System.out.println(triplet);
        }
    }
}
代码解释:
  • 数组先排序,便于后续的双指针查找。
  • 外层循环固定一个元素nums[i],然后用双指针leftright在剩余部分寻找满足和为0的两个数。
  • 为了避免重复的三元组,在每次找到有效三元组时,需要跳过重复的元素。
时间复杂度:
  • 排序的时间复杂度是O(n log n),外层循环为O(n),内层的双指针为O(n),因此整体时间复杂度为O(n^2)
空间复杂度:
  • O(1),只用了常数空间(不考虑输出结果)。

2. 滑动窗口与双指针:最小子数组之和

测试链接:

问题描述:

给定一个含有n个正整数的数组nums和一个目标值target,请找出该数组中和大于或等于target的最小子数组的长度。如果没有符合条件的子数组,返回0

思路:
  1. 使用滑动窗口的方法,双指针leftright分别代表窗口的左右边界。
  2. 初始时,right指针逐步扩大窗口,直到窗口中的元素和大于等于target
  3. 然后,移动left指针收缩窗口,尽可能地减小窗口的大小,并同时保持和大于等于target
Java代码实现:
public class MinSubArrayLen {
    public static int minSubArrayLen(int target, int[] nums) {
        int n = nums.length;
        int minLength = Integer.MAX_VALUE;
        int sum = 0;
        int left = 0;
        
        for (int right = 0; right < n; right++) {
            sum += nums[right];
            
            while (sum >= target) {
                minLength = Math.min(minLength, right - left + 1);
                sum -= nums[left++];
            }
        }
        
        return minLength == Integer.MAX_VALUE ? 0 : minLength;
    }

    public static void main(String[] args) {
        int[] nums = {2, 3, 1, 2, 4, 3};
        int target = 7;
        System.out.println("Minimum length subarray: " + minSubArrayLen(target, nums)); // Output: 2
    }
}
代码解释:
  • 使用双指针leftright表示当前滑动窗口的左右边界。
  • 逐步扩大right指针,直到窗口和满足条件,然后尝试缩小窗口(移动left指针)以寻找最小长度。
时间复杂度:
  • 由于每个元素最多被leftright指针访问一次,因此时间复杂度为O(n)
空间复杂度:
  • O(1),只用了常数空间。

3. 合并两个有序数组

测试链接:https://leetcode.cn/problems/merge-sorted-array/description/

问题描述:

给定两个已排序的数组nums1nums2,请将nums2合并到nums1中,使得nums1仍然是一个有序数组。

思路:
  • 使用双指针ij分别从nums1nums2的末尾开始,比较两个数组的当前元素,将较大的元素放到nums1的末尾。这个方法可以避免额外的空间开销。
Java代码实现:
public class MergeSortedArray {
    public static void merge(int[] nums1, int m, int[] nums2, int n) {
        int i = m - 1, j = n - 1, k = m + n - 1;
        
        while (i >= 0 && j >= 0) {
            if (nums1[i] > nums2[j]) {
                nums1[k--] = nums1[i--];
            } else {
                nums1[k--] = nums2[j--];
            }
        }
        
        while (j >= 0) {
            nums1[k--] = nums2[j--];
        }
    }

    public static void main(String[] args) {
        int[] nums1 = new int[6];
        int[] nums2 = {1, 2, 3};
        nums1[0] = 2;
        nums1[1] = 5;
        nums1[2] = 6;
        merge(nums1, 3, nums2, 3);
        
        System.out.println("Merged array: " + Arrays.toString(nums1));
    }
}
代码解释:
  • 使用两个指针ij分别指向nums1nums2的最后元素,通过比较两个元素的大小将较大的元素放到nums1的末尾。
  • nums2还有剩余元素时,直接将其拷贝到nums1中。
时间复杂度:
  • O(m + n),其中mn分别是nums1nums2的长度。
空间复杂度:
  • O(1),只使用了常数空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值