C++ 哈希表原理与实现
介绍 C++ 哈希表原理与实现。涵盖 unordered_map/set 使用、哈希概念、冲突处理、负载因子、哈希函数设计及全域散列防御。详解开放定址法(线性探测、二次探测)与链地址法实现,包含扩容策略、状态标识及关键代码。对比红黑树与哈希表性能差异,解析 STL 容器底层机制。

介绍 C++ 哈希表原理与实现。涵盖 unordered_map/set 使用、哈希概念、冲突处理、负载因子、哈希函数设计及全域散列防御。详解开放定址法(线性探测、二次探测)与链地址法实现,包含扩容策略、状态标识及关键代码。对比红黑树与哈希表性能差异,解析 STL 容器底层机制。

在介绍哈希表之前,我们先了解下 unordered_map 和 unordered_set
链接:参考文档

这里的 unordered_set 其实与 set 相似,只不过底层实现是不一样,还有其他特殊的不同。不过他们大部分功能是相同的。
unordered_set 的声明如下,Key 就是 unordered_set 底层关键字的类型
• unordered_set 默认要求 Key 支持转换为整形,如果不支持或者想按自己的需求走可以自行实现支持将 Key 转成整形的仿函数传给第二个模板参数
• unordered_set 默认要求 Key 支持比较相等,如果不支持或者想按自己的需求走可以自行实现支持将 Key 比较相等的仿函数传给第三个模板参数
• unordered_set 底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第四个参数。
• 一般情况下,我们都不需要传后三个模板参数
• unordered_set 底层是用哈希桶实现,增删查平均效率是 O(1),迭代器遍历不再有序,为了跟 set 区分,所以取名 unordered_set。
• 前面部分我们已经学习了 set 容器的使用,set 和 unordered_set 的功能高度相似,只是底层结构不同,有一些性能和使用差异。
template<class Key, // unordered_set::key_type/value_type
class Hash = hash<Key>, // unordered_set::hasher,支持 key 转型为无符号整型
class Pred = equal_to<Key>, // unordered_set::key_equal,支持 key 相等
class Alloc = allocator<Key> // unordered_set::allocator_type
class unordered_set;
• 查看文档我们会发现 unordered_set 的支持增删查且跟 set 的使用一模一样,关于使用我们这里就不再赘述和演示了。
• unordered_set 和 set 的第一个差异是对 key 的要求不同,set 要求 Key 支持小于比较,而 unordered_set 要求 Key 支持转成整形且支持等于比较,要理解 unordered_set 的这个两点要求得后续我们结合哈希表底层实现才能真正理解,也就是说这本质是哈希表的要求。
•unordered_set 和 set 的第二个差异是迭代器的差异,set 的 iterator 是双向迭代器,unordered_set 是单向迭代器,其次 set 底层是红黑树,红黑树是二叉搜索树,走中序遍历是有序的,所以 set 迭代器遍历是有序 + 去重。而 unordered_set 底层是哈希表,迭代器遍历是无序 + 去重。
•unordered_set 和 set 的第三个差异是性能的差异,整体而言大多数场景下,unordered_set 的增删查改更快一些,因为红黑树增删查改效率是 O(logN),而哈希表增删查平均效率是 O(1),具体可以参看下面代码的演示的对比差异。

