双指针力扣题(leetcode)

双指针力扣题(leetcode)

5. 最长回文子串

难度:中等

相关标签:双指针字符串动态规划

题目:

给你一个字符串 s,找到 s 中最长的 回文子串。

示例 1:

C++ 复制代码
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:

C++ 复制代码
输入:s = "cbbd"
输出:"bb"

提示:

  • \(1 <= s.length <= 1000\)
  • \(s 仅由数字和英文字母组成\)

代码:

C++ 复制代码
class Solution 
{
public:
    string longestPalindrome(string s) 
    {
        int n = s.size();
        if (n < 2) return s; // 空串/单字符直接返回
        
        int max_len = 1;     // 最长回文长度(至少1)
        int start = 0;       // 最长回文的起始下标
        
        // 第一步:枚举所有可能的子串 [left, right]
        for (int left = 0; left < n; left++)
        {
            // 优化1:提前剪枝------剩余字符数 < 当前最长,直接退出循环
            if (n - left <= max_len) break;
            
            // 优化2:只枚举长度 > max_len 的子串(right从left+max_len开始)
            for (int right = left + max_len; right < n; right++)
            {
                // 第二步:双指针验证 [left, right] 是否是回文
                int l = left;
                int r = right;
                bool is_pal = true;
                
                while (l < r)
                {
                    if (s[l] != s[r])
                    {
                        is_pal = false;
                        break; // 提前终止验证
                    }
                    l++;
                    r--;
                }
                
                // 第三步:更新最长回文记录(不再break,继续找更长)
                if (is_pal)
                {
                    max_len = right - left + 1;
                    start = left;
                }
            }
        }
        
        return s.substr(start, max_len);
    }
};
C++ 复制代码
class Solution 
{
public:
    string longestPalindrome(string s) 
    {
        int n = s.size();
        if (n < 2) return s; // 空串/单字符直接返回
        
        int max_len = 1;     // 最长回文长度(至少1)
        int start = 0;       // 最长回文的起始下标
        
        // 第一步:枚举所有可能的子串 [left, right]
        // left是子串起始,right是子串结束
        for (int left = 0; left < n; left++)
         {
            for (int right = left; right < n; right++)
             {
                // 第二步:双指针验证 [left, right] 是否是回文
                int l = left;   // 左指针:从子串开头出发
                int r = right;  // 右指针:从子串结尾出发
                bool is_pal = true; // 标记是否是回文
                
                // 双指针向中间收缩,逐一对比字符
                while (l < r)
                 {
                    if (s[l] != s[r])// 字符不相等,不是回文
                     { 
                        is_pal = false;
                        break;
                    }
                    l++; // 左指针右移
                    r--; // 右指针左移
                }
                
                // 第三步:如果是回文,且长度更长,更新记录
                if (is_pal && (right - left + 1) > max_len)
                 {
                    max_len = right - left + 1;
                    start = left; // 更新最长回文的起始位置
                }
            }
        }
        
        // 截取最长回文子串(从start开始,长度max_len)
        return s.substr(start, max_len);
    }
};
解法 双指针方式 时间复杂度 优点 缺点
暴力枚举 + 双指针验证 两端向中间收缩 O(n³) 逻辑 100% 直观,易理解 效率低(n=1000 时略慢)
中心扩展法(双指针) 中心向两侧扩散 O(n²) 效率高,空间最优 需要理解 "回文中心"
C++ 复制代码
class Solution 
{
public:
    // 核心双指针:从中心向外扩展,返回扩展后的回文长度
    int expand(const string& s, int l, int r)
     {
        // 只要左右指针没越界,且字符相等,就继续往两边扩
        while (l >= 0 && r < s.size() && s[l] == s[r]) 
        {
            l--;
            r++;
        }
        // 循环停止时,s[l] != s[r],实际长度是 (r-1) - (l+1) + 1 = r - l - 1
        return r - l - 1;
    }

