今天写LeetCode的时候发现自己虽然思路与官方题解一致,但是时间复杂度和空间复杂度却有些差距,于是研究了一下C++的类构造函数的几种写法之间的效率差距。
- LeetCode原题链接:二叉搜索树迭代器
写法比较
- 我的写法:
- 时间复杂度平均为38ms
- 空间复杂度平均为24.7M
class BSTIterator {
private:
vector<int> temp;
int i;
public:
BSTIterator(TreeNode* root): i(0) {
function<void(TreeNode *)> dfs = [&](TreeNode *node) {
if (!node) { return; }
dfs(node->left);
temp.push_back(node->val);
dfs(node->right);
};
dfs(root);
}
int next() {
return temp[i++];
}
bool hasNext() {
return i < temp.size();
}
};
- 官方写法:
- 时间复杂度平均为33ms
- 空间复杂度平均为23.6M
class BSTIterator {
private:
void inorder(TreeNode* root, vector<int>& res) {
if (!root) {
return;
}
inorder(root->left, res);
res.push_back(root->val);
inorder(root->right, res);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
inorder(root, res);
return res;
}
vector<int> arr;
int idx;
public:
BSTIterator(TreeNode* root): idx(0), arr(inorderTraversal(root)) {}
int next() {
return arr[idx++];
}
bool hasNext() {
return (idx < arr.size());
}
};
区别
- 我的写法:类构造函数中使用lambda表达式初始化
vector<int>属性 - 官方写法:
- 调用函数生成临时变量
- 在初始化列表中使用临时属性初始化
vector<int>属性
是什么带来了效率差距
一、lambda表达式?
- 修改代码,将lambda表达式变成私有函数
- 时间复杂度平均为31ms(似乎比官方题解快一点点?原因我们稍后解释)
- 空间复杂度平均为23.6M
class BSTIterator {
private:
void dfs(TreeNode *node) {
if (!node) { return; }
dfs(node->left);
temp.push_back(node->val);
dfs(node->right);
};
vector<int> temp;
int i;
public:
BSTIterator(TreeNode* root): i(0) { dfs(root); }
int next() {
return temp[i++];
}
bool hasNext() {
return i < temp.size();
}
};
分析:效率明显提升,那么为什么使用lambda表达式不如直接调用函数效率来的快呢?
- 在C++中,lambda表达式会被解释成某个匿名类的一个实例,关于实现原理详见这篇博客,也因此lambda表达式中无法使用
this或者本类中未传入的其他属性。- 这里可以解释我第一种写法中
[&]的意图
BSTIterator(TreeNode* root): i(0) { function<void(TreeNode *)> dfs = [&](TreeNode *node) { // 定义时使用[&] if (!node) { return; } dfs(node->left); temp.push_back(node->val); dfs(node->right); }; dfs(root); }- 有动手实践的同学可能会发现
[&]换成[]或[=]时,程序甚至会发生崩溃。- lambda表达式中使用了两个外部变量:
dfs()和temp。 - 当使用
[]时,编译器会告知你找不到dfs的定义。 - 当使用值拷贝
[=]时,由于栈空间有限,我们会收到stack-overflow的崩溃信息(不仅仅是因为temp的拷贝,使用[&, dfs]也依旧会stack-overflow。
- lambda表达式中使用了两个外部变量:
- 经过上面的分析,lambda中递归时
[&]确实优于[=]和[],但我们应该在lambda中使用递归吗?- 调用lambda表达式时,我们会通过匿名对象来使用函数。而恰巧递归中函数的调用十分频繁,在这种场景下,我们选择直接函数调用必定是比lambda表达式来得快的。
- lambda表达式生成的中间对象隐式捕获的一些值也占用了一部分内存空间。
- 这里可以解释我第一种写法中
对lambda的一些看法
- lambda表达式应该使用于一些调用不怎么频繁的场景下,像递归这种就不太适合。
- 隐式捕获外部参数过多的情况下,lambda生成的中间对象太笨重了。如果代码中一不小心对其进行值拷贝操作的话,内存可能会暴涨。
二、初始化列表?
- 对于官方题解,其实调用了两次
vector<int>属性的构造函数:- 第一次生成函数中的临时变量,
vector<int> inorderTraversal(TreeNode* root) { vector<int> res; // 为 res 调用默认构造函数 inorder(root, res); return res; }- 第二次为构造函数调用初始化列表
// 这里为 arr 调用构造函数 BSTIterator(TreeNode* root): idx(0), arr(inorderTraversal(root)) {} - 对于我的写法,调用了一次
vector<int>属性的构造函数:BSTIterator(TreeNode* root): i(0) { // 构造列表结束,为其他属性调用默认构造函数 dfs(root); }-
这是我认为优化后的写法比官方题解快一点的原因所在。
-