c++标准容器库与泛型编程
Posted mr.chenyuelin
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++标准容器库与泛型编程相关的知识,希望对你有一定的参考价值。
文章目录
STL六大组件
STL六大组件包括容器(container)、分配器(allocator)、算法(algorithm)、迭代器(iterator)、适配器(adapter)和仿函数(functor).
容器分类
STL中的容器大体分为序列容器、关联容器和无序容器.
vector:动态数组
随着元素的加入,它的内部机制会自行扩充空间以容纳新元素,并不是在原空间之后持续新空间,而是以原大小的两倍另外配置一块较大的空间,然后将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间
总结:
vector 常用来保存需要经常进行随机访问的内容,并且不需要经常对中间元素进行添加删除操作。
Deque:双端队列
list:双向链表
一般应遵循下面的原则:
1、如果你需要高效的随即存取,而不在乎插入和删除的效率,使用 vector;
2、如果你需要大量的插入和删除,而不关心随即存取,则应使用 list;
3、如果你需要随即存取,而且关心两端数据的插入和删除,则应使用deque。
set/multiset(multi是允许重复key值)
所有元素都会根据元素的键值自动被排序。set 的元素不像 map 那样可以同时拥有实值(value)和键值(key),set 元素的键值就是实值,实值就是键值,set不允许有相同的值。set 底层是通过红黑树(RB-tree)来实现的,由于红黑树是一种平衡二叉搜索树,自动排序的效果很不错
map/multimap
所有元素都会根据元素的键值自动被排序。map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。map不允许两个元素拥有相同的键值。底层也是用红黑树, 是一种平衡二叉搜索树,自动排序的效果很不错
注意:multimap不可使用【】插入,只能用mp.insert(pait<int,int>(1,1))插入,multiset也只能用insert方式插入
unordered_map/unordered_set
底层都为哈希表,无序,不重复
Hash与红黑树的区别
权衡三个因素: 查找速度, 数据量, 内存使用,可扩展性,有序性。
hash查找速度会比RB树快,属于常数级别;而RB树的查找速度是log(n)级别。并不一定常数就比log(n) 小,因为hash还有hash函数的耗时。当元素达到一定数量级时,考虑hash。但若你对内存使用特别严格, 希望程序尽可能少消耗内存,而且 hash的构造速度较慢。
1.红黑树是有序的,Hash是无序的,根据需求来选择。
2.红黑树占用的内存更小(仅需要为其存在的节点分配内存),而Hash事先应该分配足够的内存存储散列表,即使有些槽可能弃用
3.红黑树查找和删除的时间复杂度都是O(logn),Hash查找和删除的时间复杂度都是O(1)。
分配器(allocator)
负责内存的申请和释放
VC6.0的默认分配器std::allocator定义如下,可以看到VC6.0的分配器只是对::operator new和::operator delete的简单封装.
template<class _Ty>
class allocator
public:
typedef _SIZT size_type;
typedef _PDFT difference_type;
typedef _Ty _FARQ *pointer;
typedef _Ty value_type;
pointer allocate(size_type _N, const void *)
return (_Allocate((difference_type) _N,(pointer) 0));
void deallocate(void _FARQ *_P, size_type)
operator delete(_P);
;
private:
inline _Ty _FARQ *_Allocate(_PDFT _N, _Ty _FARQ *)
if (_N < 0) _N = 0;
return ((_Ty _FARQ *) operator new((_SIZT) _N * sizeof(_Ty)));
//...
;
gcc2.9中的分配器std::allocator与VC6.0的实现类似,但std::allocator并非gcc2.9的默认分配器,观察容器源码,可以看到,gcc2.9的默认分配器为std::alloc.
class alloc
protected:
enum _S_align = 8 ;
enum _S_max_bytes = 128 ;
enum _S_free_list_size = (size_t) _S_max_bytes / (size_t) _S_align ;
union _Obj
union _Obj *_M_free_list_link;
char _M_client_data[1]; // The client sees this.
;
// ...
std::alloc内部维护一个链表数组,数组中的每个链表保存某个尺寸的对象,减少了调用malloc的次数,从而减小了malloc带来的额外开销.
在gcc4.9以后,默认分配器变为std::allocator,变回了对::operator new和::operator delete的简单封装.gcc2.9中的std::alloc更名为__gnu_cxx::__pool_alloc.
STL设计模式OOP和GP
OOP(Object-Oriented Programming)和GP(Generic Programming)是STL容器设计中使用的两种设计模式.
OOP的目的是将数据和方法绑定在一起,例如对std::list容器进行排序要调用std::list::sort方法.
GP的目的是将数据和方法分离开来,例如对std::vector容器进行排序要调用std::sort方法.
这种不同是因为std::sort方法内部调用了iterator的-运算,std::list的iterator没有实现-运算符,而std::vector的iterator实现了-运算符.
模板特化
总结:泛化就是不指明类型,使用时写对应类型
特化直接指明类型,使用时就会调用对应类型
偏特化分为两个:
1.多个类型指明其中几个(个数偏特化)
2.将类型范围进行缩小或扩大(范围偏特化)
malloc分配内存的一点东西
使用malloc分配内存时,当分配大一点的空间,则内存里的额外开销(负载)就会小一点,分配小一点空间1,则内存里的额外开销(负载)就会大一点,这些负载信息都是需要的
容器
STL容器的各实现类关系如下图所示,以缩排形式表示衍生关系(主要是复合关系).
list
2.9版本
template<class T, class Alloc = alloc>
class list
protected:
typedef __list_node<T> list_node;
public:
typedef list_node *link_type;
typedef __list_iterator<T, T &, T *> iterator;
protected:
link_type node;
;
template<class T>
struct __list_node
typedef void *void_pointer;
void_pointer prev;
void_pointer next;
T data;
;
为实现前闭后开的特性,在环形链表末尾加入一个用以占位的空节点,并将迭代器list::end()指向该节点.
迭代器__list_iterator重载了指针的*,->,++,–等运算符
template<class T, class Ref, class Ptr>
struct __list_iterator
typedef __list_iterator<T, Ref, Ptr> self;
typedef bidirectional_iterator_tag iterator_category; // 关联类型1
typedef T value_type; // 关联类型2
typedef ptrdiff_t difference_type; // 关联类型3
typedef Ptr pointer; // 关联类型4
typedef Ref reference; // 关联类型5
typedef __list_node <T>* link_type;
link_type node; // 指向的链表节点
reference operator*() const return (*node).data;
pointer operator->() const return &(operator*());
//前置++
self& operator++()
node = (link_type) ((*node).next);
return *this;
//后置++
self operator++(int)
self tmp = *this;//调用拷贝构造函数
++*this;//调用前置++
return tmp;
;
++就是获取node的next指针,–获取pre指针,可用过*或-》获取值
注意:1.重载++运算符,括号里没参数是前置++,有类型int为后置++
2.前置++返回值为引用,而后置++返回值为值类型,防止类型连续后++
int i(6);
++++i; // 被解析为 ++(++i), 能通过编译
i++++; // 被解析为 (i++)++, 不能通过编译
list<int> c;
auto ite = c.begin();
++++ite; // 被解析为 ++(++ite), 能通过编译
ite++++; // 被解析为 (ite++)++, 不能通过编译
Iterator遵循原则
必须提供了iterator_category、value_type、difference_type、pointer和pointer5个关联类型(associated types),
一般需要知道itetator的3个关联类型:category,value_type,difference_type,其它两个几乎没有用到
迭代器是泛化的指针,当迭代器不是用在类上就退化为指针了
可以使用**Iterator Traits(萃取器)**区分class iterator和non-class iterators、
在实现上,iterator_traits类使用模板的偏特化,对于一般的迭代器类型,直接取迭代器内部定义的关联类型;对于指针和常量指针进行偏特化,指定关联类型的值.
// 针对一般的迭代器类型,直接取迭代器内定义的关联类型
template<class I>
struct iterator_traits
typedef typename I::iterator_category iterator_category;
typedef typename I::value_type value_type;
typedef typename I::difference_type difference_type;
typedef typename I::pointer pointer;
typedef typename I::reference reference;
;
// 针对指针类型进行特化,指定关联类型的值
template<class T>
struct iterator_traits<T *>
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef ptrdiff_t difference_type;
typedef T* pointer;
typedef T& reference;
;
// 针对指针常量类型进行特化,指定关联类型的值
template<class T>
struct iterator_traits<const T *>
typedef random_access_iterator_tag iterator_category;
typedef T value_type; // value_tye被用于创建变量,为灵活起见,取 T 而非 const T 作为 value_type
typedef ptrdiff_t difference_type;
typedef const T* pointer;
typedef const T& reference;
;
vector
template<class T, class Alloc= alloc>
class vector
public:
typedef T value_type;
typedef value_type* iterator;
typedef value_type& reference;
typedef size_t size_type;
protected:
iterator start;
iterator finish;
iterator end_of_storage;
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);
reference front() return *begin();
reference back() return *(end() - 1);
;
vector::push_back方法先判断内存空间是否满,若内存空间不满则直接插入;若内存空间满则调用insert_aux函数先扩容两倍再插入元素.
void push_back(const T &x)
if (finish != end_of_storage) // 尚有备用空间,则直接插入,并调整finish迭代器
construct(finish, x);
++finish;
else // 已无备用空间则调用 insert_aux 先扩容再插入元素
insert_aux(end(), x);
nsert_aux被设计用于在容器任意位置插入元素,在容器内存空间不足会现将原有容器扩容.
template<class T, class Alloc>
void vector<T, Alloc>::insert_ux(iterator position, const T &x)
if (finish != end_of_storage) // 尚有备用空间,则将插入点后元素后移一位并插入元素
construct(finish, *(finish - 1)); // 以vector最后一个元素值为新节点的初值
++finish;
T x_copy = x;
copy_backward(position, finish - 2, finish - 1);
*position = x_copy;
else
// 已无备用空间,则先扩容,再插入
const size_type old_size = size();
const size_type len = old_size != 0 ?: 2 * old_size:1; // 扩容后长度为原长度的两倍
iterator new_start = data_allocator::allocate(len);
iterator new_finish = new_start;
try
new_finish = uninitialized_copy(start, position, new_start); // 拷贝插入点前的元素
construct(new_finish, x); // 插入新元素并调整水位
++new_finish;
new_finish = uninitialized_copy(position, finish, new_finish); // 拷贝插入点后的元素
catch (...)
// 插入失败则回滚,释放内存并抛出错误
destroy(new_start, new_finish) :
data_allocator::deallocate(new_start, len);
throw;
// 释放原容器所占内存
destroy(begin(), end());
deallocate();
// 调整迭代器
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
;
其实:就是通过一个finish指针来判断是否到容器结尾,在容器内存空间不足会现将原有容器扩容为原来的两倍,先拷贝插入点前元素,然后调用拷贝构造函数插入新元素并调整finish指针,再拷贝插入点之后元素,最后调用析构函数释放原有容器内存
所以说每次插入其实都挺耗时的
deque
容器deque内部是分段连续的,对使用者表现为连续的.
template<class T, class Alloc =alloc, size_t BufSiz = 0>
class deque
public:
typedef T value_type;
typedef _deque_iterator<T, T &, T *, BufSiz> iterator;
protected:
typedef pointer *map_pointer; // T**
protected:
iterator start;
iterator finish;
map_pointer map; // 控制中心,数组中每个元素指向一个buffer
size_type map_size;
public:
iterator begin() return start;
iterator end() return finish;
size_type size() const return finish - start;
// ...
;
deque::map的类型为二重指针T**,称为控制中心,其中每个元素也为指针指向一个buffer.
迭代器deque::iterator的核心字段是4个指针:cur指向当前元素、first和last分别指向当前buffer的开始和末尾、node指向控制中心
deque::insert方法先判断插入元素在容器的前半部分还是后半部分,再将数据往比较短的那一半推.
iterator insert(iterator position, const value_type &x)
if (position.cur == start.cur) // 若插入位置是容器首部,则直接push_front
push_front(x);
return start;
else if (position.cur == finish.cur) // 若插入位置是容器尾部,则直接push_back
push_back(x);
iterator tmp = finish;
--tmp;
return tmp;
else
return insert_aux(position, x);
template<class T, class Alloc, size_t BufSize>
typename deque<T, Alloc, BufSize>::iterator deque<T, Alloc, BufSize>::insert_aux(iterator pos, const value_type &x)
difference_type index = pos - start; // 插入点前的元素数
value_type x_copy = x;
if (index < size() / 2) // 1. 如果插入点前的元素数较少,则将前半部分元素向前推
push_front(front()); // 1.1. 在容器首部创建元素
// ...
copy(front2, pos1, front1); // 1.2. 将前半部分元素左移
else // 2. 如果插入点后的元素数较少,则将后半部分元素向后推
push_back(back()); // 2.1. 在容器末尾创建元素
copy_backward(pos, back2, back1); // 2.2. 将后半部分元素右移
*pos = x_copy; // 3. 在插入位置上放入元素
return pos;
迭代器deque::iterator模拟空间的连续性.
记:当cur指针移动是否到达缓冲区边界(first或last),到达则用node回到控制中心的下一个缓冲区,如果是cur+=20,则回到控制中心看要移动几个缓冲区后,再看看移动几个位置
deque可理解为可以两端操作的动态数组
红黑树
平衡二叉查找树,查找,插入,删除时间为O(lgn),比平衡二叉树好维护
特点:
1.根节点和叶节点(null指针)为黑的
2.如果一个节点是红色的,则它的子节点必须是黑色的。
3.对于任意节点到叶节点的每条路径含相同黑节点
4.红黑树保证最长路径不超过最短路径(全黑)的二倍,因而近似平衡。
插入记忆:根节点必黑,新增是红色,只能黑连黑,不能红连红; 爸叔通红就变色,爸红叔黑就旋转,哪边黑往哪边转
插入节点一般为红,为黑的话成本太大
左旋:
y顶替x,将其左孩子成为x的右孩子
大体步骤:
1.先设置y的左孩子与x的关系
2.再设置y与x父亲的关系
3.最后设置x与y的关系
右旋:
x顶替y,将x的右孩子设为y的左孩子
大体步骤:
1.先设置x的右孩子与y的关系
2.再设置x与y父亲的关系
3.最后设置x与y的关系
插入具体步骤如下:
1.根节点为NULL,直接插入新节点并将其颜色置为黑色
2.根节点不为NULL,找到要插入新节点的位置
3.插入新节点
4.判断新插入节点对全树颜色的影响,更新调整颜色
4.1如果插入节点的父节点为黑,则不需要调整
4.2插入节点的父节点为红,则要调整,分为两种
4.2.1当父节点parent是爷爷grandfather的左节点时,此时爷爷必为黑,
如果叔叔uncle存在且为红,则将parent和uncle改为黑,grandfather改为红,将cur更新为grandfather,parent改为grandfather的父亲
如果叔叔uncle不存在或为黑,如果cur是parent的右孩子,则需要先左旋转再交换cur和parent指针,然后右旋转,将grandfather颜色改为红,parent改为黑
4.2.21当父节点parent是爷爷grandfather的右节点时,
如果叔叔uncle存在且为红,则将parent和uncle改为黑,grandfather改为红,将cur更新为grandfather,parent改为grandfather的父亲
如果叔叔uncle不存在或为黑,如果cur是parent的左孩子,则需要先右旋转再交换cur和parent指针,然后左旋转,将grandfather颜色改为红,parent改为黑
最后记得将root改为黑
面试:什么时候旋转?当叔叔uncle不存在或为黑
什么时候变色?叔叔uncle存在且为红和当叔叔uncle不存在或为黑旋转之后
源码:https://blog.csdn.net/tanrui519521/article/details/80980135
删除:
不平衡的原因:比如删除的黑色节点是左节点,导致以当前根节点的左右子树黑节点不一样
1.普通二叉树的删除
1.1找到删除节点
1.2删除节点的左节点和右节点都不为空的话
比如往左子树找最大值(最右侧),叫替代节点,将替代节点的值赋给删除节点,如果替代节点的左子树不为空,则将替代节点的父亲的right指向它
删除节点的左节点和右节点有一个为空的话,比如左节点不为空则将删除节点的父亲直接指向删除节点的left
2.修正,
2.1当删除节点为红色,不需要修正
2.当删除节点为黑色,以删除节点为左节点为例
2.1当兄弟brother为红色,则将parent跟新为红色,兄弟brother改为黑色,再将parent左旋
2.2当brother为黑色且左右节点均为黑色,将brother改为红色,将brother更新为parent,parent更新为brother的父亲继续循环
2.3.当brother为黑,且左红右黑,先交换brother和左节点颜色,再brother右旋变成2.4情况
2.4当brother为黑,且右节点为红色,将brother变为parent的颜色,然后将parent和右节点改为黑色,再parent左旋
视频:https://www.bilibili.com/video/BV1uZ4y1P7rr
源码:https://www.cnblogs.com/skywang12345/p/3245399.html
容器set和multiset
容器set和multiset以红黑树为底层容器,因此其中元素是有序的,排序的依据是key.set和multiset元素的value和key一致.
set和multiset提供迭代器iterator用以顺序遍历容器,但无法使用iterator改变元素值,因为set和multiset使用的是内部rb_tree的const_iterator.
set元素的key必須独一无二,因此其insert()调用的是内部rb_tree的insert_unique()方法;multiset元素的key可以重复,因此其insert()调用的是内部rb_tree的insert_equal()方法.
template<class Key,
class Compare = less<Key>,
class Alloc<以上是关于c++标准容器库与泛型编程的主要内容,如果未能解决你的问题,请参考以下文章