LeetCode【学习计划】:【数据结构】
350. 两个数组的交集 II
LeetCode: 350. 两个数组的交集 II
简 单 \color{#00AF9B}{简单} 简单
给你两个整数数组
nums1
和nums2
,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2,2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[4,9]
提示:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000
进阶:
- 如果给定的数组已经排好序呢?你将如何优化你的算法?
- 如果
nums1
的大小比nums2
小,哪种方法更优? - 如果
nums2
的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?
方法1:哈希表
我们可以先遍历较短的数组并把其内的所有元素存入哈希表中。由于数组内存在重复的元素,为了使多个重复元素按照在2个数组中出现的次数匹配,而不至于全部匹配,所以哈希表记录的是短数组中每个字符的计数。然后遍历一次长数组。如果长数组的某个元素已经存在于哈希表了,就把它添加到答案数组。因为成功匹配到了,所以还要将哈希表中对应字符的计数减1。
设 A A A为短数组中元素的集合(包含重复元素),则哈希表在最初的状态即为 A A A。到了最后,由于短数组 A A A和长数组 B B B的交集已经摘掉了,因此最后哈希表的状态为 A − ( A ∩ B ) A-(A \cap B) A−(A∩B)。
之所以使用较短的数组存入哈希表是为了尽可能的减少实际的空间消耗。
#include <vector>
#include <unordered_map>
using namespace std;
class Solution
{
public:
vector<int> intersect(vector<int> &nums1, vector<int> &nums2)
{
bool bigger = nums1.size() > nums2.size();
unordered_map<int, int> hashtable;
for (const int num : (bigger ? nums2 : nums1))
{
hashtable[num]++;
}
vector<int> intersection;
for (const int num : (bigger ? nums1 : nums2))
{
if (hashtable.find(num)!=hashtable.end())
{
intersection.emplace_back(num);
hashtable[num]--;
if (hashtable[num] == 0)
{
hashtable.erase(num);
}
}
}
return intersection;
}
};
复杂度分析
-
时间复杂度: O ( m + n ) O(m+n) O(m+n)。两个数组中的每个元素都要遍历一遍,同时哈希表操作的时间复杂度为 O ( 1 ) O(1) O(1),因此总时间复杂度为两者的成绩 O ( m + n ) O(m+n) O(m+n)
-
空间复杂度: O ( m i n ( m , n ) ) O(min(m, n)) O(min(m,n))。答案数组不算在内,主要为哈希表的开销。
参考结果
Accepted
55/55 cases passed (4 ms)
Your runtime beats 87.93 % of cpp submissions
Your memory usage beats 36.26 % of cpp submissions (10.2 MB)
方法2:排序 + 双指针
如果把两个数组都排序好后,使用双指针查找的思路比较容易。
在两个数组的起始项各设置一个指针。
- 每当两个指针指向的值相等,则存入答案数组,且两个指针同时向后走一步。
- 若一个指针指向的值小于另一个指针指向的值,那么这个指针必然要往后走,因为往后走必然是更大的值,才有可能和另一个指针对应上。
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
vector<int> intersect(vector<int> &nums1, vector<int> &nums2)
{
sort(nums1.begin(), nums1.end());
sort(nums2.begin(), nums2.end());
const int len1 = nums1.size(), len2 = nums2.size();
vector<int> intersection;
auto it1 = nums1.begin(), it2 = nums2.begin();
while (it1 != nums1.end() && it2 != nums2.end())
{
if (*it1 == *it2)
{
intersection.emplace_back(*it1);
it1++;
it2++;
}
else if (*it1 < *it2)
{
it1++;
}
else
{
it2++;
}
}
return intersection;
}
};
复杂度分析
-
时间复杂度: O ( m log m + n log n ) O(m\log m + n \log n) O(mlogm+nlogn)。
m
和n
分别为两个数组的长度,std::sort
的时间复杂度为 n l o g ( n ) nlog(n) nlog(n)。 -
空间复杂度: O ( log n ) O(\log n) O(logn)。答案数组不算在内,快速排序所需的空间复杂度为 O ( log n ) O(\log n) O(logn)。
参考结果
Accepted
55/55 cases passed (4 ms)
Your runtime beats 87.93 % of cpp submissions
Your memory usage beats 73.34 % of cpp submissions (9.8 MB)
结语
- 如果给定的数组已排好序,那么无论方法1还是方法2,时间复杂度都为 O ( m + n ) O(m+n) O(m+n)。而方法1中由于哈希表的长度是较短数组的长度,因此为 O ( m i n ( m , n ) ) O(min(m, n)) O(min(m,n));方法2中主要为快速排序的开销,空间复杂度为 O ( log n ) O(\log n) O(logn)。因此在有序的条件下,方法2更好。
- 如果
nums2
的元素存储在磁盘上,内存是有限的,并且不能一次加载所有的元素到内存中,这种情况下方法1更好。因为方法2需要对整个数组进行排序。
121. 买卖股票的最佳时机
LeetCode: 121. 买卖股票的最佳时机
简 单 \color{#00AF9B}{简单} 简单
给定一个数组
prices
,它的第i
个元素prices[i]
表示一支给定股票第i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回0
。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
- 1 <= prices.length <= 105
- 0 <= prices[i] <= 104
方法1:动态规划 + 滑动数组
就像我们在【LeetCode学习计划】《数据结构入门-C++》第1天 数组中的53. 最大子序和一题一样,我们不能用dp
数组存储以第i
天为开始的利润,这样的话后面每次更新都要对前面的所有dp
数组元素进行遍历更新。所以我们的dp
数组要存储以第i天为结束的[某个信息]。
接下来需要思考dp
数组具体到底存什么信息。如果直接存答案的话,也就是以第i天为结束的利润,那么我们在达到第i天时还得往前遍历整个prices
数组找到最小值,这显然不太理想。
不过到这里答案也就出来了,我们因为在达到第i
天时不知道前i-1
天中的最小值,所以使用dp
数组来存放第i天之前的最小值。然后单独定义一个变量ans
,每次遍历一天时,dp[i]
要么是到前一天为止的最小值dp[i-1]
,要么以“今天”作为最小值。因此状态转移方程为:
d p [ i ] = m i n ( d p [ i − 1 ] , p r i c e s [ i ] ) dp[i]=min(dp[i-1], prices[i]) dp[i]=min(dp[i−1],prices[i])
由状态转移方程可知:dp[i]
只和dp[i-1]
有关,因此我们使用1个变量就能完成存储。这里是用到了【滑动数组】的思想。
#include <vector>
using namespace std;
class Solution
{
public:
int maxProfit(vector<int> &prices)
{
int minprice = INT_MAX, ans = 0;
for (const int price : prices)
{
minprice = min(price, minprice);
ans = max(ans, price - minprice);
}
return ans;
}
};
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n)
-
空间复杂度: O ( 1 ) O(1) O(1)。我们只需要常量空间来存储若干变量。
参考结果
Accepted
211/211 cases passed (244 ms)
Your runtime beats 7.89 % of cpp submissions
Your memory usage beats 70.59 % of cpp submissions (91.1 MB)