为什么更多的迭代器不是随机访问?

Why Aren't More Iterators Random Access?

本文关键字:随机 访问 迭代器 为什么      更新时间:2023-10-16

我正在尝试了解有关 C++ 中 STL 迭代器的更多信息。我了解不同的数据结构如何具有不同的迭代器,但我不明白为什么有些迭代器不是 RandomAccess。例如,为什么 LinkedList 迭代器不是随机访问迭代器?我知道 LinkedList 不是一个"随机访问结构",但是我们不能实现迭代器来给人一种随机访问结构的错觉吗?例如,LinkedList 有一个双向迭代器,它不定义 + 或 += 运算符,但它定义 ++ 运算符。我们不能只使用类似的东西来定义 + 和 += 运算符吗:

iterator operator+= (int steps) {
for (int i = 0; i < steps; ++i) {
this->operator++();
}
}

在查看了 RandomAccessIterator 的需求之后,我认为我们可能可以为 LinkedList 实现大部分(如果不是全部)这些功能,那么我们为什么不呢?我猜是因为某些操作基本上具有 O(N) 时间复杂度,但我不确定这是否是关键问题。如果我们使用这种方法实现 RandomAccessIterators,这会对将 LinkedList 与 STL 算法一起使用产生什么后果?我们是否突然能够使用 std::sort 函数对 LinkedList 进行排序?

我猜是因为某些操作基本上具有 O(N) 时间复杂度,但我不确定这是否是关键问题。

是的,这正是关键问题。迭代器以指针为模型。有了指针,人们就有一定的期望。其中一个期望是指针加法和减法是非常快速的操作(特别是 O(1))。标准库的设计者决定满足这些期望。因此,如果标准库迭代器无法在 O(1) 中执行加法和减法,则它不会实现这些操作,并且不被归类为随机访问迭代器。

请注意,使用递增和递减运算符(++--),性能要求略有放宽,并且有一些迭代器在O(log n)而不是O(1)中实现这些。这种折衷是必要的,因为如果您不能增加或减少迭代器,它就没有多大用处。

如果我们使用这种方法实现 RandomAccessIterators,这会对将 LinkedList 与 STL 算法一起使用产生什么后果?我们是否突然能够使用 std::sort 函数对 LinkedList 进行排序?

是的。但它将(至少)成为 O(n^2) 算法,而不是标准承诺的 O(n log n)。

迭代器类别不仅仅是可能的;它们也是关于什么是合理的。

任何前向迭代器都可以前进 X 次。但是 ForwardIterator 不包括整数的 +=。这很重要,因为它允许针对 RandomAccessIterator 要求编写的代码在提供未显式提供此接口的迭代器时显式失败。通过这样做,这样的代码可以声明自己具有特定的性能特征。

例如std::sort是 O(n log(n))。但它只能承诺这一点,因为它需要随机访问迭代器。理论上,您可以使用任何双向迭代器实现相同的std::sort算法,但对于非随机访问迭代器,您的性能将非常糟糕。如此糟糕,以至于您可能应该对代码做一些激烈的事情,而不仅仅是接受性能损失。因此,std::sort完全禁止它。

或者换句话说,如果有人告诉你为双向迭代器实现sort,你会选择一个与RandomAccessIterator非常不同的算法。

其他算法能够更加灵活。他们可能使用随机访问迭代器实现得更快,但他们仍然使用相同的通用算法(理论上)。这就是像std::advance这样的函数存在的原因;它们仅允许前向/双向迭代器具有与随机访问迭代器相同的整数偏移行为。但是你正在使用它们,并且完全知道对于非随机访问迭代器来说,这将是O(n)。对于某些算法,这种性能差异是可以的。

std::prevstd::next都允许您使用单个函数调用来推进非随机访问迭代器。

C++标准的设计者没有暴露一个低效的+,而是说"不,那不好用"。

std::sort具有 O(n)+的随机访问迭代器需要O(n^2lgn)或更糟的时间。 快速排序不是对迭代器进行排序的有效方法。

同时,基于std::mergestd::inplace_merge的排序在前向迭代器上不会那么低效。 只需在范围上前进,并在树结构中存储指向每个 2 次幂子范围的指针。

当两个子范围的一对幂被发现并排序时,std::inplace_merge它们。 这会导致它们被排序。

像这样:

template<class It>
struct range_t {
It b = {};
It e = {};
It begin() const { return b; }
It end() const { return e; }
bool empty() const { return begin()==end(); }
};
template<class It>
struct merge_range_t:range_t<It> {
std::size_t pow2 = 0;
};
template<class It>
void merge_sort( range_t<It> to_sort ) {
std::vector< merge_range_t<It> > sorted;
auto initial = to_sort;
auto do_merge = [&]{
auto a = sorted.back(); sorted.pop_back();
auto b = sorted.back(); sorted.pop_back();
std::inplace_merge( a.begin(), a.end(), b.end() );
sorted.push_back( {{ a.begin(), b.end() }, a.pow+1} );
};
auto should_merge = [&]{
if (sorted.size() < 2) return false;
return sorted.back().pow2 == sorted[sorted.size()-2].pow2);
};
while (!to_sort.empty()) {
// elements of size 1 are always sorted:
sorted.push_back( { {to_sort.begin(), std::next(to_sort.begin())} } );
// do merges of match size as required:
while(should_merge())
do_merge();
// first element no longer needs sorting:
to_sort = {std::next(to_sort.begin()), to_sort.end()};
}
// the remaining sorted regions are not matched in size, but we still
// need to merge them:
while (sorted.size() > 1)
do_merge();
}

C++17 为简洁起见,但可以很容易地用 C++11 甚至 C++14 编写。 需要双向迭代器,除非我犯了错误,否则是 O(n lg n)。 常数系数比std::sort大得多。