001.两数之和
难度:简单
题目
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
提示:
2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
- 只会存在一个有效答案
进阶: 你可以想出一个时间复杂度小于 O(n2) 的算法吗?
方法一:暴力枚举
思路及算法
最容易想到的方法是枚举数组中的每一个数 x,寻找数组中是否存在 target - x
。
当我们使用遍历整个数组的方式寻找 target - x
时,需要注意到每一个位于 x 之前的元素都已经和 x 匹配过,因此不需要再进行匹配。而每一个元素不能被使用两次,所以我们只需要在 x 后面的元素中寻找 target - x
。
代码
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] res=new int[2];
for(int i = 0 ; i < nums.length ; i++){
for(int j = 0 ; j < nums.length ;j++){
if(nums[i]+nums[j]==target){
if(i!=j){
res[0]=i;
res[1]=j;
}
}
}
}
return res;
}
}
复杂度分析
- 时间复杂度:O(N²),其中 N 是数组中的元素数量。最坏情况下数组中任意两个数都要被匹配一次。
- 空间复杂度:O(1)。
方法二:暴力枚举改进
思路及算法
进行了一点改进,对内层 for 进行修改
int j = 0 ; j < nums.length ; j++
改成 int j = nums.length - 1 ; j > i ; j --
前者 j 从0到数组长度循环;
后者从数组长度到 i 循环;减少循环次数;
代码
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] res=new int[2];
for(int i = 0 ; i < nums.length ; i++){
for(int j = nums.length-1 ; j > i ;j--){
if(nums[i]+nums[j]==target){
if(i!=j){
res[0]=i;
res[1]=j;
}
}
}
}
return res;
}
}
用时从97ms --> 49ms,用时减半;
内存消耗从42.57MB --> 42.08MB ,几乎没有变化;
复杂度分析
- 时间复杂度:O(N²),其中 N 是数组中的元素数量。最坏情况下数组中任意两个数都要被匹配一次。
- 空间复杂度:O(1)。
方法三:哈希表
思路及算法
注意到方法一的时间复杂度较高的原因是寻找 target - x
的时间复杂度过高。因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。
使用哈希表,可以将寻找 target - x
的时间复杂度降低到从 O(N)
降低到 O(1)
。
这样我们创建一个哈希表,对于每一个 x
,我们首先查询哈希表中是否存在 target - x
,然后将 x
插入到哈希表中,即可保证不会让 x
和自己匹配。
代码
class Solution {
public int[] twoSum(int[] nums, int target) {
//创建一个 HashMap 对象 hashtable, 整型(Integer)的 key 和整型(Integer)的 value
Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; ++i) {
//hashtable.containsKey(hashtable.containsKey具体使用方法见附3) 查找 key = target - nums[i] 在哈希表中是否存在,存在则从哈希表中取出( target - nums[i] , i ),不存在则将(nums[i], i)写入哈希表
if (hashtable.containsKey(target - nums[i])) {
return new int[]{hashtable.get(target - nums[i]), i};
}
hashtable.put(nums[i], i);//写入哈希表
}
return new int[0];
}
}
复杂度分析
时间复杂度:O(N)
,其中N
是数组中的元素数量。对于每一个元素 x
,我们可以 O(1)
地寻找 target - x。
空间复杂度:O(N)
,其中 N
是数组中的元素数量。主要为哈希表的开销
方法四:哈希表改进
思路及算法
在哈希表的基础上仍可改进,即每次在哈希表中搜索补数时,设置left和right两个指针,一次搜索nums中左右两端的元素的补数,可以得到耗时0ms的效果。
代码
public int[] twoSum(int[] nums, int target) {
if(nums == null || nums.length < 2) {
return new int[] {};
}
Map<Integer, Integer> map = new HashMap<>();
int left = 0;//左指针,下标为0
int right = nums.length - 1;//右指针,下标为nums长度-1
while(left <= right) {
int ln0 = nums[left];//左值
int ln1 = target - ln0;//左值补数
int rn0 = nums[right];//右值
int rn1 = target - rn0;//右值补数
if(map.containsKey(ln1)) {//寻找哈希表中有无左值补数
return new int[] {left, map.get(ln1)};//有则返回左值补数(左下标,左值补数对应的下标)
}
else {
map.put(ln0, left++);//无则写入(左值,左下标)写入后左下标+1
}
if(map.containsKey(rn1)) {//寻找哈希表中有无右值补数
return new int[] {right, map.get(rn1)};//有则返回右值补数(右下标,右值补数对应的下标)
}
else {
map.put(rn0, right--);//无则写入(右值,右下标)写入后右下标-1
}
}
return new int[] {};
}
复杂度分析
时间复杂度取决于遍历原数组耗时,为O(n)
。使用了哈希表存储数组内容,空间复杂度O(n)
。
方法五:排序后折半查找
思路及算法
先将元素nums
拷贝一份为copiedNums
,
然后将原数组排序,
接着遍历排序后的数组,
以遍历到的数字之后的数列执行折半查找,
查找目标为target - nums[i]
。
因为原数组排序后丢失原下标信息,
因此执行折半查找得到n0和n1(n0 + n1 = target)后,
再遍历两次copiedNums分别得到n0和n1在copiedNums中的下标(原下标)。
排序和折半查找可以调用Arrays静态方法Arrays.sort()和Arrays.binarySearch(),也可以自己实现。
代码示例中排序用Arrays.sort(),折半查找用自己实现的版本。
代码
public int[] twoSumSortBinarySearch(int[] nums, int target) {
int[] res = new int[2];
int[] resVal = new int[2];
int[] copiedNums = Arrays.copyOf(nums, nums.length);//拷贝nums的元素放在copiedNums.
Arrays.sort(nums);//对nums进行排序
int n1 = -1;
for (int i = 0; i < nums.length; i++) {
resVal[0] = nums[i];
resVal[1] = target - resVal[0];
// 也可以用Arrays自带的折半查找方法Arrays.binarySearch(),
// 但要注意判断返回值的地方要做相应修改。
// n1 = Arrays.binarySearch(nums, i + 1, nums.length -1 , resVal[1]);
n1 = binarySearchBasic(nums, i + 1, resVal[1]);
if(n1 != -1) {
break;
}
}
if(n1 == -1) {
return new int[] {};
}
for (int j = 0; j < copiedNums.length; j++) {
if(copiedNums[j] == resVal[0]) {
res[0] = j;
break;
}
}
for (int k = 0; k < copiedNums.length; k++) {
// 注意不能是同一个元素,需加上 k != res[0] 条件
if(copiedNums[k] == resVal[1] && k != res[0]) {
res[1] = k;
break;
}
}
return res;
}
private int binarySearchBasic(int[] arr, int low, int target) {
int high = arr.length - 1;
while(low <= high) {
int center = (low + high) / 2;
if(target == arr[center]) {
return center;
}
else if(target < arr[center]) {
high = center - 1;
}
else {
low = center + 1;
}
}
return -1;
}
复杂度计算
时空复杂度
排序耗时O(nlogn)(假设采用O(nlogn)的排序算法),拷贝原数组耗时O(n),排序后的折半查找耗时为O(logn),最后找原下标的两个for均耗时O(n),所以总体时间复杂度为O(nlogn)。
空间复杂度为O(n)。
涉及参考、知识点详解:
1:力扣001-两数之和(题目及解答参考)
2:Java.util.HashMap 类讲解
3:java.util.HashMap.containsKey() 方法详解
4:java.util.HashMap.get() 方法详解
5:java.util.HashMap.put() 方法详解
6:Java.util.Arrays.sort()详解
7:Java.util.Arrays.copyOf()详解
8:解题方法详解参考文章
9:方法二画解
另附集合框架体系图: