哈希+解决哈希冲突的方法(哈希表,二次探测,哈希桶)(介绍+模拟实现)

目录

unordered系列关联式容器

介绍

哈希(散列)

引入

顺序结构/平衡树中

元素关键码

理想的搜索方法

介绍

哈希值特点

哈希函数/算法

插入元素

搜索元素

分类

哈希表 

介绍

哈希冲突/碰撞 

同义词

解决方法 

闭散列/开放定址法

介绍

插入元素

搜索元素

删除元素

为什么会有删除状态呢?

扩容

二次探测 

介绍

优点

模拟实现

注意点

string的处理

模板

哈希表元素类型

扩容

代码

开散列/开链法

介绍

插入元素

搜索元素

删除元素

扩容

模拟实现

注意点

代码


unordered系列关联式容器

介绍

虽然红黑树效率很高,但数据量达到一定程度时,查找效率也不够理想

所以就有了unordered系列的容器

  • 它们提供了快速的查找操作,平均时间复杂度为O(1)
  • 但是它们不会维护元素的顺序,因此不适合需要按顺序访问元素的情况

而unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构,就像set和map的底层是红黑树一样

哈希(散列)

引入

顺序结构/平衡树中

  • 元素关键码与其存储位置之间没有对应的关系
  • 因此在查找一个元素时,必须要经过关键码的多次比较
  • 顺序查找时间复杂度为O(N)
  • 平衡树中为树的高度,即 O(logN) ,他可以通过结构特点,将要比较的元素个数,压缩到了高度次
  • 他们搜索的效率取决于搜索过程中元素的比较次数
元素关键码

元素的关键码(key)是数据结构中用于标识和访问元素的重要属性或值

关键码通常用于确定元素在数据结构中的位置,以便能够对元素进行查找、插入、删除等操作

理想的搜索方法

可以不经过任何比较,一次直接从表中得到要搜索的元素(也就是 元素的存储位置与它的关键码之间建立一一 映射的关系)

如果构造一种存储结构,通过某种函数(hashFunc),可以实现这种功能
那么在查找时,通过该函数可以很快找到该元素
这种方法,也就是哈希方法

介绍

哈希是计算机科学中的一个重要概念

是一种将 任意大小的输入数据 映射为 固定大小的哈希值的技术

哈希值特点

  • 通常是一个整数或二进制序列

  • 唯一性:不同的输入数据应该映射到不同的哈希值,确保哈希值能够唯一标识输入数据。(但不能保证一个哈希值不能对应多个数据)

  • 一致性:相同的输入数据始终映射到相同的哈希值,保证数据的一致性

  • 高效性:计算哈希值的过程应该是高效的,通常在常量时间内完成,以便在实际应用中能够快速执行

  • 不可逆性:从哈希值不能反推出原始输入数据,确保数据的安全性

哈希函数/算法

定义了如何将输入数据转换为哈希值,并满足哈希函数的各种特性

插入元素

根据待插入元素的关键码,用哈希函数计算出该元素的存储位置,并按此位置进行存放

搜索元素

对元素的关键码进行同样的计算,把求得的哈希值作为元素的存储位置

分类

哈希表 

介绍

大概就是:在一个数组中,用该数组可使用空间的大小作为除数,余数就是该元素存放位置的下标

但是,如果此时,有11插入,他就会被放在2的位置

因为1坑位已经被占用了,但总不能不让他插入吧,所以该怎么办呢?

哈希冲突/碰撞 

上面提到的,当1已经有元素时,又有一个11插入进来,这种情况就被叫做哈希冲突/碰撞

也就是 -- 不同关键字通过相同的哈希函数,计算出相同的哈希地址

  • 通过设计更好的哈希函数,可以让产生哈希冲突的可能性降低
  • 但是无法避免哈希冲突

同义词

这些具有不同关键码,而具有相同哈希地址的数据元素

解决方法 

