优化LeetCode三数之和问题:从超时到高效的两指针解法

优化LeetCode三数之和问题:从超时到高效的两指针解法

本文深入探讨leetcode三数之和问题,分析常见超时解法的性能瓶颈,并详细介绍如何通过排序和双指针技术构建一个时间复杂度更优的解决方案。文章将提供清晰的代码示例,并解析其时间复杂度,帮助读者掌握高效处理数组求和问题的技巧,尤其是在避免重复结果方面的策略。

1. 问题描述

“三数之和”问题(3Sum)要求从一个整数数组 nums 中找出所有唯一的三元组 [nums[i], nums[j], nums[k]],使得 i != j、i != k、j != k,并且 nums[i] + nums[j] + nums[k] == 0。解决方案中不能包含重复的三元组。

2. 超时解法分析

原始代码尝试通过排序和嵌套的搜索函数来解决问题。虽然在小规模测试用例上可能有效,但在处理大规模或特定边缘情况时,会因为时间复杂度过高而导致“超出时间限制”(Time Limit Exceeded)。

让我们分析一下原始代码的结构和潜在的性能瓶颈:

def threeSum(nums):    sol = []    pos = 1    nums.sort() # O(N log N)    def search(p, vals): # vals 是 nums 的副本        l, r = 0, len(vals) - 1        sols = []        while l < p  0:                r -= 1            if vals[l] + vals[p] + vals[r] < 0:                l += 1        return sols    while pos < len(nums) - 1: # 外层循环 O(N)        new_sol = search(pos, nums[:]) # 关键问题:切片操作 O(N)        for n in new_sol: # 内层循环            if n not in sol: # 关键问题:in 操作 O(K) K为sol长度                sol.append(n)        pos += 1    return sol

性能瓶颈:

nums[:] 切片操作: 在每次 while pos 循环中,search 函数都接收 nums[:] 作为参数,这意味着每次调用都会创建一个 nums 数组的完整副本。这个操作的时间复杂度是 O(N),导致外层 while pos 循环的整体复杂度至少为 O(N^2)。vals.pop() 操作: 在 search 函数内部,当找到一个三元组时,会使用 vals.pop(r) 和 vals.pop(l) 来移除元素。对于 Python 列表,pop() 操作的平均时间复杂度是 O(1),但 pop(index) (当 index 不是最后一个元素时)需要移动后续所有元素,因此其时间复杂度是 O(N)。这使得 search 函数内部的循环复杂度大大增加。if n not in sol: 查重: 在外层循环中,通过 n not in sol 来检查新找到的三元组是否已存在。由于 sol 可能包含多个三元组,in 操作的平均时间复杂度是 O(K),其中 K 是 sol 的长度。在最坏情况下,sol 的长度可以达到 O(N^2),使得这个查重操作变得非常昂贵。

综合来看,这些操作导致了原始解决方案的时间复杂度远超 O(N^2),很可能达到 O(N^4) 甚至更高,因此在面对大量测试数据时会超时。

3. 高效解法:排序与双指针

解决三数之和问题的标准高效方法是结合排序和双指针技术。这种方法能够将时间复杂度优化到 O(N^2),并且能有效处理重复三元组的问题。

3.1 核心思想

排序数组: 首先对输入的数组 nums 进行排序。排序是 O(N log N) 的操作,但它为后续的双指针查找提供了便利。固定一个元素: 遍历排序后的数组,依次将每个元素 nums[i] 作为三元组的第一个元素。双指针查找: 对于每个固定的 nums[i],在 i 之后的子数组中使用两个指针 lo 和 hi 来寻找另外两个元素 nums[lo] 和 nums[hi],使得 nums[i] + nums[lo] + nums[hi] == 0。lo 指针从 i + 1 开始。hi 指针从数组末尾 len(nums) – 1 开始。跳过重复元素: 为了确保最终结果中不包含重复的三元组,在遍历 i 以及移动 lo 和 hi 指针时,需要跳过与前一个元素相同的元素。

3.2 步骤详解

