哈希表原理及stl中的简单用法

哈希表是一种数据结构,提供快速插入和查找操作,时间复杂度通常为O(1)。哈希函数用于将键转换为数组下标,处理哈希冲突的方法包括开放地址法(如线性探测)和链表法。STL中的unordered_map实现了哈希表,提供了begin(),end(),empty(),size(),erase(),at(),find()等函数进行操作。文章详细介绍了哈希表的原理以及在Java和STL中的应用。
摘要由CSDN通过智能技术生成

简介

哈希表又叫做散列表,提供了快速插入操作和查找操作,无论数据多少,插入和查找操作的时间复杂度都是O(1),哈希表的查找速度非常快。所以在很多程序中都有使用哈希表,如拼音检查器。

哈希表是基于数据的一种数据结构,数组的特点是根据下标查找时速度快,但内存扩容的成本很高,因此哈希表满时,性能下降严重。

哈希表的一个重要概念是将关键字转换为数组下标,在哈希表中,这个过程由哈希函数完成,但有的关键字可以直接转换为数组的下标而不需要哈希函数。

如在大学期间,每个人的学号是独特的,因此学号作为关键字时可直接作为数组的下标而无需哈希函数,但如果使用姓名作为关键字时,就需要一个独特的哈希函数来将其转换。

哈希函数

哈希函数的作用是将非int类型的关键字转化为int,并以此作为数组的下标。

哈希函数的写法有很多种,下面是 「Hash Map」 中的哈希函数

static final int hash(Obeject key){
	int h;
	return(key == null) ? ) : (h = key.hashCode()) ^ (h >>> 16);
}

在Java中, 「Hash Map」 使用了 「hashCode」 完成转换,但无论使用何种转换方式,都要遵循以下三点。
1.哈希值对应数组下标,因此哈希值必须为非负数
2.为了保证通过key找到表中唯一的数据,所以须保证同一个key值生成的哈希值应该是一样的。
3.哈希冲突:两个不同的值通过哈希函数之后可能生成相同值,这点在下面说明。

哈希冲突

哈希冲突不可避免,解决哈希冲突的方法有 「开放地址法」「链表法」

开放地址法

在开放地址法中,若数据不能直接存放在哈希函数计算出来的数组下标时,就需要寻找其他位置来存放。在开放地址法中有三种方式来寻找其他的位置,分别是 「线性探测」「二次探测」「再哈希法」

线性探测

线性探测插入

在线性探测哈希表中,数据的插入是线性的查找空白单元,例如我们将数88经过哈希函数后得到的数组下标是16,但是在数组下标为16的地方已经存在元素,那么就找17,17还存在元素就找18,一直往下找,直到找到空白地方存放元素。
下面是Java中实现线性探测插入的代码。

private int hash(int key) {
    return (key % size);
}

// 插入
// @param student
 
public void insert(Student student){
    int key = student.getKey();
    int hashVal = hash(key);
    while (array[hashVal] !=null && array[hashVal].getKey() !=-1){
        ++hashVal;
        // 如果超过数组大小,则从第一个开始找
        hashVal %=size;
    }
    array[hashVal] = student;
}

上述代码使用了一种常用并简单的散列函数:取模,即Hash(key)=key mod p
此外还有以下常用的散列函数:
1.线性定址法:直接取关键字的某个线性函数作为存储地址,散列函数为:
            Hash(key) = a*key + b
2.平方取中法:对关键字取平方,然后将得到结果的中间几位作为存储地址;
3.折叠法:将关键字分割为几部分,然后将这几部分的叠加和作为存储地址。

线性探测的查找

线性探测哈希表的查找过程有点儿类似插入过程。我们通过散列函数求出要查找元素的键值对应的散列值,然后比较数组中下标为散列值的元素和要查找的元素。如果相等,则说明就是我们要找的元素;否则就顺序往后依次查找。如果遍历到数组中的空闲位置,还没有找到,就说明要查找的元素并没有在哈希表中。
Java版的代码实现

public Student find(int key){
    int hashVal = hash(key);
    while (array[hashVal] !=null){
        if (array[hashVal].getKey() == key){
            return array[hashVal];
        }
        ++hashVal;
        hashVal %=size;
    }

    return null;
}

线性探测的删除

线性探测哈希表在查找的时候,一旦我们通过线性探测方法,找到一个空闲位置,我们就可以认定哈希表中不存在这个数据。但是,如果这个空闲位置是我们后来删除的,就会导致原来的查找算法失效。本来存在的数据,会被认定为不存在。
因此我们需要一个特殊的数据来顶替这个被删除的数据,因为我们的学生学号都是正数,所以我们用学号等于-1来代表被删除的数据。
在这里插入图片描述
这样会带来一个问题,如何在线性探测哈希表中做了多次操作,会导致哈希表中充满了学号为-1的数据项,使的哈希表的效率下降,所以很多哈希表中没有提供删除操作,即使提供了删除操作的,也尽量少使用删除函数。
Java实现

public Student delete(int key){
    int hashVal = hash(key);
    while (array[hashVal] !=null){
        if (array[hashVal].getKey() == key){
            Student temp = array[hashVal];
            array[hashVal]= noStudent;
            return temp;
        }
        ++hashVal;
        hashVal %=size;
    }
    return null;
}

