【C++】哈希表的改造——unordered_map和unordered_set的模拟实现(上)

简介: 【C++】哈希表的改造——unordered_map和unordered_set的模拟实现(上)

1. unordered系列的容器封装


在C++11中,增加了unordered系列的容器,其底层就是哈希原理。在之前的博客内容中,我们实现了哈希的代码部分,包括闭散列和开散列两种。由于闭散列的局限性,所以C++11标准库是采用开散列的方式封装了unordered系列容器,接下来我们将使用之前实现的**哈希桶(开散列)**代码对unordered系列的容器进行改写与封装。


1.1 改造1:模版参数类型的改造

1.1.1 HashNode改造

//改造前
template<class K, class V>
struct HashNode
{
    std::pair<K, V> _kv;
    HashNode* _next;
    HashNode(const std::pair<K, V>& kv)
        :_kv(kv)
          ,_next(nullptr)
    {}
};


由于unordered_set是K模型,而unordered_map是KV模型,为了让底层的哈希桶能够同时支持两种不同的模型,所以这里需要对哈希节点进行改造,改造后的结果如下:

//改造后
template<class T>//将模板参数变成T,对于K模型来说,传入的类型就是<Key,Key>键值对,对于KV模型来说,传入的就是<Key,Value>键值对
struct HashNode
{
    T _data;
    HashNode* _next;
    HashNode(const T& data)
      :_data(data)
        ,_next(nullptr)
      {}
};


1.1.2 HashTable改造

//改造前
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
    typedef HashNode<K, V> Node;
public:
    bool Insert(const std::pair<K,V>& kv);
    bool Erase(const K& key);
    Node* Find(const K& key);
private:
    std::vector<Node*> _tables;
    size_t _n = 0;
}


这里由于节点的模板类型已经更改,所以之前的KV也需要进行更改,同时传入仿函数KeyOfT用来从键值对T中提取出key。

//修改后
template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashTable
{
    typedef HashNode<T> Node;//这里节点传入的类型是T
public:
    HashTable()
        :_n(0)
    {
    _tables.resize(10);
    }
    bool Insert(const T& data);
    bool Erase(const K& key);
    Node* Find(const K& key);
private:
    std::vector<Node*> _tables;
    size_t _n = 0;
};


1.2 改造2:迭代器的增加与封装

在之前使用红黑树封装map&set的时候已经说明,我们对于map&set的迭代器的处理,使用的是红黑树实现的迭代器。所以这里同样的,使用的是哈希桶封装的迭代器,那么我们首先就改写哈希桶

1.2.1 迭代器类的实现

和红黑树的迭代器一样,原生指针不能支持迭代器行为,所以我们需要自己手动实现一个迭代器类,然后进行一些运算符重载。

在迭代器类的实现中,最重要的一个运算符就是++运算符的重载。

template<class _K, class _T, class _KeyOfT, class _Hash = HashFunc<_K>>
class HashTable;//由于在迭代器类里面使用了HashTable,同时在HashTable中也使用了迭代器类,所以需要进行提前声明类模板
template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
struct __HTIterator
{
    typedef HashTable<K,  T, KeyOfT, Hash> HT;
    typedef __HTIterator<K,T,KeyOfT,Hash> Self;
    typedef HashNode<T> Node;
    Node* _node;
    HT* _ht;
    __HTIterator(Node* node, HT* ht)
        :_node(node)
        ,_ht(ht)
    {}
    //这里的思路是按照table的顺序遍历哈希桶,在哈希桶里面单向遍历桶内的每个数据
    Self& operator++()
    {
        if(_node->_next)//当前桶还有元素
        {
            _node = _node->_next;
        }
        else//当前桶走完了,找下一个桶
        {
            size_t hashi = Hash()(KeyOfT()(_node->_data)) % _ht->_tables.size();//找到当前桶的哈希地址
            //找到下一个有元素的哈希桶对应的哈希地址,并将其第一个元素赋值给node
            ++hashi;
            while(hashi < _ht->_tables.size())
            {
                if(_ht->_tables[hashi])
                {
                    _node = _ht->_tables[hashi];
                    break;
                }
                else
                {
                    ++hashi;
                }
            }
            //后面没有桶了
            if(hashi == _ht->_tables.size())
            _node = nullptr;
        }
        return *this;
    }
    //其他的重载在之前的文章中有详细说明,这里道理是一样的,就不再赘述了
    T& operator*()
    {
        return _node->_data;
    }
    T* operator->()
    {
        return &_node->_data;
    }
    bool operator!=(const Self& s)
    {
        return _node != s._node;
    }
    bool operator== (const Self& s)
    {
        return _node==s._node;
    }
};


注:

类模板的声明方法:template<class type_name> class class_name;

