Mihooke的技术专栏 C++ and Python Coder

hashtable和unordered_set unordered_map hash_set hash_map


hashtable叫散列表,又叫哈希表,这种数据结构主要提供了常数平均时间的查找插入删除操作。

这里和二叉搜索树(binary search tree)做个对比,二叉搜索树是对数平均时间,但有一个假设:数据的输入必须有足够的随机性,否则的话,二叉树就不平衡了,会导致平方时间。

hashtable的关键有两个因素:表大小是一个素数;一个好的散列函数(hash function)。素数可以保证插入元素时的覆盖率(主要针对平方探测法);散列函数提供了无限输入到有限单元的映射,从这个角度上看,hashtable可看作是一个字典(map),因为hashtable大小固定,因此在插入元素时会发生碰撞(即两个不同元素落在同一位置),常用的解决办法是:线性探测法平方探测法分离链表法

在介绍这几个办法之前,先认识一个术语:填装因子(loading factor),它是指表中元素个数与表大小的比,即散列表中装的元素比例。

  • 线性探测:当发生碰撞时,往后挨个遍历表,直到找到空闲位置。

  • 平方探测:当发生碰撞时,按照1,3,5……2n-1 规则,直到找到空闲位置。

这两种方法要想取得好的效果,都要求填装因子<0.5,即表中元素数量少于一半,否则大量时间会花在遍历元素探测上。

  • 分离链表:它是一个链表数组,当发生碰撞时,附加到链表后面,这种做法填装因子为1,主要是链表分配元素消耗时间。

线性探测/平方探测和分离链表各有优劣,可根据实际来选择使用,当内存紧张时,可选用分离链表;当考虑运行速度,可选用平方探测。在使用平方探测时,若填装因子>0.5时,其效率会降低,这时候需要进行再散列,具体做法是:分配更大的空间,将元素重新散列进去。

C++11之前不同平台可能有hash_set/hash_map,其底层是hashtable,C++11标准模板库里引入了unordered_set,unordered_map,他们便是用hashtable来实现的,替代了hash_set/hash_map,当然,hash_set/hash_map目前在很多平台也还是提供了。

以下是平方探测法和分离链表法的实现。

bool isPrime(size_t value)
{
    for (int i = 2; i <= sqrt(value); ++i) {
        if (value % i == 0)
            return false;
    }
    return true;
}

size_t nextPrime(size_t value)
{
    while (!isPrime(++value))
        ;
    return value;
}

template<>
class hash<string>
{
public:
    size_t operator() (const string &value) const
    {
        size_t v = 0;
        for (auto ch : value)
            v = 37 * v + ch;
        return v;
    }
};

// 平方探测法
template<typename T>
class Hashtable
{
public:
    enum HashType {Empty, Active, Deleted};
    class HashEntry
    {
    public:
        T data;
        HashType info;
        HashEntry(const T &value=T{}, HashType t=Empty) : data(value), info(t) {}
    };

    Hashtable(size_t size) : _array(nextPrime(size))
    {
        makeEmpty();
    }
    bool isActive(int pos) const
    {
        return _array[pos].info == Active;
    }
    bool contains(const T &value) const
    {
        return isActive(findPos(value));
    }
    int findPos(const T &value) const
    {
        int currentPos = hashFunction(value);
        int offset = 1;
        while (_array[currentPos].info != Empty && _array[currentPos].data != value)
        {
            currentPos += offset;
            offset += 2;
            if (currentPos >= _array.size())
                currentPos -= _array.size();
        }
        return currentPos;
    }
    bool insert(const T &value)
    {
        int currentPos = findPos(value);
        if (isActive(currentPos)) return false;
        _array[currentPos] = {value, Active};
        if (++_size > _array.size())
            rehash();
        return true;
    }
    bool insert(T &&value)
    {
        int currentPos = findPos(value);
        if (isActive(currentPos)) return false;
        _array[currentPos] = {move(value), Active};
        if (++_size > _array.size() / 2)
            rehash();
        return true;
    }
    bool remove(const T &value)
    {
        int currentPos = findPos(value);
        if (!isActive(currentPos)) return false;
        _array[currentPos].info = Deleted;
        --_size;
        return true;
    }
    void rehash()
    {
        const vector<HashEntry> old = _array;
        _array.resize(nextPrime(old.size() * 2));
        makeEmpty();
        for(const HashEntry &entry : old)
        {
            if (entry.info == Active)
                insert(move(entry.data));
        }
    }
private:
    void makeEmpty()
    {
        for (auto &entry : _array)
            entry.info = Empty;
        _size = 0;
    }
    size_t hashFunction(const T &value)
    {
        static hash<T> hf;
        return hf(value) % _array.size();
    }
    vector<HashEntry> _array;
    size_t _size;
};
// 分离链表法
class Hashtable
{
public:
    Hashtable(size_t size) : _array(nextPrime(size))
    {
        makeEmpty();
    }
    bool contains(const T &value) const
    {
        int pos = hashFunction(value);
        list<T> &curList = _array[pos];
        return find(curList.cbegin(), curList.cend(), value) == curList.cend();
    }
    bool insert(const T &value)
    {
        if (contains(value)) return false;
        int pos = hashFunction(value);
        _array[pos].push_back(value);
        if (++_size > _array.size())
            rehash();
        return true;
    }
    bool insert(T &&value)
    {
        if (contains(value)) return false;
        int pos = hashFunction(value);
        _array[pos].push_back(move(value));
        if (++_size > _array.size())
            rehash();
        return true;
    }
    bool remove(const T &value)
    {
        if (contains(value)) return false;
        int pos = hashFunction(value);
        list<T> &curList = _array[pos];
        auto itr = find(curList.cbegin(), curList.cend(), value);
        curList.erase(itr);
        --_size;
        return true;
    }
    void rehash()
    {
        const vector<list<T>> old = _array;
        _array.resize(nextPrime(old.size() * 2));
        makeEmpty();
        for (auto &l : old)
        {
            for (auto &v : l)
                insert(move(v));
        }
    }
private:
    void makeEmpty()
    {
        for (list<T> &l : _array)
            l.clear();
    }
    size_t hashFunction(const T &value)
    {
        static hash<T> hf;
        return hf(value) % _array.size();
    }
    vector<list<T>> _array;
    size_t _size;
};

Similar Posts

上一篇 list和slist

Comments