    string longestPalindrome(string s) 
    {
        int n = s.size();
        // 基础边界处理
        if (n < 2) return s;

        int start = 0;    // 记录最长回文串的起始位置
        int max_len = 1;  // 记录最长回文串的长度

        for (int i = 0; i < n; i++)
         {
            // 每一个字符 i 都可以作为"中心"向两边扩展
            
            // 1. 奇数长度扩展:中心是一个字符(如 "aba", 中心是 'b')
            int len1 = expand(s, i, i);
            
            // 2. 偶数长度扩展:中心是两个字符之间的空隙(如 "abba", 中心是 "bb")
            int len2 = expand(s, i, i + 1);

            // 取两种情况中最长的那一个
            int cur_max = max(len1, len2);

            // 如果发现更长的,更新起始点和长度
            if (cur_max > max_len) 
            {
                max_len = cur_max;
                // 计算起始点的技巧:当前中心 i 减去 (长度-1)/2
                start = i - (max_len - 1) / 2;
            }
        }

        return s.substr(start, max_len);
    }
};

相似题目

最短回文串困难

回文排列简单

回文对困难

最长回文子序列中等

回文子串中等

不重叠回文子字符串的最大数目困难

11. 盛最多水的容器

难度:中等

相关标签:贪心数组双指针

题目:

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

**说明:**你不能倾斜容器。

示例 1:

输入:[1,8,6,2,5,4,8,3,7]

输出:49

解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

示例 2:

C++ 复制代码
输入:height = [1,1]
输出:1

提示:

  • \(n == height.length\)
  • \(2 <= n <= 10^5\)
  • \(0 <= height[i] <= 10^4\)

代码:

C++ 复制代码
class Solution 
{
public:
    int maxArea(vector<int>& height) 
    {
        int n = height.size();
        int left = 0;          // 左指针:初始在最左边
        int right = n - 1;     // 右指针:初始在最右边
        int max_water = 0;     // 记录最大盛水量
        
        // 双指针向中间移动,直到相遇
        while (left < right)
        {
            // 步骤1:计算当前容器的盛水量
            int width = right - left;// 容器的宽度(指针间距)
            int min_height = min(height[left], height[right]);//容器的高度(由矮边决定)
            int cur_water = width * min_height;// 当前盛水量
            
            // 步骤2:更新最大盛水量
            max_water = max(max_water, cur_water);
            
            // 步骤3:移动指针(谁矮谁动)
            if (height[left] < height[right])  left++; // 左指针矮,右移
            else  right--; // 右指针矮(或相等),左移
        }
        
        return max_water;
    }
};

相似题目

接雨水困难

礼盒的最大甜蜜度中等

打家劫舍 IV中等

15. 三数之和

难度:中等

相关标签:数组双指针排序

题目:

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

**注意:**答案中不可以包含重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]

输出:[[-1,-1,2],[-1,0,1]]

解释:

nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。

nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。

nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。

不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。

示例 2:

输入:nums = [0,1,1]

输出:[]

解释:唯一可能的三元组和不为 0 。

示例 3:

输入:nums = [0,0,0]

输出:[[0,0,0]]

解释:唯一可能的三元组和为 0

提示:

  • \(3 <= nums.length <= 3000\)
  • \(-10^5 <= nums[i] <= 10^5\)

代码:

