值传递会影响递归算法的渐近时间复杂度吗?

时间:2019-02-11 06:43:10

标签: c++

在下面的程序中,递归调用一个辅助函数,以便根据数组表示的前后遍历创建一个二叉树。运行时速度很快,并且在Leetcode上击败了所有提交的100%。

TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
    unordered_map<int,int> m;
    for(int i=0; i<inorder.size();i++){
        m[inorder[i]]=i;    
    }
    return helper(preorder, inorder, 0,preorder.size()-1,0, inorder.size()-1, m);
}

TreeNode* helper(vector<int>& preorder, vector<int>& inorder, int pStart, int pEnd, int inStart, int inEnd,unordered_map<int,int>& m){
    if(pStart>pEnd || inStart>inEnd) return NULL;

    TreeNode* root= new TreeNode(preorder[pStart]);
    int pivLoc=m[root->val];
    int numsLeft=pivLoc-inStart;
    root->left=helper(preorder, inorder, pStart+1, pStart+numsLeft,inStart, pivLoc-1,m);
    root->right=helper(preorder, inorder, pStart+numsLeft+1, pEnd,pivLoc+1, inEnd,m);
    return root;
}

但是,如果我更改了辅助函数,以使最后一个参数(unordered_map)按值传递,则会出现运行时超出错误。我试图了解原因。地图本身不会重新分配,其值也不会重新分配。由于映射是通过值传递的,这意味着每次调用函数时都会调用复制构造函数。这会增加函数运行时的恒定因子,还是会真正改变渐近复杂性?我相信复制构造函数会导致大量增加,但只会增加一个常数,因为复制是相对于输入的恒定时间操作。

1 个答案:

答案 0 :(得分:5)

是的

如果要复制的参数的大小(或元素数)是N的函数(而不是常数),那么它将对实现的渐近时间产生影响(即使例如,如果仅复制一次大小为O(N)的数组,则应在渐近分析中考虑到这一点(如果您的订单已经O(N)可能不会起作用或更高,但是您必须算上它。)

在递归实现中,显然您会遇到类似O(f(N))函数调用(对于搜索,O(log(N)),对排序,O(N))之类的东西,并且复制的成本会影响或甚至主宰你的时间。显然,将大小为M的参数传递给称为N的函数的开销为O(N * M)。如果大小随每次调用而变化,您仍可以使用标准技术来计算总和。

即使所讨论的参数的大小恒定且较小(但不能忽略),如果函数被调用O(f(N))次,那么您也必须在渐近时间分析中添加f(N)

复制的成本本身取决于很多事情,但是对于N个元素的容器(除非它具有一些引用计数/ COW优化等),我敢说复制的成本为{{1 }}。对于将其元素保存在一个(或几个)连续内存块中的容器,由于容器和内存管理的开销很大,因此复制操作的恒定因素将主要取决于单个元素的复制成本。小。对于链表样式的容器(包括O(N)std::map),除非您具有自定义的内存分配器和非常具体的策略,否则内存分配和遍历的成本将是巨大的(在很大程度上取决于总数)元素和堆压力以及您的OS /标准库实现等)

根据容器中元素的类型,除了复制成本之外,您可能还必须考虑销毁成本。

更新:看到更多代码后(虽然仍然不是一个有效的示例,但可能足够了),我可以为您提供更详细的分析。 (假设您输入的大小为std::set,)

函数N有两个主要部分:循环和对buildTree的递归调用。 “循环”部分的复杂度为helper(循环重复O(N * log(N))次,并且每次插入N时,其映射图的大小都是对数的,因此{{1 }}。

要计算调用std::map的成本,我们需要知道它被调用了多少次,其主体有多昂贵,以及每个递归调用中其输入缩减了多少。显然,O(N * log(N))函数总共被调用helper次(每个输入元素两次,在helper中被调用一次),这显然是2 * N + 1,并且其输入从不改变大小(确实如此,但是除了终止条件外,主体的任何部分都不依赖于输入大小。)

无论如何,buildTree体内有趣的操作是O(N)(通常被认为是helper,在这里有点简单,但是可以接受)在{{1} }(即new)和对O(1)的调用。如果我们复制任何std::mapO(log(N))参数(同样,假设内存分配和每个元素的复制为helper),则这些调用的成本为O(N)和{ {1}},如果我们不这样做。

因此,总时间是循环时间加到vector的呼叫时间,而呼叫时间是呼叫次数乘以每个呼叫的时间。循环时间为map,呼叫次数为O(1)

每次调用O(1)的时间就是分配一个新节点(helper)并查找映射中的值(O(N * log(N)))的时间加上调用时间的两倍再次O(N)

如果我们按值传递参数(即helperO(1)O(log(N))中的任何一个按值传递),则每次helper的调用时间为inorder,如果我们通过引用传递所有参数,那么该时间将为preorder。因此,将它们放在一起,如果我们按值传递较大的参数,则会得到:

m

如果仅通过引用传递,我们将拥有:

helper

就是这样。

(请注意,如果函数的参数不会被更改,并且仅通过引用传递以避免复制,则将其作为常量引用或{ {1}}。)