C++学习(2)STL八股文
1、STL实现原理及其实现
STL提供了六⼤组件,彼此之间可以组合套⽤,这六⼤组件分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器。
STL六⼤组件的交互关系:
a. 容器通过空间配置器取得数据存储空间;
b. 算法通过迭代器存储容器中的内容;
c. 仿函数可以协助算法完成不同的ᒽ略的变化;
d. 适配器可以修饰仿函数。
1.1 六⼤组件
1.1.1 容器
各种数据结构,如vector
、list
、deque
、set
、map
等,⽤来存放数据,从实实现角度来看,STL容器是⼀种class template
。
1.1.2 算法
各种常⽤的算法,如sort
、find
、copy
、for_each
。从实现的度来看,STL算法是⼀种function tempalte
。
1.1.3 迭代器
扮演了容器与算法之间的胶合剂,共有五种类型,从实现角度来看,迭代器是⼀种将operator*
, operator->
, operator++
, operator–
等指针相关操作予以以重载的class template
。所有STL容器都附带有⾃⼰专属的迭代器,只有容器的设计者才知道如何遍历⾃⼰的元素。原⽣指针(native pointer
)也是⼀种迭代器。
- 提问:
1、迭代器和指针的区别
迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的⼀些功能,通过᯿载了指针的⼀些操作符,->、*
、++、--等。迭代器封装了指针,是⼀个“可遍历STL( StandardTemplate Library)容器内全部或部分元素”的对象, 本质是封装了原⽣指针,是指针概念的⼀种提升(lift),提供了⽐指针更⾼级的⾏为,相当于⼀种智能指针,他可以根据不同类型的数据结构来实现不同的++,--等操作。
迭代器返回的是对象引⽤⽽不是对象的值,所以cout只能输出迭代器使⽤*
取值后的值⽽不能直接输出其⾃身。
2、迭代器产⽣原因
Iterator
类的访问⽅式就是把不同集合类的访问逻辑抽象出来,使得不⽤暴露集合内部的结构⽽达到循环遍历集合的效果。
1.1.4 仿函数
⾏为类似函数,可作为算法的某种策略。从实现角度来看,仿函数是⼀种重载了operator()
的class
或者class template
1.1.5 适配器
⼀种⽤来修饰容器或者仿函数或迭代器接⼝的东西。STL提供的queue
和 stack
,虽然然看似容器,但其实只能算是⼀种容器配接器,因为它们的底部完全借助deque,所有操作都由底层的deque供应。
1.1.6 空间配置器
负责空间的配置与管理。从实现角度看,配置器是⼀个实现了动态空间配置、空间管理、空间释放的class tempalte
。 ⼀般的分配器的std:alloctor
都含有两个函数allocate
与deallocte
,这两个函数分别调⽤operator new()
与delete()
,这两个函数的底层⼜分别是malloc()
和 free()
;但是每次malloc
会带来格外开销(因为每次malloc
⼀个元素都要带有附加信息)
1.2 STL的优点
STL 具有⾼可重⽤性,⾼性能,⾼移植性,跨平台的优点。
1.2.1 ⾼可重⽤性
STL 中几乎所有的代码都采⽤了模板类和模版函数的⽅式实现,这相⽐于传统的由函数和类组成的库来说提供了更好的代码重⽤机会。
1.2.2 ⾼性能
如 map
可以⾼效地从⼗万条记录⾥⾯查找出指定的记录,因为 map
是采⽤红黑树的变体实
现的。
1.2.3 ⾼移植性
如在项⽬ A 上⽤ STL 编写的模块,可以直接移植到项⽬ B 上。
STL 的⼀个重要特性是将数据和操作分离
数据由容器类别加以管理,操作则由可定制的算法定义。迭代器在两者之间充当“粘合剂”,以使算法可以和容器交互运作。
2、pair容器
保存两个数据成员,⽤来⽣成特定类型的模板。
使⽤: pair<T1, T2>p;
数据成员是public
,分别是first
和second
,可以直接访问;其中map
的元素是pair
,pair<const key_type,mapped_type>
,可以⽤来遍历关联容器;
map<string,int>p;
auto map1 = p.cbegin();
while(map1 != p.cend())
{
cout<<map1->first<<map1->second<<endl;
++map1;
}
对map
进⾏插⼊,元素类型是pair
:
p.insert({word, 1});
p.insert(pair<string, int>(word, 1));
insert
对不包含重复关键字的容器,插⼊成功返回pair<迭代器,bool>
迭代器指向给定关键字
元素,bool
指出插⼊是否成功。
vector<pair<char, int>>result(val.begin(), val.end());
sort(result.begin(), result.end(),[](auto &a, auto &b){
return a.second > b.second;
});
2、vector容器
2.1 底层实现
Vector在堆中分配了⼀段连续的内存空间来存储元素;
三个迭代器:
- first : 指向的是vector中对象的起始字节位置
- last : 指向当前最后⼀个元素的末尾字节
- end : 指向整个vector容器所占⽤内存空间的末尾字节
template <class T>
class vector
{
public:
~vector()
{
delete first;
first = last = end = nullptr;
}
private:
T* first; //顺序表的头
T* last; //顺序表有效长度位置
T* end; //顺序表末尾
};
2.2 扩容过程
如果集合已满,在新增数据的时候,就要分配⼀块更⼤的内存,将原来的数据复制过来,释放之前的内存,在插⼊新增的元素所以对vector
的任何操作,⼀旦引起空间重新配置,指向原vector
的所有迭代器都会失效。
- 堆中分配内存,元素连续存放,内存空间只会增⻓不会减少
vector
有两个函数,⼀个是capacity()
,在不分配新内存下最多可以保存的元素个数,另⼀个size()
,返回当前已经存储数据的个数 - 对于vector来说,capacity是ᬱ⼤于等于size的
capacity
和size
相等时,vector
就会扩容,capacity
变⼤(翻倍)
2.2.1 固定扩容
机制:
每次扩容的时候在原 capacity
的基础上加上固定的容量,⽐如初始 capacity 为10
0,扩容⼀次为 capacity + 20
,再扩容仍然为 capacity + 20
;
缺点:
考虑⼀种几段的情况,vector
每次添加的元素数量刚好等于每次扩容固定增加的容量 + 1
,就会造成⼀种情况,每添加⼀次元素就需要扩容⼀次,⽽扩容的时间消耗比较高。所以固定扩容可能会⾯临多次扩容的情况,时间复度较⾼;
优点:
固定扩容⽅式空间利⽤率⽐较⾼。
2.2.2 加倍扩容
机制:
每次扩容的时候原 capacity
翻倍,⽐如初始capcity = 100
, 扩容⼀次变为 200
, 再扩容变为400
;
优点:
⼀次扩容 capacity
翻倍的⽅式使得正常情况下添加元素需要扩容的次数⼤⼤减少(预留空间较多),时间复杂度较低;
缺点:
因为每次扩容空间翻倍倍,⽽很多空间没有利⽤上,空间利⽤率不如固定扩容。在实际应⽤中,⼀般采⽤空间换时间的策略。
2.2.3 resize()和reserve()
resize()
:改变当前容器内含有元素的数量(size()),⽽不是容器的容量
- 当resize(len)中len>v.capacity(),则数组中的size和capacity均设置为len;
- 当resize(len)中len<=v.capacity(),则数组中的size设置为len,⽽capacity不变;
reserve()
:改变当前容器的最⼤容量(capacity)
- 如果reserve(len)的值 > 当前的capacity(),那么会重新分配⼀块能存len个对象的空间,然后把之前的对象通过copy construtor复制过来,销毁之前的内存;
- 当reserve(len)中len<=当前的capacity(),则数组中的capacity不变,size不变,即不对容器做任何改变。
2.3 vector源码
点击查看代码
templeta<class T,class Alloc=alloc>
class vector {
public:
typedef T value_type;
typedef value_type *pointer;
typedef value_type &reference;
typedef value_type *iterator;
typedef size_t size_type;
typedef ptrdiff_t difference_type
//嵌套类型定义,也可以是关联类型定义
protected:
typedef simple_alloc <value_type, Alloc> data_alloctor
//空间配置器(分配器)
iterator start;
iterator finish;
iterator end_of_storage;
//这3个就是vector⾥的数据,所以⼀个vector就是包含3个指针12byte,下⾯有图介绍
void insert_aux(iterator position, const T &x);
//这个就是vector的⾃动扩充函数,在下⾯章节我会拿出来分析
void deallocate() {
if (start)
data_allocator::deallocate(start, end_of_storage);
}
//析构函数的部分实现函数
void fill_initialize(size_type n, const T &value) {
start = allocate_and_fill(n, value);
finish = start + n;
end_of_storage = finish;
}
//构造函数的具体实现
public:
iterator begin() { return start; };
iterator end() { return finish; };
size_type size() const { return size_type(end() - begin()); };
size_type capacity() const { return size_type(end_of_storage - begin()); }
bool empty() const { return begin() == end(); }
reference operator[](size_type n) { return *(begin() + n); };
//重载[]说明vector⽀持随机访问
vector() : start(0), end(0), end_of_storage(0) {};
vector(size_type n, const T &value)(fill_initialize(n, value););
vector(long n, const T &value)(fill_initialize(n, value););
vector(int n, const T &value)(fill_initialize(n, value););
explicit vector(size_type n) { fill_initialize(n, T()); };
//重载构造函数
~vector() {
destory(start, finish);//全局函数,析构对象
deallocate();//成员函数,释放空间
}
//接下来就是⼀些功能函数
reference front() { return *begin(); };
reference back() { return *(end() - 1); };
void push_back(const T &x) {
if (finsih != end_of_storage) {
construct(finish, x);
++finish;
} else insert_aux(end(), x);
//先扩充在添加
}
void pop_back() {
--finish;
destory(finish);
}
iterator erase(iterator position) {
if (position + 1 != end())
copy(position + 1, finish, position);
--finish;
destory(finish);
return position;
}
void resize(size_type new_size, const T &x) {
if (new_size() < size()) erase(begin() + new_size, end());
else insert(end(), new_size - size(),x);
}
void resize()(size_type new_size) { resize(new_size, T()); }
void clear() { erase(begin(), end()); }
protected:
//配置空间并填满内容
iterator allocate_and_fill(size_type n, const T &x) {
iterator result = data_allocator::allocate(n);
uninitialized_fill_n(result, n, x);//全局函数
}
}
vector
迭代器:由于vector
维护护的是⼀个线性区间,所以普通指针具备作为vector
迭代器的所有条件,就不需要重载operator+
,operator*
之类操作符。
template <class T, class Alloc = alloc>
class vector {
public:
typedef T value_type;
typedef value_type* iterator; //vector的迭代器是原⽣指针
// ...
};
vector
的数据结构:线性空间。为了降低配置空间的成本,我们必须让其容量⼤于其⼤⼩。
vector
的构造以及内存管理:当我们使⽤push_back
插⼊元素在尾端的时候,我们⾸先检查是否还有备⽤空间也就是说end
是否等于end_of_storage
,如果有直接插⼊,如果没有就扩充空间。
点击查看代码
```c++ template整个分为3个部分,配置新空间,转移元素,释放原来的元素与空间,因此⼀旦引起空间配置指向以前vector
的所有迭代器都要失效。
vector
的部分元素操作:pop_back
,erase
,clear
,insert
:
点击查看代码
void pop_back() {
--finish;
destory(finish);
}
//erase版本⼀:清除范围元素
iterator erase(iterator first, iterator last) {
interator i = copy(last, finish, first);
destory(i, finish);
finish = finish - (last - first);
return first;
}
//版本⼆:清除某个位置上的元素
iterator erase(iterator position) {
if (position + 1 != finish) {
copy(position + 1, finish, position);
--finish;
distory(finish);
return position;
}
}
void clear() { erase(begin(), end()) };
template<class T, class Alloc>
void vector<T, Alloc>::insert(iterator position, size_type n, const T&x) {
if (n != 0) {
if (size_type(end_of_strage - finish) > n) {
//备⽤空间⼤于插⼊的元素数量
T x_copy = x;
//以下计算插⼊点之后的现有元素个数
const size_type elems_after = finish - positon;
iterator old_finish = finish;
if (elems_after > n) {
//插⼊点之后的元素个数⼤于要插⼊的元素个数
uninitialiazed_copy(finish - n, finish, finish);
finish += n;//将vector的尾端标记后移
copy_backward(position, old_finish - n, old_finish);
fill(position, old_finish, x_copy);//从插⼊点之后开始插⼊新值
} else {
//插⼊点之后的元素个数⼩于要插⼊的元素个数
uninitialiazed_fill_n(finish, n - elems_after, finish);
finish += n - elems_after;
uninitialiazed_copy(position, old_finish, finish);
finish += elems_after;
fill(position, old_finish, x_copy);
}
else {
//备⽤空间⼩于要插⼊元素的个数
//⾸先决定新⻓度,原⻓度的两倍,或者⽼⻓度+新的元素个数
const size_type old_size = size();
const size_type len = old_size + max(old_size, n);
//以下配置新的空间
iterator new_start = data_allocator::allocate(len);
iterator new_finish = new_start;
_STL_TRY {
//拷⻉插⼊点之前的元素
new_finish = uninitialized_copy(start, position,new_start);
//把新增元素(初值皆为n)传⼊新空间
new_finish=uninitialized_fill_n
//拷⻉插⼊点之后的元素
new_finish=uninitialized_copy(position, finish,new_finish);
//这⼀段有利于理解上⾯的insert_aux函数
}
#ifdef _STL_USE_EXCEPTIONS
catch(){
//如果有异常发⽣
destroy(new_start,new_finish);
data_allocator::deallocate(new_start,len);
throw;
}
#endif/* _STL_USE_EXCEPTIONS*/
//析构并释放原vector
destory(begin(), end());
deallocate();
//调整迭代器指向新的vector
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
}
}
3、list(链表)
3.1 list设计
- 每个元素都是放在⼀块内存中,他的内存空间可以是不连续的,通过指针来进⾏数据的访问;
- 在哪里添加删除元素性能都很⾼,不需要移动内存,当然也不需要对每个元素都进⾏构造与析构了,所以常⽤来做随机插⼊和删除操作容器
list属于双向链表,其结点与list本身是分开设计的:
template<class T, class Alloc = alloc>
class list {
protected:
typedef listnode <T> listnode;
public:
typedef listnode link_type;
typedef listiterator<T, T &, T> iterator;
protected:
link_type node;
};
学习到了⼀个分析⽅法,拿到这样⼀个类,先看它的数据⽐如上⾯的linktype node
,然后我们再看它的前缀,linktype
,去上⾯在linktype
,找到typedef listnode linktype
;按这个⽅法继续找到上⾯的typedef listnode listnode
;我们发现list_node
是下⾯的类,我们⼀层层的找,就能看懂这些源码
template<class T>
struct _listnode {
typedef void voidpointer;
void_pointer prev;
void_pointer next;
T data;
};
list是⼀个环状的双向链表,同时它也满⾜STL对于“前闭后开”的原则,即在链表尾端可以加上空⽩节点.
list的迭代器的设计:
// 针对⼀般的迭代器类型,直接取迭代器内定义的关联类型
template<class I>
struct iterator_traits {
typedef typename I::iteratorcategory iteratorcategory;
typedef typename I::valuetype valuetype;
typedef typename I::differencetype differencetype;
typedef typename I::pointer pointer;
typedef typename I::reference reference;
};
// 针对指针类型进⾏特化,指定关联类型的值
template<class T>
struct iteratortraits<T> {
typedef randomaccessiteratortag iteratorcategory;
typedef T value_type;
typedef ptrdifft differencetype;
typedef T *pointer;
typedef T &reference;
};
// 针对指针常量类型进⾏特化,指定关联类型的值
template<class T>
struct iteratortraits<const T> {
typedef randomaccessiteratortag iteratorcategory;
typedef T valuetype; // valuetye被⽤于创建变量,为灵活起⻅,取 T ⽽⾮const T 作为 value_type
typedef ptrdifft differencetype;
typedef const T *pointer;
typedef const T &reference;
};
vector和list的区别
- vector底层实现是数组;list是双向链表
- vector是顺序内存,⽀持随机访问,list不⾏
- vector在中间节点进⾏插⼊删除会导致内存拷⻉,list不会
- vector⼀次性分配好内存,不够时才进⾏ᘉ倍扩容;list每次插⼊新节点都会进⾏内存申请
- vector随机访问性能好,插⼊删除性能差;list随机访问性能差,插⼊删除性能好
4、deque(双端数组)
⽀持快速随机访问,由于deque需要处理内部跳转,因此速度上没有vector快。
4.1 deque概述:
deque是⼀个双端开⼝的连续线性空间,其内部为分段连续的空间组成,随时可以增加⼀段新的空间并链接
注意:
由于deque
的迭代器⽐vector
要复复杂,这影响了各个运算层⾯,所以除⾮必要尽量使⽤vector
;为了提⾼效率,在对deque
进⾏排序操作的时候,我们可以先把deque
复制到vector
中再进⾏排序最后在复制回deque
。
4.2 deque中控器:
deque
是由⼀段⼀段的定量连续空间构成。⼀旦有必要在其头端或者尾端增加新的空间,便配置⼀段定量连续空间,串接在整个deque
的头端或者尾端。
优点:
避免“vector
的重新配置,复制,释放”的轮回,维护连整体连续假象,并提供随机访问的接⼝;
缺点:
其迭代器变得很复杂;
deque
采⽤⼀块map
作为主控,其中的每个元素都是指针,指向另⼀⽚连续线性空间,称之为缓存区,这个区才是⽤来储存数据的。
template<class T, class Alloc=alloc, size_t Bufsize = 0>
class deque {
public:
typedef T value_type;
typedef value_type pointer*;
typedef size_t size_type;
// ...
public:
typedef _deque_iterator<T, T &, T *, BufSiz> iterator;
protected:
typedef pointer *map_pointer;
protected:
iterator start;
iterator finish;
map_pointer map;//指向map
size_type map_size;//map内可容纳多少指针
}
//map其实是⼀个T**
deque迭代器:
template<class T, class Ref, class Ptr, size_t BufSiz>
struct _deque_iterator {
typedef _deque_iterator<T, T &, T *, BufSiz> iterator;
typedef _deque_iterator<T, cosnt T &, const T *, BufSiz> const_iterator;
static size_t buffer_size() { return _deque_buf_size(BufSiz,sizeof(T)); };
typedef randem_access_iterator_tag iterator_category;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef T **map_pointer;
typedef _deque_iterator self;
T *cur;
T *first;
T *last;
map_pointer node;
// ...
//这是⼀个决定缓存区⼤⼩的函数
inline size_t _deque_buf_size(size_t n, size_t sz) {
return n != 0 ? n :
(sz < 512 ? size_t(512 / sz) : size_t(1));
}
}
deque拥有两个数据成员:start与finish迭代器,分别由deque:begin()与deque:end()传回。
点击查看代码
//迭代器的关键⾏为,其中要注意的是⼀旦遇到缓冲区边缘,可能需要跳⼀个缓存区
void set_node(map_pointer new_node) {
node = new_node;
first = *new_node;
last = first + difference_type(buffer_size());
}
//接下来重载运算⼦是_deque_iterator<>成功运作的关键
reference operator*() const { return *cur; }
pointer operator->() const { return &(operator*()); }
difference_type operator— (const self &x) const {
return difference_type (buffer_szie()) * (node-x.node-1)+(curfirst)+(x.last-x.cur);
}
self &operator++() {
++cur;
if (cur == last) {
set_node(node + 1);
cur = first;
}
return *this;
}
self operator++(int) {
self temp = *this;
++*this;
return temp;
}
self &operator--() {
if (cur == first) {
set_node(node - 1);
cur = last;
}
--cur;
return *this;
}
self operator-(int) {
self temp = *this;
--*this;
return temp;
}
//以下实现随机存取,迭代器可以直接跳跃n个距离
self &operator+=(difference_type n) {
difference_type offest = n + (cur - first);
if (offest > 0 && offest < difference_type(buffer_size()))
cur += n;
else {
offest > 0 ? offest / fifference_type(buffer_size()) : -difference_type((-offest - 1) / buffer_size()) - 1;
set_node(node + node_offest);
cur = first + (offest - node_offest *
difference_type(buffer_size()));
}
return *this;
}
self operator+(differnece_type n) {
self tmp = *this;
return tmp += n;
}
self operator-=() { return *this += -n; }
self operator-(difference_type n) {
self temp = *this;
return *this -= n;
}
rference operator[](difference_type n) {
return *(*this + n);
}
bool operator==(const self &x) const { return cur == x.cur; }
bool operator!=(const self &x) const { return !(*this == x); }
bool operatoe<(const self &x) const {
return (node == x.node) ? (cur < x.cur) : (node - x.node);
}
deque数据结构:
deque除了维护⼀个map指针以外,还ᖌ护了start与finish迭代器分别指向第⼀缓冲区的第⼀个元素,和最后⼀个缓冲区的最后⼀个元素的下⼀个元素,同时它还必须记֘当前map的⼤⼩。具体结构和源代码看上⾯;
deque的构造与管理:
//deque⾸先⾃⾏定义了两个空间配置器
typedef simple_alloc <value_type, Alloc> data_allocator;
typedef simple_alloc <pointer, Alloc> map_allocator;
deque中有⼀个构造函数⽤于构造deque结构并赋初值:
点击查看代码
deque(int n, const value_type &value) : start(), finish(), map(0),map_size(0) {
fill_initialize(n, value);//这个函数就是⽤来构建deque结构,并设⽴初值
}
template<class T, class Alloc, size_t BufSize)
void deque<T, Alloc, BufSize>::fill_initialize(size_type n, const
value_type &value) {
creat_map_and_node(n);//安排结构
map_pointer cur;
_STL_TRY {
//为每个缓存区赋值
for (cur=start.node;cur<finish.node;++cur)
uninitalized_ fill(*cur, *cur+buffer_size(), value);
//设置最后⼀个节点有⼀点不同
uninitalized_fill(finish.first, finish.cur, value);
}
catch() {
// ...
}
}
template<class T, class Alloc, size_t Bufsize>
void deque<T, alloc, Bufsize>::creat_map_and_node(size_typenum_elements) {
//需要节点数=元素个数/每个缓存区的可容纳元素个数+1
size_type num_nodes = num_elements / Buf_size() + 1;
map_size = max(initial_map_size(), num_nodes + 2);//前后预留2个供扩充
//创建⼀个⼤⼩为map_size的map
map = map_allocator::allocate(map_size);
//创建两个指针指向map所拥有的全部节点的最中间区段
map_pointer nstart = map + (map_size() - num_nodes) / 2;
map_poniter nfinish = nstart + num_nodes - 1;
map_pointer cur;
_STL_TRY {
//为每个节点配置缓存区
for (cur=nstart;cur<nfinish;++cur)
+cur=allocate_node();
}
catch() {
// ...
}
//最后为deque内的start和finish设定内容
start.set_node(nstart);
finish.set_node(nfinish);
start.cur = start.first;
finish.cur = finish.first + num_elements % buffer_szie();
}
接下来就是插⼊操作的实现,第⼀,⾸先判断是否有扩充map的需求,若有就扩,然后就是在插⼊函数中,⾸先判断是否在结尾或者开头从⽽判断是否跳跃节点。
点击查看代码
void push_back(const value_type &t) {
if (finish.cur != finish.last - 1) {
construct(finish.cur, t);
++finish.cur;
} else
push_back_aux(t);
}
// 由于尾端只剩⼀个可⽤元素空间(finish.cur=finish.last-1),
// 所以我们必须重新配置⼀个缓存区,在设置新元素的内容,然后更改迭代器的状态
tempalate<class T, class Alloc, size_t BufSize>
void deque<T, alloc, BufSize>::push_back_aux(const value_type &t) {
value_type t_copy = t;
reserve_map_at_back();
*(finish.node + 1) = allocate_node();
_STL_TRY {
construct(finish.cur, t_copy);
finish.set_node(finish.node+1);
finish.cur=finish.first;
}
- STL_UNWIND {
deallocate_node(*(finish.node + 1));
}
}
//push_front也是⼀样的逻辑
5、stack && queue
概述:栈与队列被称之为duque的配接器,其底层是以deque为底部架构。通过deque执⾏具体操作
点击查看代码
template<class T, class Sequence=deque <T>>
class stack {//_STL_NULL_TMPL_ARGS展开为<>
friend bool operator==_STL_NULL_TMPL_ARGS(const stack &, const stack &);
friend bool operator<_STL_NULL_TMPL_ARGS(coonststack&, const stack&);
public:
typedef typename Sequence::value_type value_type;
typedef typename Sequence::size_type size_type;
typedef typename Sequence::reference reference;
typedef typename Sequence::const_reference const_refernece;
protected:
Sequence c;
public:
bool empty() const { return c.empty(); }
size_type size() const { return c.size(); }
reference top() {
return c.back();
}
const_rference top() const { return c.back(); }
void push(const value_type &x) { c.push_back(x); }
void pop_back() { c.pop_back(); }
};
template<class T, class Sequence>
bool operator==(const stack<T, Sequence> &x, const stack<T, Sequence> &y) {
return x.c == y.c;
}
template<class T, class Sequence>
bool operator<(const stack<T, Sequence> &x, const stack<T, Sequence> &y) {
return x.c < y.c;
}
6、heap && priority_queue
heap(堆):
建⽴在完全⼆݉树上,分为两种,⼤根堆,⼩根堆,其在STL中做priority_queue的助手,即,以任何顺序将元素推⼊容器中,然后取出时⼀定是从优先权最⾼的元素开始取,完全⼆݉树具有这样的性质,适合做priority_queue的底层。
priority_queue:
优先队列,也是配接器。其内的元素不是按照被推⼊的顺序排列,⽽是⾃动取元素的权值排列,确省情况下利⽤⼀个max-heap完成,后者是以vector表现的完全⼆݉树。
点击查看代码
template<class T, class Sequence=vector <T>, class Compare=less<typename Sequence::value_type>>
class priority_queue {
public:
typedef typename Sequence::value_type value_type;
typedef typename Sequence::size_type size_type;
typedef typename Sequence::reference reference;
typedef typename Sequence::const_reference const_refernece;
protected:
Sequence c;//底层容器
Compare comp//容器⽐较⼤⼩标准
public:
priority_queue() : c() {}
explicit priority_queue(const Compare &x) : c(), comp(x) {}
//以下⽤到的make_heap(),push_heap(),pop_heap()都是泛型算法
//任何⼀个构造函数都可以⽴即在底层产⽣⼀个heap
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last const Compare &x)
:c(first, last), comp(x) { make_heap(c.begin(), c.end(),comp); }
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last const Compare &x)
:c(first, last) { make_heap(c.begin(), c.end(), comp); }
bool empty() const { return c.empty(); }
size_type size() const { return c.size(); }
const_reference top() const { return c.front(); }
void push(const value_type &x) {
_STL_TRy {
c.push_back(X);
push_heap(c.begin(), c.end(), comp);
}
_STL_UNWIND{c.clear()};
}
void pop() {
_STL_TRY {
pop_heap(c.begin(), c.end(), comp);
c.pop_back();
}
_STL_UNWEIND{c.clear()};
}
};
// priority_queue⽆迭代器
7、map && set
- 共同点:都是C++的关联容器,只是通过它提供的接⼝对⾥⾯的元素进⾏访问,底层都是采⽤红黑树实现。
- 不同点:
set
:⽤来判断某⼀个元素是不是在⼀个组⾥⾯;
map
:映射,相当于字典,把⼀个值映射成另⼀个值,可以创建字典; - 优点:查找某⼀个数的时间为
O(logn)
;遍历时采⽤iterator
,效果不错; - 缺点:每次插⼊值的时候,都需要调整红黑树树,效率有⼀定影响;
- 提问:
1、为什么要成倍的扩容而不是⼀次增加⼀个固定⼤⼩的容量?
采⽤成倍⽅式扩容,可以保证常数的时间复杂度,⽽增加指定⼤⼩的容量只能达到O(n)的时间复杂度。
2、为什么是以两倍的⽅式扩容而不是三倍四倍,或者其他⽅式?
可能产⽣的堆空间浪费,所以增⻓倍数不能太⼤,⼀般是1.5或2;GCC是2;VS是1.5,k=2 每次扩展的新尺寸必然刚好⼤于之前分配的总和,之前分配的内存空间不可能被使⽤,这样对于缓存并不友好,采⽤1.5倍的增⻓⽅式可以更好的实现对内存的重复利⽤。
C++并没有规定扩容因⼦K,是由标准库的实现者决定的;
8、map && unordered_map
- 底层实现:
map
:底层是基于红黑树实现的,因此map内部元素排列是有序的。
unordered_map
:底层则是基于哈希表实现的,因此其元素的排列顺序是⽆序的。 - 优点:
map
:有序性,这是map
结构最⼤的优点,其元素的有序性在很多应⽤中都会简化很多的操作。map
的查找、删除、增加等⼀系列操作时间复杂度决定,都为O(logn)
。
unordered_map
:查找、删除、添加的速度快,时间复杂度为常数级O(1)
。 - 缺点:
map
:查找、删除、增加等操作平均时间复杂度较慢,与n相关。
unordered_map
:因为unordered_map
内部基于哈希表,以(key,value)
对的形式存储,因此空间占⽤率⾼。unordered_map
的查找、删除、添加的时间复杂度不稳定,平均为O(1)
,取决于哈希函数。极端情况下可能为O(n)
。 - 提问:
1、为什么insert
之后,以前保存的iterator
不会失效?
因为map
和set
存储的是结点,不需要内存拷⻉和内存移动。但是像vector
在插⼊数据时如果内存不够会重新开⼀块内存。map
和set
的iterator
指向的是节点的指针,vector
指向的是内存的某个位置;
2、为何map
和set
的插⼊删除效率⽐其他序列容器⾼?
因为map
和set
底部使⽤红黑树实现,插⼊和删除的时间复杂度是O(logn)
,⽽向vector
这样的序列容器插⼊和删除的时间复杂度是O(N)
;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!