07_哈希表

news2025/2/4 17:51:48

哈希表

1.为什么需要构建哈希表

现在有一组数据,我们想查找一个值(x)是否在这组数据中,通常来说,我们需要把这组数据遍历一遍,来看看有没有x这个值。

这时,我们发现这样查找数据要花费的时间复杂度为O(n),链表、顺序表都是如此。

为了降低查找数据的时间复杂度,那我们就不能去遍历所有的数据来查找,我们需要找到新的方法来查找数据,这时我们就引入了哈希表。

2.什么是哈希表

一个关键字,经过一个散列函数进行映射,得到的就是该关键字在表中的存储位置,那么符合这样的key及其映射关系后得到在表中的存储位置,那么这张表就可以称为散列表或哈希表。
哈希表的本质就是一个一维数组。将需要存取的数值通过一个映射关系存放在这个数组的相应位置。通过这种方法存储的元素在进行查找的时候查询时间复杂度为O(1),极大的提高了数据的搜索效率。

哈希表也叫做散列表,有对应的映射函数,散列函数和哈希函数

常见的哈希函数有:除留余数法
目的:

  • 计算简单(复杂会降低查找的时间)
  • 散列地址分布均匀(减少哈希冲突)

3.降低哈希冲突的方法

方法一:将哈希表的长度设置为素数

素数除了一和它本身就没有其他因数。所以在进行元素位置哈希的时候就可以很大程度上避免位置冲突。其次哈希表扩容的时候扩容的大小也应该是素数。和数组扩容不同的是:数组扩容后直接将元素拷贝到扩容后的数组空间就可以,哈希表在扩容后不仅需要将元素移动过去,还需要根据表的大小重新计算存储位置

方法二:根据装载因子进行哈希表扩容

在这里插入图片描述

4.解决哈希冲突的方法

在这里插入图片描述

方法一:线性探测法

线性探测哈希表实现

在这里插入图片描述

#include<iostream>
using namespace std;
//桶的状态
enum State
{
	STATE_UNUSE,
	STATE_USING,
	STATE_DEL,
};
//桶的类型
struct Bucket
{
	Bucket(int key = 0, State state = STATE_UNUSE)
		:key_(key),
		state_(state)
	{}
	int key_;
	State state_;
};

class HashTable
{
public:
	HashTable(int size = primes_[0], double loadFactor = 0.75)
		:useBucketNum_(0)
		,loadFactor_(loadFactor)
		,primeIdx_(0)
	{
		//把用户传入的size调整到最近的比较大的素数上
		if (size != primes_[0])
		{
			for (; primeIdx_ < PRIME_SIZE; primeIdx_++)
			{
				if (primes_[primeIdx_] > size)
				{
					break;
				}
			}
			//用户传入的size值过大,已经超过最后一个素数,调整到最后一个素数
			if (primeIdx_ == PRIME_SIZE)
			{
				primeIdx_--;
			}
		}
		tableSize_ = primes_[primeIdx_];
		table_ = new Bucket[tableSize_];
	}
	~HashTable()
	{
		delete[]table_;
		table_ = nullptr;
	}

public:
	//插入操作
	bool insert(int key)
	{
		//考虑扩容
		double factor = useBucketNum_ * 1.0 / tableSize_;
		cout << "factor:" << factor << endl;
		if (factor > loadFactor_)
		{
			expand();
		}

		int idx = key % tableSize_;
		//大量重复的代码,需要优化
		//if (table_[idx].state_ != STATE_USING)
		//{
		//	table_[idx].key_ = key;
		//	table_[idx].state_ = STATE_USING;
		//	return true;
		//}
		//for (int i = (idx + 1) % tableSize_; i != idx; i = (i + 1) % tableSize_)
		//{
		//	table_[i].key_ = key;
		//	table_[i].state_ = STATE_USING;
		//	return true;
		//}
		int i = idx;
		do
		{
			if (table_[i].state_ != STATE_USING)
			{
				table_[i].key_ = key;
				table_[i].state_ = STATE_USING;
				useBucketNum_++;
				return true;
			}
			i = (i + 1) % tableSize_;
		} while (i != idx);
		return false;
	}