(毕竟不能一冲突代码就罢工了吧)

闭散列和开散列

闭散列/开放定址法

介绍
当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的下一个空位置中去
以上方法也被叫做"线性探测法"
插入元素
  • 根据待插入元素的关键码,用哈希函数计算出该元素的存储位置,并按此位置进行存放
  • 如果该位置已有元素(因为无法真正实现哈希值和数据一对一,所以一个哈希值可以对应多个数据),就往后寻找空位
搜索元素
  • 对元素的关键码进行同样的计算,把求得的哈希值作为元素的存储位置
  • 在结构中按此位置取元素比较,若关键码相等,则搜索成功
  • 如果该位置不是,也是一样,继续往后寻找,直到遍历完整个结构
删除元素

不能简单的将元素删除

  • 比如,如果用vector的erase,就会让后面的数据往前挪一个位置,但是这样势必会影响那些数据的搜索!
  • 要记住,哈希里面,位置和数据是有关联的, 不能随意挪动!!!

也不能将该数据赋值为某个值

  • 比如,我们平常一般就可能把这个值赋值为0/-1,但是,如果真的有0/-1的元素该怎么办,这样就会产生混淆
  • 所以要额外设置一个状态变量,其中有三种状态,存在/空/删除
  • 为什么会有删除状态呢?
  • 如果只有存在/空状态,那么删除元素后,该位置状态就被设置为空
  • 会导致该位置之后的位置无法被搜索到,因为搜索是遇到空就停下的!
  • 所以,存在三种状态是必要的
扩容

还记得我们是用数组来组织这些元素的吗?所以,我们一般使用vector作为底层

  • 但是!
  • vector中的size是实际插入的元素个数,capacity虽然是容量,但比size多的那部分我们是没法直接使用的!
  • 因为vector的[ ]内部是会检查传入下标的,一定要<size()才行
  • 所以,我们得让size()充当容量,于是,resize成为我们的最佳选择,通过resize,我们就可以让size成为实际的"容量",而在哈希表内部再定义一个变量,作为实际的元素个数

  • 当元素个数达到一定程度时,我们必须得扩容了(因为,如果哈希表太满,一定会让插入时的冲突增大,最后搜索元素的复杂度很可能会变成O(N),这不符合我们的预期,所以,一定要扩容!而且不能满时再扩!!!)
  • 我们这里使用0.7作为载荷因子
  • 而且扩容可以重新分配元素的位置,以前冲突的元素,可能有一部分就不冲突了
  • 所以,为了减少冲突的元素,将元素个数=存在的元素+已删除的元素,因为插入元素就有可能会导致后面的元素产生冲突,删除后,这个冲突依然存在,所以,扩容的判断也要包括删除的元素
二次探测 

线性探测会让冲突越堆越多,因为它占用了别人的坑,所以就有其他方法的出现,比如二次探测

介绍
  • 当发生哈希冲突时,不仅线性地查找下一个可用位置,还使用二次探测来计算下一个桶的位置
  • 首先计算一个初始位置(也就是哈希值)
  • 如果初始位置已经被占用,则计算下一个位置(它不是简单地加一个固定的偏移量,而是使用一个二次函数来计算下一个位置)
  • 这个二次探测函数中的i会逐渐增加,直到找到一个空的桶,然后将键值对插入到该桶中。
优点

能够减少桶之间的聚集,因为不同键的冲突通常会导致它们在哈希表中分布均匀

然而,它仍然可能在高负载时产生聚集,因此仍然需要适当的哈希表大小和负载因子控制

模拟实现
注意点
string的处理
  • 整型家族可以直接计算出哈希值,但是, string是无法被直接计算的

-> 所以,要对string做出特殊处理

  • 如果仅仅是将首字符作为哈希值的话,非常容易冲突

-> 所以可以将整个字符串的字符都加起来

  • 但是这样也不够,字符加起来也很容易撞(比如同样字符不同顺序)

