人类发明了轮子,提高了力的使用效率。

  人类发明了自动化机械,将自己从重复的工作中解脱出来。

  提高效率的方法好像总是离不开两点:拒绝无效劳动,拒绝重复劳动。人类如此,计算机亦如是。

  前面我们说过了四数之和的递归和递推思路,递归和递推是一个比较通用的解题方法,我们可以以此为基础对解空间有一个整体的认识,优化出更加高效的算法。下面我们以三数之和为例来看一下,如何从最简单的递归一步一步得到更加高效的解法。题目很简单,主要说一下优化的思路:

  由于之前说过递归的思路,那这次我们就先用递推来解题。

  由于解空间的结构非常简单,nums.length棵3层的nums.length叉树的遍历。我们使用三重for循环来遍历这个解空间,每一层为从nums数组中取出一个数,最终便可以遍历完所有三个数的组合,伪代码:

    int length=nums.length;
for(int i=0;i<length;i++){
for(int j=0;j<length;j++){
for(int k=0;k<length;k++){
if(nums(i)+nums[j]+nums[k]==0){
得到答案
}
}
}
}

  上述代码可以遍历出nums中元素所有的三个元素的组合,但需要注意的是其中包含了重复的元素。重复的原因是我们每层循环都是从头取到尾,而没有考虑上层循环已经做过的事。比如:

  我们的i层for循环在遍历到num0时,其实应取到了包含num0的所有组合。而当i层for循环遍历的num1时,j层for循环还是遍历了num0,这就造成了结果的重复。0,1,2----1,0,2-----2,1,0其实是排序不同但实质相同的结果集。我们在j层循环不应该再去试探i层循环已经走过的元素,同理k层循环不应该去试探i,j两层循环已经走过的元素。从而避免遍历到排序不同但实质相同的结果。将上面的代码修改一下:

    int length=nums.length;
for(int i=0;i<length;i++){
for(int j=i+1;j<length;j++){
for(int k=j+1;k<length;k++){
if(nums(i)+nums[j]+nums[k]==0){
得到答案
}
}
}
}

  经过这一步的去重,我们得到了最原始的解法(结果的排序去重就不说了,主要说计算过程的优化思路):

  其实优化的第一步就是去除重复计算,但由于本题题目要求的特殊性(不允许有重复的结果),去除重复计算我们仅能得到最原始的解法。如果没有重复计算可以被优化掉,下一步我们应该考虑的是无效计算。

  我们一步一步来看,我们最终要求的是和为0的三个数。在确定了第一个数和第二个数之后,我们便已经知道了我们需要的第三个数是什么。在这种情况下,第三层循环还在傻傻的遍历便显得有些多余,我们可以在k层遍历的区间内进行二分查找来找到我们要的第三个数,将k层循环的时间复杂度从O(N)降低到O(log2N)!

  二分查找有问题的同学可以看我之前的随笔,传送门--->二分查找java实现:https://www.cnblogs.com/niuyourou/p/11885123.html

  我们来看一下将第三层优化为二分查找后的代码:

  我用相同的数组比较了一下两种算法的效率:

  可以看到速度的提升还是很明显的。但好像还是差那么一点,直觉告诉我即使第三层使用了二分查找我们的算法还是存在着无效计算。我们用优化第三层的思路来想一下,如果第一个数已经确定,那么我们的问题便是从剩下的元素中找出和为0-第一个数的组合。

  我们想象一下,如果一个数组已经排序,那么对于一个确定的数A和target来说,在结果不重复的要求下,数组中只能唯一有一个数可以满足与A的和为target。我们可不可以从外侧(从数组中的两个极值开始)开始,一步一步将无解的元素排除来减少我们的无效计算呢?

  如上图所示,对于一个已经排序的数组。我们可以维护两个指针来维护一个窗口。指针的移动原则是:当确定指针指向元素在数组中没有对应元素使它们的和为target时我们移动指针;每个指针只向一个方向移动,确定了无解的元素我们不再试探;移动过程如下:

   此时两个指针在数组的两头,如果两个指针所指向的两个数的和大于target,说明左右的数均太大。需要有一个指针左移,因为左侧指针无法左移,所以左移右指针,同时我们确定,右指针走过的“7”元素在数组中是无解的。

  两个数的和依然大于target,依然为了不越界,我们继续左移,一直移动到2。至此,右边指针走过的所有元素在数组中均没有一个匹配的值能使的两个元素的和为target。也就是说,4/5/6/7均无解,后面无论的所有计算我们均不需要再考虑这些元素。

  此时两个数的和小于target,说明我们选中的两个数太小,需要有一个指针右移。因为对右侧指针来说,其右边的元素我们已经确定无解,不再考虑。所以我们将左指针右移。此时我们得到了-2,2这对组合。

  可以看到,在我们的移动过程中,指针走过的元素或者无解,或者有解但已经被我们记录下,当两个指针相遇时我们便可以得到所有组合。

  总结一下我们的移动过程如下:

  第一次移动

  此时指针在数组的两端。如果我们得到的和大于target,右指针左移;反之左指针右移,这样我们可以确保指针走过的元素在数组中是无解的。

  后续移动

  不管第一步移动的是哪个指针,我们都只能再进行两个动作:左指针右移或右指针左移。因为左指针左边或右指针右边要么是边界,要么是我们已经确定了的在数组中无解的元素。上述移动策略保证了两个指针走过的元素在数组中是无解的。

  在移动的过程中我们遇到和为target的组合便记录下来,等两个指针相遇时我们便得到了该数组中所有和为target的组合。

  这样内部两层for循环的计算次数被我们缩减为了N次,相当一一次遍历。相对于二分查找法的N*log2N次,性能又有了提升。

  代码如下: 

  我们来看一下计算效率:

  我们的优化过程如下:

  1. 通过控制内外层循环的范围避免重复计算。

  2. 通过二分查找优化最内部的循环,避免了一部分无效计算。

  3. 而第三种方法(滑尺法)的思路是,如何更高效的判断一个元素有没有解,无解则略过来避免无效计算。

  可以看出,避免重复计算是比较容易的。动态规划也是这种思路,只是使用的是DP表缓存。但在避免无效计算时,方法不固定,套路多。需要我们结合生活经验和想象,数学都是从猜想开始的,这点上算法也类似,所以还是要多刷题多看书,去积累经验和思路,不断的刷新我们的上限。与各位共勉!(有更好的方法欢迎指出呀!)

