为什么更多的迭代器不是随机访问?
Why Aren't More Iterators Random Access?
我正在尝试了解有关 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::prev
和std::next
都允许您使用单个函数调用来推进非随机访问迭代器。
C++标准的设计者没有暴露一个低效的+
,而是说"不,那不好用"。
std::sort
具有 O(n)+
的随机访问迭代器需要O(n^2lgn)
或更糟的时间。 快速排序不是对迭代器进行排序的有效方法。
同时,基于std::merge
或std::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
大得多。
- 为什么 vector 的随机访问迭代器给出与指针不同的内存地址?
- 读取大文件(>2GB)(文本文件包含以太网数据)并通过不同参数随机访问数据的最佳方法是什么?
- 如何为我的容器实现随机访问迭代器?
- 对于C++随机访问迭代器(矢量迭代器),迭代器之间的差异是如何计算的?
- 对于随机访问迭代器(矢量迭代器),迭代器C++样式指针吗?
- Deque 中元素的随机访问如何提供恒定的时间复杂度?
- C++ STL 数据结构常时按索引推送/弹出/随机访问,并具有指向元素的可靠指针
- SFINAE - 检测类型 T 是指针、数组还是带有随机访问运算符的容器,以及给定的值类型
- 随机访问迭代器:我错过了什么?
- 随机访问元组向量中的元组值
- 有没有办法在C++中制作无锁"counter"随机访问迭代器?
- 当 95% 情况下的值为 0 或 1 时,对非常大的数组进行随机访问的任何优化
- 如何确保函数模板的参数是随机访问迭代器
- C++基于现有随机访问迭代器的反向迭代器
- C++ 写入随机访问文件
- 如何实现随机访问迭代器的"less than operator"?
- 随机访问迭代器和Deque
- 如何创建一个随机访问式的Rreference以访问.RC文件中定义的资源
- 在链表上实现随机访问迭代器
- 随机访问文件格式,用于分层组织的二进制文件和文本文件