C++ 复制代码
class Solution 
{
public:
    vector<vector<int>> threeSum(vector<int>& nums) 
    {
        vector<vector<int>> res; // 存储最终结果
        int n = nums.size();
        if (n < 3) return res;   // 边界:元素不足3个,直接返回空
        
        // 步骤1:排序数组(核心前提,方便双指针和去重)
        sort(nums.begin(), nums.end());
        
        // 步骤2:遍历固定第一个数nums[i]
        for (int i = 0; i < n; i++)
        {
            // 剪枝1:排序后第一个数>0,三数之和不可能为0,直接退出
            if (nums[i] > 0) break;
            
            // 去重1:跳过重复的nums[i](避免重复的三元组)
            if (i > 0 && nums[i] == nums[i-1]) continue;
            
            // 步骤3:双指针初始化
            int left = i + 1;    // 左指针:i的下一个位置(避免重复使用i)
            int right = n - 1;   // 右指针:数组末尾
            
            // 双指针向中间移动
            while (left < right)
            {
                int sum = nums[i] + nums[left] + nums[right];
                
                if (sum == 0) // 找到和为0的三元组
                {
                    // 加入结果集
                    res.push_back({nums[i], nums[left], nums[right]});
                    
                    // 去重2:跳过重复的nums[left]
                    while (left < right && nums[left] == nums[left+1]) left++;
                    // 去重3:跳过重复的nums[right]
                    while (left < right && nums[right] == nums[right-1]) right--;
                    
                    // 双指针同时移动,找下一组可能的数
                    left++;
                    right--;
                }
                else if (sum < 0) left++;// 和太小,左指针右移(增大数值) 
                else right--;// 和太大,右指针左移(减小数值)
            }
        }
        
        return res;
    }
};

相似题目

两数之和简单

最接近的三数之和中等

四数之和中等

较小的三数之和中等

等差三元组的数目简单

元素和最小的山形三元组 I简单

元素和最小的山形三元组 II中等

31. 下一个排列

难度:中等

相关标签:数组双指针

题目:

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1]

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2]
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2]
  • arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须**原地**修改,只允许使用额外常数空间。

示例 1:

输入:nums = [1,2,3]

输出:[1,3,2]

示例 2:

输入:nums = [3,2,1]

输出:[1,2,3]

示例 3:

输入:nums = [1,1,5]

输出:[1,5,1]

提示:

  • \(0 <= nums.length <= 100\)
  • \(0<= nums[i] <= 100\)

代码:

C++ 复制代码
class Solution 
{
public:
    void nextPermutation(vector<int>& nums) 
    {
        int n = nums.size();
        if (n <= 1) return; // 边界:长度<=1,无需调整
        
        // 步骤1:双指针找升序拐点(j为左指针,i为右指针)
        // 模板对应:j是左指针,i是右指针,维护「nums[j] < nums[i]」的性质
        int i = n - 1,j = n - 2 ; 
        while (j >= 0 && nums[j] >= nums[i]) // 修正:判断条件仍用nums[j] >= nums[i](j是左,i是右)
        {
            j--;
            i--; // 同步移动:j左移,i左移
        }
        
        // 步骤2:若找到拐点,双指针找最小更大值并交换
        if (j >= 0) // 修正:判断拐点是否存在,用j(原i)
        {
            // 模板对应:k是右指针,j是左指针,check(k,j) = nums[k] > nums[j]
            int k = n - 1;
            while (nums[k] <= nums[j]) k--; 
            swap(nums[j], nums[k]); 
        }
        
        // 步骤3:双指针反转后缀(模板中「指向一个序列」的双指针)
        int l = i, r = n - 1;
        while (l < r) 
        {
            swap(nums[l], nums[r]);
            l++;
            r--;
        }
    }
};

算法推导

如何得到这样的排列顺序?这是本文的重点。我们可以这样来分析:

  1. 我们希望下一个数 比当前数大,这样才满足 "下一个排列" 的定义。因此只需要 将后面的「大数」与前面的「小数」交换,就能得到一个更大的数。比如 123456,将 5 和 6 交换就能得到一个更大的数 123465。
  2. 我们还希望下一个数 增加的幅度尽可能的小,这样才满足"下一个排列与当前排列紧邻"的要求。为了满足这个要求,我们需要:
    • 在 尽可能靠右的低位 进行交换,需要 从后向前 查找
    • 将一个 尽可能小的「大数」 与前面的「小数」交换。比如 123465,下一个排列应该把 5 和 4 交换而不是把 6 和 4 交换
    • 将「大数」换到前面后,需要将「大数」后面的所有数 重置为升序,升序排列就是最小的排列。以 123465 为例:首先按照上一步,交换 5 和 4,得到 123564;然后需要将 5 之后的数重置为升序,得到 123546。显然 123546 比 123564 更小,123546 就是 123465 的下一个排列