-> 所以,每次相加前可以将这个数字*某个数字,这样可以避免出现不同顺序的问题

模板
  • 为了让这个哈希表具有泛性,所以采用仿函数
  • 直接将传入的不同类型的参数转换成整型值,而不是在insert内部转换
  • 在其内部直接把得到的值转成哈希值就行
哈希表元素类型

因为unordered系列的set和map底层是哈希表,所以类比红黑树版本的set和map,哈希表也应该是key-value版本的

这里就直接将pair<K,V>作为元素类型

扩容

这里扩容时,可以选择复用insert代码(也就是创建一个新对象,将已有元素插入到新对象,然后和自己互换即可)

代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;

// 哈希表
namespace my_hash_table
{
    // func用于拿到数据对应的整型值->然后可以得到对应的hashi
    template <class T>
    class HSFunc
    {
    public:
        size_t operator()(const T& val)
        {
            return val;
        }
    };
    template <>
    class HSFunc<string>
    {
    public:
        size_t operator()(const string& s)
        {
            int size = s.size();
            unsigned int seed = 131; // 31 131 1313 13131 131313 都可以
            unsigned int hashi = 0;
            for (size_t i = 0; i < size; ++i)
            {
                hashi = hashi * seed + s[i];
            }
            return hashi;
        }
    };

    enum State
    {
        EMPTY,
        EXIST,
        DELETE
    };

    template <class K, class V, class HF = HSFunc<K>>
    class HashTable
    {
        struct Elem
        {
            pair<K, V> _val;
            State _state;
        };
        typedef HashTable<K, V> Self;

    public:
        HashTable(size_t capacity = 5)
            : _ht(capacity), _size(0), _totalSize(0)
        {
            for (size_t i = 0; i < capacity; ++i)
            {
                _ht[i]._state = EMPTY;
            }
        }

        // 插入
        bool Insert(const pair<K, V>& val)
        {
            if (Find(val.first) != -1)
            {
                return false;
            }
            if (CheckCapacity()) // 设置当因子超过0.7就进行扩容
            {
                size_t newsize = _ht.size() * 2;
                Self newht(newsize);
                for (size_t i = 0; i < _ht.size(); ++i)
                {
                    if (_ht[i]._state == EXIST)
                    {
                        newht.Insert(_ht[i]._val);
                    }
                }
                Swap(newht);
            }

            size_t hashi = HashFunc(val.first);
            while (_ht[hashi]._state == EXIST)
            {
                ++hashi;
                hashi %= _ht.size();
            }
            _ht[hashi]._val = val;
            _ht[hashi]._state = EXIST;
            ++_size;
            ++_totalSize;
            return true;
        }

        // 查找
        size_t Find(const K& key)
        {
            size_t hashi = HashFunc(key);
            for (size_t i = 0; i < _size; ++i)
            {
                if (_ht[i]._state == EXIST && _ht[i]._val.first == key)
                {
                    return i;
                }
            }
            return -1;
        }

        // 删除
        bool Erase(const K& key)
        {
            size_t i = Find(key);
            if (i == -1)
            {
                return false;
            }
            else
            {
                _ht[i]._state = DELETE;
                --_size;
                return true;
            }
        }

        size_t Size() const
        {
            return _size;
        }

        bool Empty() const
        {
            return _size == 0;
        }

        void Print()
        {
            for (size_t i = 0; i < _ht.size(); ++i)
            {
                if (_ht[i]._state != EXIST)
                {
                    continue;
                }
                else
                {
                    Elem cur = _ht[i];
                    printf("[%d]->", i);
                    cout << cur._val.first << ":" << cur._val.second << endl;
                }
            }
            cout << endl;
        }

        void Swap(Self& ht)
        {
            swap(_size, ht._size);
            swap(_totalSize, ht._totalSize);
            _ht.swap(ht._ht);
        }