从三数之和看如何优化算法,递推-->递推加二分查找-->递推加滑尺的更多相关文章

  1. LeetCode 三数之和 — 优化解法

    LeetCode 三数之和 - 改进解法 题目:给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复 ...

  2. 【算法训练营day7】LeetCode454. 四数相加II LeetCode383. 赎金信 LeetCode15. 三数之和 LeetCode18. 四数之和

    [算法训练营day7]LeetCode454. 四数相加II LeetCode383. 赎金信 LeetCode15. 三数之和 LeetCode18. 四数之和 LeetCode454. 四数相加I ...

  3. 南大算法设计与分析课程OJ答案代码(4)--变位词、三数之和

    问题 A: 变位词 时间限制: 2 Sec  内存限制: 10 MB提交: 322  解决: 59提交 状态 算法问答 题目描述 请大家在做oj题之前,仔细阅读关于抄袭的说明http://www.bi ...

  4. LeeCode数组第15题三数之和

    题目:三数之和 内容: 给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组. 注意:答案中 ...

  5. zz:一个框架看懂优化算法之异同 SGD/AdaGrad/Adam

    首先定义:待优化参数:  ,目标函数: ,初始学习率 . 而后,开始进行迭代优化.在每个epoch  : 计算目标函数关于当前参数的梯度:  根据历史梯度计算一阶动量和二阶动量:, 计算当前时刻的下降 ...

  6. LeetCode 第15题-三数之和

    1. 题目 2.题目分析与思路 3.思路 1. 题目 给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且 ...

  7. [LeetCode] 3Sum Closest 最近三数之和

    Given an array S of n integers, find three integers in S such that the sum is closest to a given num ...

  8. [LeetCode] 3Sum 三数之和

    Given an array S of n integers, are there elements a, b, c in S such that a + b + c = 0? Find all un ...

  9. LeetCode 16. 3Sum Closest. (最接近的三数之和)

    Given an array S of n integers, find three integers in S such that the sum is closest to a given num ...

随机推荐

  1. Linux内核中的并发与竞态概述

    1.前言 众所周知,Linux系统是一个多任务的操作系统,当多个任务同时访问同一片内存区域的时候,这些任务可能会相互覆盖内存中数据,从而造成内存中的数据混乱,问题严重的话,还可能会导致系统崩溃. 2. ...

  2. CyclicBarrier开启多个线程进行计算,最后统计计算结果

    有一个大小为50000的数组,要求开启5个线程分别计算10000个元素的和,然后累加得到总和 /** * 开启5个线程进行计算,最后所有的线程都计算完了再统计计算结果 */ public class ...

  3. Scala中sortBy和Spark中sortBy区别

    Scala中sortBy是以方法的形式存在的,并且是作用在Array或List集合排序上,并且这个sortBy默认只能升序,除非实现隐式转换或调用reverse方法才能实现降序,Spark中sortB ...

  4. 04、状态模式(State)

    一.概念: 当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是改变了其类.[DP] 二.作用: 状态模式的主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况.吧状态的判断逻辑转 ...

  5. 小白开学Asp.Net Core 《八》

    小白开学Asp.Net Core <八> — — .Net Core 数据保护组件 1.背景 我在搞(https://github.com/AjuPrince/Aju.Carefree)这 ...

  6. vue接入腾讯防水墙代码

    vue接入腾讯防水墙代码 开始创建代码: 登陆调用方法代码

  7. 安装Ubuntu linux

    (1)下载Ubuntu http://www.ubuntu.com/download/desktop (2)制作启动U盘 1. 启动Rufus: 2. 插入U盘: 3. Rufus会提示更新,以自动选 ...

  8. Invalid attempt to spread non-iterable instance

    问题在于对数据的操作,或数据类型,或数据名称

  9. 关于 Java 关键字 volatile 的总结

    1 什么是 volatile volatile 是 Java 的一个关键字,它提供了一种轻量级的同步机制.相比于重量级锁 synchronized,volatile 更为轻量级,因为它不会引起线程上下 ...

  10. mysql的my.cnf

    配置参数详解 [client] #客户端设置,即客户端默认的连接参数port = 3307   #默认连接端口socket = /data/mysqldata/3307/mysql.sock #用于本 ...