以上就是求 "下一个排列" 的分析过程。

算法过程

标准的 "下一个排列" 算法可以描述为:

  1. 从后向前 查找第一个 相邻升序 的元素对 \((j,i)\),满足 \(nums[j] < nums[i]\)。此时 \([i,end)\) 必然是降序
  2. 在 \([i,end)\) 从后向前 查找第一个满足 \(nums[j] < nums[k]\) 的 \(k\)。\(nums[j]、nums[k]\) 分别就是上文所说的「小数」、「大数」
  3. 将 \(nums[j]\) 与 \(nums[k]\) 交换
  4. 可以断定这时 \([i,end)\) 必然是降序,逆置 \([i,end)\),使其升序
  5. 如果在步骤 \(1\) 找不到符合的相邻元素对,说明当前 \([begin,end)\) 为一个降序顺序,则直接跳到步骤 \(4\)

该方法支持数据重复,且在 \(C++ STL\) 中被采用。

作者:Imageslr

链接:https://leetcode.cn/problems/next-permutation/solutions/80560/xia-yi-ge-pai-lie-suan-fa-xiang-jie-si-lu-tui-dao-/

来源:力扣(LeetCode)

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

代码分析

先明确核心目标

「下一个排列」的本质是:找到比当前排列大的所有排列中,最小的那个 ;如果当前已经是最大排列(比如[3,2,1]),就返回最小排列([1,2,3])。

步骤 1:双指针找升序拐点(j 左指针,i 右指针)

核心逻辑

从数组末尾往前找,找到第一个「左数 < 右数」的位置(升序拐点)------ 这个拐点是「能让排列变大的最小突破口」。

代码逐句解释
C++ 复制代码
int i = n - 1,j = n - 2 ; // i(右指针)指向最后一个数,j(左指针)指向倒数第二个数
while (j >= 0 && nums[j] >= nums[i]) { // 只要j没越界,且「左数 >= 右数」(降序)
    j--; // 左指针左移
    i--; // 右指针左移
}
例子理解(以nums = [1,3,2]为例)
  • 初始:i=2(值 2)、j=1(值 3)→ nums[j]=3 >= nums[i]=2(降序),进入循环;
  • 循环后:i=1(值 3)、j=0(值 1)→ nums[j]=1 < nums[i]=3(升序),停止循环;
  • 结论:找到拐点j=0(值 1),这个位置是「能变大的最小位置」。
为什么要找这个拐点?

如果数组后缀全是降序(比如[1,3,2]的后缀[3,2]),说明这部分已经是「最大排列」,无法再变大;只有找到第一个升序对,才能通过「交换拐点和后缀里的数」让整体排列变大。

步骤 2:找后缀中最小的更大值,和拐点交换

核心逻辑

找到拐点j后,在j右侧的后缀里,找「比 nums [j] 大的所有数中最小的那个」(保证变大的幅度最小),和j交换 ------ 这是「下一个排列」的关键:既要变大,又要变的幅度最小。