	bool erase(int key)
	{
		int idx = key % tableSize_;
		int i = idx;
		do
		{
			if (table_[i].state_ == STATE_USING && table_[i].key_ == key)
			{
				table_[i].state_ = STATE_DEL;
				useBucketNum_--;
			}
			i = (i + 1) % tableSize_;
		} while (table_[i].state_ != STATE_UNUSE && i != idx);
		return true;
	}
	//查询
	bool find(int key)
	{
		int idx = key % tableSize_;
		int i = idx;
		do
		{
			if (table_[i].state_ != STATE_USING)
			{
				return true;
			}
			i = (i + 1) % tableSize_;
		} while (table_[i].state_ != STATE_UNUSE && i != idx);
		return false;
	}
	void Show()
	{
		for (int i = 0; i < tableSize_; i++)
		{
			cout << "key:"<<table_[i].key_ <<"state: "<< table_[i].state_<<" ";
		}
		cout << endl;
	}
private:
	void expand()
	{
		++primeIdx_;
		if (primeIdx_ == PRIME_SIZE)
		{
			throw"HashTable is too large,can not expand anymore";
		}
		Bucket* newTable = new Bucket[primes_[primeIdx_]];
		for (int i =0; i < tableSize_; i++)
		{
			if (table_[i].state_ == STATE_USING)
			{
				int idx = table_[i].key_ % tableSize_;
				int k = idx;
				do
				{
					if (newTable[k].state_ != STATE_USING)
					{
						newTable[k].state_ = STATE_USING;
						newTable[k].key_ = table_[i].key_;
						break;
					}
					k = (k + 1) % primes_[primeIdx_];
				} while (k != idx);
			}
		}
		delete[]table_;
		table_ = newTable;
		tableSize_= primes_[primeIdx_];
	}
private:
	Bucket* table_;
	int tableSize_;
	int useBucketNum_;
	double loadFactor_;//装载因子
	static const int PRIME_SIZE = 10;//素数表的大小
	static int primes_[PRIME_SIZE];//素数表
	int primeIdx_;//当前使用的素数下标
};
int HashTable::primes_[PRIME_SIZE] = { 3,7,23,47,97,251,443, 911,1471, 42773 };
int main()
{
	/*
	int data = 3;
	//找1-10000内的素数
	for (int i = data; i < 10000; i++)
	{//素数:除了1和它本身不能被其他数整除
		int j = 2;
		for (; j < i; j ++ )
		{
			if (i % j == 0)//i被j整除了
			{
				break;
			}
		}
		if (j == i)//如果是break出来的j<i
		{
			cout << i << " ";
		}
	}
	*/
	HashTable hs;
	hs.insert(12);
	hs.insert(23);
	hs.insert(45);
	hs.Show();
	hs.insert(24);
	hs.Show();

	return 0;
}

方法二:链地址法

链式哈希表实现

STL中无序的关联容器都是使用链式哈希表作为底层数据结构实现的;
C++中所有的容器都不是线程安全的,多线程情况下都必须使用线程的互斥操作
在这里插入图片描述


#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
using namespace std;

