双指针算法:化繁为简的优雅解法

目录

双指针算法:化繁为简的优雅解法

从两数之和到滑动窗口,双指针算法如何用O(n)的时间复杂度解决看似复杂的问题?

引言:一个经典的优化故事

想象一下这个场景:你需要在一个有序数组中找到两个数,使它们的和等于目标值。最直观的方法是什么?很多人会想到双重循环:

cpp 复制代码
// 暴力解法 O(n²)
for (int i = 0; i < n; i++) {
    for (int j = i + 1; j < n; j++) {
        if (nums[i] + nums[j] == target) {
            return {i, j};
        }
    }
}

但这样做的时间复杂度是O(n²),当数组很大时效率极低。有没有更好的方法?

双指针算法 给出了答案,可以将时间复杂度优化到O(n)

cpp 复制代码
// 双指针解法 O(n)
int left = 0, right = nums.size() - 1;
while (left < right) {
    int sum = nums[left] + nums[right];
    if (sum == target) return {left, right};
    else if (sum < target) left++;
    else right--;
}

这就是双指针的魅力所在!

什么是双指针算法?

双指针算法是一种通过两个指针(索引或引用)在数据结构上协同工作来解决问题的技巧。这两个指针按照一定规则移动,从而减少不必要的遍历,将许多O(n²)的问题优化到O(n)或O(n log n)。

核心优势

  • 时间复杂度优化:通常从O(n²)降至O(n)
  • 空间复杂度低:通常只需要O(1)的额外空间
  • 代码简洁优雅:逻辑清晰,易于理解和维护

三大经典类型详解

1. 相向双指针:从两端向中间

这是最常见的一种类型,特别适合有序数组

经典问题:盛最多水的容器(LeetCode 11)

链接:盛最多水的容器 (LeetCode 11)

cpp 复制代码
int maxArea(vector<int>& height) {
    int left = 0, right = height.size() - 1;
    int maxWater = 0;
    
    while (left < right) {
        int h = min(height[left], height[right]);
        int w = right - left;
        maxWater = max(maxWater, h * w);
        
        // 关键:移动较小的一侧
        if (height[left] < height[right])
            left++;
        else
            right--;
    }
    return maxWater;
}

为什么移动较小的一侧?

因为容器的容量由较短的边决定。移动较短的边,虽然宽度减小了,但高度可能增加,从而可能获得更大的面积。而移动较长的边,高度不会增加(仍由短边决定),宽度却减小了,容量必然减小。

三数之和问题(LeetCode 15)

链接:三数之和问题(LeetCode 15)

