关键字:Hash 表,双指针,二分搜索
归航return:(Trivial) LeetCode 1237—找出给定方程的正整数解zhuanlan.zhihu.comProblem
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2,2]
示例 2:
输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出: [4,9]
说明:
- 输出结果中每个元素出现的次数,应与元素在两个数组中出现的次数一致。
- 我们可以不考虑输出结果的顺序。
进阶:
- 如果给定的数组已经排好序呢?你将如何优化你的算法?
- 如果 nums1 的大小比 nums2 小很多,哪种方法更优?
- 如果 nums2 的元素存储在磁盘上,磁盘内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?
Solution
这道题的 tag 包含了排序,Hash 表,双指针,二分查找,那么可以考虑使用多种方法来进行求解。
Solution 1: Two Pointers
使用双指针的方法,通常需要要求问题具有一定的顺序,才能方便处理。因此,首先将两个数组进行排序:sort(nums1.begin(), nums1.end())
—这是 C++ 版,Arrays.sort(nums1)
—这是 Java 版。
得到了升序排列的结果之后便可以使用双指针法来处理问题了。同时维护两个 int
型变量(原则上应该是 unsigned long
--对于 C++,但是这样意味着需要特判一个数组长度为 0 的情况,这也不算难),分别代表在数组 nums1
和数组 nums2
中当前观察的位置,称作 i
和 j
。如果某个时候 nums1[i]
和 nums2[j]
相等,意味着找到了一个可用的解答,将这个数添加到结果中,之后 ++i, ++j
。如果 nums1[i]<nums2[j]
,意味着第一个数组的元素太小了,那么仅仅 ++i
,对偶地,如果 nums1[i]>nums2[j]
,那么只需要 ++j
。整个循环在下标 i,j
是合法的前提下进行,也就是 0 <= i < nums1.length && 0 <= j < nums2.length
,就可以得到答案了。
那么为什么这个算法是正确的呢?考虑任意一个状态i,j
。如果这个时候nums1[i] < nums2[j]
,那么可以知道,如果对于给定的j
, 存在一个可行的解答,那么必然要求这个解答中的i
是大于当前的i
的,于是就++i
,同理可以得到++j
的情况。如果这个时候nums1[i] == nums2[j]
,那么意味着如果之后还有其他的解答,一定从大于当前i
的i
和大于当前j
的j
中选取。为什么不能取等号呢?因为这样会导致重复。
综上所述,代码如下(C++ 版本写起来太熟悉了,只写出 Java 版):
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
Arrays.sort(nums1);
Arrays.sort(nums2);
List<Integer> res = new ArrayList<Integer>();
int pos1 = 0;
int pos2 = 0;
int val1, val2;
while (pos1 < nums1.length && pos2 < nums2.length){
val1 = nums1[pos1];
val2 = nums2[pos2];
if (val1 == val2){
res.add(val1);
++pos1;
++pos2;
}
else if (val1 < val2){
++pos1;
}
else{//if (val1 > val2)
++pos2;
}
}
int[] resInArray = new int[res.size()];
for (int i = 0; i < resInArray.length; ++i){
resInArray[i] = res.get(i);
}
return resInArray;
}
}
算法的时间复杂度是 O(m*logm+n*logn)
,其中,m
和 n
分别是两个数组的长度,事实上本题的耗时主要是花在排序算法上,因为后续的双指针操作最多进行 m+n
次。空间复杂度最坏是 O(m+n)
,来源是 res
这个 ArrayList
,如果是 C++ 版本,由于返回的结果直接就是一个 std::vector<int>
,因此额外空间复杂度是 O(1)
。
Solution 2: Hash Table
首先维护一个 Hash 表,储存 nums1
这个数组中所有元素的出现频率。之后遍历整个数组 nums2
,如果遇到了某个出现在 Hash 表中的元素,就将这个元素添加到结果中,并将出现频率 -1,这很重要,因为如果一个数组中有 2 个 5,另一个数组中出现了 3 个 5,当第二次遇到 5 的时候,此时已经不能把 5 当作已经出现的变量了。统计完整个数组即可得到答案,这种算法比较暴力。
代码如下,这是 C++ 版:
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int, int>nums1StatisticsHashMap;
for (auto const &x : nums1){
++nums1StatisticsHashMap[x];
}
vector<int>res;
for (auto const &x : nums2){
if (nums1StatisticsHashMap.count(x) && nums1StatisticsHashMap[x]){
res.emplace_back(x);
--nums1StatisticsHashMap[x];
}
}
return res;
}
};
这是 Java 版。比较两种代码可以看出,Java 对于 HashMap 这种数据结构的要求更为严格,必须首先判定元素是否存在才行,而上述 C++ 代码的 operator[]
如果找不到元素,就针对 int
型的 Value 直接丢一个 0 进去。
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
Map<Integer, Integer>nums1StatisticsHashMap = new HashMap<Integer, Integer>();
for (int x : nums1){
if (nums1StatisticsHashMap.containsKey(x)){
int val = nums1StatisticsHashMap.get(x);
nums1StatisticsHashMap.replace(x, ++val);
}
else{
nums1StatisticsHashMap.put(x, 1);
}
}
List<Integer> res = new ArrayList<Integer>();
for (int x : nums2){
if (nums1StatisticsHashMap.containsKey(x)){
int val = nums1StatisticsHashMap.get(x);
if (val > 0){
res.add(x);
nums1StatisticsHashMap.replace(x, --val);
}
}
}
int[] resInArray = new int[res.size()];
for (int i = 0; i < resInArray.length; ++i){
resInArray[i] = res.get(i);
}
return resInArray;
}
}
这个算法的空间复杂度是 O(m)
,因为 Hash 表需要额外的空间进行存储。时间复杂度是 O(m+n)
,因为我们对 Hash 表进行了 m+n
次修改操作和 n
次搜索操作,每个操作的均摊时间复杂度都是 O(1)
。因此如果使用这个方法,可以回答进阶问题的第一道题,如果 nums1
数组的大小远小于 nums2
的数组大小,那么应该交换这个算法中 nums1
和 nums2
的位置,从而减小实际上使用的时间。
Solution 3: Binary Search
实际上因为有重复元素的存在和计数问题,这道题目使用 binary search 比较麻烦,直接给出一个 LeetCode 题解区他人的答案:
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
if(!nums1.size()||!nums2.size()) return {};
vector<int> ans;
sort(nums1.begin(),nums1.end());//因为1短,对1排序,然后根据2在1中二分查找
for(auto v : nums2){
int l = 0, r = nums1.size() - 1;//下面就是常规二分了
while(l < r){
int mid = l + r>>1;
if(nums1[mid] >= v) r = mid;
else l = mid + 1;
}
if(nums1[l]==v){//查找成功时
ans.push_back(v),nums1.erase(nums1.begin() + l);
if(!nums1.size()) break;
}
}
return ans;
}
};
不过如果我是他,我会使用 C++ 标准库 <algorithm>
的二分查找写法来做:
int l = 0, r = nums1.size();
while (l < r){
int mid = l + (r-l)/2;
if (v > nums1[mid]){
l = mid+1;
}
else{
r = mid;
}
}
if (nums1[l] == v){
ans.emplace_back(v);
nums1.erase(nums1.begin()+v);
if (nums1.empty()){
break;
}
}
Solution 4: C++ Library Function
在很多 LeetCode 试题中,我们常常能看到语言 Python 因为各种内置函数,经常来个一行秒杀,不愧是:“人生苦短,我用 Python”。
而风水轮流转,今日到我家。LeetCode 官方题解给出了这样一个库函数直接完成结果:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
sort(begin(nums1), end(nums1));
sort(begin(nums2), end(nums2));
nums1.erase(set_intersection(begin(nums1), end(nums1),
begin(nums2), end(nums2), begin(nums1)), end(nums1));
return nums1;
}
其中库函数 set_intersection
位于 C++ 标准算法库 <algorithm>
中,使用说明可以参考 cppreference:
template< class InputIt1, class InputIt2, class OutputIt >
constexpr OutputIt set_intersection( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt d_first, Compare comp );
将 [first1, last1)
和 [first2, last2)
中的元素求交集,将元素存储到从 d_first
开始的迭代器中,保证这些元素按照排序规则 Comp 排序,返回的结果是这些交集元素范围对应的尾后迭代器,也就是最后一个元素的后一个位置。前提是,上述两个范围已经按照规则 comp 被排序完毕。在页面下方的两种可能实现中,我们发现,实现方法就是双指针的方法。
这是 Xcode 的 C++ 环境(LLVM)中的实现方法,更多的 C++ 实现方法不再列出:
template <class _Compare, class _InputIterator1, class _InputIterator2, class _OutputIterator>
_LIBCPP_CONSTEXPR_AFTER_CXX17 _OutputIterator
__set_intersection(_InputIterator1 __first1, _InputIterator1 __last1,
_InputIterator2 __first2, _InputIterator2 __last2, _OutputIterator __result, _Compare __comp)
{
while (__first1 != __last1 && __first2 != __last2)
{
if (__comp(*__first1, *__first2))
++__first1;
else
{
if (!__comp(*__first2, *__first1))
{//暗示两者相等
*__result = *__first1;
++__result;
++__first1;
}
++__first2;
}
}
return __result;
}
虽然我很想吐槽一下这个标准库中像是摩尔斯电报一样的前置下划线变量命名。。。
EOF。