【算法】哈希表

news2025/1/12 3:02:29

😀大家好,我是白晨,一个不是很能熬夜😫,但是也想日更的人✈。如果喜欢这篇文章,点个赞👍,关注一下👀白晨吧!你的支持就是我最大的动力!💪💪💪

在这里插入图片描述

文章目录

  • 📗前言
  • 📙哈希表
    • ✨哈希表定义及思路
    • 🎃模拟散列表
    • 🎄字符串前缀哈希
    • 🎋哈希表工程实现
  • 📘后记

📗前言


大家好,我是白晨,这一段时间🕊了,主要是🐏了,再加上我与生俱来的拖延症,我不是有意托更的😖,果咩纳塞。
请添加图片描述

本次为大家带来的是哈希表在算法中的应用与实践,主要讲解哈希表的思路以及在算法题中的应用,本篇文章由于是面向算法党的,对于工程实现部分讲解较为粗糙,以后会在STL中详细讲解工程实现。

当然,本篇文章会给出哈希表的两种快速实现,希望大家能将这两个模板理解记忆,方便面试中或者算法竞赛中快速实现。


📙哈希表


✨哈希表定义及思路

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,哈希表的增删查改操作都是O(1)。 这个映射函数叫做散列函数,存放记录的数组叫做散列表。

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

我们先用一个例子来演示哈希表的用途,假设我们有 10^5 个 范围为 0~10^9 的数,这时随机给出一个范围为 0~10^9 的数,求这个数是否在前面给出的10^5 个数中。

我们的第一反应肯定是前面10^5个数存储到数组中,然后逐个遍历查询,或者将前面的数排序,然后二分查找,这样的做法时间复杂度为O(N)或者O(NlogN),效率比较低,对于查询次数不多的数据或许可以顶一顶,遇到需要频繁查询的数据就很难受了。

所以,就产生了哈希表这样的数据结构,根据所给数的值,将其映射到对应的一个位置,以后,查询只需要通过**所给值(以后称为关键字)**进行映射,找到位置,就能判断是否存在这个值,时间复杂度为O(1)。从关键字求映射值的这个函数就称为 哈希函数

具体例子为:

image-20230106141336530

哈希表定义我们了解完以后,我们一起来看看哈希表的实现,哈希表的实现分为两种:

  • 开散列(拉链法)

image-20230106151146732

  • 闭散列(开放寻址法)

image-20230106151211779

这两种实现的区别在于发生哈希冲突(关键值根据哈希函数得到的映射位置相同)以后处理的方式不同,拉链法是将冲突的元素全部在一个位置上串起来,像一个拉链一样;而开放寻址法是通过再哈希,确定一个没有值使用新的位置,保证一个位置只存放一个值。


🎃模拟散列表


image-20230106132559736

🍬原题链接:模拟散列表

🪅算法思想

按照哈希表的思想进行实现,主要看下文代码实现。

🪆代码实现

  • 拉链法(开散列法)
// 拉链法
#include <iostream>
#include <cstring>

using namespace std;

const int N = 100003; // 超过10w的最小质数

int h[N]; // 哈希表,对应位置存储单链表下标
int e[N], ne[N], idx; // 单链表,模拟每个哈希结点下挂的拉链
// 这里要注意上面的模拟单链表不是一般意义上的单链表,而是用数组模拟了多个单链表,用ne[k] = -1表示 NULL
// ne[k] = -1时,表示k结点没有后驱结点了,也就是一个单链表结束

void insert(int x)
{
    int k = (x % N + N) % N; // 保证模出来的数一定为正数
    // 单链表头插
    e[idx] = x;
    ne[idx] = h[k];
    h[k] = idx++;
}

bool find(int x)
{
    int k = (x % N + N) % N;
    for (int i = h[k]; i != -1; i = ne[i])
        if (e[i] == x)
            return true;
    return false;
}

