[C++][数据结构][哈希表]详细讲解

news2024/11/26 16:39:57

目录

  • 1.哈希概念
  • 2.哈希冲突
  • 3.哈希函数
  • 4.哈希冲突解决
  • 5.闭散列
    • 1.何时扩容?如何扩容?
    • 2.线性探测
    • 3.二次探测
  • 6.开散列(哈希桶)
    • 1.概念
    • 2.开散列增容
    • 3.开散列思考
      • 只能存储key为整形的元素,其他类型怎么解决?
      • 除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?
    • 4.开散列与闭散列比较


1.哈希概念

  • 顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即 O(logN),搜索的效率取决于搜索过程中元素的比较次数
  • 理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素
  • 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素
  • 当向该结构中:
    • 插入元素
      • 根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
    • 搜索元素
      • 对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
    • 该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

2.哈希冲突

  • 对于两个数据元素的关键字k_i和k_j(i != j),有k_i != k_j,但有:Hash(k_i) == Hash(k_j)
    • 即:不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为 哈希冲突 或 哈希碰撞
  • 把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”

3.哈希函数

  • 引起哈希冲突的一个原因可能是:哈希函数设计不够合理
  • 哈希函数设计原则:
    • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
    • 哈希函数计算出来的地址能均匀分布在整个空间中
    • 哈希函数应该比较简单
  • 常见哈希函数:
    • 直接定址法 – (常用) --> 不存在哈希冲突
      • 取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
      • 优点:简单、均匀
      • 缺点:需要事先知道关键字的分布情况
      • 使用场景:适合查找比较小且连续的情况
    • 除留余数法(常用) --> 存在哈希冲突,重点解决哈希冲突
      • 设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数
      • 按照哈希函数:Hash(key) = key% p (p<=m),将关键码转换成哈希地址
    • 平方取中法 – (了解)
      • 假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
      • 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
      • 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
    • 折叠法 – (了解)
      • 折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址
      • 折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
    • 随机数法 – (了解)
      • 选择一个随机函数,取关键字的随机函数值为它的哈希地址
      • 即H(key) = random(key),其中 random为随机数函数
    • 数学分析法 – (了解) – 懒得介绍
  • 注意哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

4.哈希冲突解决

  • 解决哈希冲突两种常见的方法是:闭散列开散列

5.闭散列

  • 闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?

1.何时扩容?如何扩容?

  • 散列表的载荷因子定义为:α = 填入表中的元素个数 / 散列表的长度
    • α越大,表中元素越多,产生冲突概率越大
    • α越小,表明元素越少,产生冲突概率越小
    • 一般不要超过0.7~0.8
  • 什么时候扩容? --> 负载因子到一个基准值就扩容
    • 基准值越大,冲突越多,效率越低,空间利用率越高
    • 基准值越小,冲突越少,效率越高,空间利用率越低

2.线性探测

  • 线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止

    • 插入
      • 通过哈希函数获取待插入元素在哈希表中的位置

      • 如果该位置中没有元素则直接插入新元素

      • 如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素

        请添加图片描述

  • 删除

    • 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素,会影响其他元素的搜索
    • 比如删除元素4,如果直接删除掉,44查找起来可能会受影响
    • 因此线性探测采用标记的伪删除法来删除一个元素

3.二次探测

  • 线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找
  • 因此二次探测为了避免该问题,找下一个空位置的方法为:
    • H_i = (H_0 + i^2 ) % m 或者 H_i = (H_0 - i^2 ) % m (i = 1,2,3**…)**
    • H_0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小
  • 研究表明:
    • 表的长度为质数表载荷因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次
    • 因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容
  • 因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷
enum State
{
    EMPTY,
    EXIST,
    DELETE
};

template <class K, class V>
    struct HashData
    {
        pair<K, V> _kv;
        State _state = EMPTY;
    };

template <class K>
    struct HashFunc
    {
        size_t operator()(const K &key)
        {
            return (size_t)key;
        }
    };

template <> // 特化
struct HashFunc<string>
{
    size_t operator()(const string &key)
    {
        size_t val = 0;
        for (auto &ch : key)
        {
            val *= 131; // BKDR
            val += ch;
        }

        return val;
    }
};

