【数据结构】哈希表的原理及其实现

文章目录

  • 哈希表的概念
  • 哈希函数的设计
    • 常见的哈希函数
  • 哈希冲突
    • 1. 闭散列
      • 代码实现
    • 2. 开散列
    • 拉链法的优点
  • 针对开散列哈希的扩展
  • 基于开散列拉链法封装哈希表
    • MyHash.h
  • 基于哈希表实现unordered_map类
    • Myunordered_map.h
  • 基于哈希表实现unordered_set类
    • Myunordered_map.h

哈希表的概念

哈希表,也被称为散列表。是一种通过键可以快速找到对应值的一种数据结构。像我们常使用的unordered系列的容器,其本质就是一个哈希表。哈希表最显著的特点就是查找数据的效率非常的高,平均查找的时间复杂度为O(1)。通过哈希函数将键值映射到数组的具体位置,再对其进行操作。

哈希表查找的过程为:通过键来找到数组中的一个位置,通过这个位置我们能找到该键唯一映射的值
哈希表的y重要设计模块就是哈希函数以及解决哈希冲突的方法。下面将对此做出解释:

哈希函数的设计

哈希表的性能的一个关键因素就是哈希函数的设计。具体的来说,哈希函数指的是通过键值找到映射到数组具体位置的方法。一个好的哈希函数能将键均匀的分布到哈希表的各个位置。

常见的哈希函数

  1. 直接定值法
    取关键字的某个线性函数为散列地址,该线性函数的返回值应该是一个整数。比如Hash(key)=A*Key+B
  2. 除留余数法
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

哈希冲突

什么叫哈希冲突?哈希冲突又叫做哈希碰撞。具体是指,多个键通过一个哈希函数得到的位置相同。造成哈希冲突的主要原因是,哈希表的大小是有限的,而输入的数据可能会非常多,因此不可能避免出现哈希冲突。

那出现哈希冲突之后该如何解决呢?下面给出几种常见的解决哈希冲突的方法:

1. 闭散列

闭散列又叫开放定址法。当发生哈希冲突时,如果该哈希表还有空位,那么就从冲突位置开始往后找,直到找到空位。该如何往后找空位呢?一个一个找嘛?

  • 线性探测:线性探测,从发生冲突的位置开始,一个一个往后面找,直到找到空位。
    在这里插入图片描述
  • 二次探测:从发生冲突的位置开始,使用一个二次探测公式来寻找下一个探测的位置,探测公式为:
    H(k)=(Hash(k)+C1*i+C2*(i^2))%p
    • 其中H(k)表示的是第i次探测的位置。
    • Hash(k)表示通过哈希函数得到的初始地址
    • C1C2是一个常数,C1通常为0,C2通常为1,即H(k)=(Hash(k)+(i^2))%p
    • i是探测的次数,从0开始
    • p是哈希表的大小

二次探测的优点是能有效减少一次探测中数据过于“集中”的问题,即连续的冲突位置,会形成长链。而二次探测会使得探测的位置更加分散。

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。

装载因子又叫做负载因子,是哈希表中元素个数与哈希表大小的比值

代码实现

哈希表类主要有两个模块需要实现,一个是节点类,还有一个是存储节点的容器。容器我们选择使用STL中的vector。对于节点类,我们希望至少能实现以下功能:

  • 存储键值对
  • 存储状态

值得注意的是,由于线性探测法的特性,删除一个元素之后可能会影响后续查找元素。我们需要给每个节点赋予三种状态表示:空节点、存在值得节点、被删除得节点。有了这三种种状态,我们在查找元素时,就不会因为中间曾经被删除的节点而影响。