初始化:创建一个空列表 unique_triplets 用于存储结果。对 nums 数组进行升序排序。外层循环(固定 nums[i]):遍历 i 从 0 到 len(nums) – 3(因为需要至少三个元素)。优化:提前终止 如果 nums[i] > 0,则由于数组已排序,后续的 nums[lo] 和 nums[hi] 也将是非负数,三数之和不可能为零,因此可以直接中断循环。去重: 如果 i > 0 且 nums[i] == nums[i-1],说明当前的 nums[i] 与上一个 nums[i-1] 相同,这将导致重复的三元组,所以跳过当前 i 的迭代。内层循环(双指针 lo 和 hi):设置 lo = i + 1 和 hi = len(nums) – 1。在一个 while lo 计算当前三数之和 current_sum = nums[i] + nums[lo] + nums[hi]。如果 current_sum 说明和太小,需要增大,因此 lo += 1。如果 current_sum > 0: 说明和太大,需要减小,因此 hi -= 1。如果 current_sum == 0: 找到了一个符合条件的三元组。将 [nums[i], nums[lo], nums[hi]] 添加到 unique_triplets 中。去重: 为了避免 nums[lo] 和 nums[hi] 产生重复的三元组,需要继续移动 lo 和 hi 指针,直到它们指向的元素与前一个不同。while lo while lo 然后,正常移动 lo += 1 和 hi -= 1,继续寻找下一个可能的三元组。

3.3 代码实现

from typing import Listdef threeSum(nums: List[int]) -> List[List[int]]:    unique_triplets = []    nums.sort() # O(N log N)    # 遍历 i 作为第一个元素    for i in range(len(nums) - 2):        # 优化:如果当前 nums[i] 已经大于 0,则后续不可能找到和为 0 的三元组        if nums[i] > 0:            break        # 去重:跳过重复的 nums[i]        # 如果当前元素与前一个元素相同,则会产生重复的三元组,跳过        if i > 0 and nums[i] == nums[i - 1]:            continue        # 使用双指针查找剩余的两个元素        lo = i + 1        hi = len(nums) - 1        while lo < hi:            current_sum = nums[i] + nums[lo] + nums[hi]            if current_sum  0:                hi -= 1 # 和太大,减小 hi            else: # current_sum == 0,找到一个三元组                unique_triplets.append([nums[i], nums[lo], nums[hi]])                # 去重:跳过重复的 nums[lo] 和 nums[hi]                # 移动 lo 指针,直到它指向的元素与当前不同                while lo < hi and nums[lo] == nums[lo + 1]:                    lo += 1                # 移动 hi 指针,直到它指向的元素与当前不同                while lo < hi and nums[hi] == nums[hi - 1]:                    hi -= 1                # 正常移动指针,继续寻找下一个三元组                lo += 1                hi -= 1    return unique_triplets

4. 时间复杂度分析

原始超时解法:

nums.sort(): O(N log N)外层 while pos 循环:O(N)nums[:] 切片:O(N)search 函数内部的 pop(index):O(N)if n not in sol::最坏情况下 O(N^2)综合来看,由于多次 O(N) 操作嵌套在 O(N^2) 的结构中,整体复杂度可能接近 O(N^4) 甚至更高。

优化后的双指针解法:

nums.sort():O(N log N)。这是整个算法的初始成本。外层 for i 循环:O(N)。内层 while lo 跳过重复元素的 while 循环:这些 while 循环虽然看起来是嵌套的,但 lo 和 hi 指针在整个内层循环中只会向中间移动,总的移动次数不会超过 O(N)。因此,它们并不会增加内层循环的渐进时间复杂度。整体时间复杂度: O(N log N)(排序) + O(N N)(外层循环 内层双指针)。因此,总的时间复杂度是 O(N^2)空间复杂度: O(1)(如果忽略存储结果列表的空间,只考虑辅助空间),或者 O(N)(如果考虑结果列表可能存储 N 个三元组)。

5. 注意事项与优化点

排序的必要性: 排序是双指针方法的基础,它使得我们可以有序地移动指针,并轻松判断和的大小。同时,它也简化了去重逻辑。去重策略:外层 i 的去重: if i > 0 and nums[i] == nums[i – 1]: continue 确保第一个元素不重复。内层 lo 和 hi 的去重: while lo 提前终止: if nums[i] > 0: break 是一个重要的剪枝操作。如果数组已排序,当第一个元素 nums[i] 已经大于 0 时,由于后续元素也都是非负数,它们的和不可能为 0,可以直接结束循环,进一步优化性能。

6. 总结