// 链式哈希表
class HashTable
{
public:
	HashTable(int size = primes_[0], double loadFactor = 0.75)
		: useBucketNum_(0)
		, loadFactor_(loadFactor)
		, primeIdx_(0)
	{
		if (size != primes_[0])
		{
			for (; primeIdx_ < PRIME_SIZE; primeIdx_++)
			{
				if (primes_[primeIdx_] >= size)
					break;
			}

			if (primeIdx_ == PRIME_SIZE)
			{
				primeIdx_--;
			}
		}

		table_.resize(primes_[primeIdx_]);
	}

public:
	// 增加元素 不能重复插入key
	void insert(int key)
	{
		// 判断扩容
		double factor = useBucketNum_ * 1.0 / table_.size();
		cout << "factor:" << factor << endl;
		if (factor > loadFactor_)
		{
			expand();
		}

		int idx = key % table_.size();  // O(1)
		if (table_[idx].empty())
		{
			useBucketNum_++;
			table_[idx].emplace_front(key);
		}
		else
		{
			// 使用全局的::find泛型算法,而不是调用自己的成员方法find
			auto it = ::find(table_[idx].begin(), table_[idx].end(), key);  // O(n)
			if (it == table_[idx].end())
			{
				// key不存在
				table_[idx].emplace_front(key);
			}
		}
	}

	// 删除元素
	void erase(int key)
	{
		int idx = key % table_.size();  // O(1)
		// 如果链表节点过长:如果散列结果比较集中(散列函数有问题!!!)
		//                 如果散列结果比较离散,链表长度一般不会过长,因为有装载因子
		auto it = ::find(table_[idx].begin(), table_[idx].end(), key); // O(n)
		if (it != table_[idx].end())
		{
			table_[idx].erase(it);
			if (table_[idx].empty())
			{
				useBucketNum_--;
			}
		}
	}

	// 搜索元素
	bool find(int key)
	{
		int idx = key % table_.size();  // O(1)
		auto it = ::find(table_[idx].begin(), table_[idx].end(), key); // 
		return it != table_[idx].end();
	}

private:
	// 扩容函数
	void expand()
	{
		if (primeIdx_ + 1 == PRIME_SIZE)
		{
			throw "hashtable can not expand anymore!";
		}

		primeIdx_++;
		useBucketNum_ = 0;

		vector<list<int>> oldTable;
		// swap会不会效率很低??? 交换了两个容器的成员变量
		table_.swap(oldTable);
		table_.resize(primes_[primeIdx_]);

		for (auto list : oldTable)
		{
			for (auto key : list)
			{
				int idx = key % table_.size();
				if (table_[idx].empty())
				{
					useBucketNum_++;
				}
				table_[idx].emplace_front(key);
			}
		}
	}

private:
	vector<list<int>> table_; // 哈希表的数据结构
	int useBucketNum_;        // 记录桶的个数
	double loadFactor_;       // 记录哈希表装载因子

	static const int PRIME_SIZE = 10; // 素数表的大小
	static int primes_[PRIME_SIZE];   // 素数表
	int primeIdx_; // 当前使用的素数下标
};

int HashTable::primes_[PRIME_SIZE] = { 3, 7, 23, 47, 97, 251, 443, 911, 1471, 42773 };


int main()
{
	HashTable htable;
	htable.insert(21);
	htable.insert(32);
	htable.insert(14);
	htable.insert(15);

	htable.insert(22);

	htable.insert(67);

	cout << htable.find(67) << endl;
	htable.erase(67);
	cout << htable.find(67) << endl;

	return 0;
}

5.线性探测和链地址的对比

对比一:两种方法下哈希表效率的提升上

对比二:多线程情况下锁的力度

6.哈希表的缺点

占用内存空间比较大

链式哈希表是一个链表的结构,每一个节点既需要存储数据又需要存储地址域。拿整数来说,存储100M的整数,存放到链式哈希表中就需要100*2=200M的内存。或者说有10亿个整数需要进行查重,1亿=100000000==100M;10亿=1G;10亿个整数就是4G,那么存储4G数据到链式哈希表里面就需要8G的内存

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

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

相关文章

C++ 类和对象

C认为万事万物都皆为对象&#xff0c;对象上有其属性和行为&#xff0c;C面向对象的三大特性为&#xff1a;封装、继承、多态。 一. 封装 封装是C面向对象三大特性之一。 封装的意义&#xff1a; 将属性和行为作为一个整体&#xff0c;表现生活中的事物将属性和行为加以权限控…

【数据库】时间戳并发控制

