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. Python练习题 023:比后面的人大2岁

    [Python练习题 023] 有5个人坐在一起,问第五个人多少岁?他说比第4个人大2岁.问第4个人岁数,他说比第3个人大2岁.问第三个人,又说比第2人大两岁.问第2个人,说比第一个人大两岁.最后 问 ...

  2. 迪杰斯特拉和spfa

    迪杰斯特拉 Dijkstra算法是典型的算法.Dijkstra算法是很有代表性的算法.Dijkstra一般的表述通常有两种方式,一种用永久和临时标号方式,一种是用OPEN, CLOSE表的方式,这里均 ...

  3. 固件(Firmware)

    来源:https://baike.baidu.com/item/%E5%9B%BA%E4%BB%B6/627829 固件   固件(Firmware)就是写入EPROM(可擦写可编程只读存储器)或EE ...

  4. JVM 第二篇:垃圾收集器以及算法

    本文内容过于硬核,建议有 Java 相关经验人士阅读. 0. 引言 一说到 JVM ,大多数人第一个想到的可能就是 GC ,今天我们就来聊一聊和 GC 关系最大的垃圾收集器以及垃圾收集算法,希望能通过 ...

  5. SQL学习 小知识点(一)

    这是我在做实验的时候总结的,随着学习可能会做更多的小总结 ssms上面把所有代码写下来后一块执行是行不通的(极大可能会报错),推测可能是因为ssms先对代码的语法进行了检查.    --My expe ...

  6. 使用react Context+useReducer替代redux

    首先明确一点,Redux 是一个有用的架构,但不是非用不可.事实上,大多数情况,你可以不用它,只用 React 就够了. 曾经有人说过这样一句话. "如果你不知道是否需要 Redux,那就是 ...

  7. 多测师讲解自动化测试 _RF自定义关键字_高级讲师肖sir

    RF自定义关键字 在rf中叫关键字 在python中就叫做函数 或实例方法 我们自己可以写自定义关键字 自己创建一个库===库里面去创建模块===模块里面创建类和实例方法==>rf导入和引用 库 ...

  8. Termux基础教程(二):软件包安装

    Termux基础教程(二):软件包安装 Termux是一个在安卓手机上模拟Linux系统的高级终端,这个终端十分强大,实用. Termux可以安装Liunx的各种软件包,这就是Termux的灵魂所在. ...

  9. 判断ip地址是属于国内还是国外

    一,如何判断一个ip地址是否属于国内? 我们以前使用淘宝提供的一个api地址进行判断,但经常出现打不开的报错, 因为只需要判断是国内或国外,于是考虑自己搞一个简单的. 分配给国内的ip地址在apnic ...

  10. selenium-窗口切换

    方法一 # 获取打开的多个窗口句柄 windows = driver.window_handles # 切换到当前最新打开的窗口 driver.switch_to.window(windows[-1] ...