【算法与数据结构】哈希表

文章目录

    • 引入
    • 哈希函数
      • 介绍
      • 便利店的例子
      • Python3 中的哈希表
      • C++ 中的哈希表
    • 应用
      • 将散列表用于查找
      • 防止重复
      • 将散列表用作缓存
    • 哈希冲突与解决
      • 链地址法
      • 开放寻址
    • 总结
    • 参考资料
    • 写在最后

引入

假设你在一家便利店上班,你不熟悉每种商品的价格,在顾客需要买单是时候,你需要在价目表中一个个找出商品的价格,这需要很长的时间,按照现行搜索的方式,需要花费 O ( n ) O(n) O(n) 的时间,如果本子中的商品是按照顺序排列的,使用二分法就可以找出某件商品的价格,这时的时间复杂度为 O ( l o g n ) O(logn) O(logn)

那有没有一种可以更快的找出某样商品价格的方法呢?有,利用哈希表查找,可以将时间复杂度降到 O ( 1 ) O(1) O(1)

在数组这种数据结构中,可以通过索引 O ( 1 ) O(1) O(1) 的找到指定索引对应的元素值。一行行的查找商品的价格就是在数组中一个个枚举,这个数组中包含两个元素:商品名和价格。如果将这个数组按照商品名排序,就可以使用二分查找在其中查找商品的价格,时间复杂度为 O ( l o g n ) O(logn) O(logn)。如果用一个函数,输入商品名,输出对应在数组中的位置(索引),那么我们可以直接利用索引 O ( 1 ) O(1) O(1) 的查找到商品的价格。

这个函数被称为 哈希函数,也有的资料称之为 散列函数


哈希函数

介绍

哈希函数,你给它输入任何类型的数据,它都会输出一个数字。用专业的术语来说就是 “将输入映射到数字“。哈希函数具有一些要求:

  • 相同的输入映射到相同的数字。例如,你输入苹果到哈希函数时得到 4,你再次输入苹果时,还是会得到 4.
  • 不同的输入映射到不同的数字。例如,你输入苹果到哈希函数时得到 4,当你输入不同的水果到哈希函数会得到不同于 4 的数字,可能是 5、6 等等数字。

便利店的例子

哈希函数将输入映射到数字,这有何用途?以便利店的例子为例,你可以通过哈希函数建立商品与价格查询表,方便快速查询商品的对应价格。

首先创建一个空数组,用来存放商品的价格。

image-20240504164217444

下面将苹果的价格加入到数组中,为此,需要将 “apple” 作为输入交给哈希函数,这时哈希函数输出 3,因此我们将苹果的价格存储到数组索引 3 位置处。

image-20240504164256200

下面将香蕉的价格加入到数组中,为此,需要将 “banana” 作为输入交给哈希函数,这时哈希函数输出 0,因此我们将香蕉的价格存储到数组索引 0 位置处。

image-20240504164834762

不断重复这个过程,最终整个数组将被价格填满。

image-20240504165450849

现在假设需要找到 “chocolate” 的价格,你无需在数组中查找,只需要将 “chocolate” 输入到哈希函数。

image-20240504165854742

哈希函数会告诉你 “chocolate” 存储在索引 4 处,于是直接对数组进行索引得到 “chocolate” 的价格。

image-20240504170151696

Python3 中的哈希表

在平时的使用中,我们不需要自己去实现哈希表,任何一种优秀的程序语言都提供了哈希表的实现。在 Python3 中提供的哈希表为 字典,你可以使用函数 dict 创建哈希表。字典中的元素是一个个的对,对的一个元素是键,第二个元素是键对应的值。

比如上述便利店的例子,可以直接使用字典建立商品名到价格的映射。

book = dict()

book["apple"] = 3.8
book["banana"] = 1.8
book["egg"] = 0.8
book["milk"] = 2.5
book["chololate"] = 9.9

print(book["chololate"]) # 直接输出 "chololate" 的价格

C++ 中的哈希表

C++ 中提供的哈希表是 mapunordered_map,前者按照键进行排序的有序哈希表,后者则是无序哈希表。常用操作有:

  • 增加
  • 查询
  • 删除

以上述便利店的例子对以上两个常用操作进行简要说明:

unordered_map<string, double> book;

