DAY24: LeetCode 167:两数之和 II|从暴力枚举到左右双指针

目录

LeetCode 167:两数之和 II|从暴力枚举到左右双指针

一、题目要求

LeetCode 167:两数之和 II - 输入有序数组

给定一个按照非递减顺序排列 的整数数组 numbers,找到两个数字,使它们相加等于 target。

题目保证恰好存在一个解,并且不能重复使用同一个元素。

返回这两个数字的下标,注意:下标从 1 开始计算。

例如:

python 复制代码
numbers = [2, 7, 11, 15]
target = 9

因为:

text 复制代码
2 + 7 = 9

所以返回:

python 复制代码
[1, 2]

二、从最直接的思路开始:暴力枚举

要找两个数字,使它们相加等于 target。

最直接的办法是什么?

先固定一个数字,再依次尝试后面的数字。

例如:

python 复制代码
numbers = [2, 7, 11, 15]
target = 9

先固定 2,然后尝试 7:

text 复制代码
2 + 7 = 9

找到答案。

换成代码,就是两层循环:

python 复制代码
class Solution:
    def twoSum(self, numbers: list[int], target: int) -> list[int]:
        n = len(numbers)

        for i in range(n - 1):
            for j in range(i + 1, n):
                if numbers[i] + numbers[j] == target:
                    return [i + 1, j + 1]

这个思路没有问题,但它的效率不够高。

why? 因为, 每固定一个数字,我们都需要逐个尝试后面的数字。

最坏情况下,需要检查:

text 复制代码
(n - 1) + (n - 2) + ... + 1

也就是:

n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1)

因此时间复杂度为 O(n²)。

那么, 问题来了:

有没有办法不检查所有组合,就知道哪些数字不可能是答案?

这时候再去重新看题目,会发现一个很重要的条件:

数组已经按照从小到大的顺序排列。

既然数组有序,我们是不是可以利用数字之间的大小关系,减少不必要的搜索?


三、关键思路:利用有序性推导双指针

既然数组已经排序,那么我们至少知道:

  • 越往右,数字越大或相等。
  • 越往左,数字越小或相等。

如果两个数字相加不等于 target,我们就可以根据总和是太大还是太小,判断应该往哪个方向寻找。

那么,不妨让两个指针分别从数组两端开始。

text 复制代码
numbers = [2, 7, 11, 15]
           ↑          ↑
           i          j

初始化:

python 复制代码
i = 0
j = len(numbers) - 1

现在:

text 复制代码
numbers[i] + numbers[j] = 2 + 15 = 17

而我们的目标是 9。

1. 总和太大,怎么办?

现在:

text 复制代码
17 > 9

说明需要让总和变小。

既然数组有序,我们可以考虑移动右指针 j,换一个更小的数字。

但为什么不是移动左指针呢?

因为 2 已经是当前范围内最小的数字。

连:

text 复制代码
2 + 15 = 17 > 9

都已经超过目标值,那么用其他更大的数字与 15 相加,只会得到更大的结果。

也就是说:15 不可能与当前范围内的任何数字组成答案。

所以我们可以放心排除 15:

python 复制代码
j -= 1

这样就不需要再逐个检查:

text 复制代码
7 + 15
11 + 15

这些组合了。

继续寻找:

text 复制代码
numbers = [2, 7, 11, 15]
           ↑      ↑
           i      j

2 + 11 = 13

仍然太大,所以继续移动右指针。

text 复制代码
numbers = [2, 7, 11, 15]
           ↑  ↑
           i  j

2 + 7 = 9

找到答案!

2. 总和太小,又该怎么办?

反过来考虑另一种情况:

python 复制代码
numbers = [1, 2, 4, 6, 8]
target = 10

一开始:

text 复制代码
numbers = [1, 2, 4, 6, 8]
           ↑           ↑
           i           j

1 + 8 = 9

发现:

text 复制代码
9 < 10

这次需要让总和变大。

那应该移动哪个指针?

因为 8 已经是当前范围内最大的数字。

连:

text 复制代码
1 + 8 = 9 < 10

都达不到目标值,那么 1 与其他更小的数字相加,也不可能达到 10。

所以我们可以直接排除 1,移动左指针:

python 复制代码
i += 1

继续寻找:

text 复制代码
numbers = [1, 2, 4, 6, 8]
              ↑        ↑
              i        j

2 + 8 = 10

找到答案。

3. 总结指针的移动规则

现在就可以整理出完整的判断逻辑:

text 复制代码
current < target
→ 总和太小
→ 排除左端数字
→ i += 1

current > target
→ 总和太大
→ 排除右端数字
→ j -= 1

current == target
→ 找到答案

🚩 注意:这里最重要的不是记住指针往哪边移动,而是理解为什么可以排除某一端的数字。

因为数组有序:

  • 当总和太大时,右端数字与当前范围内最小的数字相加都超出了目标,所以右端数字可以被排除。
  • 当总和太小时,左端数字与当前范围内最大的数字相加都不够,所以左端数字可以被排除。

每次比较,都能够安全地缩小搜索范围,而不是逐个尝试所有组合。

这就是双指针能够提高效率的关键。


四、双指针完整代码

思路明确以后,代码其实很简单。

python 复制代码
class Solution:
    def twoSum(self, numbers: list[int], target: int) -> list[int]:
        n = len(numbers)

        i = 0
        j = n - 1

        while i < j:
            current = numbers[i] + numbers[j]

            if current < target:
                i += 1

            elif current > target:
                j -= 1

            else:
                return [i + 1, j + 1]

其中:

python 复制代码
while i < j:

表示两个指针还没有相遇,可以继续寻找。

而:

python 复制代码
return [i + 1, j + 1]

是因为题目要求返回从 1 开始计算的下标。