    private:
        size_t HashFunc(const K& key)
        {
            HF hf;
            return hf(key) % _ht.size();
        }

        bool CheckCapacity()
        {
            if (_totalSize * 10 / _ht.size() >= 7)
            {
                return true;
            }
            return false;
        }

    private:
        vector<Elem> _ht;
        size_t _size;
        size_t _totalSize; // 哈希表中的所有元素:有效和已删除, 扩容时候要用到
    };
}

开散列/开链法

介绍

是哈希表的一种常见实现方式

特点是在哈希表的每个位置维护一个链表或其他数据结构,用于存储具有相同哈希值的键值对

这样的话,冲突的数据就不会影响别的元素了

插入元素

还是一样的,先算出哈希值

  • 如果该位置是空,就直接赋值
  • 如果不为空,直接头插即可(这里使用单链表,因为不需要来回找,只需要一次遍历即可)
  • 这样就让冲突的元素都在自己应该的位置上,而不会污染其他位置
搜索元素
  • 把求得的哈希值作为元素的存储位置
  • 然后在该位置上的链表进行遍历
删除元素
  • 这里的删除就变成了单链表的删除
  • 因为实际上元素之间的组织方式是链表
扩容
  • 虽然引入链表之后,解决了会不断累积的哈希冲突
  • 但随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响哈希表的性能
  • 所以,还是需要扩容,然后让位置上挂的结点分散一点
  • 扩容的话,和闭散列不同,它可以直接挂结点
  • 用新设置的容量计算出哈希值,将原对象的结点直接复用,头插到新对象里就行
模拟实现
注意点

基本上其实和闭散列一样,只是组织数据的方式不同

也没啥注意点,只要方法对就行

代码
namespace my_hash
{
    // func用于拿到数据对应的整型值->然后可以得到对应的hashi
    template <class T>
    class HashFunc
    {
    public:
        size_t operator()(const T& val)
        {
            return val;
        }
    };
    template <>
    class HashFunc<string>
    {
    public:
        size_t operator()(const string& s)
        {
            int size = s.size();
            unsigned int seed = 131; // 31 131 1313 13131 131313 都可以
            unsigned int hashi = 0;
            for (size_t i = 0; i < size; ++i)
            {
                hashi = hashi * seed + s[i];
            }
            return hashi;
        }
    };


    template <class V>
    struct HashBucketNode // 每个位置下链接的结点
    {
        HashBucketNode(const V& data)
            : _next(nullptr), _data(data)
        {
        }
        HashBucketNode<V>* _next;
        V _data;
    };


    // 这里的哈希桶中key是唯一的
    template <class V, class HF = HashFunc<V>>
    class HashBucket // 哈希桶
    {
        typedef HashBucketNode<V> Node;
        typedef Node* PNode;


        typedef HashBucket<V, HF> Self;


    public:
        HashBucket(size_t capacity)
            : _size(0)
        {
            _table.resize(capacity, nullptr);
        }


        ~HashBucket()
        {
            Clear();
        }


        // 哈希桶中的元素不能重复
        PNode Insert(const V& data)
        {
            if (Find(data))
            {
                return nullptr;
            }
            if (CheckCapacity()) // 需要扩容了
            {
                size_t newsize = _size * 2;
                HashBucket<V> newhsb(newsize);


                for (size_t i = 0; i < _size; ++i)
                {
                    PNode cur = _table[i];
                    while (cur) // 把桶上的结点挂在新位置
                    {
                        PNode next = cur->_next;
                        size_t hashi = HashFunc(cur->_data, newsize);


                        cur->_next = newhsb._table[hashi];
                        newhsb._table[hashi] = cur;


                        cur = next;
                    }
                    _table[i] = nullptr;
                }
                Swap(newhsb);
            }
            PNode newnode = new Node(data);
            size_t hashi = HashFunc(data, _table.size());
            // 头插
            newnode->_next = _table[hashi];
            _table[hashi] = newnode;
            ++_size;
            return newnode;
        }


