给定一个整数数组 nums
和一个目标值 target
,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:
给定 nums = \[2, 7, 11, 15], target = 9
因为 nums\[0] + nums\[1] = 2 + 7 = 9
所以返回 \[0, 1]
暴力法很简单。遍历每个元素 x,并查找是否存在一个值与 target - x相等的目标元素。
public 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 };
}
}
}
throw new IllegalArgumentException("No two sum solution");
}
复杂度分析:
时间复杂度:O(n^2),对于每个元素,我们试图通过遍历数组的其余部分来寻找它所对应的目标元素,这将耗费 O(n)的时间。因此时间复杂度为 O(n^2)。
空间复杂度:O(1)。
为了对运行时间复杂度进行优化,我们需要一种更有效的方法来检查数组中是否存在目标元素。如果存在,我们需要找出它的索引。保持数组中的每个元素与其索引相互对应的最好方法是什么?哈希表。
通过以空间换取速度的方式,我们可以将查找时间从 O(n) 降低到 O(1)。哈希表正是为此目的而构建的,它支持以 近似 恒定的时间进行快速查找。我用“近似”来描述,是因为一旦出现冲突,查找用时可能会退化到 O(n)。但只要你仔细地挑选哈希函数,在哈希表中进行查找的用时应当被摊销为 O(1)。
一个简单的实现使用了两次迭代。在第一次迭代中,我们将每个元素的值和它的索引添加到表中。然后,在第二次迭代中,我们将检查每个元素所对应的目标元素(target - nums[i])是否存在于表中。注意,该目标元素不能是 nums[i] 本身!
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], i);
}
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement) && map.get(complement) != i) {
return new int[] { i, map.get(complement) };
}
}
throw new IllegalArgumentException("No two sum solution");
}
复杂度分析:
时间复杂度:O(n), 我们把包含有 n 个元素的列表遍历两次。由于哈希表将查找时间缩短到 O(1) ,所以时间复杂度为 O(n)。
空间复杂度:O(n), 所需的额外空间取决于哈希表中存储的元素数量,该表中存储了 n 个元素。
事实证明,我们可以一次完成。在进行迭代并将元素插入到表中的同时,我们还会回过头来检查表中是否已经存在当前元素所对应的目标元素。如果它存在,那我们已经找到了对应解,并立即将其返回。
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}
throw new IllegalArgumentException("No two sum solution");
}
复杂度分析:
时间复杂度:O(n), 我们只遍历了包含有 n 个元素的列表一次。在表中进行的每次查找只花费 O(1) 的时间。
空间复杂度:O(n), 所需的额外空间取决于哈希表中存储的元素数量,该表最多需要存储 n 个元素。
(1)
对于暴力法的优化,首先想到的是建立一个数组,数组上限为target/2(奇数就是再加一),然后将0~target/2直接存,target/2以上的反向存,当发现数组里有数时就检验是否相等,不相等的话就可以输出了。
后来看了题解才发现,自己这样做其实就等同于自己在实现一个使用范围受限的hash map……直接套用java自带的HashMap就好了,时间复杂度还是O(1)……更不要说java实现的HashMap对冲突较多的节点用红黑树去优化还有自动扩容等特点了。
数据结构学了还是想不到要用,是真的伤……还是得多看多练啊
(2)
在HashMap中通过get()
来获取value,通过put()
来插入value,ContainsKey()
则用来检验对象是否已经存在。
至于HashMap 在java8中的详细介绍,可以参考这篇文章《Java8的HashMap详解(存储结构,功能实现,扩容优化,线程安全,遍历方法)》
摘些和本题目相关的部分吧:
JDK1.8引入红黑树大程度优化了HashMap的性能,这主要体现在hash算法不均匀时,即产生的链表非常长,这时把链表转为红黑树可以将复杂度从O(n)降到O(logn);
Node[] table的初始化长度length(默认值是16),
Load factor为负载因子(默认值是0.75),
threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。
threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
(3)
把Integer拼写成Interger的错误我都不想说了……【捂脸】