数据结构 Hash 表(哈希表)
------【附源码实现】
梗概
哈希表是一种数据结构,通过哈希函数将键映射到数组的索引位置,便于快速存取数据。其主要操作包括插入、查找和删除,通常在平均情况下时间复杂度为O(1)。
本文介绍的哈希的基本概念,哈希查找的方法:
- 直接地址法:哈希表的每个位置都直接对应一个键值对。通过哈希函数计算索引位置,可以直接访问。适用于键值对的范围已知且不大时。
- 开放寻址法 :当哈希表中发生冲突(即两个键的哈希值映射到同一个位置)时,使用一定的探查策略找到下一个可用的位置。这种方法包括:
- 线性探查:每次冲突时,顺序检查下一个位置(索引 + 1, 索引 + 2,依此类推),直到找到空位置或匹配的键。
- 二次探查:冲突时,按二次方的方式检查下一个位置(索引 + 1², 索引 + 2²,依此类推),减少了冲突的聚集。
- 双重哈希:使用第二个哈希函数来决定探查的步长,从而减少冲突的情况。计算新位置的方法是原位置 + (步长 * 哈希函数的结果) % 表的大小。
- 链地址法(分离链接法):每个哈希表的位置维护一个链表(或其他数据结构),用于存储哈希值冲突的所有元素。当冲突发生时,将元素插入相应的链表中。查找时首先计算哈希值,然后在对应的链表中查找。
- 再哈希法:在哈希表负载因子(已存储元素数与表总容量的比值)超过某个阈值时,扩展哈希表的大小,并重新计算所有元素的哈希值以保持均匀分布。这种方法帮助减少冲突,提高查找效率。
我们从Python语言中,知道有字典这一数据容器,实际上,字典这一数据容器,就是基于哈希表实现的。
Python 中的字典(dict)与哈希查找紧密相关,它们在底层实现上有很多相似之处。
-
- Python 字典是基于哈希表实现的。哈希表使用哈希函数将键映射到数组的索引位置,这与哈希查找的基本概念相同。
常数时间复杂度:
-
- 字典的查找、插入和删除操作在平均情况下都是 O(1),这与哈希表在理想情况下提供的常数时间复杂度一致。这是因为哈希表通过计算键的哈希值,直接访问存储数据的位置。
冲突处理:
-
- Python 字典使用开放寻址法(具体是线性探查和二次探查的组合)来处理哈希冲突。也就是说,当两个键的哈希值映射到同一位置时,字典会探查下一个位置,直到找到合适的位置存放或查找目标元素。
再哈希:
-
- Python 字典会动态调整其容量。当字典的负载因子(即存储的元素数与表的容量的比值)达到一定阈值时,字典会扩展哈希表的大小并重新哈希已有元素。这种机制称为再哈希,旨在减少冲突,提高性能。
哈希函数:
-
- Python 字典依赖于哈希函数来计算键的哈希值。Python 使用内置的 hash() 函数对键进行哈希操作,确保哈希值的分布尽可能均匀,从而减少冲突。
动态调整和优化:
-
- Python 字典在实现上不仅考虑了哈希函数,还包括其他优化措施,如自适应哈希表和缓存机制,以提高性能和减少冲突。
总结来说,Python 字典的设计和实现依赖于哈希表的基本原理。Python 字典在实际应用中对哈希表的这些基础概念进行了优化,以提高性能和效率。
接下来,让我们来探寻一下,何为哈希;
在前面我们学习查找的过程中,我们发现不管是顺序查找,还是有序表查找,我们都是运用比较的方式,来确定其位置。
- 顺序表查找时,如果要查找某个关键字的记录,那么我们要从表头开始,依次遍历,a[index] 是否==key ,直到相等才算是查找成功,然后返回index ;
- 有序表查找时,我们常常利用a[index]与key的大小关系来折半查找(二分查找),等到相同之时,查找成功返回 index **,**也就是那个元素对应的索引下标,然后在通过顺序存储计算的方法,找到其内存地址。
一.哈希的基本概念
从数学中,自变量到因变量的一一对应关系,也就是映射,类似的我们在记录的存储位置和他的关键字之间建立一个对应的关系f,使得每个关键字k对应一个存储位置f(key),在查找时,我们就可以根据这个确定的对应关系,来找到key的映射值,这个映射关系,就被称为**哈希函数,**需要特别注意的是,哈希函数和数学函数都将一个输入映射到一个输出。对于哈希函数,这个映射通常是从较大的输入空间映射到较小的输出空间(哈希表的大小)。
哈希技术,就是我们因为有序表和顺序表的缺陷应运而生的,我们使得不经过比较的查找,就获得记录的存储位置所用到的技术
数学: y = f(x);
哈希:LOC(a[index]) = f(x);
采用哈希技术将对应的存储位置放置在一块连续的存储空间中,这块连续存储空间我们称之为哈希表 ,关键字对应的存储位置我们称之为哈希地址。
二.哈希表查找
- 哈希函数:将数据的键(如整数或字符串)转换为哈希值,这个值表示数据在哈希表中的位置。
哈希函数有多种方法来设计(类似数学中的构造函数)
-
- 直接定址法
取关键字的某个线性函数值为哈希地址,即f(key) = a * key + b;
优点:简单,均匀,不产生冲突。需要提前知道关键字的分布情况,适合查找表较小且连续的情况,如,指定数组。
-
- 除留余数法
此方法为最常用的构造哈希函数的方法。哈希表长为len,对应的哈希函数公式:
f(key) = key mod p (p <= m);
mod是取模的意思,即求余数,此方法关键在选取合适的p。
-
- 随机数法
选择一个随机数,取关键字的随机函数值为它的哈希地址。也就是f(key)=random (key)。 这里random是随机函数。当关键字的长度不等时,采用这个方法构造哈希函数是比较合适的。
- 存储:数据被存储在哈希表的对应位置。理想情况下,哈希表的每个位置只存储一个数据项。
- 查找:通过哈希函数计算键的哈希值,然后直接访问哈希表中对应的位置,获取存储的数据。
- 处理冲突 :如果两个不同的键映射到同一位置(哈希冲突),哈希表需要有冲突处理机制,如链表法(在同一位置存储一个链表)或开放定址法(寻找下一个空闲位置)。
- 开放定址法
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的哈希地址,只要散列表足够大,空的哈希地址总能找到,并将记录存入。
它的公式是:
f(key)=(f(key) +di) MOD m ( di=1,2...m-1 )
我们在不断地求余数后得到结果,但效率很差。因此我们可以改进d=1^2, -1^2, 2^2, -22...q2, -q^2, (q≤m/2),这样就等于是可以双向寻找到可能的空位置。
增加平方运算的目的是为了不让关键字都聚集在某一块区域。称这种方法为二次探测法。
f(key)=(f(key) +di) MOD m (di=1^2, -1^2, 2^2, -22...q2, -q^2, q≤m/2)
还有一种方法是,在冲突时,对于位移量di采用随机函数计算得到,称之为随机探测法。
f(key)=(f(key) +di) MOD m (di是一个随机数列)
此时一定有人问,既然是随机,那么查找的时候不也随机生成吗?如何可以获得相同的地址呢?这是个问题。这里的随机其实是伪随机数。伪随机数是说,如果我们设置随机种子相同,则不断调用随机函数可以生成不会重复的数列,我们在查找时,用同样的随机种子,它每次得到的数列是相同的,相同的d; 当然可以得到相同的哈希地址。
-
- 链地址法
将所有关键字为同义词的记录存储在一一个 单链表中,我们称这种表为同义词子表,在哈希表中只存储所有同义词子表的头指针。
关键码集合{47,7,29,11,16.92.22.8.3},散列函数为H(kep)=keymod 11,用拉链法处理冲突,构造的开散列表为:
如图:
链地址法对于可能会造成很多冲突的哈希函数来说,提供了绝不会出现找不到地址的保障。当然,这也就带来了查找时需要遍历单链表的性能损耗。
哈希表查找代码实现
C 代码示例
1.链地址法
"Hash.h"
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#define Max_Hash_Size 10
// 链表节点
typedef struct Node {
int data; // 数据域
struct Node* next; // 指针域(指向下一个节点)
}Node;
// 哈希表(内含多个链表)
Node* HashTable[Max_Hash_Size];
/*************** 接口函数 ***************/
// 哈希函数,键值对索引
int HashFunc(int key);
// 初始化
void initHash();
// 插入
void insertHash(int key);
// 查找
int searchHash(int key);
// 删除
void deleteHash(int key);
// 打印哈希表
void printHash(int key);
"Hash.c"
#include"Hash.h"
// 除留余数法
int HashFunc(int key) {
return key % Max_Hash_Size;
}
// 初始化
void initHash() {
for (int i = 0; i < Max_Hash_Size; i++)
{
HashTable[i] = NULL;
}
}
// 插入
void insertHash(int key) {
int index = HashFunc(key);
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = key;
newNode->next = HashTable[index];
HashTable[index] = newNode;
}
// 查找
int searchHash(int key) {
int index = HashFunc(key);
Node* current = HashTable[index];
while (current != NULL)
{
if (current->data == key) {
return true;
// 找到元素
}
current = current->next;
}
return 0;
}
// 删除
void deleteHash(int key) {
int index = HashFunc(key);
Node* current = HashTable[index];
Node* prev = NULL;
while (current != NULL)
{
if (current->data == key) {
if (prev == NULL) {
HashTable[index] = current->next;
// 删除头结点
}
else {
prev->next = current->next;
// 删除非头节点
}
free(current);
return;
}
prev = current;
current = current->next;
}
}
// 打印哈希表
void printHash(int key) {
for (int i = 0; i < Max_Hash_Size; i++)
{
Node* current = HashTable[i];
printf("索引 %d: ", i);
while (current != NULL)
{
printf("%d ", current->data);
current = current->next;
}
printf("\n");
}
}
"Test.c"
#include"Hash.h"
void Test() {
initHash();
insertHash(10);
insertHash(30);
insertHash(40);
insertHash(15);
printf("Hash table after insertion:\n");
printHash();
printf("Searching for 20: %s\n", searchHash(40) ? "Found" : "Not Found");
printf("Searching for 25: %s\n", searchHash(15) ? "Found" : "Not Found");
deleteHash(40);
printf("Hash table after deletion of 40:\n");
printHash();
}
int main() {
Test();
return 0;
}
运行效果:
2.开放地址法(线性探索)
"OpenAddress.h"
#pragma once
/* 头文件和头部声明 */
#include <stdio.h>
#include <stdlib.h>
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12
#define NULLKEY -32768
#define STATUS unsigned int
#define ELEMTYPE int
// 哈希表结构
typedef struct HashNode {
ELEMTYPE* emlement; // 元素存放地址,用动态数组来分配
int count; // 元素个数
}HashTable;
// 接口函数
/* 初始哈希表 */
STATUS InitHashTable(HashTable* _hashTable);
/* 定义哈希函数 */
int Hash(ELEMTYPE key);
/* 将关键字插入哈希表 */
void InsertHash(HashTable* _hashTable, ELEMTYPE key);
/* 函数功能:利用哈希表查找关键字
* 参数列表:
* 1、h 初始化过的哈希表
* 2、key 查找的关键字
* 3、*addr 用于保存关键字位置的指针
* 函数返回值:
* SUCCESS:1 UNSUCCESS:0
*/
STATUS SearchHash(HashTable* h, int key, int* address);
"OpenAddress.c"
#include"OpenAdd.h"
/* 初始哈希表 */
STATUS InitHashTable(HashTable* _hashTable) {
_hashTable->count = HASHSIZE;
_hashTable->emlement = (int*)malloc(sizeof(int) * HASHSIZE);
for (int i = 0; i < HASHSIZE; i++)
{
_hashTable->emlement[i] = NULLKEY;
}
return SUCCESS;
}
/* 定义哈希函数 */
int Hash(ELEMTYPE key) {
return key % HASHSIZE;
// 除留余数法
}
/* 将关键字插入哈希表 */
void InsertHash(HashTable* _hashTable, ELEMTYPE key) {
int address = Hash(key);
// 求哈希地址
while (_hashTable->emlement[address] != NULLKEY)
{
// 哈希表内某个位置的值不为空,冲突
address = (address + 1) % HASHSIZE;
// 线性探测
}
_hashTable->emlement[address] = key;
}
STATUS SearchHash(HashTable* h, int key, int* address) {
*address = Hash(key);
while (h->emlement[*address] != key)
{
// 不匹配,引起冲突
*address = (*address + 1) & HASHSIZE;
if (h->emlement[*address] == NULLKEY || *address == Hash(key)) {
return UNSUCCESS;
}
}
return SUCCESS;
}
"Test.c"
#include"OpenAdd.h"
void Testopen() {
int arr[12] = { 12,67,56,16,25,37,22,29,15,47,48,34 };
HashTable* hashTable = (HashTable*)malloc(sizeof(HashTable));
InitHashTable(hashTable);
for (int i = 0; i < 12; i++)
{
InsertHash(hashTable, arr[i]);
}
/* 查看初始化后的哈希表 */
for (int i = 0; i < 12; i++)
{
printf(" %2d |", i);
}
printf("\n------------------------------------------------------------\n");
for (int i = 0; i < 12; i++)
{
printf(" %2d |", (*hashTable).emlement[i]);
}
printf("\n");
free(hashTable);
}
int main() {
Testopen();
return 0;
}
运行效果:
C++代码示例
1.链地址法
Hash.h
cpp
#pragma once
#include<iostream>
#include<memory>
#include<vector>
using namespace std;
template <class T>
class HashTable
{
private:
//链表结点
struct Node
{
T data;
Node* next;
Node(T val) : data(val),next(nullptr){}
};
vector<Node*> table; //哈希表
size_t cur_size; //元素个数
size_t max_size; //最大容量
double load_factor_threshold; // 负载因子,决定是否resize
int hashFunc(T key) const {
return key % max_size;
}
void reHash() {
int old_size = max_size;
max_size *= 2; // msvc标准,resize使用两倍扩容
vector<Node*> new_table(max_size, nullptr); // 重新构造结点
// 重新计算所有元素的位置,并插入到新表中
for (size_t i = 0; i < old_size; ++i)
{
Node* cur = table[i];
while (cur != nullptr)
{
int index = hashFunc(cur->data);
Node* new_node = new Node(cur->data);
new_node->next = new_table[index];
new_table[index] = new_node;
cur = cur->next;
}
}
table = move(new_table); // 新表代替旧表
}
public:
// 构造
HashTable(int init_size = 10, double load_fac = 0.75) : cur_size(0), max_size(init_size), load_factor_threshold(load_fac) {
table.resize(max_size, nullptr);
}
void insert(T key) {
// load_fac超出,就resize
if (cur_size >= max_size * load_factor_threshold) {
reHash();
}
int index = hashFunc(key);
Node* newNode = new Node(key);
newNode->next = table[index];
table[index] = newNode;
++cur_size;
}
bool search(T key) const {
int index = hashFunc(key);
Node* cur = table[index];
while (cur!=nullptr)
{
if (cur->data == key) {
return true;
}
cur = cur->next;
}
return false;
}
void del(T key) {
int index = hashFunc(key);
Node* current = table[index];
Node* prev = nullptr;
while (current != nullptr) {
if (current->data == key) {
if (prev == nullptr) {
table[index] = current->next; // 删除头结点
}
else {
prev->next = current->next; // 删除非头结点
}
delete current;
--cur_size;
return;
}
prev = current;
current = current->next;
}
}
// 打印哈希表
void print() const {
for (int i = 0; i < max_size; ++i) {
Node* current = table[i];
cout << "索引 " << i << ": ";
while (current != nullptr) {
cout << current->data << " ";
current = current->next;
}
cout << endl;
}
}
};
Test.cpp
cpp
#include"Hash.h"
void Test1() {
cout << "链表链接法: " << endl;
// 创建一个初始大小为10,负载因子为0.75的哈希表
HashTable<int> ht;
ht.insert(10);
ht.insert(28);
ht.insert(30);
ht.insert(50);
std::cout << "当前哈希表内容:\n";
ht.print();
std::cout << "查找元素 20: " << (ht.search(20) ? "找到了" : "没有找到") << "\n";
std::cout << "查找元素 50: " << (ht.search(50) ? "找到了" : "没有找到") << "\n";
ht.del(50);
std::cout << "删除元素 50 后的哈希表内容:\n";
ht.print();
// 插入更多元素以触发扩容
ht.insert(40);
ht.insert(70);
ht.insert(64);
std::cout << "插入更多元素后的哈希表内容:\n";
ht.print();
}
int main() {
Test1();
return 0;
}
效果展示:
2.开放地址法
OpenAddress.h
cpp
#pragma once
#include <iostream>
#include <vector>
#include <stdexcept>
#define INIT_SIZE 11 // 初始哈希表大小
#define NULLKEY -32768 // 空位的标记
#define DELETEDKEY -32769 // 删除标记,用于删除操作
using namespace std;
template <class T>
class OpenAddress
{
public:
OpenAddress(int init_size = INIT_SIZE, double load_fac = 0.75) : count(init_size), load_factor_threshold(load_fac), num_elements(0) {
table.resize(count, NULLKEY); // 初始化为NULL
}
//插入
void insert(T key) {
if (num_elements >= count * load_factor_threshold) {
Expand();
}
int address = hashFunc(key);
while (table[address] != NULLKEY && table[address] != DELETEDKEY) {
if (table[address] == key) {
return; // 如果元素已经存在,就不插入
}
address = (address + 1) % count; // 线性探测
}
table[address] = key; // 插入元素
num_elements++;
}
// 查找元素
bool Search(T key, int& address) {
address = hashFunc(key);
while (table[address] != NULLKEY) {
if (table[address] == key) {
return true; // 找到元素
}
address = (address + 1) % count; // 线性探测
}
return false; // 没有找到
}
// 删除元素
bool Delete(T key) {
int address;
if (Search(key, address)) {
table[address] = DELETEDKEY; // 删除标记
num_elements--;
return true;
}
return false;
}
// 打印哈希表内容
void PrintTable() const {
for (int i = 0; i < count; ++i) {
cout << "Slot " << i << ": ";
if (table[i] == NULLKEY) {
cout << "NULL";
}
else if (table[i] == DELETEDKEY) {
cout << "DELETED";
}
else {
cout << table[i];
}
cout << std::endl;
}
}
// 获取哈希表的容量
int GetTableSize() const {
return count;
}
// 获取当前元素个数
int GetNumElements() const {
return num_elements;
}
private:
vector<T> table; // 哈希表数组
int count; // 当前哈希表大小
int num_elements; // 当前存储的元素个数
double load_factor_threshold; // 负载因子阈值
int hashFunc(T key) const {
// 取模
return key % count;
}
void Expand() {
int new_count = count * 2 + 1; // 两倍扩容并且为奇数
vector<T> new_table(new_count, NULLKEY);
// 重新插入所有元素
for (int i = 0; i < count; ++i) {
if (table[i] != NULLKEY && table[i] != DELETEDKEY) {
int address = table[i] % new_count;
while (new_table[address] != NULLKEY) {
address = (address + 1) % new_count; // 线性探测
}
new_table[address] = table[i];
}
}
table = new_table;
count = new_count;
}
};
Test.cpp
cpp
#include"OpenAddress.h"
void Test2() {
cout << "线性探索: " << endl;
OpenAddress<int> hashTable;
// 插入一些元素
hashTable.insert(5);
hashTable.insert(17);
hashTable.insert(29);
hashTable.insert(12);
hashTable.insert(56);
hashTable.insert(72);
// 打印哈希表
hashTable.PrintTable();
// 查找元素
int address;
if (hashTable.Search(17, address)) {
std::cout << "找到了 17,位于地址: " << address << std::endl;
}
else {
std::cout << "未找到 17!" << std::endl;
}
// 删除元素
if (hashTable.Delete(17)) {
std::cout << "17 删除成功!" << std::endl;
}
else {
std::cout << "未找到 17,无法删除!" << std::endl;
}
// 打印哈希表
hashTable.PrintTable();
// 再次插入元素,触发扩展
hashTable.insert(100);
hashTable.insert(150);
std::cout << "插入更多元素后:" << std::endl;
hashTable.PrintTable();
}
效果展示: