Two Sum 系列问题在 LeetCode 上有好几道,这篇文章就挑出有代表性的几道,介绍一下这种问题怎么解决。

TwoSum I

这个问题的最基本形式是这样:给你一个数组和一个整数 target,可以保证数组中存在两个数的和为 target,请你返回这两个数的索引。

比如输入 nums = [3,1,3,6], target = 6,算法应该返回数组 [0,2],因为 3 + 3 = 6。

这个问题如何解决呢?首先最简单粗暴的办法当然是穷举了:

int[] twoSum(int[] nums, int target) {

    for (int i = 0; i < nums.length; i++)
for (int j = i + 1; j < nums.length; j++)
if (nums[j] == target - nums[i])
return new int[] { i, j }; // 不存在这么两个数
return new int[] {-1, -1};
}

这个解法非常直接,时间复杂度 O(N^2),空间复杂度 O(1)。

可以通过一个哈希表减少时间复杂度:

int[] twoSum(int[] nums, int target) {
int n = nums.length;
index<Integer, Integer> index = new HashMap<>();
// 构造一个哈希表:元素映射到相应的索引
for (int i = 0; i < n; i++)
index.put(nums[i], i); for (int i = 0; i < n; i++) {
int other = target - nums[i];
// 如果 other 存在且不是 nums[i] 本身
if (index.containsKey(other) && index.get(other) != i)
return new int[] {i, index.get(other)};
} return new int[] {-1, -1};
}

这样,由于哈希表的查询时间为 O(1),算法的时间复杂度降低到 O(N),但是需要 O(N) 的空间复杂度来存储哈希表。不过综合来看,是要比暴力解法高效的。

我觉得 Two Sum 系列问题就是想教我们如何使用哈希表处理问题。我们接着往后看。

TwoSum II

这里我们稍微修改一下上面的问题。我们设计一个类,拥有两个 API:

class TwoSum {
// 向数据结构中添加一个数 number
public void add(int number);
// 寻找当前数据结构中是否存在两个数的和为 value
public boolean find(int value);
}

如何实现这两个 API 呢,我们可以仿照上一道题目,使用一个哈希表辅助 find 方法:

class TwoSum {
Map<Integer, Integer> freq = new HashMap<>(); public void add(int number) {
// 记录 number 出现的次数
freq.put(number, freq.getOrDefault(number, 0) + 1);
} public boolean find(int value) {
for (Integer key : freq.keySet()) {
int other = value - key;
// 情况一
if (other == key && freq.get(key) > 1)
return true;
// 情况二
if (other != key && freq.containsKey(other))
return true;
}
return false;
}
}

进行 find 的时候有两种情况,举个例子:

情况一:add[3,3,2,5] 之后,执行 find(6),由于 3 出现了两次,3 + 3 = 6,所以返回 true。

情况二:add[3,3,2,5] 之后,执行 find(7),那么 key 为 2,other 为 5 时算法可以返回 true。

除了上述两种情况外,find 只能返回 false 了。

对于这个解法的时间复杂度呢,add 方法是 O(1),find 方法是 O(N),空间复杂度为 O(N),和上一道题目比较类似。

但是对于 API 的设计,是需要考虑现实情况的。比如说,我们设计的这个类,使用 find 方法非常频繁,那么每次都要 O(N) 的时间,岂不是很浪费费时间吗?对于这种情况,我们是否可以做些优化呢?

是的,对于频繁使用 find 方法的场景,我们可以进行优化。我们可以参考上一道题目的暴力解法,借助哈希集合来针对性优化 find 方法:

class TwoSum {
Set<Integer> sum = new HashSet<>();
List<Integer> nums = new ArrayList<>(); public void add(int number) {
// 记录所有可能组成的和
for (int n : nums)
sum.add(n + number);
nums.add(number);
} public boolean find(int value) {
return sum.contains(value);
}
}

这样 sum 中就储存了所有加入数字可能组成的和,每次 find 只要花费 O(1) 的时间在集合中判断一下是否存在就行了,显然非常适合频繁使用 find 的场景。

三、总结

对于 TwoSum 问题,一个难点就是给的数组无序。对于一个无序的数组,我们似乎什么技巧也没有,只能暴力穷举所有可能。

一般情况下,我们会首先把数组排序再考虑双指针技巧。TwoSum 启发我们,HashMap 或者 HashSet 也可以帮助我们处理无序数组相关的简单问题。

另外,设计的核心在于权衡,利用不同的数据结构,可以得到一些针对性的加强。

最后,如果 TwoSum I 中给的数组是有序的,应该如何编写算法呢?答案很简单,前文「双指针技巧汇总」写过:

int[] twoSum(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
return new int[]{left, right};
} else if (sum < target) {
left++; // 让 sum 大一点
} else if (sum > target) {
right--; // 让 sum 小一点
}
}
// 不存在这样两个数
return new int[]{-1, -1};
}

我最近精心制作了一份电子书《labuladong的算法小抄》,分为【动态规划】【数据结构】【算法思维】【高频面试】四个章节,共 60 多篇原创文章,绝对精品!限时开放下载,在我的公众号 labuladong 后台回复关键词【pdf】即可免费下载!

