文章目录
MyTinySTL容器
容器的内容很多,我只挑选我觉得比较重要的部分讲解,很多都可以从代码中了解。
概述
分类:
- 序列容器
- 关联容器
序列式容器
vector
首先可以看到vector
定义了嵌套型别( 规则要求每个使用traits萃取器的都必须自己定义五个嵌套型别 )。
vector
的迭代器就是一个普通指针,也就可以做很多内存方面的优化了。
vector
的内存必须是一整段连续的内存空间,由三个指针指出,分别指向使用空间的头尾和可用空间的尾部。
template <class T>
class vector
{
static_assert(!std::is_same<bool, T>::value, "vector<bool> is abandoned in mystl");
public:
typedef mystl::allocator<T> allocator_type; /*! 分配器 */
typedef mystl::allocator<T> data_allocator; /*! 分配器 */
typedef typename allocator_type::value_type value_type; /*! 数据类型 */
typedef typename allocator_type::pointer pointer; /*! 数据类型指针 */
typedef typename allocator_type::const_pointer const_pointer; /*! const数据类型指针 */
typedef typename allocator_type::reference reference; /*! 数据类型引用 */
typedef typename allocator_type::const_reference const_reference; /*! const数据类型引用 */
typedef typename allocator_type::size_type size_type; /*! 数据类型大小 */
typedef typename allocator_type::difference_type difference_type; /*! 数据类型指针距离 */
typedef value_type* iterator; /*! 迭代器(这里就是指针) */
typedef const value_type* const_iterator; /*! const迭代器(这里就是const指针) */
typedef mystl::reverse_iterator<iterator> reverse_iterator; /*! 反向迭代器 */
typedef mystl::reverse_iterator<const_iterator> const_reverse_iterator; /*! 反向const迭代器 */
/*! 获取分配器 */
allocator_type get_allocator() { return data_allocator(); }
private:
iterator begin_; /*! 表示目前使用空间的头部 */
iterator end_; /*! 表示目前使用空间的尾部 */
iterator cap_; /*! 表示目前储存空间的尾部 */
。。。
}
对于vector
来说最重要的就是了解如何动态增长的。
首先我创建一个vector
对象,调用了最基础的构造函数,可以发现直接给我们分配了16内存大小(注意不是16个字节,下面说的大小的单位都是存储对象T
大小)。
vector() noexcept
{
try_init();
}
/*! 固定分配16个元素内存 若分配失败则忽略,不抛出异常 */
template <class T>
void vector<T>::try_init() noexcept
{
try
{
begin_ = data_allocator::allocate(16);
end_ = begin_;
cap_ = begin_ + 16;
}
catch (...)
{
begin_ = nullptr;
end_ = nullptr;
cap_ = nullptr;
}
}
接下来就是调用push_back
在尾部插入元素,如果还有容量就很好,如果没有多余容量就需要扩容。
template <class T>
void vector<T>::push_back(const value_type& value)
{
if (end_ != cap_) // 还有多余容量
{
data_allocator::construct(mystl::address_of(*end_), value);
++end_;
}
else // 没有多余容量
{
reallocate_insert(end_, value);
}
}
reallocate_insert
会重新分配空间并在 pos 处插入元素。
首先这边调用了get_new_cap
来得到我们要扩容到多大。(扩容机制可以看下面代码注释,简单来说就是1.5倍扩容)
然后直接分配内存,将老的内容move过去,在 pos 处构造出元素,然后释放以前的内存。
template <class T>
void vector<T>::reallocate_insert(iterator pos, const value_type& value)
{
const auto new_size = get_new_cap(1);
auto new_begin = data_allocator::allocate(new_size);
auto new_end = new_begin;
const value_type& value_copy = value;
try
{
new_end = mystl::uninitialized_move(begin_, pos, new_begin);
data_allocator::construct(mystl::address_of(*new_end), value_copy);
++new_end;
new_end = mystl::uninitialized_move(pos, end_, new_end);
}
catch (...)
{
data_allocator::deallocate(new_begin, new_size);
throw;
}
destroy_and_recover(begin_, end_, cap_ - begin_);
begin_ = new_begin;
end_ = new_end;
cap_ = new_begin + new_size;
}
/*! 获取能增加的容量 */
template <class T>
typename vector<T>::size_type vector<T>::get_new_cap(size_type add_size)
{
// 得到现有容量大小
const auto old_size = capacity();
// 如果现有容量+要增加的容量 大于 最大容量 直接异常
THROW_LENGTH_ERROR_IF(old_size > max_size() - add_size, "vector<T>'s size too big");
// 如果现有容量的1.5倍 大于 最大容量
if (old_size > max_size() - old_size / 2)
{
// 如果 现有容量 + 要增加的容量 + 16 大于 最大容量
// 则返回 现有容量 + 要增加的容量
// 否则返回 现有容量 + 要增加的容量 + 16
return old_size + add_size > max_size() - 16
? old_size + add_size : old_size + add_size + 16;
}
// 如果现有容量为0
// 则返回 要增加的容量 和 16 比较大的
// 否则返回 现有容量1.5倍 和 现有容量+要增加的容量 比较大的
const size_type new_size = old_size == 0
? mystl::max(add_size, static_cast<size_type>(16))
: mystl::max(old_size + old_size / 2, old_size + add_size);
return new_size;
}
这边的move最后会调用移动构造函数,这样移动的效率会很高。
vector(vector&& rhs) noexcept
:begin_(rhs.begin_),
end_(rhs.end_),
cap_(rhs.cap_)
{
rhs.begin_ = nullptr;
rhs.end_ = nullptr;
rhs.cap_ = nullptr;
}
如果在中间插入元素就需要移动元素后所有的元素,注意不要元素覆盖就好,还是很简单的。
list
这边实现了一个双向循环链表,优缺点大家都很清楚。
首先看节点分成数据节点和基本节点,是一个继承关系,个人感觉搞的有点复杂了。
/*! list 基本节点 用来连接前后节点的 */
template <class T>
struct list_node_base
{
typedef typename node_traits<T>::base_ptr base_ptr;
typedef typename node_traits<T>::node_ptr node_ptr;
base_ptr prev; /*! 前一节点 */
base_ptr next; /*! 下一节点 */
list_node_base() = default;
node_ptr as_node()
{
return static_cast<node_ptr>(self());
}
void unlink()
{
prev = next = self();
}
base_ptr self()
{
return static_cast<base_ptr>(&*this);
}
};
/*! list 数据节点 用来保存数据的 */
template <class T>
struct list_node : public list_node_base<T>
{
typedef typename node_traits<T>::base_ptr base_ptr;
typedef typename node_traits<T>::node_ptr node_ptr;
T value; /*! 数据域 */
list_node() = default;
list_node(const T& v) : value(v) {}
list_node(T&& v) : value(mystl::move(v)) {}
base_ptr as_base()
{
return static_cast<base_ptr>(&*this);
}
node_ptr self()
{
return static_cast<node_ptr>(&*this);
}
};
有两个节点类型就需要个萃取机
template <class T>
struct node_traits
{
typedef list_node_base<T>* base_ptr;
typedef list_node<T>* node_ptr;
};
list的迭代器和vector不一样,vector是一个指针,list需要自己设计了。
这边继承了一个双向迭代器,重载了许多运算符让它的行为像一个指针,但是不能像指针一样随机访问。
还有一个const版本的代码就不贴了。
template <class T>
struct list_iterator : public mystl::iterator<mystl::bidirectional_iterator_tag, T>
{
typedef T value_type;
typedef T* pointer;
typedef T& reference;
typedef typename node_traits<T>::base_ptr base_ptr;
typedef typename node_traits<T>::node_ptr node_ptr;
typedef list_iterator<T> self;
base_ptr node_; /*! 指向当前节点 */
/*! 构造函数 */
list_iterator() = default;
list_iterator(base_ptr x) : node_(x) {}
list_iterator(node_ptr x) : node_(x->as_base()) {}
list_iterator(const list_iterator& rhs) : node_(rhs.node_) {}
/*! 重载操作符 */
reference operator*() const { return node_->as_node()->value; }
pointer operator->() const { return &(operator*()); }
self& operator++()
{
MYSTL_DEBUG(node_ != nullptr);
node_ = node_->next;
return *this;
}
self operator++(int)
{
self tmp = *this;
++*this;
return tmp;
}
self& operator--()
{
MYSTL_DEBUG(node_ != nullptr);
node_ = node_->prev;
return *this;
}
self operator--(int)
{
self tmp = *this;
--*this;
return tmp;
}
/*! 重载比较操作符 */
bool operator==(const self& rhs) const { return node_ == rhs.node_; }
bool operator!=(const self& rhs) const { return node_ != rhs.node_; }
};
list
定义了嵌套型别,并且有一个指向末尾的节点,和节点大小。
因为是双向循环链表,所以指向末尾的节点很容易就得到头结点。
template <class T>
class list
{
public:
typedef mystl::allocator<T> allocator_type; /*! 数据分配器 */
typedef mystl::allocator<T> data_allocator; /*! 数据分配器 */
typedef mystl::allocator<list_node_base<T>> base_allocator; /*! 基本节点分配器 */
typedef mystl::allocator<list_node<T>> node_allocator; /*! 数据节点分配器 */
typedef typename allocator_type::value_type value_type; /*! 数据类型 */
typedef typename allocator_type::pointer pointer; /*! 数据类型指针 */
typedef typename allocator_type::const_pointer const_pointer; /*! const数据类型指针 */
typedef typename allocator_type::reference reference; /*! 数据类型引用 */
typedef typename allocator_type::const_reference const_reference; /*! const数据类型引用 */
typedef typename allocator_type::size_type size_type; /*! 数据类型大小 */
typedef typename allocator_type::difference_type difference_type; /*! 数据类型指针距离 */
typedef list_iterator<T> iterator; /*! 迭代器(这里就是结构体) */
typedef list_const_iterator<T> const_iterator; /*! const迭代器(这里就是const结构体) */
typedef mystl::reverse_iterator<iterator> reverse_iterator; /*! 反向迭代器 */
typedef mystl::reverse_iterator<const_iterator> const_reverse_iterator; /*! 反向const迭代器 */
typedef typename node_traits<T>::base_ptr base_ptr; /*! 基本节点指针类型 */
typedef typename node_traits<T>::node_ptr node_ptr; /*! 数据节点指针类型 */
allocator_type get_allocator() { return node_allocator(); }
private:
base_ptr node_; /*! 指向末尾节点 */
size_type size_; /*! 大小 */
。。。
}
链表的操作主要记住先连后断就好
/*! 在头部连接 [first, last] 结点 */
template <class T>
void list<T>::link_nodes_at_front(base_ptr first, base_ptr last)
{
first->prev = node_;
last->next = node_->next;
last->next->prev = last;
node_->next = first;
}
/*! 在尾部连接 [first, last] 结点 */
template <class T>
void list<T>::link_nodes_at_back(base_ptr first, base_ptr last)
{
last->next = node_;
first->prev = node_->prev;
first->prev->next = first;
node_->prev = last;
}
deque
双端队列比vector
和list
都要复杂,我们先看一下它长什么样子。
乍一看好像很复杂,但是它可以做到头尾插入和删除都是O(1)的时间复杂度, 空间也是可扩展的, 不会经常寻找新的空间。
首先我们看到map
指向一个指针数组,这边有八个指针可以指向八个小区域,也就是说理论上我们通过八个指针把八个不同的区块连成一个大的区块了(物理上还是分开的)。
这样我们扩展的时候只要扩展map
数组,代价很小。
看一下迭代器的设计,一个小区块的大小由deque_buf_size
指出,根据元素大小决定。
有四个数据成员,结合上面图理解一下,分别指向当前元素,当前元素所在缓冲区的头尾以及在map中的位置。
template <class T>
struct deque_buf_size
{
static constexpr size_t value = sizeof(T) < 256 ? 4096 / sizeof(T) : 16;
};
template <class T, class Ref, class Ptr>
struct deque_iterator : public iterator<random_access_iterator_tag, T>
{
typedef deque_iterator<T, T&, T*> iterator;
typedef deque_iterator<T, const T&, const T*> const_iterator;
typedef deque_iterator self;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef T* value_pointer;
typedef T** map_pointer;
static const size_type buffer_size = deque_buf_size<T>::value;
/*! 迭代器所含成员数据 */
value_pointer cur; /*! 指向所在缓冲区的当前元素 */
value_pointer first; /*! 指向所在缓冲区的头部 */
value_pointer last; /*! 指向所在缓冲区的尾部 */
map_pointer node; /*! 缓冲区所在节点 */
。。。
}
继承了随机访问迭代器,需要让迭代器的行为像指针一样。
这边可以做指针相减的操作,先算出map中的距离,再加上区块中的距离。
移动迭代器可能会跨越区块,但是无需遍历过去,只需要计算出在哪个区块就好。
template <class T, class Ref, class Ptr>
struct deque_iterator : public iterator<random_access_iterator_tag, T>
{
。。。
/*! 转到另一个缓冲区 */
void set_node(map_pointer new_node)
{
node = new_node;
first = *new_node;
last = first + buffer_size;
}
reference operator*() const { return *cur; }
pointer operator->() const { return cur; }
difference_type operator-(const self& x) const
{
// 距离:map间的距离 + buf间的距离
return static_cast<difference_type>(buffer_size) * (node - x.node)
+ (cur - first) - (x.cur - x.first);
}
self& operator++()
{
++cur;
if (cur == last) // 如果到达缓冲区的尾
{
set_node(node + 1);
cur = first;
}
return *this;
}
。。。
self& operator+=(difference_type n)
{
const auto offset = n + (cur - first);
if (offset >= 0 && offset < static_cast<difference_type>(buffer_size)) // 仍在当前缓冲区
{
cur += n;
}
else // 要跳到其他的缓冲区
{
const auto node_offset = offset > 0
? offset / static_cast<difference_type>(buffer_size)
: -static_cast<difference_type>((-offset - 1) / buffer_size) - 1;
set_node(node + node_offset);
cur = first + (offset - node_offset * static_cast<difference_type>(buffer_size));
}
return *this;
}
。。。
}
deque
定义了嵌套型别,拥有map_
指向一个指针数组,指针数组的大小和使用了的元素的头尾。
template <class T>
class deque
{
public:
typedef mystl::allocator<T> allocator_type; /*! 数据分配器 */
typedef mystl::allocator<T> data_allocator; /*! 数据分配器 */
typedef mystl::allocator<T*> map_allocator; /*! map节点分配器 */
typedef typename allocator_type::value_type value_type; /*! 数据类型 */
typedef typename allocator_type::pointer pointer; /*! 数据类型指针 */
typedef typename allocator_type::const_pointer const_pointer; /*! const数据类型指针 */
typedef typename allocator_type::reference reference; /*! 数据类型引用 */
typedef typename allocator_type::const_reference const_reference; /*! const数据类型引用 */
typedef typename allocator_type::size_type size_type; /*! 数据类型大小 */
typedef typename allocator_type::difference_type difference_type; /*! 数据类型指针距离 */
typedef pointer* map_pointer; /*! map节点类型指针 */
typedef const_pointer* const_map_pointer; /*! constmap节点类型指针 */
typedef deque_iterator<T, T&, T*> iterator; /*! 迭代器 */
typedef deque_iterator<T, const T&, const T*> const_iterator; /*! const迭代器 */
typedef mystl::reverse_iterator<iterator> reverse_iterator; /*! 反向迭代器 */
typedef mystl::reverse_iterator<const_iterator> const_reverse_iterator; /*! 反向const迭代器 */
/*! 获取分配器 */
allocator_type get_allocator() { return allocator_type(); }
static const size_type buffer_size = deque_buf_size<T>::value;
private:
/*! 用以下四个数据来表现一个 deque */
iterator begin_; /*! 指向第一个节点 */
iterator end_; /*! 指向最后一个结点 */
map_pointer map_; /*! 指向一块 map,map 中的每个元素都是一个指针,指向一个缓冲区 */
size_type map_size_; /*! map 内指针的数目 */
。。。
}
老规矩,来看看如何动态增长的。
初始化n个值为value的数据,先调用map_init
分配空间,这个时候begin_
和end_
就指向了分配空间的头和尾(不是所以的空间,是已经使用的空间),然后填充数据。
deque(size_type n, const value_type& value)
{
fill_init(n, value);
}
template <class T>
void deque<T>::fill_init(size_type n, const value_type& value)
{
map_init(n);
if (n != 0)
{
for (auto cur = begin_.node; cur < end_.node; ++cur)
{
mystl::uninitialized_fill(*cur, *cur + buffer_size, value);
}
mystl::uninitialized_fill(end_.first, end_.cur, value);
}
}
分配空间的时候需要头尾都留出点空间来增长,数据都集中在中央。
template <class T>
void deque<T>::map_init(size_type nElem)
{
const size_type nNode = nElem / buffer_size + 1; // 需要分配的缓冲区个数
map_size_ = mystl::max(static_cast<size_type>(DEQUE_MAP_INIT_SIZE), nNode + 2); // 需要头尾空出空间来增长
try
{
map_ = create_map(map_size_);
}
catch (...)
{
map_ = nullptr;
map_size_ = 0;
throw;
}
// 让 nstart 和 nfinish 都指向 map_ 最中央的区域,方便向头尾扩充
map_pointer nstart = map_ + (map_size_ - nNode) / 2;
map_pointer nfinish = nstart + nNode - 1;
try
{
create_buffer(nstart, nfinish);
}
catch (...)
{
map_allocator::deallocate(map_, map_size_);
map_ = nullptr;
map_size_ = 0;
throw;
}
begin_.set_node(nstart);
end_.set_node(nfinish);
begin_.cur = begin_.first;
end_.cur = end_.first + (nElem % buffer_size);
}
template <class T>
typename deque<T>::map_pointer deque<T>::create_map(size_type size)
{
map_pointer mp = nullptr;
mp = map_allocator::allocate(size);
for (size_type i = 0; i < size; ++i)
{
*(mp + i) = nullptr;
}
return mp;
}
template <class T>
void deque<T>::create_buffer(map_pointer nstart, map_pointer nfinish)
{
map_pointer cur;
try
{
for (cur = nstart; cur <= nfinish; ++cur)
{
*cur = data_allocator::allocate(buffer_size);
}
}
catch (...)
{
while (cur != nstart)
{
--cur;
data_allocator::deallocate(*cur, buffer_size);
*cur = nullptr;
}
throw;
}
}
再看一下在头部插入,如果当前小区域还有空间,直接就构造元素,否则会调用require_capacity
开确保容量。
template <class T>
void deque<T>::push_front(const value_type& value)
{
if (begin_.cur != begin_.first)
{
data_allocator::construct(begin_.cur - 1, value);
--begin_.cur;
}
else
{
require_capacity(1, true);
try
{
--begin_;
data_allocator::construct(begin_.cur, value);
}
catch (...)
{
++begin_;
throw;
}
}
}
可以看到容量不够,就会扩展map_
数组,代价非常小。
/*! 确保还有剩余容量 front变量判断是否在前端插入 */
template <class T>
void deque<T>::require_capacity(size_type n, bool front)
{
if (front && (static_cast<size_type>(begin_.cur - begin_.first) < n))
{
const size_type need_buffer = (n - (begin_.cur - begin_.first)) / buffer_size + 1;
if (need_buffer > static_cast<size_type>(begin_.node - map_))
{
reallocate_map_at_front(need_buffer);
return;
}
create_buffer(begin_.node - need_buffer, begin_.node - 1);
}
else if (!front && (static_cast<size_type>(end_.last - end_.cur - 1) < n))
{
const size_type need_buffer = (n - (end_.last - end_.cur - 1)) / buffer_size + 1;
if (need_buffer > static_cast<size_type>((map_ + map_size_) - end_.node - 1))
{
reallocate_map_at_back(need_buffer);
return;
}
create_buffer(end_.node + 1, end_.node + need_buffer);
}
}
/*! 在前端再分配更大的空间 */
template <class T>
void deque<T>::reallocate_map_at_front(size_type need_buffer)
{
const size_type new_map_size = mystl::max(map_size_ << 1, map_size_ + need_buffer + DEQUE_MAP_INIT_SIZE);
map_pointer new_map = create_map(new_map_size);
const size_type old_buffer = end_.node - begin_.node + 1;
const size_type new_buffer = old_buffer + need_buffer;
// 另新的 map 中的指针指向原来的 buffer,并开辟新的 buffer
auto begin = new_map + (new_map_size - new_buffer) / 2;
auto mid = begin + need_buffer;
auto end = mid + old_buffer;
create_buffer(begin, mid - 1);
for (auto begin1 = mid, begin2 = begin_.node; begin1 != end; ++begin1, ++begin2)
{
*begin1 = *begin2;
}
// 更新数据
map_allocator::deallocate(map_, map_size_);
map_ = new_map;
map_size_ = new_map_size;
begin_ = iterator(*mid + (begin_.cur - begin_.first), mid);
end_ = iterator(*(end - 1) + (end_.cur - end_.first), end - 1);
}
如果使用insert在中间插入元素,就需要看是靠近前面近一点,还是靠近后面近一点,然后移动在那之前后之后的所有元素,注意不要覆盖元素就好。
stack和queue
栈和队列都是配接器,内部会聚集一个底层容器,通过改变接口来修改容器的表现形式。
缺省使用 mystl::deque 作为底层容器。
可以发现stack并没有自己实现什么功能,全都是使用的容器c_
的功能。
template <class T, class Container = mystl::deque<T>>
class stack
{
public:
typedef Container container_type; /*! 容器类型 */
typedef typename Container::value_type value_type; /*! 数据类型 */
typedef typename Container::size_type size_type; /*! 数据类型大小 */
typedef typename Container::reference reference; /*! 数据类型引用 */
typedef typename Container::const_reference const_reference; /*! const数据类型引用 */
static_assert(std::is_same<T, value_type>::value, "the value_type of Container should be same with T");
private:
container_type c_; /*! 用底层容器表现 stack */
public:
/*! 构造、复制、移动函数 */
stack() = default;
explicit stack(size_type n) : c_(n) {}
stack(size_type n, const value_type& value) : c_(n, value) {}
template <class IIter>
stack(IIter first, IIter last) : c_(first, last) {}
stack(std::initializer_list<T> ilist) : c_(ilist.begin(), ilist.end()) {}
stack(const Container& c) : c_(c) {}
stack(Container&& c) noexcept(std::is_nothrow_move_constructible<Container>::value) : c_(mystl::move(c)) {}
stack(const stack& rhs) : c_(rhs.c_) {}
stack(stack&& rhs) noexcept(std::is_nothrow_move_constructible<Container>::value) : c_(mystl::move(rhs.c_)) {}
stack& operator=(const stack& rhs)
{
c_ = rhs.c_;
return *this;
}
stack& operator=(stack&& rhs) noexcept(std::is_nothrow_move_assignable<Container>::value)
{
c_ = mystl::move(rhs.c_);
return *this;
}
stack& operator=(std::initializer_list<T> ilist)
{
c_ = ilist;
return *this;
}
~stack() = default;
/*! 访问元素相关操作 */
reference top() { return c_.back(); }
const_reference top() const { return c_.back(); }
/*! 容量相关操作 */
bool empty() const noexcept { return c_.empty(); }
size_type size() const noexcept { return c_.size(); }
/*! 修改容器相关操作 */
template <class... Args>
void emplace(Args&& ...args)
{
c_.emplace_back(mystl::forward<Args>(args)...);
}
void push(const value_type& value)
{
c_.push_back(value);
}
void push(value_type&& value)
{
c_.push_back(mystl::move(value));
}
void pop()
{
c_.pop_back();
}
void clear()
{
while (!empty())
{
pop();
}
}
。。。
};
queue.h中有个优先队列,其实现就是主要依靠底层容器和heap.algo.h中的四个算法。
关联式容器
RB-tree
为什么用红黑树
简单看一下其他的树:
1、二叉查找树
左结点小于根节点,右结点大于根节点的一种排序树。
问题:有一种极端的情况,会变成一种类似线性链表似的结构。时间复杂度就为O(N)
,所以出现了二叉平衡树。
2、平衡二叉树
解决二叉查找树的问题,保证了不会成为线性的链表。
左结点小于根节点,右结点大于根节点,并且还规定了左子树和右子树的高度差不得超过1。
问题:
- 由于要维持自身的平衡,所以进行插入和删除结点操作的时候,需要对结点进行频繁的旋转。
- 每一个节点只能存放一个元素,每个节点只有两个子节点,所以查找时,需要多次磁盘IO(如果数据存放在磁盘中的,每次查询是将磁盘中的一页数据加入内存,树的每一层节点存放在一页中,不同层数据存放在不同页。)
3、B树
解决平衡二叉树的第二个问题,是一种多路平衡树,MongoDB的索引就是用B树实现的。
一个m阶的B树规定了:
- 根结点至少有两个子女。
- 每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m 。
- 每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m。
- 所有的叶子结点都位于同一层。
- 每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。
问题:
- 查找不稳定,最好的情况就是在根节点查到了,最坏的情况就是在叶子结点查到。
- 在遍历方面比较麻烦,由于需要进行中序遍历,所以也会进行一定数量的磁盘IO。
4、B+树
解决B树第二个问题,每个非叶子结点存放的元素只用于索引作用,所有数据保存在叶子结点。
一个m阶的B+树规定了:
- 有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
- 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
- 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
非叶子结点中存放的元素不存放数据,所以每一层可以容纳更多元素,也就是磁盘中的每一页可以存放更多元素。这样在查找时,磁盘IO的次数也会减少。
查找稳定,因为所有的数据都在叶子结点。每个叶子结点也通过指针指向构成了一种链表结构,所以遍历数据也会简单很多。
5、RB-Tree
解决了平衡二叉树第一个问题,不严格控制左、右子树高度或节点数之差小于等于1。
红黑树规定了:
- 节点是红色或黑色。
- 根节点是黑色。
- 每个叶子节点都是黑色的空节点(NIL节点)。
- 每个红色节点的两个子节点都是黑色。也就是说从每个叶子到根的所有路径上不能有两个连续的红色节点)。
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
红黑树在查找方面和AVL树操作几乎相同。牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,结合变色,降低了对旋转的要求,从而提高了性能。任何不平衡都会在三次旋转之内解决。
确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证O(logN)。
比较
B/B+树用在磁盘文件组织、数据索引和数据库索引中。B+树比B树更适合实际应用中操作系统的文件索引和数据库索引。
红黑树,读取略逊于AVL,维护强于AVL,每次插入和删除的平均旋转次数应该是远小于平衡树。如果插入的数据很乱AVL就不太行了,所以这里使用红黑树。
实现
header_
一个不存放值的哨兵节点
template <class T, class Compare>
class rb_tree
{
public:
typedef rb_tree_traits<T> tree_traits; /*! 树基本信息萃取机 */
typedef rb_tree_value_traits<T> value_traits; /*! 树存储值萃取机 */
typedef typename tree_traits::base_type base_type;
typedef typename tree_traits::base_ptr base_ptr;
typedef typename tree_traits::node_type node_type;
typedef typename tree_traits::node_ptr node_ptr;
typedef typename tree_traits::key_type key_type;
typedef typename tree_traits::mapped_type mapped_type;
typedef typename tree_traits::value_type value_type;
typedef Compare key_compare;
typedef mystl::allocator<T> allocator_type; /*! 数据分配器 */
typedef mystl::allocator<T> data_allocator; /*! 数据分配器 */
typedef mystl::allocator<base_type> base_allocator; /*! 基本节点分配器 */
typedef mystl::allocator<node_type> node_allocator; /*! 数据节点分配器 */
typedef typename allocator_type::pointer pointer; /*! 数据类型指针 */
typedef typename allocator_type::const_pointer const_pointer; /*! const数据类型指针 */
typedef typename allocator_type::reference reference; /*! 数据类型引用 */
typedef typename allocator_type::const_reference const_reference; /*! const数据类型引用 */
typedef typename allocator_type::size_type size_type; /*! 数据类型大小 */
typedef typename allocator_type::difference_type difference_type; /*! 数据类型指针距离 */
typedef rb_tree_iterator<T> iterator; /*! 迭代器(这里就是结构体) */
typedef rb_tree_const_iterator<T> const_iterator; /*! const迭代器(这里就是const结构体) */
typedef mystl::reverse_iterator<iterator> reverse_iterator; /*! 反向迭代器 */
typedef mystl::reverse_iterator<const_iterator> const_reverse_iterator; /*! 反向const迭代器 */
allocator_type get_allocator() const { return node_allocator(); }
key_compare key_comp() const { return key_comp_; }
private:
/*! 用以下三个数据表现 rb tree */
base_ptr header_; /*! 特殊节点,与根节点互为对方的父节点 */
size_type node_count_; /*! 节点数 */
key_compare key_comp_; /*! 节点键值比较的准则 */
。。。
}
节点需要设计和链表类似,需要连接左右节点和父节点。
/*! rb tree 基本节点 用来连接的 */
template <class T>
struct rb_tree_node_base
{
typedef rb_tree_color_type color_type;
typedef rb_tree_node_base<T>* base_ptr;
typedef rb_tree_node<T>* node_ptr;
base_ptr parent; /*! 父节点 */
base_ptr left; /*! 左子节点 */
base_ptr right; /*! 右子节点 */
color_type color; /*! 节点颜色 */
base_ptr get_base_ptr()
{
return &*this;
}
node_ptr get_node_ptr()
{
return reinterpret_cast<node_ptr>(&*this);
}
node_ptr& get_node_ref()
{
return reinterpret_cast<node_ptr&>(*this);
}
};
/*! rb tree 数据节点 用来保存数据的 */
template <class T>
struct rb_tree_node : public rb_tree_node_base<T>
{
typedef rb_tree_node_base<T>* base_ptr;
typedef rb_tree_node<T>* node_ptr;
T value; /*! 节点值 */
base_ptr get_base_ptr()
{
return static_cast<base_ptr>(&*this);
}
node_ptr get_node_ptr()
{
return &*this;
}
};
下面主要看一下树的插入和删除是如何保持平衡的,在这之前先看一下左旋(右旋类似),a
和c
的相对位置是没有变的,还在两边,中间x<b<y
,父节点从x
变成了y
,所以b
挂载的节点也变了一下,注意一下节点为根节点的情况。
/*---------------------------------------*\
| p p |
| / \ / \ |
| x d rotate left y d |
| / \ ===========> / \ |
| a y x c |
| / \ / \ |
| b c a b |
\*---------------------------------------*/
/*! 左旋,参数一为左旋点,参数二为根节点 */
template <class NodePtr>
void rb_tree_rotate_left(NodePtr x, NodePtr& root) noexcept
{
auto y = x->right; // y 为 x 的右子节点
x->right = y->left;
if (y->left != nullptr)
{
y->left->parent = x;
}
y->parent = x->parent;
if (x == root) // 如果 x 为根节点,让 y 顶替 x 成为根节点
{
root = y;
}
else if (rb_tree_is_lchild(x)) // 如果 x 是左子节点
{
x->parent->left = y;
}
else // 如果 x 是右子节点
{
x->parent->right = y;
}
// 调整 x 与 y 的关系
y->left = x;
x->parent = y;
}
插入节点,根据规则5直接将新增的节点着色为red,还有可能会违背规则4,
所以我们的核心思路就是将红色的节点移到根节点,然后将根节点设为黑色。
再看下面的case3、4、5就比较清楚了。
// 插入节点后使 rb tree 重新平衡,参数一为新增节点,参数二为根节点
//
// case 1: 新增节点位于根节点,令新增节点为黑
// case 2: 新增节点的父节点为黑,没有破坏平衡,直接返回
// case 3: 父节点和叔叔节点都为红
// 令父节点和叔叔节点为黑,祖父节点为红,然后令祖父节点为当前节点,继续处理
// case 4: 父节点为红,叔叔节点为 NIL 或黑色,父节点为左(右)孩子,当前节点为右(左)孩子,
// 让父节点成为当前节点,再以当前节点为支点左(右)旋
// case 5: 父节点为红,叔叔节点为 NIL 或黑色,父节点为左(右)孩子,当前节点为左(右)孩子,
// 让父节点变为黑色,祖父节点变为红色,以祖父节点为支点右(左)旋
template <class NodePtr>
void rb_tree_insert_rebalance(NodePtr x, NodePtr& root) noexcept
{
rb_tree_set_red(x); // 新增节点为红色
while (x != root && rb_tree_is_red(x->parent))
{
if (rb_tree_is_lchild(x->parent)) // 如果父节点是左子节点
{
auto uncle = x->parent->parent->right;
if (uncle != nullptr && rb_tree_is_red(uncle)) // case 3: 父节点和叔叔节点都为红
{
rb_tree_set_black(x->parent);
rb_tree_set_black(uncle);
x = x->parent->parent;
rb_tree_set_red(x);
}
else // 无叔叔节点或叔叔节点为黑
{
if (!rb_tree_is_lchild(x)) // case 4: 当前节点 x 为右子节点
{
x = x->parent;
rb_tree_rotate_left(x, root);
}
// 都转换成 case 5: 当前节点为左子节点
rb_tree_set_black(x->parent);
rb_tree_set_red(x->parent->parent);
rb_tree_rotate_right(x->parent->parent, root);
break;
}
}
else // 如果父节点是右子节点,对称处理
{
auto uncle = x->parent->parent->left;
if (uncle != nullptr && rb_tree_is_red(uncle)) // case 3: 父节点和叔叔节点都为红
{
rb_tree_set_black(x->parent);
rb_tree_set_black(uncle);
x = x->parent->parent;
rb_tree_set_red(x);
// 此时祖父节点为红,可能会破坏红黑树的性质,令当前节点为祖父节点,继续处理
}
else // 无叔叔节点或叔叔节点为黑
{
if (rb_tree_is_lchild(x)) // case 4: 当前节点 x 为左子节点
{
x = x->parent;
rb_tree_rotate_right(x, root);
}
// 都转换成 case 5: 当前节点为左子节点
rb_tree_set_black(x->parent);
rb_tree_set_red(x->parent->parent);
rb_tree_rotate_left(x->parent->parent, root);
break;
}
}
}
rb_tree_set_black(root); // 根节点永远为黑
}
删除节点后,有可能会违背规则2、4、5,
删除操作删除的结点可以看作删除替代结点,而替代结点最后总是在树末。
// 删除节点后使 rb tree 重新平衡,参数一为要删除的节点,参数二为根节点,参数三为最小节点,参数四为最大节点
// 1.没有儿子,即为叶结点。直接把父结点的对应儿子指针设为NULL,删除儿子结点就OK了。
// 2.只有一个儿子。那么把父结点的相应儿子指针指向儿子的独生子,删除儿子结点也OK了。
// 3.有两个儿子。这是最麻烦的情况,因为你删除节点之后,还要保证满足搜索二叉树的结构。
// 其实也比较容易,我们可以选择左儿子中的最大元素或者右儿子中的最小元素放到待删除节点的位置,就可以保证结构的不变。
// 当然,你要记得调整子树,毕竟又出现了节点删除。
// 习惯上大家选择左儿子中的最大元素,其实选择右儿子的最小元素也一样,没有任何差别,只是人们习惯从左向右。
template <class NodePtr>
NodePtr rb_tree_erase_rebalance(NodePtr z, NodePtr& root, NodePtr& leftmost, NodePtr& rightmost)
{
// y 是可能的替换节点,指向最终要删除的节点
auto y = (z->left == nullptr || z->right == nullptr) ? z : rb_tree_next(z);
// x 是 y 的一个独子节点或 NIL 节点
auto x = y->left != nullptr ? y->left : y->right;
// xp 为 x 的父节点
NodePtr xp = nullptr;
// y != z 说明 z 有两个非空子节点,此时 y 指向 z 右子树的最左节点,x 指向 y 的右子节点。
// 用 y 顶替 z 的位置,用 x 顶替 y 的位置,最后用 y 指向 z
if (y != z)
{
z->left->parent = y;
y->left = z->left;
// 如果 y 不是 z 的右子节点,那么 z 的右子节点一定有左孩子 x 替换 y 的位置
if (y != z->right)
{
xp = y->parent;
if (x != nullptr)
{
x->parent = y->parent;
}
y->parent->left = x;
y->right = z->right;
z->right->parent = y;
}
else
{
xp = y;
}
// 连接 y 与 z 的父节点
if (root == z)
{
root = y;
}
else if (rb_tree_is_lchild(z))
{
z->parent->left = y;
}
else
{
z->parent->right = y;
}
y->parent = z->parent;
mystl::swap(y->color, z->color);
y = z;
}
else // y == z 说明 z 至多只有一个孩子
{
xp = y->parent;
if (x)
{
x->parent = y->parent;
}
// 连接 x 与 z 的父节点
if (root == z)
{
root = x;
}
else if (rb_tree_is_lchild(z))
{
z->parent->left = x;
}
else
{
z->parent->right = x;
}
// 此时 z 有可能是最左节点或最右节点,更新数据
if (leftmost == z)
{
leftmost = x == nullptr ? xp : rb_tree_min(x);
}
if (rightmost == z)
{
rightmost = x == nullptr ? xp : rb_tree_max(x);
}
}
// 此时,y 指向要删除的节点,x 为替代节点,从 x 节点开始调整。
// 如果删除的节点为红色,树的性质没有被破坏,否则按照以下情况调整(x 为左子节点为例):
// case 1: 兄弟节点为红色,
// 令父节点为红,兄弟节点为黑,进行左(右)旋,继续处理
// case 2: 兄弟节点为黑色,且两个子节点都为黑色或 NIL,
// 令兄弟节点为红,父节点成为当前节点,继续处理
// case 3: 兄弟节点为黑色,左子节点为红色或 NIL,右子节点为黑色或 NIL,
// 令兄弟节点为红,兄弟节点的左子节点为黑,以兄弟节点为支点右(左)旋,继续处理
// case 4: 兄弟节点为黑色,右子节点为红色,
// 令兄弟节点为父节点的颜色,父节点为黑色,兄弟节点的右子节点为黑色,以父节点为支点左(右)旋,树的性质调整完成,算法结束
if (!rb_tree_is_red(y)) // x 为黑色时,调整,否则直接将 x 变为黑色即可
{
while (x != root && (x == nullptr || !rb_tree_is_red(x)))
{
if (x == xp->left) // 如果 x 为左子节点
{
auto brother = xp->right;
if (rb_tree_is_red(brother)) // case 1
{
rb_tree_set_black(brother);
rb_tree_set_red(xp);
rb_tree_rotate_left(xp, root);
brother = xp->right;
}
// case 1 转为为了 case 2、3、4 中的一种
if ((brother->left == nullptr || !rb_tree_is_red(brother->left)) &&
(brother->right == nullptr || !rb_tree_is_red(brother->right))) // case 2
{
rb_tree_set_red(brother);
x = xp;
xp = xp->parent;
}
else
{
if (brother->right == nullptr || !rb_tree_is_red(brother->right)) // case 3
{
if (brother->left != nullptr)
{
rb_tree_set_black(brother->left);
}
rb_tree_set_red(brother);
rb_tree_rotate_right(brother, root);
brother = xp->right;
}
// 转为 case 4
brother->color = xp->color;
rb_tree_set_black(xp);
if (brother->right != nullptr)
{
rb_tree_set_black(brother->right);
}
rb_tree_rotate_left(xp, root);
break;
}
}
else // x 为右子节点,对称处理
{
auto brother = xp->left;
if (rb_tree_is_red(brother)) // case 1
{
rb_tree_set_black(brother);
rb_tree_set_red(xp);
rb_tree_rotate_right(xp, root);
brother = xp->left;
}
if ((brother->left == nullptr || !rb_tree_is_red(brother->left)) &&
(brother->right == nullptr || !rb_tree_is_red(brother->right))) // case 2
{
rb_tree_set_red(brother);
x = xp;
xp = xp->parent;
}
else
{
if (brother->left == nullptr || !rb_tree_is_red(brother->left)) // case 3
{
if (brother->right != nullptr)
{
rb_tree_set_black(brother->right);
}
rb_tree_set_red(brother);
rb_tree_rotate_left(brother, root);
brother = xp->left;
}
// 转为 case 4
brother->color = xp->color;
rb_tree_set_black(xp);
if (brother->left != nullptr)
{
rb_tree_set_black(brother->left);
}
rb_tree_rotate_right(xp, root);
break;
}
}
}
if (x != nullptr)
{
rb_tree_set_black(x);
}
}
return y;
}
插入一个节点
template <class T, class Compare>
typename rb_tree<T, Compare>::iterator rb_tree<T, Compare>::insert_multi(const value_type& value)
{
THROW_LENGTH_ERROR_IF(node_count_ > max_size() - 1, "rb_tree<T, Comp>'s size too big");
auto res = get_insert_multi_pos(value_traits::get_key(value));
return insert_value_at(res.first, value, res.second);
}
/*! 获取可重复插入位置 */
template <class T, class Compare>
mystl::pair<typename rb_tree<T, Compare>::base_ptr, bool> rb_tree<T, Compare>::get_insert_multi_pos(const key_type& key)
{
auto x = root();
auto y = header_;
bool add_to_left = true; // 是否在左边
while (x != nullptr) // 逐个寻找比较
{
y = x;
add_to_left = key_comp_(key, value_traits::get_key(x->get_node_ptr()->value));
x = add_to_left ? x->left : x->right;
}
return mystl::make_pair(y, add_to_left);
}
/*! 在 x 节点处插入新的节点 x 为插入点的父节点, value 为要插入的值,add_to_left 表示是否在左边插入 */
template <class T, class Compare>
typename rb_tree<T, Compare>::iterator rb_tree<T, Compare>::insert_value_at(base_ptr x, const value_type& value, bool add_to_left)
{
node_ptr node = create_node(value);
node->parent = x;
auto base_node = node->get_base_ptr();
if (x == header_)
{
root() = base_node;
leftmost() = base_node;
rightmost() = base_node;
}
else if (add_to_left)
{
x->left = base_node;
if (leftmost() == x)
{
leftmost() = base_node;
}
}
else
{
x->right = base_node;
if (rightmost() == x)
{
rightmost() = base_node;
}
}
rb_tree_insert_rebalance(base_node, root());
++node_count_;
return iterator(node);
}
删除节点
/*! 删除键值等于 key 的元素,返回删除的个数 */
template <class T, class Compare>
typename rb_tree<T, Compare>::size_type rb_tree<T, Compare>::erase_multi(const key_type& key)
{
auto p = equal_range_multi(key);
size_type n = mystl::distance(p.first, p.second);
erase(p.first, p.second);
return n;
}
/*! 得到包围key的范围 */
mystl::pair<iterator, iterator> equal_range_multi(const key_type& key)
{
return mystl::pair<iterator, iterator>(lower_bound(key), upper_bound(key));
}
/*! 删除[first, last)区间内的元素 */
template <class T, class Compare>
void rb_tree<T, Compare>::erase(iterator first, iterator last)
{
if (first == begin() && last == end())
{
clear();
}
else
{
while (first != last)
{
erase(first++);
}
}
}
/*! 删除 hint 位置的节点 */
template <class T, class Compare>
typename rb_tree<T, Compare>::iterator rb_tree<T, Compare>::erase(iterator hint)
{
auto node = hint.node->get_node_ptr();
iterator next(node);
++next;
rb_tree_erase_rebalance(hint.node, root(), leftmost(), rightmost());
destroy_node(node);
--node_count_;
return next;
}
set、multiset、map和multimap
这四个都是配接器,底层容器使用的RB-tree,所以都是有序的。只是做了接口的转换,可以自己看源码。
hashtable
哈希表我们应该都很熟悉了,在插入删除等操作都可以做到O(1)的时间复杂度。
散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。
常用Hash函数
- 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a·key + b,其中a和b为常数(这种散列函数叫做自身函数)。
- 数字分析法:分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
- 平方取中法:取关键字平方后的中间几位作为散列地址。
- 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。
- 随机数法:选择一随机函数,取关键字作为随机函数的种子生成随机值作为散列地址,通常用于关键字长度不同的场合。
- 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p,p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生碰撞。
冲突处理
- 开放寻址法;Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:
- di=1,2,3,…,m-1,称线性探测再散列;
- di=12,-12,22,-22,32,…,±k2,(k<=m/2)称二次探测再散列;
- di=伪随机数序列,称伪随机探测再散列。
- 再散列法:Hi=RHi(key),i=1,2,…,k RHi均是不同的散列函数,即在同义词产生地址冲突时计算另一个散列函数地址,直到冲突不再发生,这种方法不易产生“聚集”,但增加了计算时间。
- 链地址法(拉链法)
- 建立一个公共溢出区
设计
采用链地址法解决冲突,所以整个哈希表的数据结构就像下图所示:
其中节点定义(单链表)
template <class T>
struct hashtable_node
{
hashtable_node* next; /*! 指向下一个节点 */
T value; /*! 储存实值 */
hashtable_node() = default;
hashtable_node(const T& n) : next(nullptr), value(n) {}
hashtable_node(const hashtable_node& node) :next(node.next), value(node.value) {}
hashtable_node(hashtable_node&& node) :next(node.next), value(mystl::move(node.value))
{
node.next = nullptr;
}
};
桶就用一个vector
表示,哈希函数由参数传递(默认使用算法中的哈希函数)。
mlf_
是一个比例(装入表中的元素 / 表的实际大小),越大说明冲突的可能性就越大。
template <class T, class Hash, class KeyEqual>
class hashtable
{
friend struct mystl::ht_iterator<T, Hash, KeyEqual>;
friend struct mystl::ht_const_iterator<T, Hash, KeyEqual>;
public:
typedef ht_value_traits<T> value_traits; /*! 数据类型 */
typedef typename value_traits::key_type key_type; /*! 键 */
typedef typename value_traits::mapped_type mapped_type; /*! 映射的类型 如果T是一个pair的话就是第二个元素 否则就是T */
typedef typename value_traits::value_type value_type; /*! 值 */
typedef Hash hasher; /*! 哈希函数类型 */
typedef KeyEqual key_equal; /*! 键值相等的比较函数类型 */
typedef hashtable_node<T> node_type; /*! 节点类型 */
typedef node_type* node_ptr; /*! 节点指针类型 */
typedef mystl::vector<node_ptr> bucket_type; /*! 桶数组类型 */
typedef mystl::allocator<T> allocator_type; /*! 数据分配器 */
typedef mystl::allocator<T> data_allocator; /*! 数据分配器 */
typedef mystl::allocator<node_type> node_allocator; /*! 节点分配器 */
typedef typename allocator_type::pointer pointer; /*! 数据类型指针 */
typedef typename allocator_type::const_pointer const_pointer; /*! const数据类型指针 */
typedef typename allocator_type::reference reference; /*! 数据类型引用 */
typedef typename allocator_type::const_reference const_reference; /*! const数据类型引用 */
typedef typename allocator_type::size_type size_type; /*! 数据类型大小 */
typedef typename allocator_type::difference_type difference_type; /*! 数据类型指针距离 */
typedef mystl::ht_iterator<T, Hash, KeyEqual> iterator; /*! 迭代器 */
typedef mystl::ht_const_iterator<T, Hash, KeyEqual> const_iterator; /*! const迭代器 */
typedef mystl::ht_local_iterator<T> local_iterator; /*! 迭代器(不指向其他桶) */
typedef mystl::ht_const_local_iterator<T> const_local_iterator; /*! const迭代器(不指向其他桶) */
allocator_type get_allocator() const { return allocator_type(); }
private:
/*! 用以下六个参数来表现 hashtable */
bucket_type buckets_; /*! 桶数组 使用vector */
size_type bucket_size_; /*! 桶数量 */
size_type size_; /*! 元素数量 */
float mlf_; /*! 最大桶装载比例 */
hasher hash_; /*! 哈希仿函数 */
key_equal equal_; /*! 键值相等的比较仿函数 */
。。。
}
迭代器继承的是forward_iterator_tag
,所以只能往前遍历,不能回退。
template <class T, class Hash, class KeyEqual>
struct ht_iterator_base : public mystl::iterator<mystl::forward_iterator_tag, T>
{
typedef mystl::hashtable<T, Hash, KeyEqual> hashtable;
typedef ht_iterator_base<T, Hash, KeyEqual> base;
typedef mystl::ht_iterator<T, Hash, KeyEqual> iterator;
typedef mystl::ht_const_iterator<T, Hash, KeyEqual> const_iterator;
typedef hashtable_node<T>* node_ptr;
typedef hashtable* contain_ptr;
typedef const node_ptr const_node_ptr;
typedef const contain_ptr const_contain_ptr;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
node_ptr node; /*! 迭代器当前所指节点 */
contain_ptr ht; /*! 保持与容器的连结 */
ht_iterator_base() = default;
bool operator==(const base& rhs) const { return node == rhs.node; }
bool operator!=(const base& rhs) const { return node != rhs.node; }
};
template <class T, class Hash, class KeyEqual>
struct ht_iterator : public ht_iterator_base<T, Hash, KeyEqual>
{
typedef ht_iterator_base<T, Hash, KeyEqual> base;
typedef typename base::hashtable hashtable;
typedef typename base::iterator iterator;
typedef typename base::const_iterator const_iterator;
typedef typename base::node_ptr node_ptr;
typedef typename base::contain_ptr contain_ptr;
typedef ht_value_traits<T> value_traits;
typedef T value_type;
typedef value_type* pointer;
typedef value_type& reference;
using base::node;
using base::ht;
ht_iterator() = default;
ht_iterator(node_ptr n, contain_ptr t)
{
node = n;
ht = t;
}
ht_iterator(const iterator& rhs)
{
node = rhs.node;
ht = rhs.ht;
}
ht_iterator(const const_iterator& rhs)
{
node = rhs.node;
ht = rhs.ht;
}
iterator& operator=(const iterator& rhs)
{
if (this != &rhs)
{
node = rhs.node;
ht = rhs.ht;
}
return *this;
}
iterator& operator=(const const_iterator& rhs)
{
if (this != &rhs)
{
node = rhs.node;
ht = rhs.ht;
}
return *this;
}
reference operator*() const { return node->value; }
pointer operator->() const { return &(operator*()); }
iterator& operator++()
{
MYSTL_DEBUG(node != nullptr);
const node_ptr old = node;
node = node->next;
if (node == nullptr) // 如果下一个位置为空,跳到下一个 bucket 的起始处
{
auto index = ht->hash(value_traits::get_key(old->value));
while (!node && ++index < ht->bucket_size_)
{
node = ht->buckets_[index];
}
}
return *this;
}
iterator operator++(int)
{
iterator tmp = *this;
++*this;
return tmp;
}
};
哈希表的的大小都是素数, 减少冲突。
#ifdef SYSTEM_64
#define PRIME_NUM 99
// 1. start with p = 101
// 2. p = next_prime(p * 1.7)
// 3. if p < (2 << 63), go to step 2, otherwise, go to step 4
// 4. end with p = prev_prime(2 << 63 - 1)
static constexpr size_t ht_prime_list[] =
{
101ull, 173ull, 263ull, 397ull, 599ull, 907ull, 1361ull, 2053ull, 3083ull,
4637ull, 6959ull, 10453ull, 15683ull, 23531ull, 35311ull, 52967ull, 79451ull,
119179ull, 178781ull, 268189ull, 402299ull, 603457ull, 905189ull, 1357787ull,
2036687ull, 3055043ull, 4582577ull, 6873871ull, 10310819ull, 15466229ull,
23199347ull, 34799021ull, 52198537ull, 78297827ull, 117446801ull, 176170229ull,
264255353ull, 396383041ull, 594574583ull, 891861923ull, 1337792887ull,
2006689337ull, 3010034021ull, 4515051137ull, 6772576709ull, 10158865069ull,
15238297621ull, 22857446471ull, 34286169707ull, 51429254599ull, 77143881917ull,
115715822899ull, 173573734363ull, 260360601547ull, 390540902329ull,
585811353559ull, 878717030339ull, 1318075545511ull, 1977113318311ull,
2965669977497ull, 4448504966249ull, 6672757449409ull, 10009136174239ull,
15013704261371ull, 22520556392057ull, 33780834588157ull, 50671251882247ull,
76006877823377ull, 114010316735089ull, 171015475102649ull, 256523212653977ull,
384784818980971ull, 577177228471507ull, 865765842707309ull, 1298648764060979ull,
1947973146091477ull, 2921959719137273ull, 4382939578705967ull, 6574409368058969ull,
9861614052088471ull, 14792421078132871ull, 22188631617199337ull, 33282947425799017ull,
49924421138698549ull, 74886631708047827ull, 112329947562071807ull, 168494921343107851ull,
252742382014661767ull, 379113573021992729ull, 568670359532989111ull, 853005539299483657ull,
1279508308949225477ull, 1919262463423838231ull, 2878893695135757317ull, 4318340542703636011ull,
6477510814055453699ull, 9716266221083181299ull, 14574399331624771603ull, 18446744073709551557ull
};
#else
#define PRIME_NUM 44
// 1. start with p = 101
// 2. p = next_prime(p * 1.7)
// 3. if p < (2 << 31), go to step 2, otherwise, go to step 4
// 4. end with p = prev_prime(2 << 31 - 1)
static constexpr size_t ht_prime_list[] =
{
101u, 173u, 263u, 397u, 599u, 907u, 1361u, 2053u, 3083u, 4637u, 6959u,
10453u, 15683u, 23531u, 35311u, 52967u, 79451u, 119179u, 178781u, 268189u,
402299u, 603457u, 905189u, 1357787u, 2036687u, 3055043u, 4582577u, 6873871u,
10310819u, 15466229u, 23199347u, 34799021u, 52198537u, 78297827u, 117446801u,
176170229u, 264255353u, 396383041u, 594574583u, 891861923u, 1337792887u,
2006689337u, 3010034021u, 4294967291u,
};
#endif
/*! 找出最接近并大于等于 n 的那个质数 */
inline size_t ht_next_prime(size_t n)
{
const size_t* first = ht_prime_list;
const size_t* last = ht_prime_list + PRIME_NUM;
const size_t* pos = mystl::lower_bound(first, last, n);
return pos == last ? *(last - 1) : *pos;
}
我们还是来看一下扩容的操作,首先默认构造,调用next_size
得到桶数组大小101u
。
explicit hashtable(size_type bucket_count, const Hash& hash = Hash(), const KeyEqual& equal = KeyEqual()) : size_(0), mlf_(1.0f), hash_(hash), equal_(equal)
{
init(bucket_count);
}
template <class T, class Hash, class KeyEqual>
void hashtable<T, Hash, KeyEqual>::init(size_type n)
{
const auto bucket_nums = next_size(n);
try
{
buckets_.reserve(bucket_nums);
buckets_.assign(bucket_nums, nullptr);
}
catch (...)
{
bucket_size_ = 0;
size_ = 0;
throw;
}
bucket_size_ = buckets_.size();
}
先看这个插入元素,节点键值允许重复的,先要查看是否超过最大桶装载比例,
iterator insert_multi(const value_type& value)
{
rehash_if_need(1);
return insert_multi_noresize(value);
}
template <class T, class Hash, class KeyEqual>
void hashtable<T, Hash, KeyEqual>::rehash_if_need(size_type n)
{
if (static_cast<float>(size_ + n) > (float)bucket_size_ * max_load_factor())
{
rehash(size_ + n);
}
}
如果超过了就需要扩容,先申请一个新的桶数组,重新对元素进行一遍哈希,插入到新的桶,最后swap
一下。
template <class T, class Hash, class KeyEqual>
void hashtable<T, Hash, KeyEqual>::rehash(size_type count)
{
auto n = ht_next_prime(count);
if (n > bucket_size_) // 扩大
{
replace_bucket(n);
}
else // 缩减
{
if ((float)size_ / (float)n < max_load_factor() - 0.25f && (float)n < (float)bucket_size_ * 0.75) // 值得 rehash
{
replace_bucket(n);
}
}
}
template <class T, class Hash, class KeyEqual>
void hashtable<T, Hash, KeyEqual>::replace_bucket(size_type bucket_count)
{
bucket_type bucket(bucket_count); // 申请新的桶数组
if (size_ != 0)
{
for (size_type i = 0; i < bucket_size_; ++i) // 处理每个桶
{
for (auto first = buckets_[i]; first; first = first->next) // 处理桶上的每个链表节点
{
auto tmp = create_node(first->value);
const auto n = hash(value_traits::get_key(first->value), bucket_count);
auto f = bucket[n];
bool is_inserted = false;
for (auto cur = f; cur; cur = cur->next)
{
if (is_equal(value_traits::get_key(cur->value), value_traits::get_key(first->value)))
{
tmp->next = cur->next;
cur->next = tmp;
is_inserted = true;
break;
}
}
if (!is_inserted)
{
tmp->next = f;
bucket[n] = tmp;
}
}
}
}
buckets_.swap(bucket);
bucket_size_ = buckets_.size();
}
扩容完成就可以直接插入了,这边允许值重复,所有搜索到一样的值直接插入就好了。
template <class T, class Hash, class KeyEqual>
typename hashtable<T, Hash, KeyEqual>::iterator hashtable<T, Hash, KeyEqual>::insert_multi_noresize(const value_type& value)
{
const auto n = hash(value_traits::get_key(value));
auto first = buckets_[n];
auto tmp = create_node(value);
for (auto cur = first; cur; cur = cur->next)
{
if (is_equal(value_traits::get_key(cur->value), value_traits::get_key(value))) // 如果链表中存在相同键值的节点就马上插入,然后返回
{
tmp->next = cur->next;
cur->next = tmp;
++size_;
return iterator(tmp, this);
}
}
// 否则插入在链表头部
tmp->next = first;
buckets_[n] = tmp;
++size_;
return iterator(tmp, this);
}
如果是不允许重复插入,找到相同的就直接返回了。
template <class T, class Hash, class KeyEqual>
pair<typename hashtable<T, Hash, KeyEqual>::iterator, bool> hashtable<T, Hash, KeyEqual>::insert_unique_noresize(const value_type& value)
{
const auto n = hash(value_traits::get_key(value));
auto first = buckets_[n];
for (auto cur = first; cur; cur = cur->next) // 查找链表是否有键值相同的
{
if (is_equal(value_traits::get_key(cur->value), value_traits::get_key(value)))
{
return mystl::make_pair(iterator(cur, this), false);
}
}
// 让新节点成为链表的第一个节点
auto tmp = create_node(value);
tmp->next = first;
buckets_[n] = tmp;
++size_;
return mystl::make_pair(iterator(tmp, this), true);
}
最后看一下哈希表的查找,理论上时间复杂度平均是O(1),这边直接hash
之后就能找到对应的桶,然后就是看桶挂在了多少个节点了。
template <class T, class Hash, class KeyEqual>
typename hashtable<T, Hash, KeyEqual>::iterator hashtable<T, Hash, KeyEqual>::find(const key_type& key)
{
const auto n = hash(key);
node_ptr first = buckets_[n];
for (; first && !is_equal(value_traits::get_key(first->value), key); first = first->next) {}
return iterator(first, this);
}
unordered_set、unordered_multiset、unordered_map和unordered_multimap
这四个都是配接器,底层容器使用的hashtable,所以都是无序的。只是做了接口的转换,可以自己看源码。