// 在哈希表中增加键值对
book["apple"] = 3.8;
book["banana"] = 1.8;
book["egg"] = 0.8;
book["milk"] = 2.5;
book["chololate"] = 9.9;

// 查询指定键对应的值
double price = book["chololate"];

// 删除键值对
book.erase("chololate")

应用

哈希表通常有以下几方面的应用:

  • 将散列表用于查找

  • 防止重复

  • 将散列表用作缓存

将散列表用于查找

第一点在上述便利店的例子中已经解释过了,这里不再赘述。

防止重复

第二点应用实际是利用哈希集合,本质上也是使用哈希函数将输入映射成唯一的数字,你可以理解成哈希函数输出的索引对应数组中的值为 1 或 0。如果某个元素存在于哈希集合中,那么索引对应的值为 1,否则为 0。

举一个具体的例子,你管理一个投票站,没人只允许投一票,为了避免重复投票,有人来投票时,你会询问他的名字,并将其与已投票名单进行比对:

  • 如果名字在名单中,则不允许他再次投票;
  • 否则允许他投票,并将其名字记录在已投票名单中。

利用哈希表或者哈希集合都可以在 O ( 1 ) O(1) O(1) 时间复杂度内判断出某人是否已经投过票了。可以对比看一下分别哈希表和哈希集合的代码:

/*****使用哈希表*****/ 
unordered_map<string, int> voted;

// 增加已经投票的人
voted["Jim"] = 1;
voted["Pam"] = 1;

// 查询 Jim 是否已经投过票了,如果已经投过票返回 true,否则返回 false
if (voted.find(Jim) != voted.end()) {
	return true
}
else {
	return false;
}

/*****使用哈希集合*****/
unordered_set<string> voted;

// 增加已经投票的人
voted.insert("Jim");
voted.insert("Pam");

// 查询 Jim 是否已经投过票了,如果已经投过票返回 true,否则返回 false
if (voted.find(Jim) != voted.end()) {
	return true
}
else {
	return false;
}

将散列表用作缓存

通常我们访问一个网页链接,首先会在缓存中查找是否有这个链接,如果有直接从缓存中返回链接对应的内容;如果没有才会向相应的服务器发送请求,服务器做一些处理,生成一个我们需要的网页。

这种应用实际上将网页链接记作键,链接对应的内容作为键的值,这是哈希表的一个典型的应用场景。


哈希冲突与解决

通常情况下哈希函数的输入空间远大于输出空间,这就不可避免的会造成「冲突」,即多个元素映射到同一个数值上。这种冲突也被称为哈希冲突,会导致查询结果错误。

既然这个导致冲突的原因在于输出空间不够,我们直接「扩容」就好了。这种简单、有效,但是效率太低,因为哈希表扩容需要进行大量的数据移动和哈希值的重新计算。为了提升效率,通常采用以下策略:

  • 改良哈希表的结构,使得哈希表在出现哈希冲突时仍可以正常使用
  • 在必要的时候(哈希冲突比较严重时),进行扩容

哈希表的结构改良主要包括:

  • 链地址法
  • 开放寻址法

链地址法

在原始的哈希表中,每一个哈希值都对应数组中的一个索引,每一个索引对应的是数组中的一个位置。链地址法中每一个索引对应的是一条链表,具有相同哈希值的元素会被放入这一链表中。如下图。

基于链式地址实现的哈希表的常用操作如下:

  • 查找元素:输入 key 经过哈希函数得到索引,即可访问链表的头节点,然后遍历链表并对比 key 以查找目标键值对。
  • 增加元素:通过哈希函数访问到对应的链表头节点,然后将节点添加到链表中。
  • 删除元素:通过哈希函数访问到对应链表头部,遍历链表找到目标节点并删除该节点。

以下是一个链地址法的示例代码。已经在 706. 设计哈希映射 中测试过。

class MyHashMap {
private:
    int size;           // 键值对数量
    int capacity;       // 哈希表容量
    double loadThres;   // 负载因子阈值
    int extendRatio;    // 扩容倍数
    vector<list<pair<int, int>>> data;

    // hash 函数
    int hash(int key) {
        return key % capacity;
    }