#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
#include<vector>
using namespace std;
namespace Close_Hash{enum Statu{EMPTY,EXIST,DEL};template<class K,class V>struct Node {Node():_statu(EMPTY){}pair<K, V> _kv;Statu _statu;};template<class K>struct HashFunc{size_t operator()(const K& key) {return (size_t)key;}};template<>struct HashFunc<string> {size_t operator()(const string& key) {size_t res = 0;for (auto it : key) {int u = it - '0';res = res * 131 + u;}return res;}};template<class K,class V,class Hash=HashFunc<K>>class HashTable {public:HashTable(size_t capacity = 10): _totalSize(0), _ht(capacity){}V& operator[](const K& key) {Node<K, V>* node = Find(key);if (node) {return (node->_kv).second;}else {this->Insert(make_pair(key,0));return Find(key)->_kv.second;}}// 插入bool Insert(const pair<K, V>& val) {//负载超标if (_totalSize * 10 >= 7 * _ht.size()) {HashTable<K, V> newtable;newtable._ht.resize(_ht.size() * 2);for (int i = 0; i < _ht.size(); i++) {if (_ht[i]._statu == EXIST) {newtable.Insert(_ht[i]._kv);}}_ht.swap(newtable._ht);}Hash ha;size_t pos = ha(val.first) % _ht.size();//线性探测while (_ht[pos]._statu == EXIST) {pos++;pos = pos % _ht.size();}_ht[pos]._kv = val;_ht[pos]._statu = EXIST;_totalSize++;return true;}// 查找Node<K,V>* Find(const K& key) {Hash ha;size_t pos = ha(key) % _ht.size();while (_ht[pos]._statu != EMPTY) {if (_ht[pos]._statu == EXIST && _ht[pos]._kv.first == key) {return &_ht[pos];}pos++;pos = pos % _ht.size();}return nullptr;}// 删除bool Erase(const K& key) {Node<K, V>* node = Find(key);if (!node)return false;node->_statu = DEL;return true;}size_t Size() {return _ht.size();}bool Empty() const{return _ht.empty();}private:vector<Node<K,V>> _ht;size_t _totalSize;};}

2. 开散列

开散列法又被叫做链地址法或者拉链法。和闭散列不同的是,具有相同地址的关键码属于同一集合,并用一个单链表维护这个集合。我们将这个集合称为。开散列得到哈希表的每一个元素实际上是一个名为桶的单链表。于是,当我们发生哈希冲突时,不用去哈希表中找空位了,因为当前桶是一个单链表,插入元素时直接头插就好了。
在这里插入图片描述

拉链法的优点

拉链法处理哈希冲突的效率是非常高的。即使多个元素通过哈希函数得到的地址是一样的,也不会显著影响哈希表的性能。此外,拉链法下的负载因子是可以超过1的,因此拉链法下的哈希表可以灵活的应对动态数据集的增长,不需要频繁的调整哈希表的大小。值得一提的是,虽然看上去会使用额外的空间来存放指针,但由于不需要严格维护平衡因子,空间效率并不会比开放定址法低。因为开放定址法需要开大量额外的空间来保证负载因子不溢出。

代码实现:

跟线性探测代码不一样的地方在于,拉链法的节点实际上是一个链表的头节点(桶)。所以对于哈希表的节点类,我们希望有以下功能:

  • 指向下一个元素的指针
  • 存储键值对

在插入元素时,将新节点插入到桶中。

#pragma once
#include<vector>
#include<iostream>
using namespace std;namespace hash_bucket {template<class K,class V>struct HashNode {HashNode(const pair<K, V>& kv): _kv(kv),_next(nullptr){}HashNode():_next(nullptr){}pair<K, V> _kv;HashNode<K, V>* _next;};template<class K>struct HashFunc {size_t operator()(const K& k) {return (size_t)k;}};template<>struct HashFunc<string> {size_t operator()(const string& str) {size_t res = 0;for (auto it : str) {int u = it;res = res * 131 + u;}return res;}};template<class K,class V,class Hash=HashFunc<K>>class HashTable {public:typedef HashNode<K, V> Node;typedef Node* pNode;HashTable():_table(10, nullptr), _n(0){}//随机访问V& operator[](const K& k) {if (!Find(k)) {Insert(make_pair(k,V()));}return Find(k)->_kv.second;}//插入bool Insert(const pair<K, V>& kv) {Hash ha;//负载因子为1时扩容if (_n == _table.size()) {HashTable<K, V> newtable;newtable._table.resize(_table.size() * 2);for (size_t i = 0; i < _table.size(); i++) {if (_table[i]) {pNode cur = _table[i];while (cur) {pNode temp = cur->_next;size_t pos = ha(cur->_kv.first) % newtable._table.size();if (newtable._table[pos]) {cur->_next = newtable._table[pos];}newtable._table[pos] = cur;cur = temp;}}}_table.swap(newtable._table);}size_t pos = ha(kv.first) % _table.size();pNode newnode = new Node(kv);if (_table[pos] != nullptr) {newnode->_next = _table[pos];}_table[pos] = newnode;_n++;return true;}//查找pNode Find(const K& k) {Hash ha;size_t pos= ha(k) % _table.size();pNode cur = _table[pos];while (cur) {if (cur->_kv.first == k)return cur;cur = cur->_next;}return nullptr;}//删除bool Erase(const K& k) {Hash ha;if (!Find(k))return false;size_t pos = ha(k) % _table.size();pNode pre = nullptr;pNode cur = _table[pos];if (cur->_kv.first == k) {delete cur;_table[pos] = nullptr;return true;}else {while (cur) {if (cur->_kv.first == k) {pre->_next = cur->_next;delete cur;cur = nullptr;return true;}pre = cur;cur = cur->_next;}return false;}}private:vector<pNode> _table;int _n;//存储有效桶的个数};}

上述代码大致实现了基于开散列法的哈希表,支持键值对的插入,删除、查询以及修改。

注意开散列法哈希表的扩容方案。当负载因子达到一定值时,我们选择扩大哈希表的大小,对于旧哈希表的元素不用再重新拷贝构造,而是移植到新表中。这样就节省了很多空间。具体实现如下:

//负载因子为1时扩容
if (_n == _table.size()) {HashTable<K, V> newtable;//新表newtable._table.resize(_table.size() * 2);for (size_t i = 0; i < _table.size(); i++) {if (_table[i]) {//遍历旧表,移植节点pNode cur = _table[i];while (cur) {pNode temp = cur->_next;size_t pos = ha(cur->_kv.first) % newtable._table.size();if (newtable._table[pos]) {cur->_next = newtable._table[pos];}newtable._table[pos] = cur;cur = temp;}}}_table.swap(newtable._table);
}

针对开散列哈希的扩展

如何给上述开散列哈希表类增加迭代器呢?
由于迭代器中需要访问哈希表而不仅仅是桶,而哈希表又属于哈希表类的私有成员。因此,设计迭代器模块时,我们可以将迭代器类设置为哈希表类的内部类。而一个类的内部类是这个类的友元,因此迭代器就能访问到哈希表。给出以迭代器代码,每一个迭代器对象指向哈希表中桶内的一个节点

//迭代器内部类
template<class Ptr, class Ref>
struct _HashIterator {typedef _HashIterator<Ptr, Ref> self;pNode _node;const HashTable* _pht;_HashIterator(pNode pnode, const HashTable* pht):_node(pnode), _pht(pht){}Ref operator*() {return _node->_data;}Ptr operator->() {return &_node->_data;}self& operator=(const self it) {_node = it._node;_pht = it._pht;return *this;}self& operator++() {if (_node->_next) {_node = _node->_next;}else {KeyOft kft;Hash ha;size_t pos = ha(kft(_node->_data)) % _pht->_table.size();size_t i = pos + 1;for (; i < _pht->_table.size(); i++) {if (_pht->_table[i]) {break;}}if (i == _pht->_table.size()) {_node = nullptr;}else {_node = _pht->_table[i];}}return *this;}bool operator!=(const self& iterator) {return iterator._node != _node;}
};

该迭代器主要实现了重载操作符的作用,希望能将迭代器当指针来使用。

基于开散列拉链法封装哈希表

MyHash.h

该文件封装实现了一个哈希表类,哈希函数是除留余数,解决哈希冲突的方法采用拉链法。
代码:

#pragma once
#include<vector>
#include<iostream>
using namespace std;namespace hash_bucket {template<class T>struct HashNode {HashNode(const T& data): _data(data), _next(nullptr){}HashNode():_next(nullptr){}T _data;HashNode<T>* _next;};template<class K>struct HashFunc {size_t operator()(const K& k) {return (size_t)k;}};template<>struct HashFunc<string> {size_t operator()(const string& str) {size_t res = 0;for (auto it : str) {int u = it;res = res * 131 + u;}return res;}};template<class K, class T, class KeyOft, class Hash = HashFunc<K>>class HashTable {public:typedef HashNode<T> Node;typedef Node* pNode;HashTable():_table(10, nullptr), _n(0){}//迭代器内部类template<class Ptr, class Ref>struct _HashIterator {typedef _HashIterator<Ptr, Ref> self;pNode _node;const HashTable* _pht;_HashIterator(pNode pnode, const HashTable* pht):_node(pnode), _pht(pht){}Ref operator*() {return _node->_data;}Ptr operator->() {return &_node->_data;}self& operator=(const self it) {_node = it._node;_pht = it._pht;return *this;}self& operator++() {if (_node->_next) {_node = _node->_next;}else {KeyOft kft;Hash ha;size_t pos = ha(kft(_node->_data)) % _pht->_table.size();size_t i = pos + 1;for (; i < _pht->_table.size(); i++) {if (_pht->_table[i]) {break;}}if (i == _pht->_table.size()) {_node = nullptr;}else {_node = _pht->_table[i];}}return *this;}bool operator!=(const self& iterator) {return iterator._node != _node;}};typedef _HashIterator<T*, T&> Iterator;typedef _HashIterator<const T*, const T&> Const_Iterator;//迭代器Iterator begin() {for (size_t i = 0; i < _table.size(); i++) {if (_table[i]) {return Iterator(_table[i], this);}}return Iterator(nullptr, this);}Iterator end() {return Iterator(nullptr, this);}Const_Iterator begin()const {for (size_t i = 0; i < _table.size(); i++) {if (_table[i]) {return Iterator(_table[i], this);}}return Iterator(nullptr, this);}Const_Iterator end() const {return Iterator(nullptr, this);}//随机访问/*V& operator[](const K& k) {if (!Find(k)) {Insert(make_pair(k,V()));}return Find(k)->_kv.second;}*///插入pair<Iterator, bool> Insert(const T& val) {Hash ha;KeyOft kft;Iterator it = Find(kft(val));if (it._node != nullptr)return make_pair(it, false);//负载因子为1时扩容if (_n == _table.size()) {HashTable<K, T, KeyOft> newtable;newtable._table.resize(_table.size() * 2);for (size_t i = 0; i < _table.size(); i++) {if (_table[i]) {pNode cur = _table[i];while (cur) {pNode temp = cur->_next;size_t pos = ha(kft(cur->_data)) % newtable._table.size();if (newtable._table[pos]) {cur->_next = newtable._table[pos];}newtable._table[pos] = cur;cur = temp;}}}_table.swap(newtable._table);}size_t pos = ha(kft(val)) % _table.size();pNode newnode = new Node(val);if (_table[pos] != nullptr) {newnode->_next = _table[pos];}_table[pos] = newnode;_n++;return make_pair(Iterator(newnode, this), true);}//查找Iterator Find(const K& k) {Hash ha;KeyOft kft;size_t pos = ha(k) % _table.size();pNode cur = _table[pos];while (cur) {if (kft(cur->_data) == k)return Iterator(cur, this);cur = cur->_next;}return Iterator(nullptr, this);}//删除bool Erase(const K& k) {Hash ha;KeyOft kft;if (!Find(k))return false;size_t pos = ha(k) % _table.size();pNode pre = nullptr;pNode cur = _table[pos];if (kft(cur->_data) == k) {delete cur;_table[pos] = nullptr;return true;}else {while (cur) {if (kft(cur->_data) == k) {pre->_next = cur->_next;delete cur;cur = nullptr;return true;}pre = cur;cur = cur->_next;}return false;}}private:vector<pNode> _table;int _n;};}

基于哈希表实现unordered_map类

基于MyHash.h封装的哈希表类提供的接口,实现一个unordered_map。unordered_map的值是一个键值对。

Myunordered_map.h

该文件基本实现了unordered_map类。
代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include"MyHash.h"namespace bit {template<class K, class V, class Hash =	hash_bucket::HashFunc<K> >class unordered_map {struct MapKeyOft {const K& operator()(const pair<K, V>& kv) {return kv.first;}};public:typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOft, Hash>::Iterator iterator;//typedef typename hash_bucket::HashTable<K, pair<const K, V>, KeyOft, Hash>::Const_Iterator const_iterator;iterator begin() {return _ht.begin();}iterator end() {return _ht.end();}V& operator[](const K& k) {pair<iterator,bool>res=insert(make_pair(k, V()));return res.first->second;}pair<iterator, bool> insert(const pair<K,V>& kv) {return _ht.Insert(kv);}bool erase(const K& k) {return _ht.Erase(k);}pair<iterator, bool> find(const K& k) {return _ht.Find(k);}private:hash_bucket::HashTable<K, pair<const K, V>, MapKeyOft, Hash> _ht;};}

在这里插入图片描述

基于哈希表实现unordered_set类

基于MyHash.h封装的哈希表类提供的接口,实现一个unordered_set类。

Myunordered_map.h

该文件基本实现了unordered_set类。
代码:

#include"MyHash.h"namespace bit {template<class K,class Hash = hash_bucket::HashFunc<K> >class unordered_set {struct SetKeyOft {const K& operator()(const K& k) {return k;}};public:typedef typename hash_bucket::HashTable<K, const K, SetKeyOft, Hash>::Iterator iterator;typedef typename hash_bucket::HashTable<K, const K, SetKeyOft, Hash>::Const_Iterator const_iterator;iterator begin() {return _ht.begin();}iterator end() {return _ht.end();}pair<iterator, bool> insert(const K& kv) {return _ht.Insert(kv);}bool erase(const K& k) {return _ht.Erase(k);}pair<iterator, bool> find(const K& k) {return _ht.Find(k);}private:hash_bucket::HashTable<K, const K, SetKeyOft, Hash> _ht;};}

在这里插入图片描述

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

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

相关文章

匠心独运的掺Si量子势垒策略,显著提升了AlGaN基深紫外LED出光率

WHU团队凭借匠心独运的三明治式掺Si量子势垒策略&#xff0c;显著提升了AlGaN基深紫外光LED的效率&#xff0c;这一创新成果为中国武汉大学的研究团队所取得。他们巧妙地设计出一种三明治状Si掺杂&#xff08;未掺杂&#xff09;方案&#xff0c;应用于Al0.6Ga0.4N量子势垒中&a…

WSL安装CentOS系统

1.首选找一个linux系统&#xff0c;执行docker命令 docker run -it --rm centos:7 bash 2.开一个新窗口&#xff0c;将系统导出 docker export e0ee25406703 -o centos.tar 3.切换到wsl命令&#xff0c;导入tar包 wsl --import centos D:\wsl\centos D:\wsl\centos.tar cen…

llvm实践日志(1)---安装llvm

参考了官方教程&#xff1a;Getting Started with the LLVM System — LLVM 19.0.0git documentation 首先从github拷贝一份源码&#xff0c;使用了官方推荐的方法&#xff1a; git clone --depth 1 https://github.com/llvm/llvm-project.git 我的系统是ubuntu22&#xff0…

人工智能安全方面存在哪些问题

人工智能的安全方面存在多个问题&#xff0c;这些问题可以归纳为以下几个方面&#xff1a; 一、网络安全问题 数据泄露风险&#xff1a;人工智能系统依赖于大量数据进行学习和推理&#xff0c;这些数据可能包含敏感信息&#xff0c;如用户隐私、商业机密等。如果系统存在安全…

Java面试题: 解释一下Java中的Happens-Before规则。

Java中的Happens-Before规则是Java内存模型&#xff08;Java Memory Model, JMM&#xff09;的核心概念之一&#xff0c;它定义了多线程环境中操作之间的内存可见性关系。这些规则确保了在并发执行的线程之间&#xff0c;对共享数据的访问能够保持正确的顺序和可见性。 Happen…

3. CSS的色彩与背景

3.1 CSS3中的色彩 CSS3扩展了颜色的定义方式&#xff0c;使得开发者能够使用更多样化和灵活的颜色表达方式。这包括RGB、RGBA、HSL、HSLA等格式&#xff0c;以及支持透明度和渐变的特性。 3.1.1 颜色格式 十六进制颜色 十六进制颜色是最常用的颜色表示法&#xff0c;以#开头…

queue学习

std::queue 类是一种容器适配器&#xff0c;它提供队列的功能——尤其是 FIFO&#xff08;先进先出&#xff09;数据结构。此类模板用处为底层容器的包装器——只提供特定的函数集合。queue 在底层容器尾端推入元素&#xff0c;从首端弹出元素。 元素访问 front 访问第一个元素…

20240526每日后端---------分享一些开发必备网站

代码开发工具: https://www.matools.com/ 前端开发网站&#xff1a; https://ui.bqrdh.com/#google_vignette 后端开发网站&#xff1a; https://javaguide.cn/ 设计模式分析&#xff1a; https://refactoringguru.cn/design-patterns/catalog

Elastic Cloud 将 Elasticsearch 向量数据库优化配置文件添加到 Microsoft Azure

作者&#xff1a;来自 Elastic Serena Chou, Jeff Vestal, Yuvraj Gupta 今天&#xff0c;我们很高兴地宣布&#xff0c;我们的 Elastic Cloud Vector Search 优化硬件配置文件现已可供 Elastic Cloud on Microsoft Azure 用户使用。 此硬件配置文件针对使用 Elasticsearch 作…

web如何做接口层面自动化测试?

接口层面约等于集成化测试&#xff0c;且需要启动web容器 一般web项目的&#xff0c;代码都是按照分层开发的&#xff0c;业务主要是集中在service和dao层&#xff0c;而我们如果仅仅是利用之前的单元测试,然后把依赖的代码直接mock掉&#xff0c;仅仅测试controller这一块是没…

国内github镜像站点

以下几个国内的GitHub镜像站点来提高访问速度&#xff1a; 1. kgithub.com&#xff1a;您可以将GitHub源网址前面加k来使用&#xff0c;例如将https://github.com/username/repo.git改为https://kgithub.com/username/repo.git。这个镜像站点允许查看代码、Issue和评论&#x…

ICP算法的具体计算流程

ICP算法的具体计算流程 初始对齐&#xff1a; 输入&#xff1a;两组点云&#xff0c;源点云和目标点云。初始化&#xff1a;选择一个初始的对齐变换&#xff0c;通常是单位矩阵&#xff08;表示没有初始变换&#xff09;。 迭代过程&#xff1a; 步骤1&#xff1a;最近点查找&a…

数据库SQL语言实战(十)(最后一篇)

目录 前言 练习题 实验八 实验九 题目一 题目二 总结 前言 本篇练习题的重点有两个&#xff1a; 一、测试提交commit和回滚rollback的作用,了解锁等待、授权等知识。 二、学会复制表结构、学会插入数据&#xff0c;特别是学会如何避免重复插入&#xff0c;也就是如何避…

ASP.NET MVC 快速入门(图文版)

今年是2024年了&#xff0c;没有多少人在ASP.NET 去做开发&#xff0c;都使用ABP框架 &#xff0c;不过我们仍然需要了解ASP.NET MVC 的一个开发流程 MVC概述 MVC是当前比较流行的WEB程序开发模式之一&#xff0c;ASP.NET MVC是.Net对MVC的一种实现。MVC&#xff08;Model View…

vue全局修改设置滚动条样式

vue全局修改设置滚动条样式 具体代码&#xff1a; ::-webkit-scrollbar{ /*滚动条整体样式*/width: 6px; /*高宽分别对应横竖滚动条的尺寸*/height: 1px; } ::-webkit-scrollbar-thumb{ /*滚动条里面小方块*/border-radius: 5px;background: #cfcbcb; } ::-webkit-scrollbar-…

websocket的使用及建立连接

1. websocket 1.客户端与服务端建连接2.客户端向服务端发送消息3.服务端在接受消息后以“response-接受的消息内容“的形式返回给客户端4.当服务端收到第5条信息的时候&#xff0c;主动关闭与客户端的连接 客户端代码 ​private void clientWebSocket(String url) {OkHttpClie…

声压级越大,STIPA 越好,公共广播就越清晰吗?

在公共广播中&#xff0c;有些朋友经常问到是不是声压越大&#xff0c;广播清晰度就越高&#xff0c;下面我从搜集了一些专业技术资料&#xff0c;供大家参考。 一、声压级越大&#xff0c;STIPA 越好吗&#xff1f; 不完全是。最初&#xff0c;人们认为当声压级达到 60 dBA 以…

氢燃料电池汽车行业发展

文章目录 前言 市场分布 整车销售 发动机配套 氢气供应 发展动能 参考文献 前言 见《氢燃料电池技术综述》 见《燃料电池工作原理详解》 见《燃料电池发电系统详解》 见《燃料电池电动汽车详解》 市场分布 纵观全球的燃料电池汽车市场&#xff0c;截至2022年底&#xff…

2024最新 Jenkins + Docker实战教程(一) - Jenkins介绍及安装

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

Android BACK键和HOME键应用差异详解

文章目录 1、应用层分析1.1 BACK键功能实现 1.2 HOME键功能实现 1.3 BACK键与HOME键的区别 2、系统层分析2.1 BACK键的处理2.2 HOME键的处理2.3 代码分析BACK键HOME键BACK键的系统代码分析HOME键的系统代码分析BACK键HOME键 3、优缺点分析3.1 BACK键3.2 HOME键 4、项目中的使用…