#include <unordered_map>
#include <set>
#include <iostream>
using namespace std;
int test_set2() {
const size_t N = 1000000;
unordered_set<int> us;
set<int> s;
vector<int> v;
v.reserve(N);
srand(time(0));
for (size_t i = 0; i < N; ++i) {
//v.push_back(rand()); // N 比较大时,重复值比较多
v.push_back(rand() + i); // 重复值相对少
//v.push_back(i); // 没有重复,有序
}
// 21:15
size_t begin1 = clock();
for (auto e : v) { s.insert(e); }
size_t end1 = clock();
cout << "set insert:" << end1 - begin1 << endl;
size_t begin2 = clock();
us.reserve(N);
for (auto e : v) { us.insert(e); }
size_t end2 = clock();
cout << "unordered_set insert:" << end2 - begin2 << endl;
int m1 = ;
begin3 = ();
( e : v) {
ret = s.(e);
(ret != s.()) { ++m1; }
}
end3 = ();
cout << << end3 - begin3 << << m1 << endl;
m2 = ;
begin4 = ();
( e : v) {
ret = us.(e);
(ret != us.()) { ++m2; }
}
end4 = ();
cout << << end4 - begin4 << << m2 << endl;
cout << << s.() << endl;
cout << << us.() << endl << endl;
begin5 = ();
( e : v) { s.(e); }
end5 = ();
cout << << end5 - begin5 << endl;
begin6 = ();
( e : v) { us.(e); }
end6 = ();
cout << << end6 - begin6 << endl << endl;
;
}
{
();
;
}
• 查看文档我们会发现 unordered_map 的支持增删查改且跟 map 的使用一模一样,关于使用我们这里就不再赘述和演示了。
•unordered_map 和 map 的第一个差异是对 key 的要求不同,map 要求 Key 支持小于比较,而 unordered_map 要求 Key 支持转成整形且支持等于比较,要理解 unordered_map 的这个两点要求得后续我们结合哈希表底层实现才能真正理解,也就是说这本质是哈希表的要求。
•unordered_map 和 map 的第二个差异是迭代器的差异,map 的 iterator 是双向迭代器,unordered_map 是单向迭代器,其次 map 底层是红黑树,红黑树是二叉搜索树,走中序遍历是有序的,所以 map 迭代器遍历是 Key 有序 + 去重。而 unordered_map 底层是哈希表,迭代器遍历是 Key 无序 + 去重。
•unordered_map 和 map 的第三个差异是性能的差异,整体而言大多数场景下,unordered_map 的增删查改更快一些,因为红黑树增删查改效率是 O(logN),而哈希表增删查平均效率是 O(1),具体可以参看下面代码的演示的对比差异。
有上面可以得知,unordered_set、unordered_map 底层是哈希表实现的,我们不禁好奇,哈希表是怎么样的呢?哈希表是怎么样实现 O(1) 的效率的呢?为什么会无序呢?
哈希 (hash) 又称散列,是一种组织数据的方式。从译名来看,有散乱排列的意思。本质就是通过哈希函数把关键字 Key 跟存储位置建立一个映射关系,查找时通过这个哈希函数计算出 Key 存储的位置,进行快速查找。
当关键字的范围比较集中时,直接定址法就是非常简单高效的方法,比如一组关键字都在 [0,99] 之间,那么我们开一个 100 个数的数组,每个关键字的值直接就是存储位置的下标。再比如一组关键字值都在 [a,z] 的小写字母,那么我们开一个 26 个数的数组,每个关键字 ascii 码-'a' ascii 码就是存储位置的下标。也就是说直接定址法本质就是用关键字计算出一个绝对位置或者相对位置。这个方法我们在计数排序部分已经用过了,其次在 string 章节的下面 OJ 也用过了。
// 直接定址法:这种方法高效,简单
class Solution {
public:
int firstUniqChar(string s) {
// 每个字母的 ascii 码-'a'的 ascii 码作为下标映射到 count 数组,数组中存储出现的次数
int count[26] = {0};
// 统计次数
for (auto ch : s) { count[ch - 'a']++; }
for (size_t i = 0; i < s.size(); ++i) {
if (count[s[i] - 'a'] == 1) return i;
}
return -1;
}
};
总结:直接定地址法:是最简洁、最高效的哈希,但是也有限制条件:数据要集中。如果数据离散那么就会导致空间利用率低、关键字范围限制、机动性差
哈希冲突体现了直接定址法的弊端:
直接定址法的缺点也非常明显,当关键字的范围比较分散时,就很浪费内存甚至内存不够用。假设我们只有数据范围是 [0, 9999] 的 N 个值,我们要映射到一个 M 个空间的数组中(一般情况下 M >= N),那么就要借助哈希函数 (hash function) hf,关键字 key 被放到数组的 h(key) 位置,这里要注意的是 h(key) 计算出的值必须在 [0, M) 之间。这里存在的一个问题就是,两个不同的 key 可能会映射到同一个位置去,这种问题我们叫做哈希冲突,或者哈希碰撞。理想情况是找出一个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的,所以我们尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出决冲突的方案。
总结:在哈希表中避免不了哈希映射,为了解决或适应哈希冲突,人们要去设计出减缓哈希冲突或适应哈希冲突的方案。
假设哈希表中已经映射存储了 N 个值,哈希表的大小为 M,那么 load_factor = N / M,负载因子有些地方也翻译为载荷因子/装载因子等,他的英文为 load factor。负载因子越大,哈希冲突的概率越高,空间利用率越高;负载因子越小,哈希冲突的概率越低,空间利用率越低;
我们将关键字映射到数组中位置,一般是整数好做映射计算,如果不是整数,我们要想办法转换成整数,这个细节我们后面代码实现中再进行细节展示。下面哈希函数部分我们讨论时,如果关键字不是整数,那么我们讨论的 Key 是关键字转换成的整数。
【注意】 这是对哈希的数据类型 Key 值的要求如下: 要求 1:Key 值要由 string、float、double------>无符号整形 要求 2:Key 值要支持比较相等
一个好的哈希函数应该让 N 个关键字被等概率的均匀的散列分布到哈希表的 M 个空间中,但是实际中却很难做到,但是我们要尽量往这个方向去考量设计。
【说明】 哈希函数功能: 映射对应数据的位置,以方便在哈希表中存储,减少哈希冲突**
• 除法散列法也叫做除留余数法,顾名思义,假设哈希表的大小为 M,那么通过 key 除以 M 的余数作为映射位置的下标,也就是哈希函数为:h(key) = key % M。
•当使用除法散列法时,要避免 M 为某些值,如 2 的冥,10 的冥等。如果是 2^x,那么 key %2^x 本质相当于保留 key 的后 X 位,那么后 x 位相同的值,计算出的哈希值都是一样的,就冲突了。如:{63 , 31} 看起来没有关联的值,如果 M 是 16,也就是 2^4,那么计算出的哈希值都是 15,因为 63 的二进制后 8 位是 00111111,31 的二进制后 8 位是 00011111。如果是 10^x,就更明显了,保留的都是 10 进值的后 x 位,如:{112, 12312},如果 M 是 100,也就是 10^2,那么计算出的哈希值都是 12。
•当使用除法散列法时,建议 M 取不太接近 2 的整数次幂的一个质数 (素数)。
出了除法散列之外还有许多的哈希函数,这提一下: 乘法散列法
• 乘法散列法对哈希表大小 M 没有要求,他的大思路第一步:用关键字 K 乘上常数 A (0<A<1),并抽 取出 kA 的小数部分。第二步:后再用 M 乘以 kA 的小数部分,再向下取整。
• h(key) = floor(M × ((A × key)%1.0)),其中 floor 表示对表达式进行下取整,A∈(0,1),这里最重要的是 A 的值应该如何设定,Knuth 认为 (黄金分割点) 比较好。h(key) = floor(M × ((A ×key)%1.0)) A = (√5 − 1)/2 = 0.6180339887…
• 乘法散列法对哈希表大小 M 是没有要求的,假设 M 为 1024,key 为 1234,A = 0.6180339887, Akey = 762.6539420558,取小数部分为 0.6539420558, M×((A×key)%1.0) = 0.65394205581024 =669.6366651392,那么 h(1234) = 669。
全域散列法
• 如果存在一个恶意的对手,他针对我们提供的散列函数,特意构造出一个发生严重冲突的数据集,比如,让所有关键字全部落入同一个位置中。这种情况是可以存在的,只要散列函数是公开且确定的,就可以实现此攻击。解决方法自然是见招拆招,给散列函数增加随机性,攻击者就无法找出确定可以导致最坏情况的数据。这种方法叫做全域散列。
解决方案
• h (key) = ((a × key + b)%P )%M ,P 需要选一个足够大的质数,a 可以随机选 [1,P-1] 之间的任意整数,b 可以随机选 [0,P-1] 之间的任意整数,这些函数构成了一个 P(P-1) 组全域散列函数组。 假设 P=17,M=6,a = 3,b = 4, 则 h34 (8) = ((3 × 8 + 4)%17)%6 = 5*
• 需要注意的是每次初始化哈希表时,随机选取全域散列函数组中的一个散列函数使用,后续增删查改都固定使用这个散列函数,否则每次哈希都是随机选一个散列函数,那么插入是一个散列函数,查找又是另一个散列函数,就会导致找不到插入的 key 了。
在开放定址法中所有的元素都放到哈希表里,当关键字 key 用哈希函数计算出的位置冲突了,则按照某种规则找到一个没有存储数据进行存储,开放定址法中负载因子一定是小于 1 的。这里的规则有三种:线性探测、二次探测、双重探测。
线性探测
• 从发生冲突的位置开始,依次线性向后探测,直到寻找到下一个没有存储数据的位置为止,如果走到哈希表尾,则回绕到哈希表头的位置。
•h(key) = hash0 = key % M, hash0 位置冲突了,则线性探测公式为:hc(key,i) = hashi =(hash0 + i) % M,i = {1, 2, 3, …, M −1},因为负载因子小于 1,则最多探测 M-1 次,一定能找到一个存储 key 的位置。
•线性探测的比较简单且容易实现,线性探测的问题假设,hash0 位置连续冲突,hash0,hash1,hash2 位置已经存储数据了,后续映射到 hash0,hash1,hash2,hash3 的值都会争夺 hash3 位置,这种现象叫做群集/堆积。下面的二次探测可以一定程度改善这个问题。
• 下面演示 {19,30,5,36,13,20,21,12} 等这一组值映射到 M=11 的表中。
h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) =10,h(12) = 1