        // 删除哈希桶中为data的元素(data不会重复)
        bool Erase(const V& data)
        {
            PNode del = Find(data);
            if (del == nullptr) // 如果没找着
            {
                return false;
            }

            size_t hashi = HashFunc(data, _table.size());
            PNode prev = _table[hashi];
            if (prev == del) // 如果删除的是第一个结点
            {
                _table[hashi] = del->_next;
            }
            else
            {
                while (prev && prev->_next->_data != data) // 找到上一个结点
                {
                    prev = prev->_next;
                }
                prev->_next = del->_next;
            }
            delete del;
            del = nullptr;
            --_size;
            return true;
        }


        PNode Find(const V& data)
        {
            size_t hashi = HashFunc(data, _table.size());
            PNode cur = _table[hashi];
            while (cur)
            {
                if (cur->_data == data)
                {
                    return cur;
                }
                cur = cur->_next;
            }
            return nullptr;
        }


        size_t Size() const
        {
            return _size;
        }


        bool Empty() const
        {
            return 0 == _size;
        }


        void Print()
        {
            for (size_t i = 0; i < _table.size(); ++i)
            {
                PNode cur = _table[i];
                printf("[%d]:", i);
                while (cur)
                {
                    PNode next = cur->_next;
                    cout << cur->_data << " ";
                    cur = next;
                }
                cout << endl;
            }
            cout << endl;
        }


        void Clear()
        {
            for (size_t i = 0; i < _table.size(); ++i)
            {
                PNode cur = _table[i];
                while (cur)
                {
                    PNode next = cur->_next;
                    delete cur;
                    cur = next;
                }
                _table[i] = nullptr;
            }
        }


        size_t BucketCount() const
        {
            return _table.size();
        }


        void Swap(Self& ht)
        {
            _table.swap(ht._table);
            swap(_size, ht._size);
        }


    private:
        size_t HashFunc(const V& data, size_t size) // 将数据转换成hashi
        {
            HF hf;
            return hf(data) % size;
        }


        bool CheckCapacity()
        {
            if (_size == _table.size())
            {
                return true;
            }
            else
            {
                return false;
            }
        }


    private:
        vector<PNode> _table;
        size_t _size; // 哈希表中有效元素的个数
    };
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是使用二次探测解决哈希冲突实现操作的C语言代码: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #define TABLE_SIZE 11 typedef struct { char* key; char* value; } entry; entry* table[TABLE_SIZE]; int hash(char* key) { int hash_value = 0; for (int i = 0; i < strlen(key); i++) { hash_value += key[i]; } return hash_value % TABLE_SIZE; } void insert(char* key, char* value) { int index = hash(key); int i = 1; while (table[index] != NULL) { index = (hash(key) + i*i) % TABLE_SIZE; i++; } entry* new_entry = (entry*) malloc(sizeof(entry)); new_entry->key = key; new_entry->value = value; table[index] = new_entry; } char* search(char* key) { int index = hash(key); int i = 1; while (table[index] != NULL) { if (strcmp(table[index]->key, key) == 0) { return table[index]->value; } index = (hash(key) + i*i) % TABLE_SIZE; i++; } return NULL; } int main() { insert("apple", "red"); insert("banana", "yellow"); insert("grape", "purple"); printf("%s\n", search("apple")); printf("%s\n", search("banana")); printf("%s\n", search("grape")); printf("%s\n", search("orange")); return 0; } ``` 该代码中, `entry` 结构体表示哈希表中的每个条目,包含键和值。哈希表是一个指针数组,每个指针指向一个 `entry` 结构体。`hash` 函数根据键计算哈希值。`insert` 函数使用二次探测解决哈希冲突,并将新的 `entry` 结构体插入哈希表中。`search` 函数根据键在哈希表中查对应的值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值