    // 计算负载因子
    double loadFactor() {
        return double(size) / double(capacity);
    }

public:
    // 构造函数
    MyHashMap(): size(0), capacity(8), loadThres(0.7), extendRatio(2), data(capacity) {}

    // 析构函数
    ~MyHashMap() {}

    // 添加键值对,若存在则更改键对应的值
    void put(int key, int val) {
        ++size;
        if (loadFactor() > loadThres) {
            extend();
        }
        int h = hash(key);
        for (auto it = data[h].begin(); it != data[h].end(); ++it) {
            if ((*it).first == key) {
                (*it).second = val;
                return;
            }
        }
        data[h].push_back(make_pair(key, val));
    }

    // 查找
    int get(int key) {
        int h = hash(key);
        for (auto it = data[h].begin(); it != data[h].end(); ++it) {
            if ((*it).first == key) {
                return (*it).second;
            }
        }
        return -1;
    }

    // 删除
    void remove(int key) {
        int h = hash(key);
        for (auto it = data[h].begin(); it != data[h].end(); ++it) {
            if ((*it).first == key) {
                data[h].erase(it);
                --size;
                return;
            }
        }
    }

    // 扩容
    void extend() {
        vector<list<pair<int, int>>> dataTmp = data;
        capacity *= extendRatio;
        data.clear();
        data.resize(capacity);
        size = 0;
        for (auto& ele : dataTmp) {
            for (auto it = ele.begin(); it != ele.end(); ++it) {
                put((*it).first, (*it).second);
            }
        }
    }
};

以上给出的是使用 C++ 中的 vector 容器和 list 实现链地址哈希表,初始化哈希表的长度为 8,当负载因子超出阈值 0.7 时,将哈希表扩容为原来的 2 倍。哈希表的初始长度、负载因子和扩容倍数都是超参数,可以根据实际情况进行修改。

开放寻址

开放寻址不引入额外的数据结构,而是通过“多次探测”来处理哈希冲突,说白了就是遇到哈希冲突就通过一些策略找到不冲突的位置放置元素。这些策略包括:

  • 线性探测
  • 平方探测
  • 多次哈希

线性探测

线性探测采用固定步长的线性搜索来进行探测,其操作方法与普通哈希表有所不同。

在插入元素时,通过哈希函数计算数组索引,若发现数组内已有元素,则从冲突位置向后线性遍历(步长通常为 1 ),直至找到空数组,将元素插入其中。

在查找元素时:若发现哈希冲突,则使用相同步长向后进行线性遍历,直到找到对应元素,返回 value 即可;如果遇到空数组,说明目标元素不在哈希表中,返回 None

平方探测

平方探测与线性探测类似,都是开放寻址的常见策略之一。当发生冲突时,平方探测不是简单地跳过一个固定的步数,而是跳过“探测次数的平方”的步数,即 1,4,9,… 步。

平方探测主要具有以下优势。

  • 平方探测通过跳过探测次数平方的距离,试图缓解线性探测的聚集效应。
  • 平方探测会跳过更大的距离来寻找空位置,有助于数据分布得更加均匀。

然而,平方探测并不是完美的。

  • 仍然存在聚集现象,即某些位置比其他位置更容易被占用。
  • 由于平方的增长,平方探测可能不会探测整个哈希表,这意味着即使哈希表中有空桶,平方探测也可能无法访问到它。

多次哈希

顾名思义,多次哈希方法使用多个哈希函数 f 1 ( x ) f_1(x) f1(x) f 2 ( x ) f_2(x) f2(x) f 3 ( x ) f_3(x) f3(x)、… 进行探测。

  • 插入元素:若哈希函数 f 1 ( x ) f_1(x) f1(x) 出现冲突,则尝试 f 2 ( x ) f_2(x) f2(x) ,以此类推,直到找到空位后插入元素。
  • 查找元素:在相同的哈希函数顺序下进行查找,直到找到目标元素时返回;若遇到空位或已尝试所有哈希函数,说明哈希表中不存在该元素,则返回 None

与线性探测相比,多次哈希方法不易产生聚集,但多个哈希函数会带来额外的计算量。

