目录
前言
声明:本文仅为学习记录,图片以及题目资源来自力扣(leetcode)网,如有侵权请联系删除
题目描述
给定一个整数数组 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 <= 10
4
-10
9
<= nums[i] <= 10
9
-10
9
<= target <= 10
9
- 只会存在一个有效答案
解题方法
方法一:暴力求解法
思路以及实现
循环遍历两遍数组两次分别取出两个数字进行相加,直到取出的两个数字之和为目标值。
注意:两个数字不能相同
所以代码如下:
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)。
方法二:暴力枚举改进
思路及其实现
注意到每次遍历时比如现有数组[1,2,3,4,5] 实际上1 + 2与2 + 1 是一样的意思且又由于不能使用相同的数字,实际上第二次遍历就只需要遍历第一次遍历的数字之后的即可,即1+2,1+3,1+4,1+5;2+3,2+4;2+5等等。代码表现为for(int j = i + 1;j<n;j++)
class Solution {
public int[] twoSum(int[] nums, int target) {
int n = nums.length;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
return new int[0];
}
}。
复杂度分析
- 时间复杂度:O(N²)
对于每个元素 nums[i]
,它只与 i+1
之后的元素进行比较,这样就避免了与自己和之前元素的比较,减少了比较次数。因此,每个元素只被比较 n-1
次,但是每次比较都是与不同的元素进行的,所以总的比较次数是 n*(n-1)/2
,即 O(n^2)。但是,由于每次比较都是与不同的元素进行,所以实际上这个算法的时间复杂度仍然是 O(n^2),但是常数因子较小,因此执行速度会比之前的代码快。
- 空间复杂度:O(1)。
代码中没有使用额外的数据结构来存储中间结果,只是返回了一个包含两个索引的数组,这个数组的大小与输入数组 nums
的大小无关。因此,空间复杂度是 O(1),即常数级别的空间复杂度。
方法三:使用哈希表
思路及其实现
注意到方法一的时间复杂度较高的原因是寻找 target - x 的时间复杂度过高。因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。
问题转化为目标值与数组中的数值相减存入哈希表,若哈希表中有相同的值,证明他们两个相加等于这个数。使用哈希表,可以将寻找 target - x 的时间复杂度降低到从 O(N) 降低到 O(1)。
这样我们创建一个哈希表,对于每一个 x,我们首先查询哈希表中是否存在 target - x,然后将 x 插入到哈希表中,即可保证不会让 x 和自己匹配。
代码
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; ++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 是数组中的元素数量。主要为哈希表的开销。
方法四:哈希表改进
思路及其实现
算法意图:
- 边界条件检查:首先检查输入数组
nums
是否为空或者长度小于2,如果是,则返回一个空数组。 - 初始化哈希表:创建一个
HashMap
来存储遍历过程中遇到的数字及其索引。 - 双指针法:使用两个指针
left
和right
分别指向数组的起始和结束位置。 - 循环遍历:在
while
循环中,同时从数组的两端向中间遍历,尝试找到两个数的和等于目标值target
。 - 查找补数:对于左指针指向的值
ln0
,计算它的补数ln1
(即target - ln0
),并检查这个补数是否已经在哈希表中。如果在,说明找到了两个数的和等于target
,并返回它们的索引。对于右指针指向的值rn0
,也做同样的操作。 - 更新哈希表:如果左指针的补数不在哈希表中,则将左指针的值和索引存入哈希表,并将左指针向右移动。如果右指针的补数不在哈希表中,则将右指针的值和索引存入哈希表,并将右指针向左移动。
- 返回结果:如果循环结束都没有找到符合条件的两个数,则返回一个空数组。
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)
。
运行时间能达到0ms
方法五:排序后折半查找
思路及其实现
先将元素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)
自己做
- 自己的思路
- 答案思路
- 自己为什么没想到
- 数据结构知识点
- Java语句
进阶:你可以想出一个时间复杂度小于 O(n
2
)
的算法吗?
参考资料:
1:1. 两数之和 - 力扣(LeetCode)(题目及解答参考)