CC++ & Algorithm

前向迭代器、双向迭代器与随机访问迭代器

较难2
语言版本:通用
概述:用“翻书”、“遥控车”和“电梯”三个比喻生动介绍前向、双向和随机访问迭代器,包括各自支持的操作和典型容器。

迭代器三兄弟:前向、双向和随机访问

什么是迭代器?为什么要有等级?

迭代器就像你在程序中“走路”的工具。你有一个容器(比如一个装满零食的篮子、一堆成绩单、或一串游戏角色),你想一个一个地访问里面的元素,或者修改它们。迭代器就是帮你做这件事的“导航仪”。

但是,不同容器的“内部结构”不一样:有些像一本只能往前翻的书(单向链表),有些像可以前进后退的遥控车(双向链表),有些像电梯可以直接去任意楼层(数组)。所以迭代器也分成了不同等级,每种等级能做的事情不同。等级越高,功能越强大,但并不是所有容器都能提供最高等级的迭代器。就好像你不能要求一辆遥控车直接“瞬移”到20米外一样——它只能一步一步走。

下面我们用三个生活例子来认识这三位“兄弟”:

1. 前向迭代器:只能向前翻的书

想象你有一本只有单面页码的笔记本,你从第一页开始看,只能往后翻(++),不能往前翻。你可以随时停下来读这一页的内容(解引用),也可以在这一页上写笔记(写入)。你可以反复翻看同一页很多次(多遍扫描),甚至可以用书签标记某一页,回头再来看(复制迭代器)。但如果你翻过一页后想回到前一页,对不起,你只能从头重新翻一遍。

前向迭代器就是这样的工具:它只能单向移动(++),但支持读、写和多遍扫描。

支持的操作

  • *it:读取当前元素
  • *it = 新值:修改当前元素(如果允许修改)
  • ++itit++:移到下一个元素
  • it1 == it2it1 != it2:比较两个迭代器是否指向同一位置
  • it1 = it2:赋值,让it1指向it2的位置
  • 可以复制迭代器(保存副本),各自独立前进

典型容器forward_list(单向链表)、unordered_setunordered_map(无序集合/映射)

生活场景:你在排队做核酸,队伍很长,你只能往前移动,不能后退。你可以记住当前位置(比如记下前面人的衣服颜色),然后继续走,回头再用这个记忆找到那个位置。

新手常见错误

  • 对前向迭代器使用--(会编译错误)
  • 以为只能遍历一次(实际上可以保存副本多次遍历,但不能后退)

2. 双向迭代器:可以前进后退的遥控车

遥控车可以向前开(++),也可以倒车(--),但你不能直接“传送”到很远的地方,必须一步一步地移动。你可以随时停在某个位置,查看或修改周围的东西(比如检查车底下有没有石头)。

双向迭代器支持所有前向操作,再加上--操作。

支持的操作

  • 所有前向迭代器的操作
  • --itit--:移到上一个元素

典型容器list(双向链表)、setmapmultisetmultimap

生活场景:你玩一个关卡游戏,可以按“前进”键走到下一关,按“后退”键回到上一关,但不能直接跳到第10关。

3. 随机访问迭代器:直达的电梯

电梯可以直接按任意楼层,不需要一层层经过。你可以比较不同楼层(比如12楼在15楼前面),计算两楼层之间隔了几层。这就是随机访问迭代器:支持“跳转”和“算术运算”。

支持的操作

  • 所有双向迭代器的操作
  • it + nit - n:向前或向后跳n个元素(O(1)时间)
  • it[n]:等价于 *(it + n)
  • it1 < it2it1 > it2it1 <= it2it1 >= it2:比较位置前后
  • it2 - it1:返回两个迭代器之间的距离(整数值)
  • it += nit -= n:一次跳多个位置

典型容器vectordequearraystring

生活场景:你翻开一本新华字典,可以直接翻到第100页,不用一页一页翻。你还可以比较“猫”字在第多少页,“狗”字在第多少页,算出它们相隔几页。

为什么需要这么多等级?—— 容器的“物理限制”

  • 链表forward_listlist)的节点在内存里是分散的,每个节点只记录下一个(或上一个)节点的地址。所以你想访问第5个节点,只能从头开始一个一个走,没办法直接跳到5。这就是为什么链表只提供低等级的迭代器。
  • 数组vectorarray)的所有元素在内存里是连续排列的,就像一排连续的门牌号。你知道第一个门牌号,想知道第10个,直接加9就能得到地址。所以它可以提供随机访问迭代器。