容易踩的坑:双指针不是随便移动两个指针

双指针并不意味着随便设置两个位置,然后根据结果尝试移动。

例如,如果两个指针都从左边开始,就需要额外考虑:当前总和太大时,究竟应该移动谁?

如果没有利用数组的有序性建立明确的移动规则,就可能提前结束搜索,甚至漏掉答案。

所以做这类题时,我觉得更应该先问自己:

移动这个指针以后,我能确定排除了哪些不可能的答案吗?

如果无法解释这一点,就说明指针移动的依据还不够充分。


五、补充思路:固定一个数字 + 二分查找

其实除了左右双指针,这道题还可以用二分查找。

这个思路可以从最开始的暴力枚举自然推导出来。

回顾暴力枚举:

python 复制代码
for i in range(n - 1):
    for j in range(i + 1, n):
        if numbers[i] + numbers[j] == target:
            return [i + 1, j + 1]

当时我们的想法是:

固定一个数字,再去后面寻找另一个数字。

这个思路本身没有问题。

真正耗时的是:内层循环需要逐个检查所有数字。

既然数组已经有序,那么能不能把内层的遍历换成二分查找?

1. 固定第一个数字

例如:

python 复制代码
numbers = [2, 7, 11, 15]
target = 9

先固定:

python 复制代码
i = 0

第一个数字是:

text 复制代码
numbers[i] = 2

既然:

text 复制代码
第一个数字 + 第二个数字 = target

那么第二个数字必须等于:

python 复制代码
need = target - numbers[i]

也就是:

text 复制代码
need = 9 - 2 = 7

现在问题就变成了:

在 i 后面的有序数组中,能不能找到数字 7?

这就可以使用二分查找。

2. 用二分查找寻找 need

先设置搜索范围:

python 复制代码
left = i + 1
right = n - 1

为什么从 i + 1 开始?

因为第一个数字已经固定为 numbers[i],第二个数字不能重复使用同一个下标。

接下来取中间位置:

python 复制代码
mid = (left + right) // 2

然后比较:

  • numbers[mid] == need:找到答案。
  • numbers[mid] < need:中间数字太小,去右半边寻找。
  • numbers[mid] > need:中间数字太大,去左半边寻找。

每次比较都能排除一半不可能的搜索范围。

3. 完整代码

python 复制代码
class Solution:
    def twoSum(self, numbers: list[int], target: int) -> list[int]:
        n = len(numbers)

        for i in range(n - 1):
            need = target - numbers[i]

            left = i + 1
            right = n - 1

            while left <= right:
                mid = (left + right) // 2

                if numbers[mid] == need:
                    return [i + 1, mid + 1]

                elif numbers[mid] < need:
                    left = mid + 1

                else:
                    right = mid - 1

这样就把原来的内层暴力遍历替换成了二分查找。

不过要注意:

二分查找和双指针虽然都利用了数组有序的特点,但思路并不完全相同。

二分查找是先固定一个数字,再寻找另一个确定的目标值。

双指针则不需要提前固定某个数字,而是根据当前总和的大小,直接排除左边或右边的数字。


六、复杂度分析

方法 时间复杂度 额外空间复杂度
暴力枚举 O(n²) O(1)
固定一个数 + 二分查找 O(n log n) O(1)
左右双指针 O(n) O(1)

暴力枚举:

两层循环,最坏情况下需要检查 O(n²) 组数字。

二分查找:

外层固定一个数字需要 O(n),每次二分查找需要 O(log n)。

因此整体是 O(n log n)。

左右双指针:

两个指针分别从数组两端向中间移动。

每次循环至少移动一个指针,而且不会回头。

所以总移动次数不超过 n - 1,时间复杂度为 O(n)。

三种方法都只需要固定数量的辅助变量,因此额外空间复杂度均为 O(1)。


七、总结

这道题最值得注意的,其实就是题目给出的数组有序这个条件。

最开始,我们可以通过暴力枚举找到答案,但这样需要检查大量组合。

而利用数组有序的特点,就可以根据当前总和与目标值的大小关系,直接排除不可能的数字。

这也是为什么左右双指针可以把时间复杂度从 O(n²) 降到 O(n)。

至于二分查找,则提供了另一条优化路径:保留固定一个数字的思路,只把内层的逐个查找换成更高效的搜索。

所以. 我觉得这道题值得记住的是:

双指针的关键,不是两个指针从哪里开始,而是每次移动时,能否利用题目条件,确定被跳过的部分不可能包含答案。

相关推荐
木井巳3 小时前
【BFS 解决拓扑排序】课程表
java·算法·leetcode·广度优先·宽度优先·推荐算法·图搜索算法
kimnoic3 小时前
Python常用命令提示符使用方法详解
开发语言·python
扶风ff4 小时前
练题簿在线免费刷题:会员线下组卷、Word 试卷与成绩导入,课堂检测更方便
java·开发语言·算法·小程序·word
Είναι η κοπέλα4 小时前
模型量化完全指南:GGUF、GPTQ、AWQ 怎么选
人工智能·pytorch·python
yi0114 小时前
LeetCode 15:三数之和|排序 + 双指针,如何避免重复答案?
笔记·python·算法·leetcode·双指针
weixin199701080164 小时前
《1688消息服务落地方案:alibaba.message.* 与 Webhook 回调的可靠性与重试策略》(附Python源码)
开发语言·python
北风toto4 小时前
数据库笔记:Armstrong 公理系统与集合论的深度辨析
数据库·笔记
EasyBr指纹浏览器4 小时前
我的开源项目:小红书笔记归档,把来源和缺项一起留下
笔记·小红书·开源工具·素材管理·内容归档
海海不掉头发4 小时前
图论及其应用_课堂笔记整理_CSDN博客
笔记·图论