散列与跳表

217 阅读9分钟

image.png

字典的链表描述

搜索、删除、插入均为 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
}

跳表

有序数组可以折半查找, 时间复杂度 O(logn)O(logn)

普通有序链表查找时间复杂度 O(n)O(n)

添加一个指向链表中部的指针, 如果要找的数小于中部值, 就在左链表开始找, 否则以中部指针为起点, 在右链表中查找

image.png

指针可以添加多个, 进一步减少查找次数, 比如下图, 可以进行折半查找:

image.png

散列表

适用范围:

  • key 的取值范围比较宽泛
  • 待处理的 key 值不多
  • 存储空间有限
  • 需要快速查找

理想散列

对关键字做一个线性计算,把计算结果当做散列地址

address=hash(key)address = hash(key)

又称为 直接定址法

比如令学生 ID 为 key, ID 范围为 951000~952000, 则可以利用

hash(ID)=ID951000hash(ID)=ID-951000

将学生 ID 映射到 0~1000 上, 再利用一维数组 table[1000] 存储

但如果学生只有 10 名, 用长度 1000 的数组来存就非常 浪费

不理想散列

因为关键词的范围太大, 不易使用理想散列函数

数字分析法

image.png

4 5 6 7 位随机性更大, 可以随便取两个来作为关键字, 但是 1 2 3 8 位重复较多, 容易发生冲突

数字分析法就是找出数字的规律, 尽可能利用这些数据来构造冲突几率较低的散列地址

平方取中法

取关键字平方后的中间几位作为散列地址

image.png

折叠法

关键词位数很多时, 将关键字 分割位数相同 的几部分(最后一部分的位数可以不同), 然后取这几部分的 叠加和(舍去进位) 作为哈希地址

image.png

  • 移位叠加法: 把各部分的最后一位对齐相加

  • 间界叠加法(分界叠加法): 各部分 来回 折叠, 然后对齐相加

除留余数法
hash(key)=key%p(pm)hash(key)=key \% p(p≤m)

m 是散列表表长

p 是最接近 m 的质数或者不包含小于 20 的质因数的合数

image.png

伪随机数法
伪随机数发生器
rand(x+1)=[a×rand(x)]%Mrand(x+1)=[a×rand(x)] \% M

之所以称为 伪随机 , 是因为所生成的随机数是根据特定的秩 x 在特定的 规则 上生成固定的值( x 通常取当前时间)

伪随机数法
hash(key)=rand(key)=[rand(0)×akey]%Mhash(key)=rand(key)=[rand(0)×a^{key}]\%M

两者很类似, 因此可以使用伪随机数生成散列值, 将生成散列值的设计难题推给伪随机数的设计者

多项式法

有些 key 不是整数, 比如 字符串 , 因此需要特定的算法, 将其转成非负整数

hash(str=x0xxn1)=x0an1+x1an2++xn1hash(str=x_0x\cdots x_{n-1})=x_0a^{n-1}+x_1a^{n-2}+\cdots+x_{n-1}

字符串其实是一个 ASCII 码数组, 利用规定的常数 a 对其每个元素进行多项式运算, 将结果作为散列值

多项式运算可以利用 秦九韶算法

hash(key)=((((x0×a)+x1)×a+x2))×a+xn1hash(key)=((((x_0×a)+x_1)×a+x_2)\cdots)×a+x_{n-1}
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);
    }
};

处理冲突

开放寻址法
线性探查法

当使用除留余数法时可能存在冲突

image.png

使用线性探查法, 寻找下一个未被占用的地方放置关键字

image.png

下一个不行, 就下下个

image.png

甚至可以成

image.png

当循环一圈回到初始桶还没匹配成功, 或者中途遇到空桶, 说明数对不存在

image.png

删除时需要把元素前移, 但不能移动正确的元素

image.png

image.png

优点: 只要表不满, 就可以插入

缺点: 存在聚集问题, 即一个地方出现冲突, 会导致后续的连续冲突

时间复杂度:

初始化: Θ(divisor)Θ(divisor)

插入, 搜索: Θ(n)Θ(n)

🐣 完整代码
#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;
}

平方探测法

出现冲突时, 每次移动(试探) i2i^2 个单位

image.png

能够 解决聚集 问题, 但是试探足迹只能遍及几个桶:

image.png

当散列表长 m素数 时, 最多只能够遍及 m2\lceil \frac{m}{2} \rceil 个桶

双向平方探测法

交替 的进行平方试探, 有利于提高某些表长桶的利用率

image.png

对于 m=7 m=11 刚好能遍及全部桶, 而 m=5 m=13 就不行了

image.png

因此表长可以选取 模 4 余 3 的素数, 比如 711

双散列法(再哈希法)

两个散列函数, 一个正常计算散列值, 一旦出现冲突, 用第二个散列函数计算下一个地址(或者偏移量)

链表法(拉链法)

每个桶都是一个链表, 可以无限延伸, 不存在冲突

image.png

此时只需要将哈希表替换成字典链表描述中的 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;
}
公共溢出区法

有冲突, 就新开辟一块空间用来存放冲突数据, 顺序存入

先在散列表中查找匹配项, 不匹配, 就到公共溢出区 顺序 查找

最大间隙问题

164. 最大间距 - 力扣(LeetCode)

普通做法

对这几个数进行排序, 再比较相邻两个数的差

一般排序算法最好的复杂度为 O(nlogn)O(nlogn) , 这不能在线性的时间内完成

桶排序

image.png

利用 index=iminmaxmin×nindex = \frac{i - min}{max-min} × n 将数据映射到 [0, n] 数组上

因此最大值在最后一个桶, 并且只可能有最大值

同一个桶中数据间隙不会超过 maxminn+1\frac{max-min}{n+1}, 比如上图, 桶内间距最大不会超过 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;
}