在下面的程序中,递归调用一个辅助函数,以便根据数组表示的前后遍历创建一个二叉树。运行时速度很快,并且在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)按值传递,则会出现运行时超出错误。我试图了解原因。地图本身不会重新分配,其值也不会重新分配。由于映射是通过值传递的,这意味着每次调用函数时都会调用复制构造函数。这会增加函数运行时的恒定因子,还是会真正改变渐近复杂性?我相信复制构造函数会导致大量增加,但只会增加一个常数,因为复制是相对于输入的恒定时间操作。
答案 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::map
或O(log(N))
参数(同样,假设内存分配和每个元素的复制为helper
),则这些调用的成本为O(N)
和{ {1}},如果我们不这样做。
因此,总时间是循环时间加到vector
的呼叫时间,而呼叫时间是呼叫次数乘以每个呼叫的时间。循环时间为map
,呼叫次数为O(1)
。
每次调用O(1)
的时间就是分配一个新节点(helper
)并查找映射中的值(O(N * log(N))
)的时间加上调用时间的两倍再次O(N)
。
如果我们按值传递参数(即helper
,O(1)
或O(log(N))
中的任何一个按值传递),则每次helper
的调用时间为inorder
,如果我们通过引用传递所有参数,那么该时间将为preorder
。因此,将它们放在一起,如果我们按值传递较大的参数,则会得到:
m
如果仅通过引用传递,我们将拥有:
helper
就是这样。
(请注意,如果函数的参数不会被更改,并且仅通过引用传递以避免复制,则将其作为常量引用或{ {1}}。)