我的LeetCode代码仓:https://github.com/617076674/LeetCode
原题链接:https://leetcode-cn.com/problems/two-sum/description/
题目描述:
知识点:哈希表
对于数组中的每一个元素进行遍历,设立索引i从0遍历到nums.length - 1。对于每一个元素,由于要求的是一个组合,而不是一个排列,所以我们对第二个元素索引的遍历可以从i + 1一直到nums.length - 1为止。事实上,即使这道题要求的是排列,由于只有两个元素,我们也可以按这种做法来尽可能地减少我们总的循环次数。
由于代码中存在着双重循环,很明显这种做法的时间复杂度是是O(n ^ 2)级别的,其中n表示nums数组的长度。而对于空间复杂度来说,由于只存储result这个只有2个元素的1维数组,因此空间复杂度是O(1)级别的。
JAVA代码:
public class Solution {
public int[] twoSum(int[] nums, int target) {
int[] result = new int[2];
for (int i = 0; i < nums.length - 1; i++) {
for (int j = i + 1; j < nums.length; j++) {
if(nums[i] + nums[j] == target) {
result[0] = i;
result[1] = j;
return result;
}
}
}
return result;
}
}
JAVA解题报告:
C++代码:
class Solution {
public:
vector twoSum(vector& nums, int target) {
vector result;
for(int i = 0; i < nums.size(); i++){
for(int j = i + 1; j < nums.size(); j++){
if(nums[i] + nums[j] == target){
result.push_back(i);
result.push_back(j);
return result;
}
}
}
}
};
C++解题报告:
对于这道题来说,如果题目要求我们返回的是满足条件的两个数字,而不是这两个数字所对应的索引值,那么我们完全可以先对nums数组进行一次排序,再用对撞双指针的方法求解。
根据这个思路进一步思考,我们可以用一个哈希表来记录nums数组中每一个索引对应的数字值。我们再新建一个元素值等于索引值的长度为nums.length的数组。接下来,我们就只要根据哈希表中记录的每一个索引的值的大小来对这个新创建的数组进行排序。然后再用双对撞指针的方法求解这个问题。注意,这里的对撞双指针法中比较的不应该是数组中的值,而应该是数组中的值所对应的哈希表中的值。
此方法中排序我们采用的是三路快排的方式,由于排序的时间复杂度是O(nlogn),这里的n同样是nums数组的长度,而遍历新数组的时间复杂度是O(n)级别的,因此总的时间复杂度是O(nlogn)级别的。而对于空间复杂度来说,需要一个哈希表,还需要一个用来排序的新建数组,因此空间复杂度是O(n)级别的。和思路一相比,时间复杂度得到的优化,而代价就是空间复杂度变大,这和哈希表空间换时间的思想是一致的。
JAVA代码:
public class Solution {
public int[] twoSum(int[] nums, int target) {
int[] result = new int[2];
HashMap hashMap = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
hashMap.put(i, nums[i]);
}
Integer[] numsCopy = new Integer[nums.length];
for (int i = 0; i < numsCopy.length; i++) {
numsCopy[i] = i;
}
sort(numsCopy, hashMap);
int left = 0;
int right = numsCopy.length - 1;
while(left < right) {
if(hashMap.get(numsCopy[left]) + hashMap.get(numsCopy[right]) == target) {
result[0] = numsCopy[left];
result[1] = numsCopy[right];
return result;
}else if(hashMap.get(numsCopy[left]) + hashMap.get(numsCopy[right]) > target) {
right--;
}else {
left++;
}
}
return result;
}
private void sort(Integer[] arr, HashMap hashMap) {
sort(arr, 0, arr.length - 1, hashMap);
}
private void sort(Integer[] arr, int left, int right, HashMap hashMap) {
if(left > right) {
return;
}
swap(arr, left, (int)(Math.random() * (right - left + 1)) + left);
int lessThan = left; //[left + 1, lessThan] is less than left
int greaterThan = right + 1; //[greaterThan, right] is greater than left
int i = left + 1; //[lessThan + 1, i) is the same as left
while(i < greaterThan) {
if(hashMap.get(arr[i]).compareTo(hashMap.get(arr[left])) == 0) {
i++;
}else if(hashMap.get(arr[i]).compareTo(hashMap.get(arr[left])) > 0) {
swap(arr, i, greaterThan - 1);
greaterThan--;
}else {
swap(arr, i, lessThan + 1);
lessThan++;
i++;
}
}
swap(arr, left, lessThan);
sort(arr, left, lessThan - 1, hashMap);
sort(arr, greaterThan, right, hashMap);
}
private void swap(Integer[] arr, int i, int j) {
Integer temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
JAVA解题报告:
C++代码:
bool compare(pair pair1, pair pair2){
if(pair1.second >= pair2.second){
return true;
}else{
return false;
}
}
class Solution {
public:
vector twoSum(vector& nums, int target) {
vector > pairs;
pair tempPair;
for(int i = 0; i < nums.size(); i++){
tempPair.first = i;
tempPair.second = nums[i];
pairs.push_back(tempPair);
}
sort(pairs.begin(), pairs.end(), compare);
int left = 0;
int right = pairs.size() - 1;
vector result;
while(left < right){
if(pairs[left].second + pairs[right].second == target){
result.push_back(pairs[left].first);
result.push_back(pairs[right].first);
return result;
}else if(pairs[left].second + pairs[right].second < target){
right--;
}else{
left++;
}
}
}
};
C++解题报告:
(1)设立一个HashMap,其键用以存放数组中的值,而其键所对应的值就是该键的值在数组中的索引。
(2)遍历数组nums,一个一个元素地添加进HashMap,同时寻找答案。如果当前遍历到了nums[i]这个元素,则在HashMap中寻找是否有target - nums[i]这个键,如果有则返回该键对应的索引和索引i。如果HashMap中没有target - nums[i]这个键,则将nums[i]新添加进HashMap中。
如果HashMap中已经有了nums[i]这个键,则更新其键对应的值为新的索引i,这种情况其实是相当于数组中两个不同的索引对应相同的值的情况,由于我们在前一步已经确定了nums[i] + nums[i] != target,因此我们可以放心地舍弃前一个nums[i]对应的索引值。
如果HashMap中还没有nums[i]这个键,亦将其键对应的值设为i。
在此方法中,我们只遍历了一遍整个数组,因此时间复杂度为O(n),其中n为数组nums的长度。对于空间复杂度,我们额外设立了一个哈希表用来将数组中的值反向关联到索引,而对于数组中相同的值,我们覆盖了其先进入哈希表的索引,因此空间复杂度为O(m),其中m为数组nums中存储的不同值的数量。
JAVA代码:
public class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap hashMap = new HashMap<>();
int[] result = new int[2];
for (int i = 0; i < nums.length; i++) {
int anotherNum = target - nums[i];
if(hashMap.containsKey(anotherNum)) {
result[0] = hashMap.get(anotherNum);
result[1] = i;
return result;
}else {
hashMap.put(nums[i], i);
}
}
return result;
}
}
JAVA解题报告:
C++代码:
class Solution {
public:
vector twoSum(vector& nums, int target) {
map record;
map::iterator it;
vector result;
for(int i = 0; i < nums.size(); i++){
it = record.find(target - nums[i]);
if(it != record.end()){
result.push_back(i);
result.push_back(it->second);
return result;
}else{
record[nums[i]] = i;
}
}
}
};
C++解题报告:
从三份LeetCode解题报告中我们可以看出,思路一的方法用时34ms,思路二的方法用时36ms,思路三的方法用时8ms。但是我们在之前的时间复杂度分析中显示,思路一的时间复杂度为O(n ^ 2)而思路二的时间复杂度为O(nlogn),与测试结果相反,这是为什么呢?难道是我们的时间复杂度分析出错了吗?
为了验证三种思路的时间复杂度,我们可以自己自定义测试用例,在自己的机子上跑,每次让测试使用数组的规模扩大2倍,观察时间的变化情况。
n的规模 | 思路一 | 思路二 | 思路三 |
---|---|---|---|
10 | 0.0002ms | 0.0051ms | 0.0008ms |
20 | 0.0ms | 0.006ms | 0.0004ms |
40 | 0.0001ms | 0.0103ms | 0.0004ms |
80 | 0.0001ms | 0.015ms | 0.0002ms |
160 | 0.0001ms | 0.0563ms | 0.0007ms |
320 | 0.0ms | 0.1129ms | 0.0004ms |
640 | 0.0001ms | 0.2324ms | 0.0004ms |
1280 | 0.0001ms | 0.4983ms | 0.0003ms |
2560 | 0.0ms | 1.1072ms | 0.0004ms |
由于数据产生的随机性,对于思路一和思路三而言,我们发现两者的时间过于小,数据几乎没有意义。但对于思路二的数据,随着n规模的倍增,思路二所用的时间也几乎以O(nlogn)的规模在逐渐增大,这是因为不管数据怎么随机,在思路二中都会存在着三路快排这个环节,这至少证明了我们思路二的时间复杂度分析是没有任何问题的。至于思路一和思路三,有兴趣的朋友可以自己去验证验证,我觉得上述的时间复杂度分析应该也是正确的。