欢迎关注我的公众号 labuladong,技术公众号的清流,坚持原创,致力于把问题讲清楚!

twoSum问题的核心思想的更多相关文章

  1. 《深入理解Spark:核心思想与源码分析》——SparkContext的初始化(叔篇)——TaskScheduler的启动

    <深入理解Spark:核心思想与源码分析>一书前言的内容请看链接<深入理解SPARK:核心思想与源码分析>一书正式出版上市 <深入理解Spark:核心思想与源码分析> ...

  2. 《深入理解Spark:核心思想与源码分析》(前言及第1章)

    自己牺牲了7个月的周末和下班空闲时间,通过研究Spark源码和原理,总结整理的<深入理解Spark:核心思想与源码分析>一书现在已经正式出版上市,目前亚马逊.京东.当当.天猫等网站均有销售 ...

  3. 《深入理解Spark:核心思想与源码分析》(第2章)

    <深入理解Spark:核心思想与源码分析>一书前言的内容请看链接<深入理解SPARK:核心思想与源码分析>一书正式出版上市 <深入理解Spark:核心思想与源码分析> ...

  4. 《深入理解Spark:核心思想与源码分析》一书正式出版上市

    自己牺牲了7个月的周末和下班空闲时间,通过研究Spark源码和原理,总结整理的<深入理解Spark:核心思想与源码分析>一书现在已经正式出版上市,目前亚马逊.京东.当当.天猫等网站均有销售 ...

  5. 《深入理解Spark:核心思想与源码分析》正式出版上市

    自己牺牲了7个月的周末和下班空闲时间,通过研究Spark源码和原理,总结整理的<深入理解Spark:核心思想与源码分析>一书现在已经正式出版上市,目前亚马逊.京东.当当.天猫等网站均有销售 ...

  6. Hibernate核心思想—ORM机制(一)

    转:http://blog.csdn.net/wanghuan203/article/details/7566518 hibernate是一个采用ORM(Object/Relation Mapping ...

  7. hadoop的核心思想

    hadoop的核心思想 1.1.1. hadoop的核心思想 Hadoop包括两大核心,分布式存储系统和分布式计算系统. 1.1.1.1. 分布式存储 为什么数据需要存储在分布式的系统中哪,难道单一的 ...

  8. 何谓IOC的核心思想

    IOC(Inversion of Control)即控制反转,是在面试或平常交流中经常遇到了词汇:我也曾经仿照Spring,利用JDK的反射和动态代理实现了一个简单的IOC框架,感觉算是知其然也知其所 ...

  9. vue.js学习笔记(一):什么是mvvm框架,vue.js的核心思想

    一:MVVM框架 MVVM框架的应用场景:  1.针对具有复杂交互逻辑的前端应用 2.提供基础的架构抽象 3.提供ajax数据持久化,保证前端用户体验 二:vue.js的核心思想 (一):数据驱动 ( ...

随机推荐

  1. Apache CXF基本使用

    一.服务端开发 1.创建web项目 2.导入jar包 3.web.xml中配置Servlet 1 <!-- 配置CXF框架提供的Servlet --> 2 <servlet> ...

  2. python3 连接数据库~

    ~目前记录的是针对python3写的数据库连接,不适用于pyhon2.python3如果想要与数据库进行连接,则需要先下载对应各数据库的插件包,然后导入包.python3的插件下载地址:https:/ ...

  3. Python练习题 005:三个数字由大到小排序输出

    [Python练习题 005]输入三个整数x,y,z,请把这三个数由小到大输出. ----------------------------------------------------------- ...

  4. NodeJS二进制合并

    Node.js 批量文件合并code cnblogs @ Orcim  本 文主要介绍使用 Node 进行 ACB 序列文件(Atom CueSheet Binary,编译 AtomCueSheet ...

  5. 028 01 Android 零基础入门 01 Java基础语法 03 Java运算符 08 逻辑“或”运算符

    028 01 Android 零基础入门 01 Java基础语法 03 Java运算符 08 逻辑"或"运算符 本文知识点:Java中的逻辑"或"运算符 逻辑& ...

  6. Python中matplotlib.pyplot.imshow画灰度图的多种方法

    转载:https://www.jianshu.com/p/8f96318a153f matplotlib库的教程和使用方法此处就不累赘了,网上有十分多优秀的教程资源.此处直接上代码: def demo ...

  7. swoole父进程和子进程之间通信的例子

    <?php /** 这是一个swoole父进程和子进程之间通信的例子 */ //进程创建成功后回调处理 function handle(swoole_process $worker){ //从进 ...

  8. mycat 单库分表实践

    参考 https://blog.csdn.net/sq2006hjp/article/details/78732227 Mycat采用的水平拆分,不管是分库还是分表,都是水平拆分的.分库是指,把一个大 ...

  9. IGBT以及MOSFET驱动参数的计算方法

  10. [Docker] redis 全配置

    启动容器,加载配置文件并持久化数据 docker run -d --privileged=true -p 6379:6379 --restart always -v /usr/redis/conf:/ ...