当前位置: 首页 > news >正文

38.C++哈希3(哈希表底层模拟实现 - 开散列拉链法和哈希桶)

⭐上篇文章:37.C++哈希2(哈希表底层分析与模拟实现-闭散列key模型与key-value模型)-CSDN博客

⭐本篇代码:c++学习/20.哈希与哈希表 · 橘子真甜/c++-learning-of-yzc - 码云 - 开源中国 (gitee.com)

⭐标⭐是比较重要的部分

目录

一. 拉链法与哈希桶

 二. 开散列哈希表代码实现

2.1 哈希节点

2.2 哈希表框架 

2.3 Insert

2.4 Print 

2.5 测试1

2.6 Find

2.7 Erase

三. 完整测试

3.1 key模型

​编辑 3.2 key-value模型


一. 拉链法与哈希桶

        哈希映射是有冲突的,闭散列通过探测的方式,如果发现自己映射的位置被抢占了,就去抢占其他空闲的位置,这会让哈希冲突变多,从而导致效率降低。

        而拉链法和哈希桶解决哈希冲突的方式是:将映射相同位置的所有节点以链表的方式连在一起,这样一来就不会去抢占其他人的位置!

        将具有相同地址映射的的节点归于一个子集,每一个子集称为一个哈希桶。桶中的元素通过链表或者其他方式连接起来,链表的头节点放在哈希表中。

 如下图,将相同冲突的元素通过链表进行连接。

 二. 开散列哈希表代码实现

2.1 哈希节点

        与闭散列不同,开散列的哈希节点不需要存放该节点的状态,而是需要存放一个next指针。只用指向下一个节点(如果使用红黑树等结构需要更多的指针)。

节点代码如下:

template <class T>
struct HashData
{
    T _data;
    HashData<T> *_next;

    // 构造函数
    HashData(const T &data = T())
        : _data(data), _next(nullptr) {}
};

2.2 哈希表框架 

        与闭散列一样,需要插入函数,删除函数,查找函数,遍历,以及获取当前数据的key值

        成员变量为:_num表示有效数据,_table表示哈希表,这里使用vector作为哈希桶,内部的数据类型的HashData*

代码如下:

// 使用KeyOfT来获取pair的key值
// unordered_set -> <Key,Key>
// unordered_map -> <Key,Value>
template <class K>
struct SetKeyOfT
{
    const K &operator()(const K &key)
    {
        return key;
    }
};

template <class K, class T>
struct MapKeyOfT
{
    const K &operator()(const pair<K, T> &kv)
    {
        return kv.first;
    }
};

//哈希表,默认为set(key模型)
template <class K, class T, class KeyOfT = SetKeyOfT<int>>
class OpenHashTable
{
    typedef ::HashData<T> HashData;

public:
    bool Insert(const T &data) {}

    HashData *Find(const K &key) {}

    bool Erase(const K &key) {}
    
    void Print() {}

private:
    vector<HashData *> _table;
    size_t _num = 0;
};

2.3 Insert⭐

        与闭散列一样,开散列也有一个负载因子,只要哈希表中的有效数据与哈希表的长度一样多时候。表明这个哈希表中的数据冲突已经达到了理想状态,此时就需要"扩容"。

        理想状态:每一个哈希桶桶只有一个数据,效率达到最高为O(1)。

        最糟状态:所有的有效数据都集中在一个哈希桶中,效率退化到O(N)。

        如何扩容,需要注意什么?扩容的时候需要遍历旧表,将旧表中的所有数据重新映射到新表中。由于取模的数字变大,每一次扩容都会降低哈希冲突!

        由于每一个哈希桶中都是链表,插入数据的时候采用头插法更简单!

