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

news2024/12/29 9:56:22

文章目录

    • 引入
    • 哈希函数
      • 介绍
      • 便利店的例子
      • 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 算法

图解算法


写在最后

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

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

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

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

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

相关文章

WPF之XmlDataProvider使用

1&#xff0c;WPF XAML支持数据提供&#xff08;DataProvider&#xff09;&#xff0c;但其提供的数据只供查看不可进行修改&#xff0c;删除&#xff0c;添加等。 数据提供者都继承自System.Windows.DataSourceProvider类&#xff0c;目前&#xff0c;WPF只提供两个数据提供者…

一键自动化博客发布工具,chrome和firfox详细配置

blog-auto-publishing-tools博客自动发布工具现在已经可以同时支持chrome和firefox了。 很多小伙伴可能对于如何进行配置和启动不是很了解&#xff0c;今天带给大家一个详细的保姆教程&#xff0c;只需要跟着我的步骤一步来就可以无障碍启动了。 前提条件 前提条件当然是先下…

c++ 红黑树学习及简单实现

1. 了解红黑树 1.1. 概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个节点增加一个存储位表示节点的颜色&#xff0c;可以是红色&#xff0c;或是黑色&#xff0c;通过对任何一条从根到叶子的路径上各个节点的着色方式进行限制&#xff0c;红黑树确保没有一条路…

DIM层数据处理

一、了解DIM层 这个就是数仓开发的分层架构 我们现在是在DIM层&#xff0c;从ods表中数据进行加工处理&#xff0c;导入到dwd层&#xff0c;但是记住我们依然是在DIM层&#xff0c;而非是上面的ODS和DWD层。 二、处理维度表数据 ①先确认hive的配置 -- 开启动态分区方案 -- …

ubuntu20文件安装和卸载cuda11.6

搜索cuda 11.6 nvidia&#xff0c;进入官网https://developer.nvidia.com/cuda-11-6-0-download-archive 选择linux --> runfile 用安装包安装 wget https://developer.download.nvidia.com/compute/cuda/11.6.0/local_installers/cuda_11.6.0_510.39.01_linux.run sudo s…

飞书API(7):MySQL 入库通用版本

一、引入 在上一篇介绍了如何使用 pandas 处理飞书接口返回的数据&#xff0c;并将处理好的数据入库。最终的代码拓展性太差&#xff0c;本篇来探讨下如何使得上一篇的最终代码拓展性更好&#xff01;为什么上一篇的代码拓展性太差呢&#xff1f;我总结了几点&#xff1a; 列…

开源免费的网盘项目Cloudreve,基于Go云存储个人网盘系统源码(七牛、阿里云 OSS、腾讯云 COS、又拍云、OneDrive)

项目简介&#xff1a; 在现今的网盘服务中&#xff0c;用户经常遭遇限速和价格上涨的问题&#xff0c;这无疑增加了使用上的困扰。 为此&#xff0c;我今天要介绍一款开源且免费的网盘项目——Cloudreve。 这个项目是基于Go语言开发的云存储个人网盘系统&#xff0c;支持多种…

免费开源,无需 GPU,本地化部署大语言模型的对话系统

免费开源&#xff0c;无需 GPU&#xff0c;本地化部署大语言模型的对话系统 分类 编程技术 项目名: FreeAskInternet -- 本地化部署大语言模型的对话系统 Github 开源地址&#xff1a; https://github.com/nashsu/FreeAskInternet FreeAskInternet 是一个免费开源的工具&am…

「 网络安全常用术语解读 」通用漏洞报告框架CVRF详解

1. 背景 ICASI在推进多供应商协调漏洞披露方面处于领先地位&#xff0c;引入了通用漏洞报告框架&#xff08;Common Vulnerability Reporting Format&#xff0c;CVRF&#xff09;标准&#xff0c;制定了统一安全事件响应计划&#xff08;USIRP&#xff09;的原则&#xff0c;…

Python中无法pip的解决办法和pip的介绍

什么是pip&#xff1f; PIP是通用的Python包管理工具&#xff0c;提供了对 Python 包的查找、下载、安装、卸载、更新等功能。安装诸如Pygame、Pymysql、requests、Django等Python包时&#xff0c;都要用到pip。 注意&#xff1a;在Python3.4&#xff08;一说是3.6&#xff09…

Electron 对 SQLite 进行加密