这种分级设计让C++的算法很聪明:一个算法如果只需要前向迭代器,它就能用在所有容器上(比如std::find)。如果它需要随机访问迭代器,就只能用在vector等少数容器上(比如std::sort)。这样算法写一次,能用的容器更多,代码更通用。

算法对迭代器等级的要求(一张表)

算法所需最低迭代器等级时间复杂度举例生活场景
find输入迭代器O(n)在一堆作业本里找小明的那本,只能一本一本地翻
for_each输入迭代器O(n)给每个同学发一颗糖,一个一个发
copy输入/输出迭代器O(n)把成绩单抄一份到另一个本子上
reverse双向迭代器O(n)把一摞书从第一本到最后一本翻转顺序,需要能后退
sort随机访问迭代器O(n log n)给一叠扑克牌排序,需要能直接交换两张牌的位置,快速找到中间牌
binary_search随机访问迭代器O(log n)在电话本里找“张三”,直接翻到中间比较

C++完整代码示例(带详细中文注释)

#include <iostream>
#include <vector>       // 向量,提供随机访问迭代器
#include <list>         // 双向链表,提供双向迭代器
#include <forward_list> // 单向链表,提供前向迭代器
#include <algorithm>    // 算法库,如 find, sort, reverse
using namespace std;

int main() {
    // ---------- 1. 前向迭代器示例:forward_list ----------
    forward_list<int> flist = {5, 2, 8, 1};  // 单向链表
    cout << "forward_list (只能向前): ";
    auto it_f = flist.begin();  // 获取起始迭代器
    auto saved = it_f;          // 保存副本(可以多次遍历)
    while (it_f != flist.end()) {  // 第一次遍历
        cout << *it_f << " ";      // 读取元素
        ++it_f;                    // 前进一步
    }
    cout << endl;

    // 用保存的副本再次遍历
    cout << "第二次遍历 saved: ";
    while (saved != flist.end()) {
        cout << *saved << " ";
        ++saved;
    }
    cout << endl;

    // ---------- 2. 双向迭代器示例:list ----------
    list<int> lst = {9, 4, 7, 2};  // 双向链表
    auto it_l = lst.begin();      // 起始迭代器
    // 向前走两步
    ++it_l;   // 现在指向 4
    ++it_l;   // 现在指向 7
    cout << "list 当前元素: " << *it_l << endl;   // 输出 7
    // 后退一步
    --it_l;   // 现在指向 4
    cout << "后退后元素: " << *it_l << endl;     // 输出 4

    // 反向遍历(使用双向迭代器的递减)
    cout << "反向遍历list: ";
    auto rit = lst.end();   // 注意:end() 指向最后一个元素之后
    while (rit != lst.begin()) {
        --rit;              // 先退一步,再读取
        cout << *rit << " ";
    }
    cout << endl;

    // ---------- 3. 随机访问迭代器示例:vector ----------
    vector<int> vec = {1, 2, 3, 4, 5, 6};  // 动态数组
    auto it_v = vec.begin();               // 起始迭代器
    // 直接跳转
    cout << "vector 第3个元素 (it+2): " << *(it_v + 2) << endl;  // 输出 3
    cout << "vector 第5个元素 via []: " << it_v[4] << endl;      // 输出 5
    // 计算两个迭代器之间的距离
    cout << "两个迭代器距离 (end - begin): " << (vec.end() - vec.begin()) << endl;  // 输出 6

    // sort 需要随机访问迭代器,vector 满足
    sort(vec.begin(), vec.end());  // 默认从小到大排序
    cout << "排序后 vector: ";
    for (int x : vec) cout << x << " ";
    cout << endl;

    // ---------- 4. 算法对不同迭代器等级的要求 ----------
    // find 只需要输入迭代器,vector 的随机访问迭代器也可以使用
    auto found = find(vec.begin(), vec.end(), 4);
    if (found != vec.end()) {
        cout << "找到4在位置: " << (found - vec.begin()) << endl;
    }

    // reverse 需要双向迭代器,list 满足
    reverse(lst.begin(), lst.end());  // 翻转 list
    cout << "翻转后 list: ";
    for (int x : lst) cout << x << " ";
    cout << endl;

    // 下面这行会编译错误,因为 forward_list 不是双向:
    // reverse(flist.begin(), flist.end());  // 错误!

    return 0;
}

代码解释关键点

  • forward_list 的迭代器只能 ++,不能 --。你仍然可以保存副本多次遍历,但无法后退。
  • list 的迭代器支持 ++--,但 list 没有 sort 成员函数?不,list 有自己的 sort() 成员(lst.sort()),但标准库的 std::sort 不能用,因为需要随机访问。
  • vector 的迭代器支持所有运算,甚至可以用 < 比较大小(如 it1 < it2),但上面没演示。

