一. deque
双端队列,支持快速随机访问,在头尾位置插入和删除很快
像上图,一般介绍deque是右上角这样的,两端都可以push和pop,并且可以像数组一样支持随机访问,一段连续的内存
其实他的实现并不是使用一整段连续内存来实现的,毕竟这样做,效率不高,而且扩展性不强
实际GUN的是使用多个大小相同的内存块,而不是一个连续的内存来实现的,并且使用一个类似索引的map来管理他们,并且在已有的内存块的用尽之后,如果继续向前向后添加元素时,是以一个内存块为单位进行申请的,然后索引添加到map中相应的位置 (上图中的map就是索引,map_size就是索引的大小)
一个deque的成员除了map,map_size外,还有两个迭代器,iterator start,iterator finish; start是指向第一个内存块首元素, finish是指向最后一个内存块尾元素的下一个元素
deque并且之所以可以像连续内存一样使用他,主要功劳是deque的iterator进行了大量的操作符重载,也包括随机访问的operator[]
1. iterator
继续看上图中的iterator部分,一个deque的iterator是由 cur, frist, last三个元素指针,一个指向索引的node指针组成
前面说了,deque是使用索引加内存块实现的,它用来标识iterator位置的方式是 元素位于索引中那个内存块中的那个元素
node就是当前内存块的在map的索引
cur是元素在当前内存块的位置
first是当前内存块首元素的地址
last是当前内存块尾元素的下一个元素的地址
通过了解的这些信息,我们来看下如何通过iterator插入一个元素,下列是GUN2.9中,insert的主干
iterator insert(iterator position, const value_type& x)
{
if(position.cur == start.cur) { //如果安插点是deque的最前端
push_front(x);
return start;
} else if (position.cur == finish.cur) { //如果是deque的尾端
push_back(x);
iterator tmp = finish; //finish是尾端的下一个元素,
--tmp; //所以,新插入的元素在finish前面
return tmp;
} else { //如果在中间,在进行操作
return insert_aux(positon, x);
}
}
如果插入点是在deque的中间部分,需要考虑插入的点是距离首端近,还是距离尾端近,这样用来判断是移动待插入元素的前段的元素,还是尾端的元素,来给新插入的元素空出位置
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) { //如果安插点之前的元素个数少
push_front(front()); //把首元素向前拷贝一份
...
copy(fornt2, pos1, front1); //把安插位置到首端的元素向前移动一个
} else { //安插点之后的元素个数较少
push_back(back());
...
copy_backward(pos, back2, back1);
}
*pos = x_copy; //在安插点上放置要安插的元素
return pos;
}
如何通过iterator模拟一段连续空间,最主要的通过 -, i++, i--,i+=4; i-=8; i[10]等操作来体现出来的,下面我们来说一下deque关于操作符的重载
//首先使用node相减 乘以buffer的大小,获得中间完整buffer有多少
//然后在把cur前边的和last的后边的剪掉,得到的就是距离
difference_type operator-(const self&x) const
{
return difference_type(buffer_size())*(node-x.node-1) +
(cur-first) + (x.last - x.cur);
}
self& operator++() {
++cur; //切换下一元素
if (cur == last) { //如果++之后,超过了尾部元素
set_node(node+1); //那就后移一个node,然后cur指向当前的首元素
cur = first;
}
return *this;
}
self operator++(int) {
self tmp = *this;
++*this;
return tmp;
}
self& operator--() {
if(cur == frist) { //如果已经是当前buffer的首元素了,就向前移动一个node
set_node(node-1);
cur = last; //并且指到尾元素
}
--cur;
return *this;
}
self operator--(int) {
self tmp = *this;
--*this;
return tmp;
}
void set_node(map_pointer new_node) {
node = new _node;
first = *new_node;
last = first + difference_type(buffer_size());
}
self& operator+=(difference_type n) {
difference_type offset = n + (cur - first);
if(offset >= 0 && offset < difference_type(buffer_size()))
cur += n; //如果目标位置在同一buffer内
else {
difference_ype node_offset = offset > 0 ?
offset/difference_type(buffer_size())
: -difference_type((-offset-1)/buffer_size())-1;
set_node(node + node_offset); //切换到正确的buffer
cur = first + (offset-node_offset + difference_type(buffer_size()));
}
return *this;
}
self operator+(difference_type n) const {
self tmp = *this;
return tmp += n;
}
...
通过阅读源码;学习到应该尽量把单一功能的封成一个函数,并且如果能够通过已有函数实现的功能,就不要再重新写了,这也方便修改
为了达到上面的目的,就要求再规划函数的时候考虑仔细,提前梳理那些函数,是可以复用的
queue stack
这两个容器都是不同程序的封装deque完成的
template <class T, Sequeue=deque<T>>
class queue {
...
protected:
Sequence c; //底部容器
public:
bool empty() const { return c.empty();}
size_type size() { return c.size() };
reference front() {return c.front();}
const_reference front() const { return c.front();}
reference back() { return c.back();}
const_reference back() { return c.back();}
void push(const value_type &x) { c.push_back(x);}
void pop() {c.pop_front();}
}
//并且可手动指定queue的底层容器
queue<string, List<string>> c; //制定底层容器为链表
容器 rb_tree 红黑树
红黑树是平衡二元搜索树中常被使用的一种。
平衡二元搜索树的特点:排列规则有利于search和insert,并保持适度平衡--没有任何一个节点过深
template <class Key,
class Value, // --> Key+Data = Value
class KeyOfValue, //这个参数说明 怎么从value中获取key
class Compare, //这个函数对象,用来比对Key的大小,
class Allc = alloc> //分配器
class rb_tree {
protected:
typedef __rb_tree_node<Value> rb_tree_node;
...
public:
typedef rb_tree_node *link_type;
...
protected:
//RB_tree 使用这三个成员变量可以完整表示
size_type node_count; //rb_tree 的大小 (节点的数量)
link_type header;
Compare key_compare; //key的大小比较准则; 应该是一个函数类
};
创建一个红黑树需要5个参数
1. Key的类型
2. Value的类型 是key和data的集合的类型
3. KeyOfValue 是个仿函数,用来Value的类型中获取Key
4. Compare 仿函数,用来比对Key的大小
5. 分配器
下面是用例
rb_tree<int,
int, //这个
identity<int>, //这个仿函数是直接返回自己
less<int>, //比较Int的仿函数
alloc>
myTree;
//////////////////////
rb_tree有两种插入节点的方式
//不能插入重复的元素,如果插入重复的,不会报错,只是不起作用
myTree.insert_unique(3);
myTree.insert_unique(4);
//可以插入重复的元素
myTree.inser_equal(5);
myTree.inser_equal(5);
容器 set,multiset
set/multiset 以rb_tre为底层结构,因此有元素自动排序的特性,排序的依据是key;
set/multiset 元素的value和key合一;value就是key 就像上面的rb_tree的例子
我们无法使用set/muliset的iterator改变元素,
set的key不可以重复,是使用rb_tree的insert_unque()
multiset元素的key可以重复,是使用rb_tree的insert_equal()
下面是set的代码,可以看到set的迭代器是const的,所以不能改变
template <class Key,
class Compare = less<Key>,
class Alloc=alloc>
class set {
typedef Key key_type;
typedef Key value_type;
typedef Compare key_compare;
typedef Compare value_compare;
private:
typedef rb_tree<key_type, value_type,
identity<value_type>, key_compare, Alloc> rep_type;
rep_type t;
public:
typedef typename rep_type::const_iterator iterator;
};
map和multimap
map和multimap底层也是使用的rb_tree
只是在value是一个pair的类型 例: make_pair<int, string> 这种就是一个Value
然后Compare的仿函数是取的 make_pair<int, string>的key也就是int类型的值
tempalte <class Key,
class T,
class Compare = less<Key>,
class Alloc = alloc>
class map{
public:
typedef Key Key_type;
typedef T data_type;
typedef T mapped_type;
typedef pair<const Key, T> value_type;
typedef Compare key_compare;
pirvate:
typedef rb_tree<key_type, value_type,
select1st<value_type>, key_compare, Alloc> rep_type;
rep_type t;
public:
typedef typename rep_type::iterator iterator;
};
hashtable 容器
hashtable是一个存储的容器,存储的元素是散列的,没有规律的,查找非常迅速的容器
存储的方式大致为:
1. 首先把这个元素进行hash,得到一个int/long类型的哈希值
2. 然后根据当前hashtable的大小,把元素的哈希值进行modulus计算,得到一个在当前hashtable范围内的值,并且把这个元素放到哪里
3. 如果两个元素之后modulus的值相同,就把元素往后排,使用链表来存储modulus相同的元素
4. 如果modulus相同的值过多,导致链表的长度超过hashtable,此时再查找一个元素的速度就会变慢,所以当链表的长度超过hashtable时,hashtable的长度会扩充(当前长度的两倍之后的第一个质数)
5. hashtable扩充之后,重新计算每个元素的modulus的值,然后放入hashtable
6. 每次hashtable扩充时的大小不是计算的,而是提起人为规定的,{53, 97, 193, 389, ...}
template<class Vaule, //Value = data+key 和rb_tree的Value一样
calss Key, //主键的类型
class HashFunc, //获取主键hash值的仿函数
class ExtractKey, //萃取的仿函数,从Value中获取Key
class EquelKey, //判断Key是否相等的仿函数
class Alloc=alloc>
class hashtable {
public:
typedef HashFunc hasher;
typedef EquelKey ky_equal;
typedef size_t size_type;
private:
hasher hash;
key_equal equals;
ExtractKey get_key;
typedef __hashtable_node<Value> node; //hashtable上的node
vector<node*, Alloc> buckets; //hashtable
size_type num_elements;
public:
size_type bucket_count() const { return buckets.size(); }
...
};
使用样例
hashtable<const char*,
const char *,
hash<const char *>,
identity<const char *>,
eqstr,
alloc>
ht(50, hash<const char*>(), eqstr()); //构造函数,制定hashtable的大小,hash仿函数类的对象实例, 对比两个key相等的仿函数类对象实例
ht.insert_unique("111");
ht.insert_unique("222");
////
struct eqstr{
bool operaotr() (const char *s1, const char *s1) const
{ return strcmp(s1, s2)==0; }
};
下面是计算modulus的代码,最后是使用计算key的哈希值的int的hash值除以hashtable大小的余数来决定的