map 和 set
\(map\) 是映射,\(set\) 是集合,都是通过红黑树来实现的。他们的操作行为,都是转调红黑树的操作行为。
- \(map\) 中元素为 (键-值)key-value,关键字起到索引作用,值保存相关数据。\(set\) 元素为 (键)key,值即是键,
- \(map\) 允许修改 \(value\),不允许修改 \(key\),\(set\) 的迭代器是 \(const\) 的,不允许修改元素。原因: \(map\) 和 \(set\) 是通过关键字排序来实现有序的,如果要修改,需要删除原本键值,在插入新的键值,每次操作后都需要重新调节平衡,这样破坏了原本的结构,使得迭代器失效,不知道指向改变前的还是改变后的位置。
- \(map\) 支持下标操作,\(set\) 不支持。\(map\) 下标操作 \([key]\) ,是通过关键字 \(key\) 去查找,返回该关键字的值,若该关键字不存在,则会插入一个具有该关键字和 \(value\) 类型默认值到 \(map\) 中。
map 和 unordered_map
map | unordered_map |
---|---|
通过红黑树实现 | 通过 \(hash\) 表实现 |
操作复杂度 \(log\) 级别 | 操作复杂度常数级别 |
内部有序 | 内部无序 |
适用于对顺序有要求的场景 | 适用于频繁查找的场景 |
\(unordered\_map\) 基于 \(hash\) 表,需要用 \(vector\) 来解决冲突,所以查找和存储的时间大大减少,而代价是花费更多内存。
STL 的组成
\(STL\) 由六部分组成:算法、容器、迭代器、仿函数、适配器、内存分配器。
- 通过迭代器,可以实现对容器内容的读和写。
- 对于重载了 \(()\) 的类,可以实现类似函数调用过程,叫做仿函数。
- 适配器将一个类的接口适配成用户指定的形式,使原本不兼容的类可以一起工作。
- 内存分配器负责空间的配置和管理。
容器和容器适配器
容器适配器是对容器的一种再封装,容器适配器不支持迭代。
\(STL\) 自带的容器有 \(vector、list、map、set、deque\),同时还提供了一些特别的容器适配器,比如 \(stack、queue、priority\_queue\)。
区别在于 \(stack、queue\) 的底层实现用到了 \(deque\),\(priority\_queue\) 的底层实现用到了 \(vector\),而 \(vector\) 的底层实现没有用到别的容器。
vector 和 list
- \(vector\) 存在扩容机制:
在向 \(vector\) 添加元素时,如果还有剩余的空间,那么会直接添加到指定位置,如果没有剩余的空间,那么会重新开辟原本容器的两倍空间,然后将旧的数据复制到新开辟的空间,释放旧内存。
vector | list | |
---|---|---|
类型 | 动态数组 | 动态链表 |
底层实现 | 数组实现 | 双向链表实现 |
访问 | 支持随机访问,\(O(1)\) | 不支持随机访问,\(O(n)\) |
插入 | 在末尾 \(O(1)\),在中间 \(O(n)\) | 很快,\(O(1)\) |
删除 | 在末尾 \(O(1)\),在中间 \(O(n)\) | 很快,\(O(1)\) |
内存来源 | 从堆区分配空间 | 从堆区分配空间 |
内存使用 | \(vector\) 是顺序内存 | \(list\) 不是顺序内存 |
内存分配 | \(vector\) 一次性分配好,不够时扩容 | \(list\) 每次插入节点都需要进行内存申请 |
性能 | \(vector\) 访问性能好,插入删除性能差 | \(list\) 插入删除性能好,访问性能差 |
适用场景 | 经常随机访问,不在乎插入和删除效率 | 经常插入删除,不在乎访问效率 |
STL 中 resize 和 reserve
\(resize()\) 改变当前容器内含有元素的数量。例如 \(vector
\(reserve()\) 改变当前容器可存放元素的最大容量,例如操作 \(g.reserve(n)\),如果 \(n\) 值大于当前容器容量 \(g.capacity()\),则会重新分配一块能存 \(n\) 个对象的空间,然后把容器内元素复制过来并销毁之前的内存,否则不会发生变化。
迭代器和指针
迭代器就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部结构而达到遍历集合的效果。
迭代器不是指针,是类模板。迭代器只是模拟出指针的一些功能,本质是封装了原生指针,提供了比指针更高级的行为。迭代器返回的是对象的引用,而不是对象的值。