字典的链表描述
搜索、删除、插入均为 O(n)
搜索
template<class K, class E>
pair<const K, E>* SortedChain<K, E>::find(const K& key) const {
pairNode<K, E>* cur = firstNode;
// 一直搜索到 key 相等或者遍历结束
while (cur != nullptr && cur->element.first != key) {
cur = cur->next;
}
// 判断是否匹配
if (cur != nullptr && cur->element.first == key) {
return &cur->element;
}
return nullptr;
}
插入
template<class K, class E>
void SortedChain<K, E>::insert(const pair<const K, E>& e) {
pairNode<K, E>* cur = firstNode, * pre = nullptr;
// 字典中键值有序排列
while (cur != nullptr && cur->element.first < e.first) {
pre = cur;
cur = cur->next;
}
// 相同键值就替换
if (cur != NULL && cur->element.first == e.first) {
cur->element.second = e.second;
}
else {
// 不相等就加入新节点, 加在 cur 前面
pairNode<K, E>* newNode = new pairNode<K, E>(e, cur);
if (pre == nullptr) firstNode = newNode;
else pre->next = newNode;
size++;
}
}
删除
template<class K, class E>
void SortedChain<K, E>::erase(const K& key) {
pairNode<K, E>* cur = firstNode, * pre = nullptr;
while (cur != nullptr && cur->element.first < key) {
pre = cur;
cur = cur->next;
}
// 如果匹配了
if (cur != nullptr && cur->element.first == key) {
// 匹配节点是首节点, 则首节点指向下一个节点
if (pre == nullptr) firstNode = cur->next;
else pre->next = cur->next;
delete cur;
size--;
}
}
🎉 完整代码
#include<iostream>
using namespace std;
// 节点类
template<class K, class E>
class pairNode {
public:
pair<const K, E> element;
pairNode<K, E>* next;
pairNode(const pair<K, E>& e) :element(e) {
next = nullptr;
};
pairNode(const pair<K, E>& e, pairNode<K, E>* next) :element(e), next(next) {};
};
// 字典类
template<class K, class E>
class SortedChain {
public:
SortedChain() {
size = 0;
firstNode = nullptr;
};
~SortedChain();
bool isEmpty() const { return size == 0; }
int length() const { return size; };
pair<const K, E>* find(const K& key) const;
void erase(const K&);
void insert(const pair<const K, E>&);
private:
pairNode<K, E>* firstNode;
int size;
};
// 析构函数
template<class K, class E>
SortedChain<K, E>::~SortedChain() {
while (firstNode != nullptr) {
// 不断删除首节点, 则下一个节点就成为首节点
pairNode<K, E>* nextNode = firstNode->next;
delete firstNode;
firstNode = nextNode;
}
}
// 查找匹配元素
template<class K, class E>
pair<const K, E>* SortedChain<K, E>::find(const K& key) const {
pairNode<K, E>* cur = firstNode;
while (cur != nullptr && cur->element.first != key) {
cur = cur->next;
}
// 判断是否匹配
if (cur != nullptr && cur->element.first == key) {
return &cur->element;
}
return nullptr;
}
// 插入元素
template<class K, class E>
void SortedChain<K, E>::insert(const pair<const K, E>& e) {
pairNode<K, E>* cur = firstNode, * pre = nullptr;
while (cur != nullptr && cur->element.first < e.first) {
pre = cur;
cur = cur->next;
}
// 相同键值就替换
if (cur != nullptr && cur->element.first == e.first) {
cur->element.second = e.second;
}
else {
// 不相等就加入新节点, 加在 cur 前面
pairNode<K, E>* newNode = new pairNode<K, E>(e, cur);
if (pre == nullptr) firstNode = newNode;
else pre->next = newNode;
size++;
}
}
// 根据 key 删除元素
template<class K, class E>
void SortedChain<K, E>::erase(const K& key) {
pairNode<K, E>* cur = firstNode, * pre = nullptr;
while (cur != nullptr && cur->element.first < key) {
pre = cur;
cur = cur->next;
}
// 如果匹配了
if (cur != nullptr && cur->element.first == key) {
// 匹配节点是首节点, 则首节点指向下一个节点
if (pre == nullptr) firstNode = cur->next;
else pre->next = cur->next;
delete cur;
size--;
}
}
int main() {
SortedChain<int, int> sc;
// 随便插入几个元素
for (int i = 0; i < 10; i++) {
pair<int, int> a(i, i);
sc.insert(a);
}
cout << sc.find(8)->second << endl; // 8
}
跳表
有序数组可以折半查找, 时间复杂度
普通有序链表查找时间复杂度
添加一个指向链表中部的指针, 如果要找的数小于中部值, 就在左链表开始找, 否则以中部指针为起点, 在右链表中查找
指针可以添加多个, 进一步减少查找次数, 比如下图, 可以进行折半查找:
散列表
适用范围:
- key 的取值范围比较宽泛
- 待处理的 key 值不多
- 存储空间有限
- 需要快速查找
理想散列
对关键字做一个线性计算,把计算结果当做散列地址
又称为 直接定址法
比如令学生 ID 为 key, ID 范围为 951000~952000, 则可以利用
将学生 ID 映射到 0~1000 上, 再利用一维数组 table[1000] 存储
但如果学生只有 10 名, 用长度 1000 的数组来存就非常 浪费
不理想散列
因为关键词的范围太大, 不易使用理想散列函数
数字分析法
4 5 6 7 位随机性更大, 可以随便取两个来作为关键字, 但是 1 2 3 8 位重复较多, 容易发生冲突
数字分析法就是找出数字的规律, 尽可能利用这些数据来构造冲突几率较低的散列地址
平方取中法
取关键字平方后的中间几位作为散列地址
折叠法
关键词位数很多时, 将关键字 分割 成 位数相同 的几部分(最后一部分的位数可以不同), 然后取这几部分的 叠加和(舍去进位) 作为哈希地址
-
移位叠加法: 把各部分的最后一位对齐相加
-
间界叠加法(分界叠加法): 各部分
来回折叠, 然后对齐相加
除留余数法
m 是散列表表长
p 是最接近 m 的质数或者不包含小于 20 的质因数的合数
伪随机数法
伪随机数发生器
之所以称为 伪随机 , 是因为所生成的随机数是根据特定的秩 x 在特定的 规则 上生成固定的值( x 通常取当前时间)
伪随机数法
两者很类似, 因此可以使用伪随机数生成散列值, 将生成散列值的设计难题推给伪随机数的设计者
多项式法
有些 key 不是整数, 比如 字符串 , 因此需要特定的算法, 将其转成非负整数
字符串其实是一个 ASCII 码数组, 利用规定的常数 a 对其每个元素进行多项式运算, 将结果作为散列值
多项式运算可以利用 秦九韶算法
class Hash {
public:
// 将关键词转成非负整数
size_t operator() (const string key) const {
unsigned long hashValue = 0;
int len = (int)key.length();
// a = 5
for (int i = 0; i < len; i++) {
hashValue = 5 * hashValue + key[i];
}
return size_t(hashValue);
}
};
处理冲突
开放寻址法
线性探查法
当使用除留余数法时可能存在冲突
使用线性探查法, 寻找下一个未被占用的地方放置关键字
下一个不行, 就下下个
甚至可以成 环
当循环一圈回到初始桶还没匹配成功, 或者中途遇到空桶, 说明数对不存在
删除时需要把元素前移, 但不能移动正确的元素
优点: 只要表不满, 就可以插入
缺点: 存在聚集问题, 即一个地方出现冲突, 会导致后续的连续冲突
时间复杂度:
初始化:
插入, 搜索:
🐣 完整代码
#include<iostream>
#include<string>
using namespace std;
class Hash {
public:
// 将关键词转成非负整数
size_t operator() (const string key) const {
unsigned long hashValue = 0;
int len = (int)key.length();
for (int i = 0; i < len; i++) {
hashValue = 5 * hashValue + key[i];
}
return size_t(hashValue);
}
};
template<class K, class E>
class hashTable {
private:
pair<const K, E>**table; // 散列表
Hash hash;
int size; // 字典数对个数
int divisor; // 散列函数除数(桶数)
public:
hashTable(int divisor) : divisor(divisor) {
size = 0;
table = new pair<const K, E>*[divisor];
for (int i = 0; i < divisor; i++) {
table[i] = nullptr;
}
}
~hashTable() { delete[] table; };
int search(const K&) const;
pair<const K, E>* find(const K& key) const;
void insert(const pair<const K, E>& thePair);
};
// 查找关键字所在桶号
template<class K, class E>
int hashTable<K, E>::search(const K& key) const {
int i = (int)hash(key) % divisor; // 起始桶
int j = i;
do{
if (table[j] == nullptr || table[j]->first == key) {
return j; // 匹配就返回其位置
}
j = (j + 1) % divisor; // 下一个桶
} while (j != i); // 直到返回初始桶才结束, 表示找不到元素
return j;
}
// 查找关键字
template<class K, class E>
pair<const K, E>* hashTable<K, E>::find(const K& key) const {
int b = search(key);
// 没有匹配项
if (table[b] == nullptr || table[b]->first != key) {
return nullptr;
}
return table[b];
}
// 插入数对
template<class K, class E>
void hashTable<K, E>::insert(const pair<const K, E>& thePair) {
int b = search(thePair.first);
if (table[b] == nullptr) {
table[b] = new pair<const K, E>(thePair);
size++;
}
else if(table[b]->first == thePair.first){
// 存在重复关键词数对, 覆盖
table[b]->second = thePair.second;
}
else {
cout << "哈希表满" << endl;
}
}
int main() {
hashTable<string, int> ht(3);
ht.insert(pair<string, int>("xiaoming", 10));
ht.insert(pair<string, int>("xiaoli", 20));
ht.insert(pair<string, int>("xiaohong", 30));
ht.insert(pair<string, int>("xiaowang", 20)); // 哈希表满
pair<const string, int>* res = ht.find("xiaowang");
if (res != nullptr) {
cout << res->second;
}
else {
cout << "找不到元素";
}
return 0;
}
平方探测法
出现冲突时, 每次移动(试探) 个单位
能够 解决聚集 问题, 但是试探足迹只能遍及几个桶:
当散列表长 m 为 素数 时, 最多只能够遍及 个桶
双向平方探测法
即 交替 的进行平方试探, 有利于提高某些表长桶的利用率
对于 m=7 m=11 刚好能遍及全部桶, 而 m=5 m=13 就不行了
因此表长可以选取 模 4 余 3 的素数, 比如 7 和 11
双散列法(再哈希法)
两个散列函数, 一个正常计算散列值, 一旦出现冲突, 用第二个散列函数计算下一个地址(或者偏移量)
链表法(拉链法)
每个桶都是一个链表, 可以无限延伸, 不存在冲突
此时只需要将哈希表替换成字典链表描述中的 SortedChain 类数组即可
template<class K, class E>
class ChainHashTable {
public:
ChainHashTable(int divisor) : divisor(divisor) {
table = new SortedChain<K, E>[divisor];
size = 0;
};
~ChainHashTable() { delete[] table; };
pair<const K, E>* find(const K& key) const;
void insert(const pair<const K, E>& thePair);
void erase(const K& k);
private:
int divisor;
Hash hash;
int size;
SortedChain<K, E>* table; // 前文提到的字典链表类
};
🌟 完整代码
#include<iostream>
#include<string>
using namespace std;
// 节点类
template<class K, class E>
class pairNode {
public:
pair<const K, E> element;
pairNode<K, E>* next;
pairNode(const pair<K, E>& e) :element(e) {
next = nullptr;
};
pairNode(const pair<K, E>& e, pairNode<K, E>* next) :element(e), next(next) {};
};
// 字典类
template<class K, class E>
class SortedChain {
public:
SortedChain() {
size = 0;
firstNode = nullptr;
};
~SortedChain();
bool isEmpty() const { return size == 0; }
int length() const { return size; };
pair<const K, E>* find(const K& key) const;
void erase(const K&);
void insert(const pair<const K, E>&);
private:
pairNode<K, E>* firstNode;
int size;
};
// 析构函数
template<class K, class E>
SortedChain<K, E>::~SortedChain() {
while (firstNode != nullptr) {
// 不断删除首节点, 则下一个节点就成为首节点
pairNode<K, E>* nextNode = firstNode->next;
delete firstNode;
firstNode = nextNode;
}
}
// 查找匹配元素
template<class K, class E>
pair<const K, E>* SortedChain<K, E>::find(const K& key) const {
pairNode<K, E>* cur = firstNode;
while (cur != nullptr && cur->element.first != key) {
cur = cur->next;
}
// 判断是否匹配
if (cur != nullptr && cur->element.first == key) {
return &cur->element;
}
return nullptr;
}
// 插入元素
template<class K, class E>
void SortedChain<K, E>::insert(const pair<const K, E>& e) {
pairNode<K, E>* cur = firstNode, * pre = nullptr;
while (cur != nullptr && cur->element.first < e.first) {
pre = cur;
cur = cur->next;
}
// 相同键值就替换
if (cur != nullptr && cur->element.first == e.first) {
cur->element.second = e.second;
}
else {
// 不相等就加入新节点, 加在 cur 前面
pairNode<K, E>* newNode = new pairNode<K, E>(e, cur);
if (pre == nullptr) firstNode = newNode;
else pre->next = newNode;
size++;
}
}
// 根据 key 删除元素
template<class K, class E>
void SortedChain<K, E>::erase(const K& key) {
pairNode<K, E>* cur = firstNode, * pre = nullptr;
while (cur != nullptr && cur->element.first < key) {
pre = cur;
cur = cur->next;
}
// 如果匹配了
if (cur != nullptr && cur->element.first == key) {
// 匹配节点是首节点, 则首节点指向下一个节点
if (pre == nullptr) firstNode = cur->next;
else pre->next = cur->next;
delete cur;
size--;
}
}
class Hash {
public:
// 将关键词转成非负整数
size_t operator() (const string key) const {
unsigned long hashValue = 0;
int len = (int)key.length();
for (int i = 0; i < len; i++) {
hashValue = 5 * hashValue + key[i];
}
return size_t(hashValue);
}
};
/***********************
* 链表法 *
************************/
template<class K, class E>
class ChainHashTable {
public:
ChainHashTable(int divisor) : divisor(divisor) {
table = new SortedChain<K, E>[divisor];
size = 0;
};
~ChainHashTable() { delete[] table; };
pair<const K, E>* find(const K& key) const;
void insert(const pair<const K, E>& thePair);
void erase(const K& k);
private:
int divisor;
Hash hash;
int size;
SortedChain<K, E>* table;
};
// 查找匹配元素
template<class K, class E>
pair<const K, E>* ChainHashTable<K, E>::find(const K& key) const {
return table[(int)hash(key) % divisor].find(key);
}
// 插入元素
template<class K, class E>
void ChainHashTable<K, E>::insert(const pair<const K, E>& thePair) {
int b = (int)hash(thePair.first) % divisor;
int sz = table[b].length();
table[b].insert(thePair);
// 如果不是重复数据, 则 size 增加
if (table[b].length() > sz) {
size++;
}
}
// 删除元素
template<class K, class E>
void ChainHashTable<K, E>::erase(const K& key) {
int b = (int)hash(key) % divisor;
int sz = table[b].length();
table[b].erase(key);
// 如果删除成功
if (table[b].length() < sz) {
size--;
}
}
int main() {
ChainHashTable<string, int> cht(3);
cht.insert(pair<string, int>("xiaoming", 10));
cht.insert(pair<string, int>("xiaoli", 20));
cht.insert(pair<string, int>("xiaohong", 30));
cht.insert(pair<string, int>("xiaowang", 40));
pair<const string, int>* res = cht.find("xiaowang");
if (res != nullptr) {
cout << res->second << endl;
}
else {
cout << "找不到元素" << endl;
}
cht.erase("xiaowang");
res = cht.find("xiaowang");
if (res != nullptr) {
cout << res->second;
}
else {
cout << "找不到元素" << endl;
}
return 0;
}
公共溢出区法
有冲突, 就新开辟一块空间用来存放冲突数据, 顺序存入
先在散列表中查找匹配项, 不匹配, 就到公共溢出区 顺序 查找
最大间隙问题
普通做法
对这几个数进行排序, 再比较相邻两个数的差
一般排序算法最好的复杂度为 , 这不能在线性的时间内完成
桶排序
利用 将数据映射到 [0, n] 数组上
因此最大值在最后一个桶, 并且只可能有最大值
同一个桶中数据间隙不会超过 , 比如上图, 桶内间距最大不会超过 25, 因此我们创建 n+1 个桶, 就算每个数占一个桶, 肯定会存在一个空的, 并且这个空的不会是第一个, 也不会是最后一个, 那么肯定有两个数间隙大于 25 , 这样最大间隙只可能存在与桶间, 而不是桶内
#include<iostream>
#include<limits.h>
using namespace std;
// 伪随机数发生器
class Random {
public:
Random(int s = 1) : seed(s) {};
int rand() {
return(((seed = seed * 214013L + 2531011L) >> 16) & 0x7fff);
};
int rand32() {
return ((rand() << 16) + (rand() << 1) + rand() % 2);
}
private:
int seed;
};
// 桶排序计算最大间隙
int calMaxGap(int* data, int len) {
// 只有一个数, 没有间隙
if (len < 2) return 0;
// 先计算最大值和最小值
int min = INT_MAX;
int max = INT_MIN;
for (int i = 0; i < len; i++) {
min = min < data[i] ? min : data[i];
max = max > data[i] ? max : data[i];
}
// 最大最小值相同, 间隙为 0
if (max == min) return 0;
// 新建 len + 1 个桶
// 为了防止最大间距两个数被放在同一个桶中
int* maxBucket = new int[len + 1];
int* minBucket = new int[len + 1];
// 桶中是否有数据
bool* hasData = new bool[len + 1];
for (int i = 0; i <= len; i++) {
hasData[i] = false;
}
for (int i = 0; i < len; i++) {
int index = (int)((double)(data[i] - min) / (max - min) * len);
if (hasData[index]) {
// 桶中存在数据
// 比较后替换桶中数据
maxBucket[index] = data[i] > maxBucket[index] ? data[i] : maxBucket[index];
minBucket[index] = data[i] < minBucket[index] ? data[i] : minBucket[index];
}
else {
// 桶中没数据, 直接存进去
hasData[index] = true;
maxBucket[index] = data[i];
minBucket[index] = data[i];
}
}
// 开始查找相邻桶间隙
// 最大间隙为: 当前桶 min - 左边非空桶 max
int max_ = maxBucket[0];
int maxGap = 0;
for (int i = 0; i <= len; i++) {
if (hasData[i]) {
int curGap = minBucket[i] - max_;
if (curGap > maxGap) {
maxGap = curGap;
}
max_ = maxBucket[i];
}
}
delete[] maxBucket;
delete[] minBucket;
delete[] hasData;
return maxGap;
}
int main() {
int seed; // 伪随机数种子
int num; // 随机数个数
cin >> num >> seed;
Random r(seed);
int* data = new int[num];
// 生成指定数目的伪随机数
// 模拟无序数组
for (int i = 0; i < num; i++) {
data[i] = r.rand32();
}
int maxGap = calMaxGap(data, num);
cout << maxGap;
delete[] data;
return 0;
}