Timestamp ordering(T/O) 根据事务的时间戳来决定顺序。 如果T1 的时间戳小于T2 的时间戳&#xff0c;那么执行的结果要等价于T1 执行早于T2 的执行。 时间戳的实现策略&#xff1a; 系统时钟 逻辑计数 混合方法 Basic Timestamp Ordering&#xff08;T/O&#xff09;Prtot…

【WPF绑定2】 ComboBox MVVM SelectedValue复杂数据类型绑定

前言 这次绑定是一次非常痛苦的经历&#xff0c;因为SelectedValue总是不能生效&#xff01;我一度怀疑是wpf的Bug。其实还是自己没搞清楚。 在之前的一篇文章中&#xff1a; http://t.csdn.cn/A4W6Ahttp://t.csdn.cn/A4W6A我也写个ComboBox的绑定&#xff0c;但是当时没有指…

【实时数仓】DWM层订单宽表之实现基本的维度查询、加入旁路缓存模式

文章目录一 DWM层-订单宽表1 维表关联代码实现&#xff08;1&#xff09;首先实现基本的维度查询功能a 封装Phoenix查询的工具类PhoenixUtilb 封装查询维度的工具类DimUtil&#xff08;2&#xff09; 优化1&#xff1a;加入旁路缓存模式a 缓存策略的几个注意点b 缓存的选型c 在…

AnimateGAN 迁移部署

文章目录1. 模型概述2. 迁移过程2.1 将ckpt的权重文件转换为pb的权重文件。2.2 将pb的权重文件迁移为 BM1684 bmodel模型2.3 迁移后pipeline搭建2.4 使用streamlit部署3. 效果展示AnimateGAN 是一个基于 GAN 的动漫生成模型&#xff0c;可以将真实的场景照片转换成动漫形式。本…

CASA(Carnegie-Ames-Stanford Approach)模型

植被作为陆地生态系统的重要组成部分对于生态环境功能的维持具有关键作用。植被净初级生产力&#xff08;Net Primary Productivity, NPP&#xff09;是指单位面积上绿色植被在单位时间内由光合作用生产的有机质总量扣除自养呼吸的剩余部分。植被NPP是表征陆地生态系统功能及可…

设计模式之美总结(创建型篇)

title: 设计模式之美总结&#xff08;创建型篇&#xff09; date: 2022-11-03 13:58:36 tags: 设计模式 categories:技术书籍及课程 cover: https://cover.png feature: false 文章目录1. 单例模式&#xff08;Singleton Design Pattern&#xff09;1.1 为什么要使用单例&…

如何在高密度的IB学习中杀出重围?

建议选择IB所需具备的能力/特点 ▣ 敢于挑战自我&#xff0c;愿意通过努力换取个人能力的飞跃 ▣ 如果擅长或喜欢写作&#xff08;中英文&#xff09;&#xff0c;IB对于你来说可能不会那么难。 ▣ 有自主学习、自主研究的能力。有些老师可能教的并不太让人满意&#xff0c;因此…

OpenTelemetry系列 (三)| 神秘的采集器 - Opentelemetry Collector

前言 上个篇章中我们主要介绍了OpenTelemetry的客户端的一些数据生成方式&#xff0c;但是客户端的数据最终还是要发送到服务端来进行统一的采集整合&#xff0c;这样才能看到完整的调用链&#xff0c;metrics等信息。因此在这个篇章中会主要介绍服务端的采集能力。 客户端数…

学Python能做哪些副业?我一般不告诉别人

前两天一个朋友找到我吐槽&#xff0c;说工资一发交完房租水电&#xff0c;啥也不剩&#xff0c;搞不懂朋友圈里那些天天吃喝玩乐的同龄人钱都是哪来的&#xff1f; 确实如此&#xff0c;刚毕业的大学生工资起薪都很低&#xff0c;在高消费、高租金的城市&#xff0c;别说存钱…

日志篇- ES+Logstash+Filebeat+Kibana+Kafka+zk 安装配置与使用详解