代码逐句解释
复制代码
if (j >= 0) { // 如果找到拐点(j没越界,说明不是最大排列)
    int k = n - 1; // k从数组末尾开始找(后缀是降序,末尾是最小的数)
    while (nums[k] <= nums[j]) k--; // 找第一个「比nums[j]大」的数
    swap(nums[j], nums[k]); // 交换,让排列变大,且幅度最小
}
例子理解(接上面nums = [1,3,2]
  • 拐点j=0(值 1),k 从末尾(k=2,值 2)开始找;
  • nums[k]=2 > nums[j]=1,停止循环;
  • 交换\(j=0\)和\(k=2\),数组变为\([2,3,1]\)?❌ 纠正:原数组是\([1,3,2]\),交换后是\([2,3,1]\)?不,原数组\([1,3,2]\)的拐点\(j=0(1)\),\(k=2(2)\),交换后是\([2,3,1]\)?(补充:如果是\(nums=[1,2,3]\),拐点\(j=1\)(2),k=2(3),交换后是\([1,3,2]\),这就是「下一个排列」)。
为什么从末尾找?

因为\(j\)右侧的后缀是降序 (步骤 1 找拐点的过程已经保证),所以从末尾往前找,第一个比\(nums[j]\)大的数,就是「最小的更大值」(比如后缀\([5,4,3]\),找比 2 大的数,3 是最小的)。

步骤 3:反转拐点右侧的后缀

核心逻辑

交换后,\(j\)右侧的后缀还是降序(因为步骤 1 的降序没被破坏),反转这个后缀,让它变成升序 ------ 这是为了保证「变大后的排列是最小的」(升序是后缀的最小排列)。

代码逐句解释
C++ 复制代码
int l = i, r = n - 1; // l指向后缀的起始位置(步骤1结束后,i就是j+1)
while (l < r) // 双指针向中间移动,反转后缀
{ 
    swap(nums[l], nums[r]);
    l++;
    r--;
}
例子理解(接nums=[1,3,2]交换后的[2,3,1]
  • \(l=i=1\)(值 3),\(r=2\)(值 1);
  • 交换\(l=1\)和\(r=2\),数组变为\([2,1,3]\)------ 这就是\([1,3,2]\)的下一个排列(验证:\([1,3,2]→[2,1,3]\),是比它大的最小排列)。
为什么要反转?

比如交换后后缀是\([3,1]\)(降序),反转后是\([1,3]\)(升序),这是该后缀的最小排列 ------ 如果不反转,\([2,3,1]\)比\([2,1,3]\)大,不符合\(「下一个排列(最小的更大值)」\)的要求。

极端情况:当前是最大排列(比如[3,2,1]

  • \(步骤 1:j 会一直左移到 - 1(没找到拐点),跳过步骤 2\);
  • 步骤 3:\(l=i=0,r=2\),反转整个数组,得到\([1,2,3]\)(最小排列),符合题目要求。

三个步骤的核心逻辑总结

步骤 目标 本质
1. 找拐点 找到「能变大的最小位置」 打破后缀的降序,找到调整的突破口
2. 交换 让排列「刚好变大一点」 找最小的更大值,保证变大幅度最小
3. 反转 让变大后的排列「最小」 把后缀转为升序,得到最小排列

这三个步骤环环相扣,最终实现「找到比当前大的最小排列」的核心目标,且全程只用双指针,时间复杂度 O (n),符合题目「原地修改、常数空间」的要求。

相似题目

第一个错误的版本简单

蜡烛之间的盘子中等

找出数组排序后的目标下标简单

42. 接雨水

难度:困难

相关标签:数组双指针动态规划单调栈

题目:

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

示例 2:

C++ 复制代码
输入:nums = [3,1,3,4,2]
输出:3

示例 3:

C++ 复制代码
输入:nums = [3,3,3,3,3]
输出:3

提示:

  • \(0 <= nums.length <= 10^5\)
  • \(-10^9 <= nums[i] <= 10^9\)
  • \(nums\) 是一个非递减数组
  • \(-10^9 <= target <= 10^9\)

代码:

C++ 复制代码
class Solution 
{
public:
    int trap(vector<int>& height) 
    {
        int n = height.size();
        if (n <= 2) return 0; // 边界:少于3根柱子,无法接水
        
        // 双指针初始化:l=左指针(区间左边界),r=右指针(区间右边界)
        int l = 0, r = n - 1;
        int left_max = height[l], right_max = height[r];
        int res = 0; // 总接水量
        
        // 模板对应:遍历区间(l/r向中间移动,等价于模板中i遍历、j维护)
        while (l < r) 
        {
            // 核心单调性:left_max < right_max → l位置的水量由left_max决定
            if (left_max < right_max) 
            {
                l++; // 左指针右移(对应模板中j移动)
                // 维护left_max(区间[0,l]的最大高度)
                left_max = max(left_max, height[l]);
                // 业务逻辑:计算当前l位置的接水量
                res += left_max - height[l];
            } 
            else 
            {
                r--; // 右指针左移(对应模板中i移动)
                // 维护right_max(区间[r,n-1]的最大高度)
                right_max = max(right_max, height[r]);
                // 业务逻辑:计算当前r位置的接水量
                res += right_max - height[r];
            }
        }
        
        return res;
    }
};

先考察一下,以这个情况为例子,第一个困惑,我们是竖着这样一条一条的算呢,还是横着一层一层的算呢?我们可以都试一下,先看一看竖着算。

第一层是可以装水的,因为两边都是墙;第二层也是可以装水的,因为两边都有高于2的墙,虽然不是紧挨着这个格子,但是装满水的时候,两边就是水墙,一样能 \(hold\) 住。

第三层就不行了,因为左边最高的墙是二高,光2的水就都漏光了,右边就算有个3也没有用。大家一定都听说过木桶原理,也就是最短的那块木板决定能装多少水。

所以结论来了,位置 \(i\) 的这个竖条里能装多少水,就要看它左边最高的墙和右边最高的墙,取这俩里面更矮的那一个,在这个图里就是粉色的那个墙,再用这个高度减去当前位置 \(i\)​ 的高度。

为啥要剪掉自己原本的高度?比如这里 \(i\)​ 等于三的时候,墙本身就有一格高水位,虽然最高能到二格,但是底下的第一格已经被墙占了,所以能装的水就是 2-1=1

这里我们给左边最高和右边最高分别起个名字,left_max 和 right_max。用 Python 写位置 i 的这条竖条里能装的水就是这样:先取 left_max 和 right_max 里面最小的那一个,如果这个值大于 height [i] 的话,用这个值减去 height [i],不然的话就是零。

这里要注意一点,左右两边最高的墙不一定要紧贴着,隔得远远的,中间隔着一堆水或者一堆墙也没有关系,重要的就是 \(i\)​ 左右两边最高的那两个墙。

至此问题就变成了,怎样高效的求出每个位置的左右两边的最大值。如果你每次都从 i 出发往两边扫一遍,那时间复杂度就是\(O(n²)\),面试肯定凉凉,因为你在不停的重复计算,没有利用到之前计算的结果,太浪费了。

所以换一个思路,既然我们同时要维护左边最大和右边最大,那就直接用双指针,一个从左往右走,一个从右往左走,边走边更新 \(left_{max}\) 和 \(right_{max}\)​,这样一共遍历一次就可以搞定。

所以我们这里一共需要四个变量:\(left\) 和 \(right\) 是指针负责移动,然后 \(left_{max}\) 和 \(right_{max}\)​ 负责记录到目前为止左右最大的墙的高度。

比如在同一个例子上,一开始 \(left\) 指着开头,\(right\)​ 指着结尾,最开始左边最大和右边最大都可以假想成零,因为两边是没有墙的。

接下来就是灵魂拷问,我们怎样一边移动指针,一边计算载水量呢?具体一点说,就是每次是动左边的指针还是动右边的指针呢?这里动哪个指针,就表示要算哪个指针所在的竖条的载水量,这就要看哪边可以算了。

比如现在这个情况,左边最大是二,右边最大是三,也就是说对于 \(left\) 指向的这一列来说,左边最高已经定死了是二了,之后不会改变了,即使右边以后蹦出了更高的墙,对于这个 \(left\) 来说,短板依然是现在左边的这个 \(left_{max}\),也就是说现在我们已经知道它的上限了,所以现在可以直接算了。

反观右边 \(right\) 这列,我们知道 \(right_{max}=3\) 比 \(left_{max}=2\) 大,如果后面它的左侧遇到更高的墙,那就说明 right 的上限有可能被抬高到现在的 right_max 上,所以 right 的上限现在还不能决定,要放一放,一会儿再算。

至此我们就已经看出了规则:左边最大和右边最大,其中哪个更小,就说明那一边的短板已经可以确定了,所以就可以算那一列,而另一边大的那一边短板还不能确定,所以要放一放。

一句话小结,面试的时候我们就可以这样解释:看 \(left_{max}\) 和 \(right_{max}\)​,谁小就先算哪边,因为较小的那一边水位的上限已经出现了,所以我们可以算;较大的那一边还可能被未来更高的另一侧抬高上限,所以我们现在先不算。

算完了还要记得更新那一侧的最大 \(max\),然后把指针往中心移动一步。这样整体的算法对于这个列表只扫一遍,时间复杂度就是 \(O(n)\),而且我们只用了四个变量,也没有用列表,空间复杂度就是 \(O(1)\),这就是这道题的最优解法。

如果两边最高都一样的话怎么办?这种情况其实两边都可以算,因为彼此的高度就是短板,水位就算之后某一侧冒出了更高的墙,对于当前的这两列来说,短板依然是现在的高度。

写代码的时候,为了简洁,可以把相等的情况并进一边,比如并到左边,就是这样:\(if left_max <= right_max\)​,然后继续。

还有一个细节就是终止条件要注意一下,我们这里用 while left <= right 作为终止条件,也就是说两个指针相遇、指向同一个墙的时候也要算一遍,确保我们的每一个竖条都有被算到。

总结一下这道题:我们先试了一下竖条的计算方式,然后发现位置 i 能装的水,取决于它两边最高的墙其中更矮的那一边的高度。所以问题就变成,对于每个位置 i 怎么能不浪费的算两边分别最高的墙。

这里就可以利用双指针,一个指最左,一个指最右,再来两个变量,一个记左边最大,一个记右边最大。怎么判断左右两边的指针哪边可以算呢?这就要比较左边最大和右边最大的大小,小的那一边表示指针位置 i 的瓶颈已经出现,可以算,而大的那一边还不行。也就是说算左边最大和右边最大之中更小的那一边,再把指针往中间挪一步,进入下一个循环,这也是面试官最满意的答案。

其实说了半天,如果我们能放松空间复杂度,接受 \(O(n)\) 的话,其实就能很简单的把每个位置 i 的两边最大求出来。怎么做?用两个列表存,空间换时间,先从左边扫一遍,求出每个位置的左边最大值,存到第一个列表里;再从右边扫一遍,求出每个位置的右边最大值,放到第二个列表里。最后把这两个列表对应的 \(i\),根据刚才的水的公式算一遍,都加起来就 \(OK\) 了。

相似题目

盛最多水的容器中等

除了自身以外数组的乘积中等

接雨水 II困难

倒水中等

有序三元组中的最大值 II中等

75. 颜色分类

难度:中等

相关标签:数组双指针排序

题目:

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 012 分别表示红色、白色和蓝色。

必须在不使用库内置的 sort 函数的情况下解决这个问题。

示例 1:

输入:nums = [2,0,2,1,1,0]

输出:[0,0,1,1,2,2]

示例 2:

输入:nums = [2,0,1]

输出:[0,1,2]

提示:

  • \(n == nums.length\)
  • \(1 <= n <= 300\)
  • \(nums[i] 为 0、1或 2\)

代码:

C++ 复制代码
class Solution 
{
public:
    void sortColors(vector<int>& nums) 
    {
        int n = nums.size();
        if (n <= 1) return; // 边界:长度<=1无需排序
        
        // 指针初始化:l=左指针(0区间右边界),cur=当前指针(遍历用,对应模板i),r=右指针(2区间左边界)
        int l = 0, cur = 0, r = n - 1;
        
        // 模板对应:cur作为右指针遍历(替代for循环),l/r维护区间性质
        while (cur <= r) 
        {
            // 情况1:当前数是0 → 交换到0的区间,l和cur都右移(维护[0,l)全0)
            if (nums[cur] == 0) 
            {
                if(nums[cur] != nums[l])  swap(nums[cur], nums[l]);
                l++;
                cur++;
            }
            // 情况2:当前数是1 → 属于中间区间,直接右移cur(维护[l,cur)全1)
            else if (nums[cur] == 1)  cur++;
            else // 情况3:当前数是2 → 交换到2的区间,r左移(维护(r,n-1]全2)
            {
                if(nums[cur] != nums[r]) swap(nums[cur], nums[r]);
                r--;
                // 注意:cur不移动(交换过来的数可能是0/1,需要重新检查)
            }
        }
    }
};

相似题目

排序链表中等

摆动排序中等

摆动排序 II中等

分析过程

[581. 最短无序连续子数组]

难度:中等

相关标签:贪心数组双指针排序单调栈

题目:

给你一个整数数组 nums ,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。

请你找出符合题意的 最短 子数组,并输出它的长度。

示例 1:

输入:nums = [2,6,4,8,10,9,15]

输出:5

解释:你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。

示例 2:

输入:nums = [1,2,3,4]

输出:0

示例 3:

输入:nums = [1]

输出:0

提示:

  • \(1 <= nums.length <= 10^4\)
  • \(-10^5 <= nums[i] <= 10^5\)

进阶: 你可以设计一个时间复杂度为 O(n) 的解决方案吗?

代码:

C++ 复制代码
class Solution
 {
public:
    int findUnsortedSubarray(vector<int>& nums)
     {
        int n = nums.size();
        if (n <= 1) return 0;  // 数组长度小于2时直接返回0
        
        int left = 0, right = -1;  // 初始化左右边界
        int max_val = nums[0], min_val = nums[n-1];
        
        // 从左到右遍历,维护最大值,更新右边界
        for (int i = 1; i < n; ++i)//无序中段的最大值应该小于有序右段的所有元素
         {  
            /*
              **元素小于前面最大值的时候才可以更新边界**,最后一次更新边界就是那个元素小于前面
              某个位置的值(这个值就是最大值),这个点右侧是升序的一定大于前面那个最大值,否则right
              就又要更新
            */
            if (nums[i] < max_val) right = i;  // 当前元素小于最大值,需要排序,更新右边界
            else max_val = nums[i];  // 更新最大值
        }
        
        // 从右到左遍历,维护最小值,更新左边界
        for (int i = n - 2; i >= 0; --i) 
        {
            /* 
              **元素大于后缀最小值的时候才可以更新边界**,最后一次更新边界就是那个元素大于后缀中
              某个位置的值(这个值就是最大值),这个点左侧是升序的一定小于后缀中那个最小值,否则left
              就又要更新
            */
            if (nums[i] > min_val)  left = i;  // 当前元素大于最小值,需要排序,更新左边界
            else  min_val = nums[i];  // 更新最小值
        }
        
        return right - left + 1;  // 返回需要排序的子数组长度
    }
};

相似题目

排序每个滑动窗口中最小的子数组中等

核心思想

将数组分为三段:有序左段、无序中段、有序右段

  1. 左边界确定 :从右向左遍历,维护最小值
    • 无序中段的最小值应该大于有序左段的所有元素
    • 当遇到比当前最小值大的数时,更新左边界
  2. 右边界确定 :从左向右遍历,维护最大值
    • 无序中段的最大值应该小于有序右段的所有元素
    • 当遇到比当前最大值小的数时,更新右边界
  3. 最终结果:[左边界, 右边界]即为需要排序的最短子数组

关键要点

  • 双指针思想:分别从两端向中间遍历,利用单调性优化
  • 时间复杂度:O(n),空间复杂度:O(1)
  • 本质:找到破坏数组有序性的最小区间,使得排序该区间后整个数组有序

这个解法巧妙地利用了数组的分段特性,通过维护最大值和最小值来确定无序子数组的边界,避免了排序等高时间复杂度操作。