template <class K, class V, class Hash = HashFunc<K>> // Hash允许用户自己提供HashFunc
    class HashTable
    {
        public:
        bool Insert(const pair<K, V> &kv)
        {
            if (Find(kv.first)) // 元素已存在则不插入
            {
                return false;
            }

            // 负载因子到了就扩容
            if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7) // 将载荷因子α定为 0.7
            {
                size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
                HashTable<K, V> newHT; // 构建一个新的HashTable对象,来进行映射逻辑
                newHT._tables.resize(newsize);

                // 旧表的数据映射到新表
                for (auto &e : _tables)
                {
                    if (e._state == EXIST) // 状态为存在则进行映射
                    {
                        newHT.Insert(e._kv);
                    }
                }

                _tables.swap(newHT._tables);
            }
            // 线性探测
            Hash hash;
            size_t hashi = hash(kv.first) % _tables.size(); // 哈希地址计算
            while (_tables[hashi]._state == EXIST)
            {
                ++hashi;
                hashi %= _tables.size(); // 防止已经遍历完vector,若遍历完,则回头
            }

            _tables[hashi]._kv = kv;
            _tables[hashi]._state = EXIST;
            ++_size;

            // 二次探测
            // Hash hash;
            // size_t start = hash(kv.first) % _tables.size();
            // size_t i = 0;
            // size_t hashi = start;

            // while (_tables[hashi]._state == EXIST)
            //{
            //  ++i;
            //  hashi = start + i * i; // 二次探测的哈希地址跳跃
            //  hashi %= _tables.size(); // 防止已经遍历完vector,若遍历完,则回头
            // }

            //_tables[hashi]._kv = kv;
            //_tables[hashi]._state = EXIST;
            //++_size;

            return true;
        }

        HashData<K, V> *Find(const K &key)
        {
            if (_tables.size() == 0)
            {
                return nullptr;
            }

            Hash hash;
            size_t hashi = hash(key) % _tables.size();
            while (_tables[hashi]._state != EMPTY)
            {
                if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key)
                {
                    return &_tables[hashi];
                }

                ++hashi;
                hashi %= _tables.size();
            }

            return nullptr;
        }

        bool Erase(const K &key)
        {
            HashData<K, V> *ret = Find(key);
            if (ret)
            {
                ret->_state = DELETE; // 标记删除即可
                --_size;
                return true;
            }
            else
            {
                return false;
            }
        }
        private:
        vector<HashData<K, V>> _tables;
        size_t _size = 0; // 存储有效数据的个数
    };

6.开散列(哈希桶)

1.概念

  • 开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中
    请添加图片描述

  • 从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素

2.开散列增容

  • 桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?
  • 开散列最好的情况是:每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容

3.开散列思考

  • 只能存储key为整形的元素,其他类型怎么解决?

  • 哈希函数采用处理余数法,被模的key必须要为整形才可以处理,此处提供将key转化为整形的方法

    • 利用仿函数
  • 除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?

4.开散列与闭散列比较

  • 应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销
  • 事实上:
    • 由于开放定址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7
    • 而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间
template <class K, class V>
    struct HashNode
    {
        pair<K, V> _kv;
        HashNode<K, V> *_next;

        HashNode(const pair<K, V> &kv)
            : _kv(kv)
                , _next(nullptr)
            {}
    };

template <class K>
    struct HashFunc
    {
        size_t operator()(const K &key)
        {
            return (size_t)key;
        }
    };

template <> // 特化
struct HashFunc<string>
{
    size_t operator()(const string &key)
    {
        size_t val = 0;
        for (auto &ch : key)
        {
            val *= 131; // BKDR
            val += ch;
        }

        return val;
    }
};