上一篇讲了如何在 Electron使用 SQLite&#xff0c;如果 SQLite 中存有敏感数据&#xff0c;客户端采用明文存储风险很高&#xff0c;为了保护客户数据&#xff0c;就需要对数据进行加密&#xff0c;由于 electron 对代码并不加密&#xff0c;所以这里排除通过逆向工程进行数据…

ArcGIS软件:地图投影的认识、投影定制

这一篇博客介绍的主要是如何在ArcGIS软件中查看投影数据&#xff0c;如何定制投影。 1.查看地图坐标系、投影数据 首先我们打开COUNTIES.shp数据&#xff08;美国行政区划图&#xff09;&#xff0c;并点击鼠标右键&#xff0c;再点击数据框属性就可以得到以下的界面。 我们从…

深入理解分布式事务⑨ ---->MySQL 事务的实现原理 之 MySQL 中的XA 事务(基本原理、流程分析、事务语法、简单例子演示)详解

目录 MySQL 事务的实现原理 之 MySQL 中的XA 事务&#xff08;基本原理、流程分析、事务语法、简单例子演示&#xff09;详解MySQL 中的 XA 事务1、XA 事务的基本原理1-1&#xff1a;XA 事务模型图&#xff1a;1-2&#xff1a;XA 事务模型的两阶段提交操作&#xff1a;Prepare …

MLP手写数字识别(3)-使用tf.data.Dataset模块制作模型输入(tensorflow)

1、tensorflow版本查看 import tensorflow as tfprint(Tensorflow Version:{}.format(tf.__version__)) print(tf.config.list_physical_devices())2、MNIST数据集下载与预处理 (train_images,train_labels),(test_images,test_labels) tf.keras.datasets.mnist.load_data()…

02_Java综述

目录 面向对象编程两种范式抽象OOP 三原则封装继承多态多态、封装与继承协同工作 面向对象编程 面向对象编程(Object-Oriented Programming&#xff0c;OOP)在Java中核心地位。几乎所有的Java程序至少在某种程度上都是面向对象的。OOP与java是密不可分的。下面说一下OOP的理论…

【已解决】VSCode 连接远程 Ubuntu :检测到 #include 错误。请更新 includePath。

文章目录 1. 环境声明2. 解决过程 1. 环境声明 即使是同一个报错&#xff0c;在不同的环境中&#xff0c;报错原因、解决方法都是不同的&#xff0c;本文只能解决跟我类似的问题&#xff0c;如果你发现你跟我遇到的问题不太一样&#xff0c;建议寻找其他解法。 必须要吐槽的是…

吴恩达2022机器学习专项课程C2(高级学习算法)W1(神经网络):2.1神经元与大脑

目录 神经网络1.初始动机*2.发展历史3.深度学习*4.应用历程 生物神经元1.基本功能2.神经元的互动方式3.信号传递与思维形成4.神经网络的形成 生物神经元简化1.生物神经元的结构2.信号传递过程3.生物学术语与人工神经网络 人工神经元*1.模型简化2.人工神经网络的构建3.计算和输入…

基于51单片机的智能台灯proteus仿真设计( proteus仿真+程序+原理图+报告+讲解视频)

基于51单片机的红外光敏检测智能台灯控制系统仿真( proteus仿真程序原理图报告讲解视频&#xff09; 1.主要功能&#xff1a; 基于51单片机的红外检测光照检测智能台灯仿真设计 1、检测光照强度并显示在数码管上。 2、具备红外检测人体功能。 3、灯光控制模式分为自动模式…

RabbiMQ(Docker 单机部署)

序言 本文给大家介绍如何使用 Docker 单机部署 RabbitMQ 并与 SpringBoot 整合使用。 一、部署流程 拉取镜像 docker pull rabbitmq:3-management镜像拉取成功之后使用下面命令启动 rabbitmq 容器 docker run \# 指定用户名-e RABBITMQ_DEFAULT_USERusername \# 指定密码-e R…

golang for经典练习 金字塔打印 示例 支持控制台输入要打印的层数

go语言中最经典的for练习程序 金字塔打印 &#xff0c;这也是其他语言中学习循环和条件算法最为经典的联系题。 其核心算法是如何控制内层循环变量j 每行打印的*号数量 j<i*2-1 和空格数量 j1 || j i*2-1 golang中实现实心金字塔 Solid Pyramid和空心金字塔 Hollow Pyram…