总结

  • 哈希表和哈希集合都是通过哈希函数将输入映射到数字,通过这些数字完成 O ( 1 ) O(1) O(1) 时间复杂度的索引。
  • 重点需要掌握解决哈希冲突的链地址。对应的练习题目有 705. 设计哈希集合 和 706. 设计哈希映射,此二题题解可见 【重难点算法题】设计哈希集合、哈希映射。

参考资料

Hello 算法

图解算法


写在最后

如果您发现文章有任何错误或者对文章有任何疑问,欢迎私信博主或者在评论区指出 💬💬💬。

如果大家有更优的时间、空间复杂度的方法,欢迎评论区交流。

最后,感谢您的阅读,如果有所收获的话可以给我点一个 👍 哦。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

hadoop学习---基于Hive的教育平台数据仓库分析案例(一)

案例背景&#xff1a; 大数据技术的应用可以从海量的用户行为数据中进行挖掘分析&#xff0c;根据分析结果优化平台的服务质量&#xff0c;最终满足用户的需求。教育大数据分析平台项目就是将大数据技术应用于教育培训领域&#xff0c;为企业经营提供数据支撑。 案例数据产生流…

Ubuntu20安装torch1.13和pytorch_geometric2.3.0(对应cuda11.6)

在torch下载页面搜索1.13https://pytorch.org/get-started/previous-versions/&#xff0c;wheel安装方式&#xff08;激活conda虚拟环境&#xff09; pip install torch1.13.0cu116 torchvision0.14.0cu116 torchaudio0.13.0 --extra-index-url https://download.pytorch.org…

快速构建vscode pytest 开发测试环境

如果不想用 heavy 的pycharm vscode 也是1个很好的选择 安装python SDK pacman -S python [gatemanmanjaro-x13 tmp]$ pacman -Q python python 3.11.8-1安装Vscode 很多中方法 yay -S visual-studio-code-bin [gatemanmanjaro-x13 tmp]$ pacman -Q | grep -i visual visua…

关于怎么计算重复路段的算法问题

这是代码&#xff1a; #define _CRT_SECURE_NO_WARNINGS #define MAX_TEST 100 #include<stdio.h> int main() {int l, m;int u0, v0,j0;int arr[MAX_TEST][10] {0};int count0;scanf("%d", &l);scanf("%d", &m);if (l < 500 &…

性能优化(一):ArrayList还是LinkedList?

引言 集合作为一种存储数据的容器&#xff0c;是我们日常开发中使用最频繁的对象类型之一。JDK为开发者提供了一系列的集合类型&#xff0c;这些集合类型使用不同的数据结构来实现。因此&#xff0c;不同的集合类型&#xff0c;使用场景也不同。 很多同学在面试的时候&#x…

短视频矩阵系统ai剪辑 矩阵 文案 无人直播四合一功能核心独家源头saas开发

抖去推矩阵AI小程序是一款针对短视频平台的智能创作和运营工具&#xff0c;它具有以下功能特点&#xff1a; 1.批量视频生成&#xff1a;抖去推可以在短时间内生成大量视频&#xff0c;帮助商家快速制作出适合在短视频平台上推广的内容 2.全行业覆盖&#xff1a;适用于多个行业…

环形链表面试题详解

A. 环形链表1 给你一个链表的头节点 head &#xff0c;判断链表中是否有环. 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置…

每日OJ题_贪心算法二⑦_力扣942. 增减字符串匹配

目录 力扣942. 增减字符串匹配 解析代码 力扣942. 增减字符串匹配 942. 增减字符串匹配 难度 简单 由范围 [0,n] 内所有整数组成的 n 1 个整数的排列序列可以表示为长度为 n 的字符串 s &#xff0c;其中: 如果 perm[i] < perm[i 1] &#xff0c;那么 s[i] I 如果 …

奈氏准则和香农定理

一、奈奎斯特和香农 哈里奈奎斯特&#xff08;Harry Nyquist&#xff09;(左) 克劳德艾尔伍德香农&#xff08;Claude Elwood Shannon&#xff09;(右) 我们应该在心里记住他们&#xff0c;记住所有为人类伟大事业做出贡献的人&#xff0c;因为他们我们的生活变得越来越精彩&…

计算机毕业设计Python+Spark考研预测系统 考研推荐系统 考研数据分析 考研大数据 大数据毕业设计 大数据毕设