二次探测
• 从发生冲突的位置开始,依次左右按二次方跳跃式探测,直到寻找到下一个没有存储数据的位置为止,如果往右走到哈希表尾,则回绕到哈希表头的位置;如果往左走到哈希表头,则回绕到哈希表 尾的位置;
• h(key) = hash0 = key % M , hash0 位置冲突了,则二次探测公式为:hc(key,i) = hashi = (hash0 ± i2 ) % M,i = {1, 2, 3, …, 2/M}
• 二次探测当 hashi = (hash0 − i2)%M 时,当 hashi<0 时,需要 hashi += M • 下面演示 {19,30,52,63,11,22} 等这一组值映射到 M=11 的表中。
开放定址法在实践中,不如下面讲的链地址法,因为开放定址法解决冲突不管使用哪种方法,占用的 都是哈希表中的空间,始终存在互相影响的问题。所以开放定址法,我们简单选择线性探测实现即可。
开放定址法的哈希表结构
enum State { EXIST, EMPTY, DELETE };
template<class K, class V>
struct HashData {
pair<K, V> _kv;
State _state = EMPTY;
};
template<class K, class V>
class HashTable {
private:
vector<HashData<K, V>> _tables;
size_t _n = 0; // 表中存储数据个数
};
要注意的是这里需要给每个存储值的位置加一个状态标识,否则删除一些值以后,会影响后面冲突的 值的查找。如下图,我们删除 30,会导致查找 20 失败,当我们给每个位置加一个状态标识 {EXIST,EMPTY,DELETE} ,删除 30 就可以不用删除值,而是把状态改为 DELETE,那么查找 20 时是遇到 EMPTY 才能,就可以找到 20。
扩容 这里我们哈希表负载因子控制在 0.7,当负载因子到 0.7 以后我们就需要扩容了,我们还是按照 2 倍扩 容,但是同时我们要保持哈希表大小是一个质数,第一个是质数,2 倍后就不是质数了。那么如何解决 了,一种方案就是上面 1.4.1 除法散列中我们讲的 Java HashMap 的使用 2 的整数幂,但是计算时不能直 接取模的改进方法。另外一种方案是 sgi 版本的哈希表使用的方法,给了一个近似 2 倍的质数表,每次去 质数表获取扩容后的大小。
inline unsigned long __stl_next_prime(unsigned long n) {
// Note: assumes long is at least 32 bits.
static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] = {
53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433, 1572869, 3145739,
6291469, 12582917, 25165843, 50331653, 100663319, 201326611,
402653189, 805306457, 1610612741, 3221225473, 4294967291
};
const unsigned long* first = __stl_prime_list;
const unsigned long* last = __stl_prime_list + __stl_num_primes;
const unsigned long* pos = lower_bound(first, last, n);
return pos == last ? *(last - ) : *pos;
}
Key 不能取模的问题
当 key 是 string/Date 等类型时,key 不能取模,那么我们需要给 HashTable 增加一个仿函数,这个仿函 数支持把 key 转换成 一个可以取模的整形,如果 key 可以转换为整形并且不容易冲突,那么这个仿函 数就用默认参数即可,如果这个 Key 不能转换为整形,我们就需要自己实现一个仿函数传给这个参数,实 现这个仿函数的要求就是尽量 key 的值都参与到计算中,让不同的 key 转换出的整形值不同。string 做哈希表的 key 非常常见,所以我们可以考虑把 string 特化一下。
//如果要支持 Key 的比较,那么就需要仿函数来实现,如 string 映射,在映射到哈希表中
//默认的哈希仿函数,float,负数,转为无符号整形
template<class K>
struct HashFunc {
size_t operator()(const K& key) {
return (size_t)key;
}
};
//如果是 string 类型,那么我们就可以单独的给 string,建立一个仿函数实现第一层映射
//走特化
template<>
struct HashFunc<string> {
size_t operator()(const string& key) {
size_t m = 0;
//把 ASCII 码值加起来
for (auto& e : key) {
//用 ASCII 来实现,字符转数字
m += e;
}
return m;
}
};
完整代码:
enum State { EXIST, EMPTY, DELETE };
//哈希结点
template<class K, class V>
struct HashData {
pair<K, V> _kv;
State _state = EMPTY;
};
//如果要支持 Key 的比较,那么就需要仿函数来实现,如 string 映射,在映射到哈希表中
//默认的哈希仿函数,float,负数,转为无符号整形
template<class K>
struct HashFunc {
size_t operator()(const K& key) {
return (size_t)key;
}
};
//如果是 string 类型,那么我们就可以单独的给 string,建立一个仿函数实现第一层映射
//走特化
template<>
struct HashFunc<string> {
size_t operator()(const string& key) {
size_t m = 0;
//把 ASCII 码值加起来
for (auto& e : key) {
//用 ASCII 来实现,字符转数字
m += e;
}
return m;
}
};
inline unsigned long __stl_next_prime(unsigned long n) {
// Note: assumes long is at least 32 bits.
static const int __stl_num_primes = ;
__stl_prime_list[__stl_num_primes] = {
, , , , , , , , , ,
, , , , , , ,
, , , , , ,
, , , ,
};
* first = __stl_prime_list;
* last = __stl_prime_list + __stl_num_primes;
* pos = (first, last, n);
pos == last ? *(last - ) : *pos;
}
< , , = HashFunc<K>>
HashTable {
:
() : _tables(__stl_next_prime()), _n() {}
HashFunc con;
{
((kv.first)) {
;
}
(_n * / _tables.() >= ) {
HashTable<K, V, HashFunc> newht;
newht._tables.(__stl_next_prime(_tables.() + ));
(& e : _tables) {
(e._state == EXIST)
{
newht.(e._kv);
}
}
_tables.(newht._tables);
}
Hash0 = (kv.first) % _tables.();
Hashi = Hash0;
i = ;
(_tables[Hashi]._state == EXIST)
{
Hashi = (Hash0 + i) % _tables.();
i++;
}
_tables[Hashi]._kv = kv;
_tables[Hashi]._state = EXIST;
++_n;
;
}
{
Hash0 = (key) % _tables.();
Hashi = Hash0;
i = ;
(_tables[Hashi]._state != EMPTY) {
(_tables[Hashi]._state != DELETE && _tables[Hashi]._kv.first == key) {
&_tables[Hashi];
}
Hashi = (Hash0 + i) % _tables.();
i++;
}
;
}
{
HashData<K, V>* ret = (key);
(ret == ) {
;
} {
ret->_state = DELETE;
;
}
}
:
vector<HashData<K, V>> _tables;
_n;
};
开放定址法中所有的元素都放到哈希表里,链地址法中所有的数据不再直接存储在哈希表中,哈希表 中存储一个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把 这些冲突的数据链接成一个链表,挂在哈希表这个位置下面,链地址法也叫做拉链法或者哈希桶。
链地址法代码实现
inline unsigned long __stl_next_prime(unsigned long n) {
// Note: assumes long is at least 32 bits.
static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] = {
53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433, 1572869, 3145739,
6291469, 12582917, 25165843, 50331653, 100663319, 201326611,
402653189, 805306457, 1610612741, 3221225473, 4294967291
};
const unsigned long* first = __stl_prime_list;
const unsigned long* last = __stl_prime_list + __stl_num_primes;
const unsigned long* pos = lower_bound(first, last, n);
return pos == last ? *(last - ) : *pos;
}
< >
{
T _data;
Bucket_Node<T>* _next;
( T& data) : _data(data), _next() {}
};
< >
{
{
()key;
}
};
<>
<string> {
{
m = ;
(& e : key) {
m += e;
}
m;
}
};
< , , , >
;
< , , , , , >
{
Bucket_Node<T> Node;
hash_bucket<K, T, KeyOfT, Hash> HT;
Hash_Iterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
Node* _node;
HT* _ht;
(Node* node, HT* ht) : _node(node), _ht(ht) {}
Ref *() { _node->_data; }
Ptr ->() { &_node->_data; }
!=( Self& s) { _node != s._node; }
Self& ++() {
(_node->_next) {
_node = _node->_next;
} {
KeyOfT kot;
Hash hash;
hashi = ((_node->_data)) % _ht->_tables.();
++hashi;
(hashi < _ht->_tables.()) {
_node = _ht->_tables[hashi];
(_node) ;
++hashi;
}
(hashi == _ht->_tables.()) {
_node = ;
}
}
*;
}
};
< , , , >
{
< , , , , , >
;
Bucket_Node<T> Node;
:
Hash_Iterator<K, T, T&, T*, KeyOfT, HashFunc> Iterator;
Hash_Iterator<K, T, T&, T*, KeyOfT, HashFunc> Const_Iterator;
() : _n(), _tables(__stl_next_prime()) {}
~() {
( i = ; i < _tables.(); i++) {
Node* cur = _tables[i];
(cur) {
Node* next = cur->_next;
cur;
cur = next;
}
_tables[i] = ;
}
}
{
(_n == ) {
(, );
}
( i = ; i < _tables.(); i++) {
Node* cur = _tables[i];
(cur) {
(cur, );
}
}
}
{
(, );
}
{
(, );
}
{
(_n == ) {
(, );
}
( i = ; i < _tables.(); i++) {
Node* cur = _tables[i];
(cur) (cur, );
}
}
{
KeyOfT kot;
HashFunc hash;
( == (_n / _tables.())) {
Iterator it = ((data));
(it != ()) {it, };
;
( i = ; i < _tables.(); i++) {
Node* cur = _tables[i];
(cur) {
Node* next = cur->_next;
hashi = ((cur->_data)) % newtables.();
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
_tables[i] = ;
}
_tables.(newtables);
}
hashi = ((data)) % _tables.();
Node* newnode = (data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
{(newnode, ), };
}
{
KeyOfT kot;
HashFunc hash;
hashi = (key) % _tables.();
Node* cur = _tables[hashi];
(cur) {
((cur->_data) == key) {
(cur, );
}
}
(, );
}
{
KeyOfT kot;
HashFunc hash;
Node* prev = ;
hashi = (key) % _tables.();
Node* cur = _tables[hashi];
(cur) {
((cur->_data) == key) {
(prev) {
prev->_next = cur->_next;
} {
_tables[hashi] = prev;
}
cur;
_n--;
;
} {
prev = cur;
cur = cur->_next;
}
}
;
}
:
_n = ;
vector<Node*> _tables;
};

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online