int main()
{
    int m;
    scanf("%d", &m);
    memset(h, 0xff, sizeof h);

    while (m--)
    {
        char op[2];
        int x;
        scanf("%s%d", op, &x);

        if (op[0] == 'I') insert(x);
        else
        {
            if (find(x)) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}
  • 开放寻址法(闭散列法)
// 开放寻址法
#include <iostream>
#include <cstring>

using namespace std;

const int N = 200003, null = 0x3f3f3f3f; // null表示这个位置为空

int h[N]; // 开散列法时间复杂度主要取决于冲突次数,所以将数组大小开成要求大小的2~3倍

// 查找成功返回下标,失败返回该数应该被插入的下标
int find(int x)
{
    int k = (x % N + N) % N;

    // 不考虑数组被占满的情况
    while (h[k] != null && h[k] != x)
    {
        k++;
        if (k == N) k = 0;
    }
    return k;
}

void insert(int x)
{
    int k = find(x);
    if (h[k] == null) h[k] = x;
}

int main()
{
    memset(h, 0x3f, sizeof(h));
    int m;
    scanf("%d", &m);

    while (m--)
    {
        char op[2];
        int x;
        scanf("%s%d", op, &x);

        if (op[0] == 'I') insert(x);
        else
        {
            int k = find(x);
            if (h[k] != null) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

🎄字符串前缀哈希


image-20230106152609567

🍬原题链接:字符串哈希

🪅算法思想

先要注意,这个前缀哈希与md5那种字符串哈希不是一个东西,字符串前缀哈希是针对一个字符串的哈希,将字符串的字母视为 131 或者 13331 进制的数。如abcd,就是 a ∗ 13 1 3 + b ∗ 13 1 2 + c ∗ 13 1 1 + d ∗ 13 1 0 a * 131^3 + b * 131^2 + c * 131^1 + d * 131^0 a1313+b1312+c1311+d1310
这样将一个字符串从左到右的哈希值都存到一个数组h[]中, h [ 0 ] = 0 , h [ 1 ] = a ∗ 13 1 0 , h [ 2 ] = a ∗ 13 1 1 + b ∗ 13 1 0 , h [ 3 ] = a ∗ 13 1 2 + b ∗ 13 1 1 + c ∗ 13 1 0 h[0] = 0,h[1] = a * 131^0,h[2] = a * 131^1 + b * 131^0,h[3] = a * 131^2 + b * 131^1 + c * 131^0 h[0]=0h[1]=a1310h[2]=a1311+b1310h[3]=a1312+b1311+c1310,依次类推就是字符串前缀哈希数组

  • 那么这个哈希有什么用呢?

如果要求一个字符串和另一个字符串是否相等,一般做法就是逐个比较字符,时间复杂度为O(N),如果使用两个字符串的哈希值比较,那么时间复杂度就能降低为O(1),这个方法在很大程度上可以代替KMP算法。

但是这样有一个问题,字符串长度如果很大到达10w位左右,那么哈希值就是 13 1 10 w 131^{10w} 13110w 的大小,非常夸张,这样的值如何保存呢? 我们这里选择只保留 2^64 大小的值,相当于一个unsigned long long 的大小,但是这样保存的话就无法保证哈希值的唯一性,如果发生哈希冲突就无法比较两个字符串是否真的相等。

所以,这样的做法并不能保证完全正确,所以我们必须尽量避免冲突,做法就是 将字符串的字母视为 131 或者 13331 进制的数,而不是其他进制的数,有数学证明这个进制产生的冲突是最少的。

即使这种方法有瑕疵,但是并不能掩盖其优秀的能力,如 求一个字符串中 l1~r1 和 l2~r2 这两段子串是否相等 l1~r1 和 l2~r2 这两段子串的哈希值可以从字符串前缀哈希数组中求得: h [ l ∼ r ] = h [ r ] − h [ l − 1 ] ∗ 13 1 ( r − l + 1 ) h[l \sim r] = h[r] - h[l - 1] * 131^{(r - l + 1)} h[lr]=h[r]h[l1]131(rl+1) 比较两段哈希值,相等即可认为字符串相等。

🪆代码实现

#include <iostream>

using namespace std;

typedef unsigned long long ULL;

const int N = 100010, P = 131;

char s[N];
ULL h[N]; // 字符串前缀哈希
ULL p[N]; // P进制的N次方

ULL getHash(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

int main()
{
    int n, m;
    scanf("%d%d%s", &n, &m, s + 1);
    // 初始化哈希值,大于2^64的值会自动溢出,相当于直接模2^64
    p[0] = 1;
    for (int i = 1; i <= n; ++i)
    {
        h[i] = h[i - 1] * P + s[i]; //递推
        p[i] = p[i - 1] * P;
    }

    while (m--)
    {
        int l1, r1, l2, r2;
        scanf("%d%d%d%d", &l1, &r1, &l2, &r2);

        if (getHash(l1, r1) == getHash(l2, r2)) puts("Yes");
        else puts("No");
    }
    return 0;
}

🎋哈希表工程实现


工程实现多使用开散列(拉链法),这里我用图解一下开散列的框架思路:

  • 开散列结点实现:

image-20230106153816865

  • 开散列实现:

image-20230106153856410

  • 哈希函数实现:
template<class K>
struct Hash
{
	size_t operator()(const K& key)
	{
		return key;
	}
};

// BKDR法
template<>
struct Hash<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto& ch : key)
		{
			hash *= 31;
			hash += ch;
		}
		return hash;
	}
};
  • 开散列代码实现:
namespace LinkHash
{
    // 哈希结点
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _data;
		HashData<K, V>* _next = nullptr;

		HashData(const pair<K, V>& data)
			:_data(data)
			,_next(nullptr)
		{}
	};

	template<class K, class V, class HashFunc = Hash<K>>
	class HashTable
	{
		typedef HashData<K, V> Node;

	public:
		Node* Find(const K& key)
		{
			if (_table.size() == 0)
				return nullptr;

			HashFunc hf;
			size_t index = hf(key) % _table.size();
			Node* cur = _table[index];
			
			while (cur)
			{
				if (cur->_data.first == key)
					return cur;
				cur = cur->_next;
			}
			return nullptr;
		}

		bool Insert(const pair<K, V>& data)
		{
			if (Find(data.first))
				return false;

			HashFunc hf;

			// 负载因子超过1就增容
			if (_table.size() == 0 || _n / _table.size() >= 1)
			{
				size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;

				HashTable<K, V, HashFunc> newHT;
				newHT._table.resize(newSize);

				for (size_t i = 0; i < _table.size(); ++i)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t index = hf(cur->_data.first) % newSize;
						
						// 头插
						cur->_next = newHT._table[index];
						newHT._table[index] = cur;
						
						cur = next;
					}
				}
				_table.swap(newHT._table);
			}

			size_t index = hf(data.first) % _table.size();

			if (_table[index] == nullptr)
			{
				_table[index] = new Node(data);
			}
			else
			{
				Node* cur = _table[index];
				Node* tar = new Node(data);
				tar->_next = cur;
				_table[index] = tar;
			}
			++_n;
			return true;
		}

		bool Erase(const K& key)
		{
			if (_table.size() == 0)
				return false;

			HashFunc hf;
			size_t index = hf(key) % _table.size();

			Node* prev = nullptr;
			Node* cur = _table[index];

			while (cur)
			{
				if (cur->_data.first == key)
				{
					if (prev == nullptr)
					{
						_table[index] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					--_n;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}
	private:
		vector<Node*> _table;
		size_t _n;
	};
}
  • 闭散列实现:

闭散列使用情况较少,所以不进行讲解,代码提供给大家作为参考。

namespace CloseHash
{
	enum Status
	{
		EMPTY,
		EXIST,
		DELETE
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _data;
		Status _status = EMPTY;
	};

	template<class K, class V, class HashFunc = Hash<K>>
	class HashTable
	{
		typedef HashData<K, V> Node;

	public:
		Node* Find(const K& key)
		{
			if (_table.size() == 0)
				return nullptr;

			HashFunc hf;
			size_t start = hf(key) % _table.size();
			size_t i = 0;
			size_t index = start;

			while (_table[index]._status != EMPTY)
			{
				if (_table[index]._status == EXIST && _table[index]._data.first == key)
				{
					return &_table[index];
				}
				++i;
				if (i % 2 == 1)
				{
					index += i / 2 + 1;
				}
				else
				{
					index -= i / 2;
				}

				index %= _table.size();
			}

			return nullptr;
		}

		bool Insert(const pair<K, V>& data)
		{
			if (Find(data.first))
				return false;

			HashFunc hf;

			if (_table.size() == 0 || _n * 10 / _table.size() >= 7)
			{
				// 扩容
				// 现代写法:通过开一个新表调用Insert插入结点,再交换内部数据。
				size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;

				HashTable<K, V, HashFunc> newHashTable;
				newHashTable._table.resize(newSize);
				
				for (auto& e : _table)
				{
					if (e._status == EXIST)
					{
						newHashTable.Insert(e._data);
					}
				}

				_table.swap(newHashTable._table);
			}


			size_t start = hf(data.first) % _table.size();
			size_t i = 0;
			size_t index = start;

			while (_table[index]._status == EXIST)
			{
				// 线性探测
				/*++i;
				index += i;
				index %= _table.size();*/
				// 二次探测
				++i;
				if (i % 2 == 1)
				{
					index += i / 2 + 1;
				}
				else
				{
					index -= i / 2;
				}

				index %= _table.size();
			}

			_table[index]._data = data;
			_table[index]._status = EXIST;

			++_n;

			return true;
		}

		bool Erase(const K& key)
		{
			Node* ret = Find(key);
			if (ret == nullptr)
				return false;
			ret->_status = DELETE;
			_n--;
			return true;
		}

	private:
		vector<Node> _table;
		size_t _n = 0; // 有效数据个数
	};

	void TestHashTable()
	{
		int a[] = { 1, -1, 11, 51,5,-5,15,15};
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}
		ht.Insert(make_pair(25, 25));

		cout << ht.Find(5) << endl;
		ht.Erase(5);
		cout << ht.Find(5) << endl;
	}
}

📘后记


哈希的思想不止用在哈希表中,在许多算法中也使用了哈希的思想,希望大家能在本篇中初步了解哈希思想的应用,未来在学习STL等知识点时可以更加得心应手。

如果解析有不对之处还请指正,我会尽快修改,多谢大家的包容。

如果大家喜欢这个系列,还请大家多多支持啦😋!

如果这篇文章有帮到你,还请给我一个大拇指 👍和小星星 ⭐️支持一下白晨吧!喜欢白晨【算法】系列的话,不如关注👀白晨,以便看到最新更新哟!!!

我是不太能熬夜的白晨,我们下篇文章见。


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

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

相关文章

Unity3D打包Assetbundle丢失Shader问题

详情见&#xff1a;https://www.pianshen.com/article/5391338163/1、Unity3D在打包Assetbundle时&#xff0c;可能会遇到Shader丢失的问题&#xff0c;解决方法&#xff1a;打开Edit->Project Settings->Graphics&#xff0c;在Always Included Shaders列表添加上所需的…

微信小程序测试(简单项目测试)

Flex布局简介 布局的传统解决方案&#xff0c;基于盒状模型&#xff0c;依赖 display属性 position属性 float属性 什么是flex布局&#xff1f; Flex是Flexible Box的缩写&#xff0c;意为”弹性布局”&#xff0c;用来为盒状模型提供最大的灵活性。 任何一个容器都可以指…

小程序 - 起步:小程序的构成、宿主环境、协同工作和发布

小程序 - 起步:小程序的构成、宿主环境、协同工作和发布 Date: January 5, 2023 Sum: 小程序的构成、宿主环境、协同工作和发布 小程序简介 小程序与普通网页开发的区别 1. 运行环境不同 网页运行在浏览器环境中 小程序运行在微信环境中 2. API 不同 由于运行环境的不同…

P1308 [NOIP2011 普及组] 统计单词数————C++

题目 [NOIP2011 普及组] 统计单词数 题目描述 一般的文本编辑器都有查找单词的功能&#xff0c;该功能可以快速定位特定单词在文章中的位置&#xff0c;有的还能统计出特定单词在文章中出现的次数。 现在&#xff0c;请你编程实现这一功能&#xff0c;具体要求是&#xff1…

数字验证学习笔记——SystemVerilog芯片验证21 ——覆盖率类型

一、覆盖率类型 覆盖率是衡量设计验证完备性的一个通用词语。随着测试逐步覆盖各种合理的组合&#xff0c;仿真过程过程会慢慢勾画你的设计情况。覆盖率工具会在仿真过程中收集信息&#xff0c;然后进行后续处理并且得到覆盖率报告。通过这个报告找出覆盖之外的盲区&#xff0…

冒泡排序模拟qsort函数

欢迎来到 Claffic 的博客 &#x1f49e;&#x1f49e;&#x1f49e; 前言&#xff1a; 学习C语言&#xff0c;一般情况下都会接触到冒泡排序&#xff0c;你知道吗&#xff0c;用冒泡排序的思想可以模拟实现qsort函数&#xff08;库函数的一种&#xff0c;可以实现快排&#xff…

图解面试题:经典50题!掌握这些题,面试也太简单了!

已知有如下4张表&#xff1a;学生表&#xff1a;student(学号,学生姓名,出生年月,性别)成绩表&#xff1a;score(学号,课程号,成绩)课程表&#xff1a;course(课程号,课程名称,教师号)教师表&#xff1a;teacher(教师号,教师姓名)1.汇总分析-查询学生的总成绩并进行排名/* 【知…

CSS基础知识(盒子模型)

继承上一篇CSS的三大特性的优先级继续讲解。 1.1优先级 优先级注意点&#xff1a; 权重是有4组数字组成的&#xff0c;但是不会有进位。可以理解为类选择器永远大于元素选择器&#xff0c;id选择器永远大于类选择器以此类推。等级判断从左向右&#xff0c;如果某一位数值相同…

前端学习之CSS基础

前言 html标签就不说了&#xff0c;这次学习CSS样式&#xff0c;就是美化html标签。 快速了解什么是css 普通标签&#xff1a; 加了css样式&#xff1a; <img src"https://static.runoob.com/images/icon/mobile-icon.png" style"height:100px" /&…

No module named ‘pycocotools’

网上搜了之后 按照网上的去做 全都无果 开始自己探索 原本我pycharm里选的环境是 python3.8 (pytorch)winR输入cmd进去后 输入 python –V返回的是本地python版本2.7 所以我当前系统python版本和我pycharm里选的不一样 然而pycocotools这个包本质上应该是安装在我pycharm里…

Referer;盗链;防盗链的工作原理

目录 Referrer-policy 如何设置referer 盗链 防盗链的工作原理 绕过图片防盗链 设置meta 设置referrerpolicy"no-referrer" 客户端在请求时修改header头部 利用https网站盗链http资源网站&#xff0c;refer不会发送 常见防盗链方法 利用nginx 服务器端判…

python基础语法一

一、变量 1.1、变量定义 变量就是可变的量&#xff0c;对于一些有可能会经常变化的数据&#xff0c;我们需要使用一个符号&#xff0c;这样才能计算中使 用它&#xff0c;就像我们在小学时学过的一元方程中的"x"一样。比如说&#xff0c;我们在控制台内输入&#xf…

Java 集合List接口介绍和使用

List接口的基本介绍 1.List接口是Collection的子接口 2.List中的元素都是有序的除了LinkedList。 一些实用的方法 1.add()添加元素 2.get()得到指定位置的元素 3.addAll&#xff08;&#xff09;追加一个List 4.indexOf&#xff08;&#xff09;返回元素的位置 5.remove…

人工智能 -多任务编程、进程、线程介绍

目录1&#xff0c; 多任务的概念2&#xff0c;进程2.1进程的介绍2.2多进程完成多任务2.3进程执行带有参数的任务2.4获取进程编号2.5进程间不共享全局变量2.6主进程和子进程的结束顺序3、线程3.1多线程完成多任务3.2线程执行带有参数的任务3.3主线程和子线程的结束顺序3.4线程中…

连续子数组的最大和(从暴力理解到DP)

连续子数组的最大和题目思路暴力解题思路画出矩阵进行分析确定转移方程DP代码题目 思路 从leetcode上看到的题解&#xff0c;突然恍然大悟&#xff0c;之前不容易理解转移方程终于理解了&#xff0c;这个思路真的对新手很友好&#xff0c;现在出一个C版本&#xff0c;而且&…

谷歌出品,数据集搜索引擎上线了!

文 | 小戏记得在刚入门 ML 时&#xff0c;希望找到一个关于特定领域下的数据集&#xff0c;涉世未深的我在中文互联网不断搜索&#xff0c;可每每点进链接出来的都是某 SDN 下载的高价勒索。用惯了直接从老师同学那里讨来的数据集的我第一次感受到了“寻找数据集”这样一个简单…

【并查集】实现思路及例题

一、应用场景 用于处理不相交集合的合并和查询问题 示例&#xff1a; n 个元素&#xff08;分属不同的的 n 个集合&#xff09;&#xff0c;进行两种操作&#xff1a; 并 —— 给出两个元素的关系&#xff0c;合并两个集合查 —— 查询两个元素是否在同一个集合 二、并查集…

「数据密集型系统搭建」原理篇|用什么方式存储数据最合适

本篇来聊聊数据存储的内容&#xff0c;看看程序世界里数据是以什么形式存在的&#xff1f;为了描述数据并把它们和这个现实世界关联起来我们一般都是如何去进行表达的&#xff1f;最后通过我们习惯的表达方式再结合数据结构是如何存储下来的&#xff1f; 在进行技术方案设计的时…

分享102个PHP源码,总有一款适合您

PHP源码 分享117个PHP源码&#xff0c;总有一款适合您 PHP源码下载链接&#xff1a;https://pan.baidu.com/s/1Ike0x99BcMfZPy6tFSpM9w?pwdzqem 提取码&#xff1a;zqem import os from time import sleepimport requests from bs4 import BeautifulSoup from docx import D…

Linux 系统Bash的常用功能

了解了基本的Linux文件文件系统的概念后,我们将更深入的了解一下Linux的其他方面的内容,那就是我们所使用的用户接口,也就是大家常听到的 Shell ,是一种Linux的命令接口,在 Linux 的世界中,默认使用的是 GNU 开发出来的 shell ,称为 BASH Shell,简单来说,我们之前使用的几个命令…