template <class K, class V, class Hash = HashFunc<K>>
    class HashTable
    {
        typedef HashNode<K, V> Node;
        public:
        ~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;
            }

            // vector本身不需要手动析构,析构函数会去自动调用所有成员变量的析构函数
        }

        inline size_t __stl_next_prime(size_t n) // STL中素数空间优化
        {
            static const size_t __stl_num_primes = 28;
            static const size_t __stl_prime_list[__stl_num_primes] =
            {
                53, 97, 193, 389, 769,
                1543, 3079, 6151, 12289, 24593,
                49157, 98317, 196613, 393241, 786433,
                1572869, 3145739, 6291469, 12582917, 25165843,
                50331653, 100663319, 201326611, 402653189, 805306457,
                1610612741, 3221225473, 4294967291};

            for (size_t i = 0; i < __stl_num_primes; ++i)
            {
                if (__stl_prime_list[i] > n)
                {
                    return __stl_prime_list[i];
                }
            }

            return -1;
        }

        bool Insert(const pair<K, V> &kv)
        {
            // 去重
            if (Find(kv.first))
            {
                return false;
            }

            Hash hash;
            // 负载因子到1就扩容
            if (_size == _tables.size())
            {
                // size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
                vector<Node *> newTables;
                // newTables.resize(newsize, nullptr);
                newTables.resize(__stl_next_prime(_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;
            ++_size;

            return true;
        }
        Node *Find(const K &key)
        {
            if (_tables.size() == 0)
            {
                return nullptr;
            }

            Hash hash;
            size_t hashi = hash(key) % _tables.size();
            Node *cur = _tables[hashi];

            while (cur)
            {
                if (cur->_kv.first == key)
                {
                    return cur;
                }

                cur = cur->_next;
            }

            return nullptr;
        }

        bool Erase(const K &key)
        {
            if (_tables.size() == 0)
            {
                return true;
            }

            Hash hash;
            size_t hashi = hash(key) % _tables.size();
            Node *prev = nullptr;
            Node *cur = _tables[hashi];

            while (cur)
            {
                if (cur->_kv.first == key)
                {
                    // 1.头删
                    // 2.中间删
                    if (prev == nullptr)
                    {
                        _tables[hashi] = cur->_next;
                    }
                    else
                    {
                        prev->_next = cur->_next;
                    }

                    delete cur;
                    --_size;

                    return true;
                }

                prev = cur;
                cur = cur->_next;
            }

            return false;
        }

        size_t Size()
        {
            return _size;
        }

        // 表的长度
        size_t TablesSize()
        {
            return _tables.size();
        }

        // 桶的个数
        size_t BucketNum()
        {
            size_t num = 0;
            for (auto &hashNode : _tables)
            {
                if (hashNode)
                {
                    ++num;
                }
            }

            return num;
        }

        size_t MaxBucketLength()
        {
            size_t maxLen = 0;

            for (auto &hashNode : _tables)
            {
                size_t len = 0;
                Node *cur = hashNode;

                while (cur)
                {
                    ++len;
                    cur = cur->_next;
                }

                if (len > maxLen)
                {
                    maxLen = len;
                }
            }

            return maxLen;
        }
        private:
        vector<Node *> _tables;
        size_t _size = 0; // 存储有效数据个数
    };

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1833596.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

新手如何入门Web3?

一、什么是Web3&#xff1f; Web3是指下一代互联网&#xff0c;它基于区块链技术&#xff0c;致力于将各种在线活动变得更加安全、透明和去中心化。Web3是一个广义的概念&#xff0c;涵盖了包括数字货币、去中心化应用、智能合约等在内的多个方面。它的主要特点包括去中心化、…

C++初学者指南第一步---5.介绍std::vector

C初学者指南第一步—5.介绍std::vector 目录 C初学者指南第一步---5.介绍std::vector1.初始化/访问2.添加元素3.Resizing调整大小4.在尾部删除元素5. 复制一直是深拷贝&#xff01; 注意std代表C标准库的命名空间&#xff0c;vector&#xff08;向量&#xff09;是标准库中的一…

Golang | Leetcode Golang题解之第162题寻找峰值

题目&#xff1a; 题解&#xff1a; func findPeakElement(nums []int) int {n : len(nums)// 辅助函数&#xff0c;输入下标 i&#xff0c;返回 nums[i] 的值// 方便处理 nums[-1] 以及 nums[n] 的边界情况get : func(i int) int {if i -1 || i n {return math.MinInt64}re…

关于在word中使用Axmath的报错的解决

介绍 Axmath是数学公式编辑器软件。官网如下。 AxMath/AxGlyph/AxCells (amyxun.com) 支持正版。 在word中使用Axmath 点击word中的“文件”→“选项”。 选择“加载项” 选择“word加载项” 在Axmath默认的安装目录如下&#xff1a; C:\Program Files (x86)\AxMathhao&am…

GPTZero:引领AI内容检测

随着人工智能技术的飞速发展,AI生成内容(AIGC)正在迅速改变我们获取和消费信息的方式。然而,AIGC的激增也带来了一系列挑战,尤其是在内容真实性和版权方面。正是在这样的背景下,一家由00后团队创立的公司——GPTZero,以其独特的AI检测工具,迅速崛起为行业的领军者。 一…

AWS Lambda + Flask 应用示例

前言 AWS Lambda 本身是一个以事件驱动的 Serverless 服务, 最简单的应用就是在入口函数中对接收到的事件/请求进行处理并返回响应. 对于像 Flask 这样的 Web 框架, 并不能直接在 Lambda 上提供服务, 不过我们可以借助 AWS Lambda Web Adapter 实现一个基于 Flask 框架的 Web …

SpringBoot配置第三方专业缓存技术jetcache远程缓存方案和本地缓存方案

JetCache 是一个基于 Java 的分布式缓存解决方案&#xff0c;旨在提供高性能和可扩展性。它支持多种后端存储&#xff0c;如 Redis、Hazelcast、Tair 等&#xff0c;可以作为应用程序的缓存层&#xff0c;有效地提升数据访问性能和响应速度。 JetCache 的主要特点包括&#x…

Elasticsearch过滤器(filter):原理及使用

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;C语言 &#x1f680;本系列文章为个人学习…

AWS——01篇(AWS入门 以及 AWS之EC2实例及简单实用)AWS

AWS——01篇&#xff08;AWS入门 以及 AWS之EC2实例及简单实用&#xff09; 1. 前言 2. 创建AWS账户 3. EC2 3.1 启动 EC2 新实例 3.1.1 入口 3.1.2 设置名称 选择服务 3.1.3 创建密钥对 3.1.4 网络设置——安全组 3.1.4.1 初始设置 3.1.4.2 添加安全组规则&#xff08;开放新…

【数据库编程-SQLite3(三)】Ubuntu下sqlite3的使用

学习分享 1、安装sqlite3命令2、sqlite3点命令3、在Linux命令行下&#xff0c;启动sqlite33.1、编写sql脚本3.2、脚本编写--DDL3.3、进入xxx.db数据库&#xff0c;读取脚本。3.4、再次查看数据库中的表。证明表创建成功。3.5、查看数据表中用户内容3.6、查看表结构3.7、在数据库…

Golang | Leetcode Golang题解之第164题最大间距

题目&#xff1a; 题解&#xff1a; type pair struct{ min, max int }func maximumGap(nums []int) (ans int) {n : len(nums)if n < 2 {return}minVal : min(nums...)maxVal : max(nums...)d : max(1, (maxVal-minVal)/(n-1))bucketSize : (maxVal-minVal)/d 1// 存储 (…

C#(C Sharp)学习笔记_多态【十九】

前言 个人觉得多态在面向对象编程中还比较重要的&#xff0c;而且不容易理解。也是学了一个下午&#xff0c;才把笔记写得相对比较完善&#xff0c;但仍欠缺一些内容。慢慢来吧…… 什么是多态&#xff1f; 基本概念 在编程语言和类型论中&#xff0c;多态&#xff08;Poly…

DAY3-力扣刷题

1.罗马数字转整数 13. 罗马数字转整数 - 力扣&#xff08;LeetCode&#xff09; 罗马数字包含以下七种字符: I&#xff0c; V&#xff0c; X&#xff0c; L&#xff0c;C&#xff0c;D 和 M。 字符 数值 I 1 V 5 X 10 L …

MySQL常见的命令

MySQL常见的命令 查看数据库&#xff08;注意添加分号&#xff09; show databases;进入到某个库 use 库; 例如&#xff1a;进入test use test;显示表格 show tables;直接展示某个库里面的表 show tables from 库&#xff1b; 例如&#xff1a;展示mysql中的表格 show tabl…

无广告、简单、实用的高性能 PDF 处理工具

一、简介 1、无广告、简单、实用的高性能 PDF 处理工具。它安装包大小在 240MB 左右&#xff0c;目前仅支持 Windows 平台。 二、下载 1、文末有下载链接,不明白可以私聊我哈&#xff08;麻烦咚咚咚&#xff0c;动动小手给个关注收藏小三连&#xff0c;我将继续努力为大家寻找以…

【Linux】进程_8

文章目录 五、进程10. 进程等待阻塞等待和非阻塞等待 11. 进程程序替换 未完待续 五、进程 10. 进程等待 上一篇我们知道了 wait 和 waitpid 函数都有一个 status 参数&#xff0c;这个参数是什么呢&#xff1f;这个参数其实就是进程的返回结果&#xff0c;当子进程结束的时候…

银行数仓项目实战(一)--什么是数据仓库

文章目录 数据仓库特点目的&#xff1a;监管报送监管报送的系统主要有&#xff1f;监管报送报送的数据 OLTP和OLAP 架构 数据仓库 数据仓库是一个面向主题的&#xff0c;集成的&#xff0c;非易失的且随时间变化的数据集合&#xff0c;用来支持管理人员的决策。 数据仓库是一个…

Nuxt快速学习开发 - Nuxt3静态资源Assets

Nuxt 使用两个目录来处理样式表、字体或图像等资产。 public/目录内容按原样在服务器根目录中提供。 assets/目录包含您希望构建工具&#xff08;Vite 或 webpack&#xff09;处理的所有资产。 public/目录 public目录用作静态资产的公共服务器&#xff0c;可在您的应用程序定…

CEM美国培安消解罐内管 CEM40位 55ML 微波消解罐

内罐采用高纯实验级进口增强改性处理TFM材料或PFA材料&#xff0c;我厂加工的微波罐能与原厂仪器匹配&#xff0c;而且是盖、体通配&#xff0c;无尺寸误差。精选材质&#xff0c;未添加回料&#xff0c;洁净的加工环境&#xff0c;优化了加工工艺&#xff0c;确保低本底&#…

Java多线程设计模式之不可变对象(Immutable Object)模式

简介 多线程共享变量的情况下&#xff0c;为了保证数据一致性&#xff0c;往往需要对这些变量的访问进行加锁。而锁本身又会带来一些问题和开销。Immutable Object模式使得我们可以在不加锁的情况下&#xff0c;既保证共享变量访问的线程安全&#xff0c;又能避免引入锁可能带…