cpp 复制代码
vector<vector<int>> threeSum(vector<int>& nums) {
    vector<vector<int>> result;
    sort(nums.begin(), nums.end());
    
    for (int i = 0; i < nums.size(); i++) {
        // 去重
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        
        int left = i + 1, right = nums.size() - 1;
        while (left < right) {
            int sum = nums[i] + nums[left] + nums[right];
            
            if (sum == 0) {
                result.push_back({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;
}

2. 同向双指针:快慢指针的妙用

这种模式中,两个指针从同一端开始,但移动速度不同。

删除有序数组中的重复项(LeetCode 26)

链接:删除有序数组中的重复项(LeetCode 26)

cpp 复制代码
int removeDuplicates(vector<int>& nums) {
    if (nums.empty()) return 0;
    
    int slow = 0;  // 慢指针:指向当前不重复元素的位置
    
    for (int fast = 1; fast < nums.size(); fast++) {
        // 当发现新元素时,更新慢指针位置
        if (nums[fast] != nums[slow]) {
            slow++;
            nums[slow] = nums[fast];
        }
    }
    
    return slow + 1;  // 返回新长度
}

理解这个算法:

  • slow指针维护"处理好的部分"
  • fast指针遍历整个数组
  • fast找到新元素时,将其放到slow+1的位置

这个过程就像我们整理书架:slow指向已经整理好的部分,fast寻找新书,找到后放到整理区的末尾。

链表中的快慢指针:检测环
cpp 复制代码
bool hasCycle(ListNode *head) {
    if (!head || !head->next) return false;
    
    ListNode *slow = head;
    ListNode *fast = head->next;
    
    while (slow != fast) {
        // 快指针到达末尾,说明无环
        if (!fast || !fast->next) 
            return false;
        
        slow = slow->next;      // 慢指针走一步
        fast = fast->next->next; // 快指针走两步
    }
    return true;  // 相遇说明有环
}

有趣的事实: 如果有环,快慢指针一定会相遇,就像两个人在环形跑道上跑步,速度快的人总会追上速度慢的人。

实际应用场景

场景1:合并两个有序数组

cpp 复制代码
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
    int p1 = m - 1, p2 = n - 1, p = m + n - 1;
    
    // 从后向前合并,避免覆盖
    while (p1 >= 0 && p2 >= 0) {
        if (nums1[p1] > nums2[p2]) {
            nums1[p--] = nums1[p1--];
        } else {
            nums1[p--] = nums2[p2--];
        }
    }
    
    // 如果nums2还有剩余元素
    while (p2 >= 0) {
        nums1[p--] = nums2[p2--];
    }
}

场景2:接雨水问题(LeetCode 42)

cpp 复制代码
int trap(vector<int>& height) {
    int left = 0, right = height.size() - 1;
    int leftMax = 0, rightMax = 0;
    int water = 0;
    
    while (left < right) {
        // 左边较矮,计算左边
        if (height[left] < height[right]) {
            if (height[left] >= leftMax) {
                leftMax = height[left];
            } else {
                water += leftMax - height[left];
            }
            left++;
        } 
        // 右边较矮,计算右边
        else {
            if (height[right] >= rightMax) {
                rightMax = height[right];
            } else {
                water += rightMax - height[right];
            }
            right--;
        }
    }
    
    return water;
}

算法模板总结

通用模板

cpp 复制代码
// 相向双指针
void oppositePointers(vector<int>& nums) {
    int left = 0, right = nums.size() - 1;
    while (left < right) {
        // 根据具体条件决定移动哪个指针
        if (condition1) left++;
        else if (condition2) right--;
        else {
            // 找到结果,同时移动两个指针
            left++;
            right--;
        }
    }
}

// 快慢指针
void fastSlowPointers(vector<int>& nums) {
    int slow = 0;  // 慢指针:维护结果位置
    for (int fast = 0; fast < nums.size(); fast++) {
        // 当快指针满足条件时,更新慢指针
        if (condition) {
            nums[slow] = nums[fast];
            slow++;
        }
    }
}

常见问题与技巧

1. 如何选择双指针类型?

  • 有序数组查找 → 相向双指针
  • 去重/元素操作 → 快慢指针
  • 链表问题 → 快慢指针

2. 指针移动的条件是什么?

这是双指针算法的关键!通常:

  • 比较两指针指向的值
  • 根据目标值与当前值的关系
  • 根据是否满足某种条件

3. 边界条件处理

  • 空数组/空字符串
  • 单元素情况
  • 指针越界检查
  • 重复元素处理

实战练习题目

难度 题目 类型 关键点
简单 移动零 快慢指针 将非零元素前移
简单 反转字符串 相向双指针 交换首尾字符
中等 三数之和 相向双指针 排序+去重
中等 盛最多水的容器 相向双指针 移动较小边
中等 无重复字符的最长子串 滑动窗口 字符计数
困难 最小覆盖子串 滑动窗口 字符需求统计

结语

双指针算法之所以强大,是因为它利用数据的特性 (如有序性)和问题的约束,避免了不必要的计算。

相关推荐
爱装代码的小瓶子2 小时前
【c++知识铺子】封装map和set(详细版)
android·java·c++
Aaron15882 小时前
RFSOC+VU13P在无线信道模拟中的技术应用分析
数据结构·人工智能·算法·fpga开发·硬件架构·硬件工程·射频工程
明洞日记2 小时前
【VTK手册026】高性能网格简化——vtkQuadricClustering 深度解析
c++·图像处理·vtk·图形渲染
咸鱼加辣2 小时前
“刻意强调” O(1)
数据结构·算法
南烟斋..2 小时前
Linux进程管理完全指南:创建、终止、回收与替换
linux·算法
xiaoye-duck3 小时前
C++入门基础指南:引用全解析(从入门到精通)
c++
点我头像干啥3 小时前
机器学习算法之动量法:优化梯度下降的“惯性”策略
人工智能·神经网络·算法·机器学习
XFF不秃头3 小时前
力扣刷题笔记-下一个排列
c++·笔记·算法·leetcode
Lv11770083 小时前
Visual Studio中Array数组的常用查询方法
笔记·算法·c#·visual studio