【问题标题】:Why is single iteration for map insertion and map.find() much slower than two separate iterations for insert and for map.find()为什么 map 插入和 map.find() 的单次迭代比 insert 和 map.find() 的两次单独迭代慢得多
【发布时间】:2018-09-27 09:47:38
【问题描述】:

当我尝试优化我的 leetcode 二和问题 (https://leetcode.com/problems/two-sum/description/) 的解决方案时,我发现了一个有趣的现象。 二和问题的 Leetcode 描述为:

给定一个整数数组,返回两个数字的索引,使它们相加到一个特定的目标。 您可以假设每个输入都只有一个解决方案,并且您不能两次使用相同的元素。

最初,我通过使用两个循环来解决这个问题。首先,我遍历输入数组以将数组值和数组索引作为对存储到映射中。然后我再次遍历输入数组以循环每个元素并检查它是否存在于地图中。以下是我从 leetcode 得到的解决方案:

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) 
    {
        vector<int> res;
        map<int, int> store;

        for(int i = 0; i < nums.size(); ++i)
        {
            store[nums[i]] = i;
        }

        for(int i = 0; i < nums.size(); ++i)
        {
            auto iter = store.find(target - nums[i]);
            if(iter != store.end() && (iter -> second) != i)
            {
                res.push_back(i);
                res.push_back(iter -> second);
                break;
            }
        }
        return res;
    }
};

这个解决方案在 leetcode 提交中需要 4ms。由于我在同一个数组中循环了两次,所以我想通过将插入操作和 map.find() 组合到一个循环中来优化我的代码。因此,我可以在插入元素时检查解决方案。然后我有以下解决方案:

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) 
    {
        vector<int> res;
        map<int, int> store;

        for(int i = 0; i < nums.size(); ++i)
        {
            auto iter = store.find(target - nums[i]);
            if(iter != store.end() && (iter -> second) != i)
            {
                res.push_back(i);
                res.push_back(iter -> second);
                break;
            }
            store[nums[i]] = i;
        }

        return res;
    }
};

但是,单循环版本比两个单独的循环慢得多,需要 12 毫秒。

为了进一步研究,我做了一个输入大小为 100000001 的测试用例,此代码的解决方案将是 [0, 100000001](第一个索引和最后一个索引)。以下是我的测试代码:

#include <iostream>
#include <vector>
#include <algorithm>
#include <map>
#include <iterator>
#include <cstdio>
#include <ctime>

using namespace std;

vector<int> twoSum(vector<int>& nums, int target) 
{
    vector<int> res;
    map<int, int> store;

    for(int i = 0; i < nums.size(); ++i)
    {
        auto iter = store.find(target - nums[i]);
        if(iter != store.end() && (iter -> second) != i)
        {
            res.push_back(i);
            res.push_back(iter -> second);
            break;
        }
        store[nums[i]] = i;
    }

    return res;
}


vector<int> twoSum2(vector<int>& nums, int target) 
{
    vector<int> res;
    map<int, int> store;

    for(int i = 0; i < nums.size(); ++i)
    {
        store[nums[i]] = i;
    }

    for(int i = 0; i < nums.size(); ++i)
    {
        auto iter = store.find(target - nums[i]);
        if(iter != store.end() && (iter -> second) != i)
        {
            res.push_back(i);
            res.push_back(iter -> second);
            break;
        }
    }
    return res;
}

int main()
{

    std::vector<int> test1;
    test1.push_back(4);
    for (int i = 0; i < 100000000; ++i)
    {
        test1.push_back(3);
    }
    test1.push_back(6);

    std::clock_t start;
    double duration;

    start = std::clock();
    auto res1 = twoSum(test1, 10);
    duration = ( std::clock() - start ) / (double) CLOCKS_PER_SEC;
    std::cout<<"single loop: "<< duration <<'\n';   
    cout << "result: " << res1[1] << ", " << res1[0] << endl;

    start = std::clock();
    res1 = twoSum2(test1, 10);
    duration = ( std::clock() - start ) / (double) CLOCKS_PER_SEC;
    std::cout<<"double loops: "<< duration <<'\n';   
    cout << "result: " << res1[0] << ", " << res1[1] << endl;

}

我仍然得到类似的结果,单循环版本(7.9s)比双循环版本(3.0s)慢: results

我真的不明白为什么单循环组合版本比双循环分离版本慢?我认为单循环版本应该减少一些冗余循环。是不是因为STL map的实现,插入和map.find()操作分别在两个循环中进行,而不是插入和map.find()在一个循环中交替进行?

顺便说一句,我正在使用 MAC 操作系统并使用 Apple LLVM 版本 10.0.0 (clang-1000.10.44.2)。

【问题讨论】:

  • 嗯...您的两个版本不等效,是吗?在单循环版本中,插入store[nums[i]] = i; 不会在每一轮中都发生,因为你提前从循环中中断了?

标签: c++ c++11 data-structures


【解决方案1】:

让我们看看在这两种情况下实际发生了什么。

在两个循环的场景中,您在映射中执行 N 次插入,但随后只执行一次查找,因为当映射完全输入时,您会在第一次迭代时获得预期结果。

在单循环场景中,必须等待最后一次插入才能找到结果。所以你做了 N-1 次插入和 N-1 次查找。

在最坏情况测试中花费两倍的时间也就不足为奇了......

对于随机用例,两个循环场景将导致恰好 N 次插入,并统计 N​​/2 次查找。最佳情况 N 插入 1 个查找,最坏情况 N 插入 N-1 个查找。

在单循环中,只要地图不为空,您就会开始查找。最好的情况是 1 次插入和 1 次查找(远远好于两个循环),最坏的情况是 N-1 次插入和 N-1 次查找。我知道在概率上很容易被误导,但我希望统计 3N/4 插入和 N/2 发现。所以比两个循环场景稍微好一点。

TL/DR:两个循环场景的结果比单循环场景更好,因为您的测试用例对于两个循环来说是最好的,而对于单循环来说是最差的。

【讨论】:

    猜你喜欢
    • 2015-10-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-19
    • 2011-04-29
    • 2019-07-30
    • 2014-11-25
    • 2013-07-20
    相关资源
    最近更新 更多