LeetCode 的“三数之和”问题是一个经典的算法题,它很好地展示了如何通过结合排序和双指针技术来优化解决方案。从一个直观但低效的暴力(或接近暴力)解法,通过识别并消除昂贵的操作(如列表切片、pop 移除和 in 查重),可以将其时间复杂度从 O(N^4) 甚至更高降低到高效的 O(N^2)。理解并掌握这种模式对于解决其他类似的数组求和问题(如两数之和、四数之和)至关重要。

以上就是优化LeetCode三数之和问题:从超时到高效的两指针解法的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1380645.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月14日 21:58:32
下一篇 2025年12月14日 21:58:39

相关推荐

  • 使用Docplex Python API识别并处理模型不可行约束

    本文旨在指导用户如何利用docplex python api中的冲突精炼器(conflict refiner)功能,精确识别导致优化模型不可行的具体约束。通过介绍refine_conflict()、display()和iter_conflicts()等关键方法,文章将展示如何从不可行解状态中提取并分…

    2025年12月14日
    000
  • python模块如何传入参数

    Python模块通过函数传参、模块级变量或命令行参数实现外部输入。1. 函数传参:定义函数接收参数,调用时传入值;2. 模块级变量:导入前修改模块变量用于配置;3. 命令行参数:在if __name__ == “__main__”中使用sys.argv或argparse处理运…

    2025年12月14日
    000
  • python如何实现自定义异常类

    自定义异常类需继承Exception类,可添加错误码等属性,通过raise抛出并用try-except捕获,提升错误处理的可读性和维护性。 在Python中,自定义异常类非常简单,只需要继承内置的 Exception 类或其子类即可。通过自定义异常,可以更清晰地表达程序中特定错误的含义,提升代码可读…

    2025年12月14日
    000
  • Python多线程如何共享数据 Python多线程数据安全传递方案

    Python多线程共享数据主要依靠全局变量加锁、queue.Queue、threading.local和concurrent.futures。1. 全局变量配合threading.Lock确保原子操作,避免竞态;2. queue.Queue实现线程安全的生产者-消费者通信;3. threading.…

    2025年12月14日 好文分享
    000
  • python collections.Counter的计数

    Counter是Python中用于统计元素频次的高效工具,支持列表、字符串等可迭代对象;其以字典形式返回结果,键为元素,值为出现次数;可进行访问计数、获取最常见元素、更新或减去数据及数学运算;适用于词频统计、判断异位词和算法题等场景。 Python 的 collections.Counter 是一个…

    2025年12月14日
    000
  • 如何使用conda创建Python环境_conda创建与管理Python环境详细教程

    答案:Conda可创建隔离Python环境避免依赖冲突,先安装Anaconda或Miniconda并验证版本,用conda create建立带指定Python版本的环境,如conda activate激活、conda deactivate退出,通过conda env list查看环境,conda i…

    2025年12月14日
    000
  • Python类怎么定义_Python类的定义与面向对象编程基础

    答案:Python中类使用class定义,采用大驼峰命名,通过__init__初始化实例,self指代对象本身,支持类属性、静态方法、类方法,可通过继承扩展父类并实现多态,super()调用父类方法,是OOP基础。 在Python中定义类非常直观,是面向对象编程(OOP)的核心。通过类可以创建具有属…

    2025年12月14日
    000
  • Python管道破裂错误BrokenPipeError解决方法

    BrokenPipeError发生在向已关闭的管道写入时,如Python脚本输出被head截断;可通过捕获异常、忽略SIGPIPE信号或封装stdout为安全写入类来优雅处理,确保程序在管道中断时平稳退出。 在使用Python进行程序开发,特别是在处理子进程、管道通信或输出重定向时,可能会遇到Bro…

    2025年12月14日 好文分享
    000
  • python GIL锁的底层原理探究

    GIL是CPython为保证线程安全而引入的全局锁,确保同一时刻仅一个线程执行字节码,因引用计数需原子操作,避免频繁细粒度加锁而采用此机制。 Python 的 GIL(Global Interpreter Lock,全局解释器锁)是 CPython 解释器中一个互斥锁,它的存在直接影响了多线程程序的…

    2025年12月14日
    000
  • Python3安装时缺少依赖怎么办_Python3依赖库缺失问题解决方案

    首先检查系统开发工具与依赖库是否完整,依次通过包管理器安装基础依赖、补充特定缺失模块、使用pyenv管理版本或下载官方预编译包;随后在Python环境中导入关键模块验证功能,并结合sysconfig与pip命令确认配置正确性;最后利用虚拟环境隔离项目依赖,通过requirements.txt实现高效…

    2025年12月14日
    000
  • Python爬虫怎样避免被反爬_Python爬虫防止被网站封禁的常见策略

    要避免被反爬,需模拟真实用户行为。1. 设置常见且轮换的User-Agent和Referer请求头;2. 用随机延迟控制请求频率,降低服务器压力;3. 使用代理IP池分散请求来源,防止IP被封;4. 针对JavaScript渲染和验证码,采用Selenium等工具模拟浏览器操作或接入打码平台;5. …

    2025年12月14日
    000
  • python正负索引的使用

    Python支持正负索引访问序列元素,正索引从0开始从前向后,负索引从-1开始从后向前,如lst=[‘a’,’b’,’c’,’d’]中lst[0]为’a’,lst[-1]为&#82…

    2025年12月14日
    000
  • 如何安装Python扩展模块_安装Python第三方扩展模块的详细操作说明

    安装Python扩展模块需使用pip命令,如pip install 模块名,推荐结合虚拟环境隔离依赖,避免版本冲突。 安装Python扩展模块是使用第三方库的前提,无论是数据分析、Web开发还是自动化脚本,都离不开这些模块。下面介绍几种常用的安装方法,适合不同操作系统和使用场景。 使用pip命令安装…

    2025年12月14日
    000
  • Python并集是什么意思?

    并集是将多个集合的不重复元素合并成新集合。Python中set为无序不重复容器,可用{}或set()创建,通过|操作符或union()方法求并集,适用于去重合并数据场景。 Python中的并集指的是将两个或多个集合中的所有不重复元素合并在一起,形成一个新的集合。简单来说,就是把几个集合里的元素“合起…

    2025年12月14日
    000
  • python协程的作用

    协程主要用于高效处理I/O密集型任务,通过单线程并发提升性能。利用async/await语法简化异步编程,实现非阻塞的网络请求、文件读写等操作,在等待I/O时切换任务,由事件循环管理执行,避免线程开销。相比多线程,协程上下文切换成本低,无需锁机制,可轻松创建大量协程,显著节省系统资源。结合aioht…

    2025年12月14日
    000
  • python中filter()的多种筛选

    在 Python 中,filter() 函数是一个内置函数,用于从可迭代对象中筛选出满足条件的元素。它的基本语法是: filter(function, iterable) 返回一个迭代器,包含原序列中使 function 返回 True 的元素。下面介绍几种常见的 filter() 使用方式。 1.…

    2025年12月14日
    000
  • Python入门的实战项目推荐_Python入门练手项目的详细解析

    1、控制台计算器项目通过定义函数、条件判断和异常处理,帮助初学者掌握Python基础语法与用户输入处理逻辑。2、简易闹钟项目结合datetime和playsound模块,实现时间比较与音频提醒,强化模块导入与时间操作能力。3、文字冒险游戏项目利用变量、条件分支和字符串格式化构建交互式剧情,提升编程趣…

    2025年12月14日
    000
  • PythonScikitLearn怎么用_PythonScikitLearn库的使用方法与实例

    首先加载数据集并划分训练测试集,接着选择模型训练并预测,最后评估性能;以线性回归为例,使用sklearn实现全流程,包括数据预处理、模型拟合、预测及指标计算,核心步骤为数据准备、模型调用、训练预测和评估,掌握这些即可快速上手sklearn。 Scikit-learn(简称 sklearn)是 Pyt…

    2025年12月14日
    000
  • Python官网如何优化Python代码性能_Python官网性能调优技巧汇总

    使用内置函数、优化数据结构、生成器、局部变量、C扩展和分析工具可显著提升Python性能。具体包括:优先用map、filter、set和collections模块;选deque替代list,dict维护键值对,array.array存数值;用yield减少内存占用;将频繁访问的变量转为局部变量;通过…

    2025年12月14日
    000
  • python实现异步的两种框架

    asyncio是Python标准库,基于事件循环和协程,适用于异步Web服务、爬虫等;2. Tornado是独立异步网络库,内置高性能服务器,适合实时通信场景;选择取决于需求。 Python实现异步编程主要依赖于两种框架:asyncio 和 Tornado。它们都能处理高并发I/O操作,但设计思路和…

    2025年12月14日
    000

发表回复

登录后才能评论
关注微信