【发布时间】:2021-02-07 09:47:04
【问题描述】:
我正在学习 C++,我正在玩链表。
考虑以下极其简单的实现:
static std::size_t n_constructors {0};
template <typename T>
class list {
public:
list(T n) : item{n} { n_constructors++; }
list(T n, list* l) : item{n}, next {l} { n_constructors++; }
list(const list&);
~list() { delete next; }
void insert(T n);
void print() const;
list reverse() const;
list reverse_iter() const;
private:
T item;
list* next {nullptr};
};
template <typename T>
list<T>::list(const list& src)
:
item {src.item}
{
n_constructors++;
if (src.next) {
next = new list {*src.next};
}
}
template <typename T>
void list<T>::insert(T n) {
if (next == nullptr)
next = new list {n};
else
next->insert(n);
}
template <typename T>
void list<T>::print() const {
std::cout << item << " ";
if (next)
next->print();
else
std::cout << std::endl;
}
template <typename T>
list<T> list<T>::reverse() const {
if (next) {
auto s = next->reverse();
s.insert(item);
return s;
} else {
return list {item};
}
}
template <typename T>
list<T> list<T>::reverse_iter() const {
auto sptr = new list<T> {item};
auto prev = next;
auto link = next;
while (link) {
auto tmp = new list<T>(link->item, sptr);
link = link->next;
prev = link;
sptr = tmp;
}
return *sptr;
}
如您所见,我编写了两个反向函数:一个迭代函数和一个递归函数。 为了测试它们,我尝试了这个:
int main() {
list<int> s{1};
for (int i = 2; i < 10000; ++i)
s.insert(i);
std::cout << "initial constructor\n";
std::cout << "called " << n_constructors << " constructors.\n";
auto t0 = std::chrono::high_resolution_clock::now();
auto s2 = s.reverse_iter();
auto t = std::chrono::high_resolution_clock::now();
std::cout << "iterative reverse\n";
std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(t - t0).count() <<" ms\n";
std::cout << "called " << n_constructors << " constructors.\n";
t0 = std::chrono::high_resolution_clock::now();
auto s3 = s.reverse();
t = std::chrono::high_resolution_clock::now();
std::cout << "recursive reverse\n";
std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(t - t0).count() <<" ms\n";
std::cout << "called " << n_constructors << " constructors.\n";
}
这是我的基准测试的典型结果(使用 g++ 9.3.0 编译,使用 -O2 打开优化器):
initial constructor
called 9999 constructors.
iterative reverse
0 ms
called 29997 constructors.
recursive reverse
1692 ms
called 50034995 constructors.
性能上的差异是惊人的。 我猜递归版本的问题是分配的数量要大得多,所以我实现了一个移动构造函数:
template <typename T>
list<T>::list(list&& src)
:
item {src.item},
next {src.next}
{
n_constructors++;
src.next = nullptr;
src.item = 0;
}
这是结果:
initial constructor
called 9999 constructors.
iterative reverse
0 ms
called 29997 constructors.
recursive reverse
90 ms
called 49994 constructors.
很好,现在至少递归函数执行的分配次数与迭代函数一样多。 然而,性能差异仍然很大。 我用 100 000 个元素再试一次:
initial constructor
called 99999 constructors.
iterative reverse
7 ms
called 299997 constructors.
recursive reverse
9458 ms
called 499994 constructors.
在我看来,递归逆向比迭代逆向更具可读性和优雅。
为什么递归版本这么慢? 有什么办法可以让它更快吗?
编辑
按照 Will Ness 在 cmets 中的建议,我添加了两个算法的经验渐近增长顺序的图。
【问题讨论】:
-
你好像在问similar question。
-
reverse_iter是线性的,reverse是二次的(因为它调用insert本身是线性的)。你花时间遍历链接,而不是调用构造函数。 -
@JaMiT 如果没有办法让递归比循环更快,我很高兴知道为什么递归要慢得多(3 个数量级!)尽管分配与迭代一样多版本。
-
这与递归本身无关。这是关于每次都必须遍历半构建列表以在末尾插入节点。
reverse的工作方式是,它遍历列表到最后,并复制最后一个节点。然后它将倒数第二个节点的副本插入到这个单元素列表中。然后它将前一个音符的副本插入到这个二元素列表中,遍历它到最后。等等。对于每个节点,它最终都会从头到尾遍历部分构建的反向列表。相比之下,reverse_iter一次性完成所有工作。 -
@IgorTandetnik 说得通,谢谢。我想我可以通过保持指向列表最后一个元素的指针再次使递归算法线性化,听起来对吗?
标签: c++ recursion linked-list