再哈希法

当第一个哈希后的值产生冲突时,调用第二个哈希函数。
i= (Hash1(key) + i * Hash2(key)) % p

链表法

将具有相同存储地址的关键字链成一单链表, m个存储地址就设m个单链表,然后用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构,假设散列函数为 Hash(key) = key %13,其拉链存储结构为:
在这里插入图片描述

如何使用STL库中的哈希表

导入头文件

#include<unordered_map>

声明与初始化
声明:

unordered_map<elemType_1, elemType_2> var_name; //声明一个没有任何元素的哈希表,
//其中elemType_1和elemType_2是模板允许定义的类型,如要定义一个键值对都为Int的哈希表:
unordered_map<int, int> map;

以上在声明哈希表的时候并没有给unordered_map传递任何参数,因此调用的是unordered_map的默认构造函数,生成一个空容器。初始化主要有一下几种方式:

  • 在定义哈希表的时候通过初始化列表中的元素初始化:
unordered_map<int, int> hmap{ {1,10},{2,12},{3,13} };
//如果知道要创建的哈希表的元素个数时,也可以在初始化列表中指定元素个数
unordered_map<int, int> hmap{ {{1,10},{2,12},{3,13}},3 };
  • 通过下标运算添加元素
//当我们想向哈希表中添加元素时也可以直接通过下标运算符添加元素,格式为: mapName[key]=value;
//如:hmap[4] = 14;
//但是这样的添加元素的方式会产生覆盖的问题,也就是当hmap中key为4的存储位置有值时,
//再用hmap[4]=value添加元素,会将原哈希表中key为4存储的元素覆盖
hmap[4] = 14;
hmap[5] = 15;
cout << hmap[4];  //结果为14
  • 复制构造
unordered_map<int, int> hmap{ {1,10},{2,12},{3,13} };
unordered_map<int, int> hmap1(hmap);

STL中哈希表的常用函数

  1. begin()函数:返回一个指向哈希表开始位置的迭代器。
unordered_map<int, int>::iterator iter = hmap.begin(); //申请迭代器,并初始化为哈希表的起始位置
cout << iter->first << ":" << iter->second;
  1. end()函数:返回一个指向哈希表结尾位置下一个元素的迭代器
unordered_map<int, int>::iterator iter = hmap.end();
  1. cbegin() 和 cend():这两个函数的功能和begin()与end()的功能相同,唯一的区别是cbegin()和cend()是面向不可变的哈希表(const)
const unordered_map<int, int> hmap{ {1,10},{2,12},{3,13} };
unordered_map<int, int>::const_iterator iter_b = hmap.cbegin(); //注意这里的迭代器也要是不可变的const_iterator迭代器
unordered_map<int, int>::const_iterator iter_e = hmap.cend();
  1. empty()函数:判断哈希表是否为空
bool isEmpty = hmap.empty();
  1. size()函数:返回哈希表的大小
int size = hmap.size();
  1. erase函数:删除某个位置的元素,或删除某个位置开始到某个位置结束这一范围内的元素,或者传入key值删除键值对。
unordered_map<int, int> hmap{{1,7},{2,17},{3,27}, };
unordered_map<int, int>::iterator iter_begin = hmap.begin();
unordered_map<int, int>::iterator iter_end = hamp.end();
hmap.erase(iter.begin());			// 删除开始位置的元素
hmap.erase(iter_begin, iter_end);   // 删除开始位置和结束位置之间的元素
hmap.erase(3);						// 删除key == 3的键值对
  1. at函数:返回对应key值哈希表中的元素(而非键值对)
unordered_map<int, int> hmap{{1,7},{2,17},{3,27},};
int elm = hamp.at(3);
  1. clear函数():清空指定哈希表内的所有元素。
hmap.clear();
  1. find()函数:以key作为参数寻找哈希表中的元素,若存在,则返回该位置的迭代器;若不存在,则返回哈希表的最后一个元素下一位置上的迭代器
unordered_map<int, int> hmap{{1,7},{2,17},{3,27},};
unordered_map<int, int>::iterator iter;
iter = hmap.find(2);     				//返回key == 2的迭代器,可以通过iter->second访问该key对应的元素。
if(iter != hmap.end())	cout << iter->second;
  1. bucket()函数:以key寻找哈希表中该元素的储存的bucket编号(unoredered_map的源码处理哈希冲突时采用的是链表法。通过一个个bucket存储元素)
int pos = hmap.bucket(key);
  1. bucket_count()函数:该函数返回哈希表中存在的存储桶总数(一个存储桶可以用来存放多个元素,也可以不存放元素,并且bucket的个数大于等于元素个数)
int count = hmap.bucket_count();
  1. count()函数: 统计某个key值对应的元素个数, 因为unordered_map不允许重复元素,所以返回值为0或1
int count = hmap.count(key);
  1. 哈希表的遍历: 通过迭代器遍历。
unordered_map<int, int> hmap{{1,7},{2,17},{3,27},}
unordered_map<int, int>::iterator iter = hmap.begin();
for(;iter != hmap.end();iter++){
	cout << "key: " << iter->first << "value: " << iter->second << endl;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值