安顺学院本科毕业论文(设计)题目申请表 院别&#xff1a;数学与计算机科学 专业&#xff1a;数据科学与大数据 时间&#xff1a;2022年 5月26日 题 目 情 况 题目名称 基于hive数据仓库的考研信息离线分析系统的设计与实现 学生姓名 杨娣荧 学号 201903144042 …

【Hadoop】--基于hadoop和hive实现聊天数据统计分析,构建聊天数据分析报表[17]

目录 一、需求分析 1、背景介绍 2、目标 3、需求 4、数据内容 5、建库建表 二、ETL数据清洗 1、数据问题 2、需求 3、实现 4、扩展概念&#xff1a;ETL 三、指标计算 1、指标1&#xff1a;统计今日消息总量 2、指标2&#xff1a;统计每小时消息量、发送量和接收用…

shpfile转GeoJSON;控制shp转GeoJSON的精度;如何获取GeoJSON;GeoJSON是什么有什么用;GeoJSON结构详解(带数据示例)

目录 一、GeoJSON是什么 二、GeoJSON的结构组成 2.1、点&#xff08;Point&#xff09;数据示例 2.2、线&#xff08;LineString&#xff09;数据示例 2.3、面&#xff08;Polygon&#xff09;数据示例 2.4、特征&#xff08;Feature&#xff09;数据示例 2.5、特征集合&…

Leetcode—1056. 易混淆数【简单】Plus

2024每日刷题&#xff08;126&#xff09; Leetcode—1056. 易混淆数 &#x1f4a9;山实现代码 class Solution { public:bool confusingNumber(int n) {int arr[10] {0};int notNum 0;int arr2[12] {0};int size 0;while(n) {int x n % 10;arr[x] 1;arr2[size] x;if(…

自动化测试适用场景

日常大家都用自动化去写测试脚本。但是自动化可不仅仅可以工作写脚本&#xff0c;还可以应用到如下领域&#xff1a; 1. 自动化测试脚本&#xff1a;自动化测试是软件测试 领域中最常见的自动化应用领域。它可以通过 自动化测试工具和脚本来自动化执行测试用例 &#xff0c…

水仙花数问题

问题描述&#xff1a; 求出0&#xff5e;100000之间的所有“水仙花数”并输出。 “水仙花数”是指一个n位数&#xff0c;其各位数字的n次方之和确好等于该数本身&#xff0c;如:153&#xff1d;1^3&#xff0b;5^3&#xff0b;3^3&#xff0c;则153是一个“水仙花数”。 #in…

VS Code 保存+格式化代码

在 VSCode 中&#xff0c;使用 Ctrl S 快捷键直接保存并格式化代码&#xff1a; 打开 VSCode 的设置界面&#xff1a;File -> Preferences -> Settings在设置界面搜索框中输入“format on save”&#xff0c;勾选“Editor: Format On Save”选项&#xff0c;表示在保存…

Java 【数据结构】常见排序算法实用详解(下) 冒泡排序/快速排序/归并排序/非基于比较排序【贤者的庇护】

登神长阶 上古神器-常见排序算法 冒泡排序/快速排序/归并排序/非基于比较排序 &#x1f4b0;一.前言 为保障知识获取的可读性&#xff0c;以及连贯性&#xff0c;再开始可以适当的重新温习前文内容 &#xff1a;Java 【数据结构】常见排序算法实用详解&#xff08;上&#xf…

TWS 蓝牙耳机 ESD EOS保护方案

1. TWS 蓝牙耳机 TWS&#xff08;True Wireless Stereo&#xff09;蓝牙耳机是指没有传统连接线的完全无线耳机&#xff0c;通常由两个分别放置在耳朵中的独立耳机组成&#xff0c;提供立体声音效。这类耳机在近年来越来越受欢迎&#xff0c;因为它们提供了更自由、更便捷的音…

有限单元法-编程与软件应用(崔济东、沈雪龙)【PDF下载】

专栏导读 作者简介&#xff1a;工学博士&#xff0c;高级工程师&#xff0c;专注于工业软件算法研究本文已收录于专栏&#xff1a;《有限元编程从入门到精通》本专栏旨在提供 1.以案例的形式讲解各类有限元问题的程序实现&#xff0c;并提供所有案例完整源码&#xff1b;2.单元…