代码如下: 

    bool Insert(const T &data)
    {
        KeyOfT koft{};

        // 1. 判断是否需要扩容
        if (_table.size() == 0 || _num * 100 / _table.size() > 75)
        {
            // 开辟新表,减少哈希冲突
            size_t newsize = (_table.size() == 0) ? 10 : _table.size() * 2;
            vector<HashData *> newtable;
            newtable.resize(newsize);

            // 遍历旧表,重新哈希映射到新表
            for (int i = 0; i < _table.size(); ++i)
            {
                // 判断每一个哈希桶,通过头插法插入到新表中
                HashData *cur = _table[i];
                while (cur != nullptr)
                {
                    HashData *next = cur->_next;
                    int index = koft(cur->_data) % newsize;

                    cur->_next = newtable[index];
                    newtable[index] = cur;
                    cur = next;
                }
                _table[i] = nullptr;
            }
            _table.swap(newtable); // 使用交换快速替换两张表,旧表直接不要了
        }
        // 2. 插入新的数据
        // 首先需要判断表中有无该数据
        int index = koft(data) % _table.size();
        HashData *cur = _table[index];
        while (cur != nullptr)
        {
            // 只需要判断key值即可,因为key-value。不同的key可能存在value相同的情况
            if (koft(cur->_data) == koft(data))
                return false;
            cur = cur->_next;
        }

        HashData *newnode = new HashData(data);
        newnode->_next = _table[index];
        _table[index] = newnode;

        ++_num; // 别忘了增加有效数据
        return true;
    }

2.4 Print 

        既然插入的代码写完了,这时候需要遍历来测试代码是否正确。遍历比较简单,只需要循环遍历哈希表,表头不为空遍历这个链表即可。

代码如下:这里只打印了data的key值


    void Print()
    {
        KeyOfT koft{};
        for (int i = 0; i < _table.size(); ++i)
        {
            HashData *cur = _table[i];

            std::cout << i << ": ";
            while (cur != nullptr)
            {
                std::cout << koft(cur->_data) << " -> ";
                cur = cur->_next;
            }
            std::cout << "nullptr" << std::endl;
        }
    }

2.5 测试1

        随机插入20个数据,打印哈希表中的内容。查看有无bug

测试代码如下:

#include <iostream>
#include "HashTable.h"
using namespace std;

void testset()
{
    OpenHashTable<int, int> ht;

    for (int i = 0; i < 20; i++)
    {
        ht.Insert(rand() % 100);
    }
    ht.Print();
}

int main()
{
    srand(time(0) ^ rand());
    testset();
    return 0;
}

测试结果如下:

可以看到,结果是正确的!

2.6 Find

        这个函数是用于查找某一个节点是否在哈希表中,不能去遍历查找,这样的效率是O(N)。而应该先求出该数据在哈希表中的映射位置,在这个哈希桶中查找。间效率提升至O(1)。


    HashData *Find(const K &key)
    {
        if (_table.empty())
            return nullptr;
        KeyOfT koft;
        size_t index = key % _table.size();
        HashData *cur = _table[index];
        while (cur != nullptr)
        {
            if (koft(cur->_data) == key)
                return cur;
            cur = cur->_next;
        }
        return nullptr;
    }

2.7 Erase⭐

        不可以通过Find查找然后去删除,因为桶中是链表,删除某一个节点之后。需要将该节点的前后节点连接起来!

        链表删除头节点,只需要将头节点的next节点赋值给头节点即可

 bool Erase(const K &key)
    {
        if (_table.empty())
            return false;
        KeyOfT koft;
        int index = key % _table.size();
        HashData *prev = nullptr;
        HashData *cur = _table[index];
        while (cur != nullptr)
        {
            if (koft(cur->_data) == key)
            {
                // 头节点就是删除的节点,需要将头节点置为next
                if (prev != nullptr)
                    prev->_next = cur->_next;
                else
                    _table[index] = cur->_next;

                delete cur;
                return true;
            }
            prev = cur;
            cur = cur->_next;
        }
        return false;
    }

三. 完整测试

3.1 key模型

#include <iostream>
#include "HashTable.h"
using namespace std;

void testset()
{
    OpenHashTable<int, int> ht;

    for (int i = 0; i < 15; i++)
    {
        ht.Insert(rand() % 100);
    }
    ht.Print();

    while (true)
    {
        cout << "删除数据输入1, 查找数据输入2, 退出输入0:";
        int n = 0;
        cin >> n;
        if (n == 0)
            break;
        else if (n == 1)
        {
            cout << "请输入删除的数字:";
            int num;
            cin >> num;

            bool flag = ht.Erase(num);
            if (flag)
            {
                cout << "删除成功,哈希表如下" << endl;
                ht.Print();
            }
            else
                cout << "没有这个数据,删除失败!" << endl;
        }
        else if (n == 2)
        {
            cout << "请输入查找的数字:";
            int num;
            cin >> num;

            HashData<int> *data = ht.Find(num);
            if (data == nullptr)
                cout << "这个数据不存在" << endl;
            else
                cout << "这个数据存在" << data->_data << endl;
        }
        else
        {
            cout << "非法输入" << std::endl;
            continue;
        }
    }
}