Python 中的类似概念(虽然等级不严格)

Python 的列表(list)天生就是随机访问的,可以下标访问、切片、反向遍历。但 Python 没有像 C++ 那样把迭代器分成明确的等级。不过我们可以用不同类型来感受差异:

from collections import deque

# 1. 前向迭代器模拟(用生成器只能向前)
def forward_iterator(iterable):
    """只允许前向移动的迭代器(通过yield from)"""
    it = iter(iterable)  # 获取迭代器
    for x in it:         # 只能往前
        yield x

# 使用
flist = [5, 2, 8, 1]
print("前向迭代器遍历:")
for x in forward_iterator(flist):
    print(x, end=" ")
print()

# 2. 双向迭代器模拟(deque 支持正向和反向遍历)
d = deque([9, 4, 7, 2])
print("\ndeque 正向遍历:")
for x in d:
    print(x, end=" ")
print("\ndeque 反向遍历:")
for x in reversed(d):
    print(x, end=" ")
print()

# 3. 随机访问迭代器(Python 列表天然支持)
vec = [1, 2, 3, 4, 5, 6]
print("\n向量第3个元素:", vec[2])   # 下标访问
print("向量第5个元素:", vec[4])
print("索引距离:", len(vec))       # 长度
vec.sort()                         # 排序
print("排序后:", vec)
print("倒数第2个元素:", vec[-2])   # 也支持负索引

新手常犯的错误

  1. 对前向迭代器用 --:比如对 forward_list 的迭代器执行 --it,编译器会报错“no operator--”。
  2. 对双向迭代器用 it + 5:比如对 list 的迭代器写 it + 5,会编译错误,因为不是随机访问。
  3. std::sortlist 排序:很多人想当然地说“用 sort(lst.begin(), lst.end())”,但 list 的迭代器不是随机的,所以会报错。正确做法是用 lst.sort() 成员函数。
  4. 忘记 end() 指向最后一个元素之后:反向遍历时,常常需要先 --rit 再读,否则会越界。
  5. 混淆迭代器与指针:虽然随机访问迭代器支持 [] 和算术,但不代表它就是原生指针,比如 vector<int> 的迭代器在调试模式下可能是一个类对象。

总结要点

  • 前向迭代器:只能 ++,支持多遍扫描,适用 forward_listunordered_set 等。
  • 双向迭代器:能 ++--,适用 listsetmap 等。
  • 随机访问迭代器:能跳转、比较、计算距离,适用 vectordequearraystring
  • 迭代器等级决定了容器可以搭配哪些算法:sort 需要随机访问,reverse 需要双向,find 只需要输入。
  • auto 自动推导迭代器类型,省时省力。

相关指引

  • 如果你对迭代器分类还不太熟,可以先学习输入迭代器和输出迭代器(本章的前置知识)。
  • 下一节我们会介绍反向迭代器rbegin() / rend())和插入迭代器back_inserter 等),它们让遍历和插入变得非常方便。
  • 想深入了解各容器迭代器特性的同学,可以查看C++参考手册中关于 iterator tags 的内容。

例题精讲

1单选题

在C++中,前向迭代器(Forward Iterator)只能向前移动,不能后退。以下哪个操作是前向迭代器的合法操作?

A支持 p-- 操作
B支持 p + n 操作(n为整数)
C支持 ++p 操作
D支持 p[n] 操作
2单选题

以下关于随机访问迭代器(Random Access Iterator)的描述中,哪个是错误的?

A可以用 p + 5 直接跳转到第5个元素之后
B可以用 p[3] 访问第3个元素
C可以用 p - q 计算两个迭代器之间的距离
D支持 p-- 但不支持 p + n
3判断题

双向迭代器(Bidirectional Iterator)在功能上包含了前向迭代器(Forward Iterator)的所有操作,因此任何使用前向迭代器的代码都可以替换为双向迭代器。

4填空题
以下代码使用前向迭代器(假设容器为std::forward_list)遍历元素并求和。请补全空白处的代码。

std::forward_list<int> flist = {1,2,3,4,5};
int sum = 0;
for (auto it = flist.begin(); it != flist.end(); ___) {
    sum += *it;
}
5填空题
下面代码利用随机访问迭代器实现二分查找(假设数组已排序)。请补全函数签名中迭代器类型的声明。

template <typename ___>
bool binary_search(Iterator first, Iterator last, int target) {
    auto left = first;
    auto right = last - 1;
    while (left <= right) {
        auto mid = left + (right - left) / 2;
        if (*mid == target) return true;
        else if (*mid < target) left = mid + 1;
        else right = mid - 1;
    }
    return false;
}