同时,这里需要调用HashTable中的私有成员,所以需要给出友元类。

友元类的声明方法:template<class type_name> friend class class_name

这里有一个点需要注意:

由于在clang下,使用同一个模版参数名会出现报错:Declaration of '模版参数名' shadows template parameter,所以本次实现时在迭代器类的模版参数前加上_以示区分(详细代码见后文)。


1.2.2 迭代器的封装

实现了迭代器类之后,就可以在HashTable中封装迭代器接口

typedef __HTIterator<K, T, KeyOfT, Hash> iterator;
//迭代器
iterator begin()
{
    //遍历,找到第一个有元素的桶
    for(size_t i = 0; i < _tables.size(); ++i)
    {
        if(_tables[i])
        {
            return iterator(_tables[i], this);
        }
    }
    return iterator(nullptr, this);
}
//这里采用空指针当作最后一个节点的下一个元素指针
iterator end()
{
    return iterator(nullptr, this);
}


1.3 改造3:insert的改写封装

//改造前
bool Insert(const std::pair<K,V>& kv)
{
    if(Find(kv.first))
        return false;
    if(_n == _tables.size())
    {
        std::vector<Node*> newTables;
        newTables.resize(2* _tables.size(), nullptr);
        for(size_t i = 0; i < _tables.size(); ++i)
        {
            Node* cur = _tables[i];
            while(cur)
            {
                Node* next = cur->_next;
                size_t hashi = Hash()(cur->_kv.first) % newTables.size();
                cur->_next = newTables[hashi];
                newTables[hashi] = cur;
                cur = next;
            }
            _tables[i] = nullptr;
        }
        _tables.swap(newTables);
    }
    size_t hashi = Hash()(kv.first) % _tables.size();
    Node* newnode = new Node(kv);
    newnode->_next = _tables[hashi];
    _tables[hashi] = newnode;
    ++_n;
    return true;
}


为了实现[]重载,所以这里和之前的map&set一样,需要对底层数据结构的insert进行改写,这里需要让返回值变成一个pair,其中的第一个成员是迭代器,第二个成员是原来的bool类型,所以代码如下

//改造后代码
std::pair<iterator, bool> Insert(const T& data)//这里将返回值修改为std::pair类型
{
    iterator it = Find(KeyOfT()(data));//使用仿函数从data中提取到key
    if(it != end())
        return make_pair(it, false);//如果当前对应的key存在,那么就返回当前key的迭代器类型和false组成的pair
    if(_n == _tables.size())
    {
        std::vector<Node*> newTables;
        newTables.resize(2* _tables.size(), nullptr);
        for(size_t i = 0; i < _tables.size(); ++i)
        {
            Node* cur = _tables[i];
            while(cur)
            {
                Node* next = cur->_next;
                size_t hashi = Hash()(KeyOfT()(cur->_data)) % newTables.size();//使用仿函数从data中提取到key
                cur->_next = newTables[hashi];
                newTables[hashi] = cur;
                cur = next;
            }
            _tables[i] = nullptr;
        }
        _tables.swap(newTables);
    }
    size_t hashi = Hash()(KeyOfT()(data)) % _tables.size();//使用仿函数从data中提取到key
    Node* newnode = new Node(data);
    newnode->_next = _tables[hashi];
    _tables[hashi] = newnode;
    ++_n;
    return make_pair(iterator(newnode, this), true);//使用newnode和this构造一个迭代器,将这个迭代器和插入情况构造成一个pair返回。
}


1.4 析构函数的实现

由于在构造或者插入的过程中使用new创建节点了,所以需要对new的资源进行手动释放,所以为了避免内存泄漏,需要进行析构函数的实现。

析构函数的实现原理就是:遍历哈希桶,依次释放节点

所以代码就显而易见了:

~HashTable()
{
    //遍历整个哈希表,依次释放节点
    for(size_t i = 0; i < _tables.size(); ++i)
    {
        Node* cur = _tables[i];
        while(cur)
        {
            Node* next = cur->_next;//保存下个节点,防止出现当前节点释放,找不到下个节点的情况
            delete cur;
            cur = next;
        }
        _tables[i] = nullptr;//将当前桶的指针置空,防止出现野指针的情况
    }
}


相关文章
|
4月前
|
C++ 容器
【C++】map和set封装
【C++】map和set封装
34 2
|
4月前
|
存储 C++ 容器
【C++】map和set深度讲解(下)
【C++】map和set深度讲解(下)
59 2
|
4月前
|
存储 C++ 容器
【C++】开散列实现unordered_map与unordered_set的封装
【C++】开散列实现unordered_map与unordered_set的封装
46 0
|
14天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
19 4
|
14天前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
16 4
|
13天前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
14 1
|
24天前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
24天前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
25天前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
51 1
|
26天前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
17 1