int main()
{
    srand(time(0) ^ rand());
    testset();
    return 0;
}

测试结果如下:

 3.2 key-value模型

        key-value模型需要修改print,让其打印kv键值对

    void PrintMap()
    {
        KeyOfT koft{};
        for (int i = 0; i < _table.size(); ++i)
        {
            HashData *cur = _table[i];

            std::cout << i << ": ";
            while (cur != nullptr)
            {
                std::cout << cur->_data.first << ":" << cur->_data.second << " -> ";
                cur = cur->_next;
            }
            std::cout << "nullptr" << std::endl;
        }
    }
#include <iostream>
#include "HashTable.h"
using namespace std;

void testset()
{
    OpenHashTable<int, pair<int, char>, MapKeyOfT<int, char>> ht;

    for (int i = 0; i < 15; i++)
    {
        int t = rand() % 26;
        ht.Insert(make_pair(i, (char)(i + 'a')));
    }
    ht.PrintMap();

    while (true)
    {
        cout << "删除数据输入1, 查找数据输入2, 退出输入0:";
        int n = 0;
        cin >> n;
        if (n == 0)
            break;
        else if (n == 1)
        {
            cout << "请输入删除的数字:";
            int num;
            cin >> num;

            bool flag = ht.Erase(num);
            if (flag)
            {
                cout << "删除成功,哈希表如下" << endl;
                ht.PrintMap();
            }
            else
                cout << "没有这个数据,删除失败!" << endl;
        }
        else if (n == 2)
        {
            cout << "请输入查找的数字:";
            int num;
            cin >> num;

            HashData<pair<int, char>> *data = ht.Find(num);
            if (data == nullptr)
                cout << "这个数据不存在" << endl;
            else
                cout << "这个数据存在" << data->_data.first << ":" << data->_data.second << endl;
        }
        else
        {
            cout << "非法输入" << std::endl;
            continue;
        }
    }
}

int main()
{
    srand(time(0) ^ rand());
    testset();
    return 0;
}

 测试结果如下:

以上已经基本完成了哈希表的简单实现,还需要改进哈希函数(字符串映射),迭代器实现,封装为unordered_set 与 unordered_map 

http://www.dtcms.com/a/98609.html

相关文章:

  • 异常与捕获
  • Android7 Input(二)Linux 驱动层输入事件管理
  • Protobuf 的快速使用(二)
  • SVTAV1函数分析-svt_av1_cost_coeffs_txb
  • (二)创建实例
  • 人工智能之数学基础:实对称矩阵
  • AI大模型最新发布[update@202503]
  • [Vue2]v-model用于表单
  • fio磁盘测试工具使用笔记
  • Appium中元素定位的注意点
  • springboot-Spring Boot DevTools工具的使用
  • VSCODE npm: 因为在此系统上禁止运行脚本。有关详细信息,请参阅 ...
  • 浏览器 ➔ 服务器or服务器 ➔ 浏览器:
  • 第二卷:海盐城血战(37-72回)正反人物群像
  • 第一篇:系统分析师首篇
  • DFS飞机降落
  • 《HelloGitHub》第 108 期
  • AUTOSAR_StbM_详解
  • 浅谈Thread类及常见方法与线程的状态(多线程编程篇2)
  • fetch`的语法规则及常见用法
  • Document对象的常用属性和方法
  • 蓝桥杯[每日一题] 真题:管道(java版)
  • tryhackme——Windows Local Persistence
  • std::reference_wrapper 和 std::function的详细介绍
  • MySQL数据库入门到大蛇尚硅谷宋红康老师笔记 高级篇 part13
  • 【QT】QT样式设计
  • openwrt24.10.0版本上安装istoreOS的屏幕监控插件
  • CentOS 安装 zip
  • 零基础入门多媒体音频(4)-GENIVIProjectAudioManager总览
  • gdb 调试mysql