1- 学习目标 ELK基本概念&#xff0c;特点安装部署 Kibana ES集群 Logstash Filebeat Kafka集群性能瓶颈以及优化QA汇总 2- 介绍 2.1- 基本概念 Elasticsearch 分布式搜索和分析引擎&#xff0c;具有高可伸缩、高可靠和易管理等特点。基于 Apache Lucene 构建&#xff0c…

xv6---Lab4 traps

参考&#xff1a; Lab: Traps 关于寄存器s0和堆栈https://pdos.csail.mit.edu/6.828/2020/lec/l-riscv-slides.pdf RISC-V assembly Q: 哪些寄存器包含函数的参数?例如&#xff0c;哪个寄存器在main对printf的调用中保存了传参13 ? A: a2保存13(通过gdb调试可看出寄存器a2的…

【设备管理系统】如何助力制造企业实现精益生产?

随着企业对于机械设备的依赖性越来越高&#xff0c;生产设备日益大型化、自动化&#xff0c;流程线生产流程问题逐渐浮于表面&#xff0c;现阶段设备管理的各项制度已经不能够满足日常的生产工作。企业逐渐都面临着设备管理的复杂问题&#xff0c;尤其是设备的保养、维修、日常…

JMeter—HTTP压测

目录&#xff1a;导读 一、创建线程组 二、添加HTTP 三、查看结果树 四、响应断言 五、聚合报告 六、自定义变量 七、CSV可变参数压测 结语 一、创建线程组 右击-->添加-->Threads(Users)-->线程组 下面对比较重要的几个参数&#xff0c;讲解下&#xff1a; …

Vue基础7

Vue基础7生命周期引出生命周期用css animation实现用定时器实现错误&#xff1a;用methods实现使用生命周期函数mounted实现生命周期定义分析生命周期挂载流程beforeCreate()created()beforeMount()mounted()template的作用更新流程beforeUpdate()updated()销毁流程beforeDestr…

【数据库】二阶段锁

Two-phase locking (2PL) is a concurrency controlprotocol that determines whether a txn can access an object in the database on the fly. The protocol does not need to know all the queriesthat a txn will execute ahead of time. 分为两个阶段&#xff1a; 一阶…

颅内EEG记录揭示人类DMN网络的电生理基础

使用无创功能磁共振成像&#xff08;fMRI&#xff09;的研究为人类默认模式网络&#xff08;DMN&#xff09;的独特功能组织和深远重要性提供了重要的见解&#xff0c;但这些方法在跨多个时间尺度上解决网络动力学的能力有限。电生理技术对于应对这些挑战至关重要&#xff0c;但…

RAID 0 添加新磁盘

1&#xff1a;查看当前可用挂载磁盘 lsblk 2&#xff1a;可见 sda 与 sdb 已被挂载&#xff0c;需要挂载 sdc 和 sdd 由于硬盘的默认分区格式是MBR&#xff0c;这种格式的硬盘支持的最大挂载容量为2T&#xff0c;为了满足我们的要求&#xff0c;需要将硬盘格式转化为MBR&…

Node.js 编写接口入门学习(GET、POST)

一、简介 nvm 安装、卸载与使用&#xff08;详细步骤&#xff09;&#xff0c;用于管理/切换 Node 多版本环境。 node 是否安装成功 $ node -v安装完成之后&#xff0c;通过 node 直接运行 test.js。 // test.js console.log(Hello Node)# 命令行执行 $ node test.js二、简单的…

[ 数据结构 -- 手撕排序算法第七篇 ] 归并排序

文章目录前言一、常见的排序算法二、归并排序的基本思想三、归并排序3.1 归并排序的递归版本3.2 归并排序的非递归版本四、归并排序的特性总结前言 手撕排序算法第七篇&#xff1a;归并排序&#xff01; 从本篇文章开始&#xff0c;我会介绍并分析常见的几种排序&#xff0c;例…