散列表(哈希表)

散列表

知识点讲解可直接点击这里
代码参考:
https://blog.csdn.net/qq_35644234/category_6521704.html?spm=1001.2014.3001.5482

【问题】如何快速搜索到需要的关键词?如果关键词不方便比较怎么办?

查找的本质:已知对象找位置

  • 有序安排对象:全序、半序
  • 直接 “算出” 对象位置:散列

散列查找法的两项基本工作:

  • 计算位置:构造散列函数确定关键词存储位置
  • 解决冲突:应用某种策略解决多个关键词位置相同的问题

时间复杂度几乎是常数:O(1) ,即查找时间与问题规模无关!

1. 什么是散列函数

装填因子(Loading Factor):设散列表空间大小为m,填入表中元素个数是n,则称α = n / m 为散列表的装填因子。

“散列(Hashing)” 的基本思想:

  1. 以关键字key为自变量,通过一个确定的函数h(散列函数),计算出对应的函数值h(key),作为数据对象的存储地址
  2. 可能不同的关键字会映射到同一个散列地址上,即h(key i) = h(key j) (当key i ≠ key j),称为“冲突(collision)”。——需要某种冲突解决策略

2. 散列函数的构造方法

一个 “好” 的散列函数一般应考虑下列两个因素:

  1. 计算简单,以便提高转换速度
  2. 关键词对应的地址空间分布均匀,以尽量减少冲突

2.1 数字关键词 的散列函数构造

  1. 直接定址法

    取关键词的某个线性函数值为散列地址,即h(key) = a * key + b(a、b为常数)

  2. 除留余数法

    散列函数为:h(key) = key mod p

    • 这里:p = tableSize = 17
    • 一般,p取素数
  3. 分析数字关键字在各位上的变化情况,取比较随机的位作为散列地址

  4. 折叠法

    把关键词分割成位数相同的几个部分,然后叠加

  5. 平均取中法

2.2 字符关键词 的散列函数构造

在这里插入图片描述

3. 冲突处理方法

常用处理冲突的思路:

  • 换个位置:开放地址法
  • 同一个位置的冲突对象组织在一起:链地址法

一旦产生了冲突(该地址已有其他元素),就按某种规则去寻找另一空地址。

  • 若发生了第 i 次冲突,试探的下一个地址将增加 di ,基本公式:hi(key) = (h(key) + di) mod tableSize (1 ≤ i < tableSize)。
  • di决定了不同的解决冲突方案:线性探测(di = i)、平方探测(di = ±i²)、双散列(di = i*h2(key))。

  • 线性探测法:以增量序列 1,2,… …,(tableSize - 1) 循环试探下一个存储地址。

  • 平方探测:以增量序列 1²,-1²,2²,-2²,… …,q²,-q² 且q ≤[tableSize / 2] 循环试探下一个存储地址

    有定理显示:如果散列表长度tableSize是某个 4k + 3 (k是正整数)形式的素数时,平方探测法就可以探查到整个散列表空间。

  • 分离链接法:将相应位置上的冲突的所有关键词存储在同一个单链表中

4. 散列表的性能分析

  • 平均查询长度(ASL)用来度量散列表查找效率:成功、不成功

  • 关键词的比较次数,取决于产生冲突的多少

    影响产生冲突多少有以下三个因素:

    1. 散列函数是否均匀
    2. 处理冲突的方法
    3. 散列表的填装因子α

4.1 线性探测法的查找性能

可以证明,线性探测法的期望探测次数 满足下列公式:

在这里插入图片描述

4.2 平方探测法和双散列探测法的查找性能

可以证明,平方探测法和双散列探测法探测次数 满足下列公式:

在这里插入图片描述

4.3 分离链接法的查找性能

所有地址链表的平均长度定义成 装填因子α,α有可能超过1。

可以证明:其期望探测次数p为:

在这里插入图片描述

4.4 总结&比较

  • 选择合适的 h(key),散列法的查找效率期望是 常数O(1),它几乎与关键字的空间的大小n无关!也适合于关键字直接比较计算量大的问题
  • 它是以 较小的α为前提。因此,散列方法是一个以 空间换时间
  • 散列方法的存储对关键字是随机的,不便于 顺序查找关键字,也不适合于 范围查找,或 最大值最小值查找。

开放地址法:

  • 优点:散列表是一个数组,存储效率高,随机查找
  • 缺点:散列表有 “聚集” 现象

分离链法:

  • 散列表是顺序存储和链式存储的结合,链表部分的存储效率和查找效率都比较低
  • 优点:关键字删除不需要 “懒惰删除” 法,从而没有存储 “垃圾” 。
  • 缺点:太小的 α 可能导致空间浪费,大的 α 又将付出更多的时间代价。不均匀的链表长度导致时间效率的严重下降。

5. 实现一个散列表

5.1第一次实现

#include <iostream>

using namespace std;

#define MAXSIZE 100
typedef int ElemType;
struct HashTable {
    ElemType* data; // 散列表存储数据数组
    int count;  // 插入数据的数量
    int size;   // 散列表的最大容量
};

void initHash(HashTable& H)
{
    H.size = MAXSIZE;
    H.count = 0;
    H.data = new ElemType[H.size];
    for (int i = 0; i < H.size; ++i) {
        H.data[i] = -99999;
    }
}

// 求散列函数,这里用 除留余数法
// 插入操作
bool insertHash(HashTable& H, ElemType key)
{
    if (H.count == H.size) {
        return false;
    }
    int index = key % H.size;   // 计算散列地址
    // 判断是否有冲突
    while (H.data[index] != -99999) {
        index = (index + 1) % H.size;
    }
    // 插入值
    H.data[index] = key;
    H.count++;
}

// 查找并返回索引位置
bool searchHash(HashTable& H, ElemType key, int& location)
{
    int index = key % H.size;   // 计算散列地址
    // 判断求出的散列地址是否为该值
    while (H.data[index] != key) {
        index = (index + 1) % H.size;
        // 不存在的情况,即一种散列地址处的值为初始值,另一种循环一圈到原位置
        if (H.data[index] == -99999 || index == key % H.size) {
            return false;   // 说明关键字不存在
        }
    }
    location = index;   // 存在,索引位置
    return true;
}

5.2 第二次实现

第一次实现的效果并不好,下面是第二次实现的成果

1. 开放地址法
/* !!!线性探测 & 平方探测!!!*/

#include<iostream>
using namespace std;
typedef int keyType;
// 质数的集合
int hashsize[] = { 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107,
109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223,
227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337,
347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457,
461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593,
599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719,
727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857,
859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997 };

// 每次存储关键字空间的大小都是取质数 最接近的质数

struct Hash {
    keyType* data;  // 记录哈希表中的元素
    bool* isfull;   // 记录哈希表是否有元素了,标志位--------true说明不为空,false说明为空
    int count;      // 哈希表中已存放元素的个数
    int sizeindex;  // 对应的最接近的质数索引~Hashsize
};

void createHash(Hash& H) {
    H.count = 0;
    H.sizeindex = 0;
    H.data = new keyType[hashsize[H.sizeindex]];    // 初始哈希表的长度
    H.isfull = new bool[hashsize[H.sizeindex]];
    for (int i = 0; i < hashsize[H.sizeindex]; ++i) {
        H.isfull[i] = false;
    }
}

// 线性探测---查找对应的关键字
// p为最后插入的索引位置;c为偏移;hashsize为质数的集合--方便我们的计算
bool searchHash(Hash H, keyType key, int& p, int* hashsize) {
    int c = 0;
    p = key % hashsize[H.sizeindex];      // 计算要插入位置的索引
    while (H.isfull[p] && H.data[p] != key && c < hashsize[H.sizeindex] - 1) {
        ++c;
        //继续往下寻找下一个散列结点
        p = (key + c) % hashsize[H.sizeindex];
    }
    if (H.data[p] == key) {
        return true;    // 查找成功
    }
    return false;       // 查找失败 
}


// 平方探测---查找对应的关键字
bool searchHash(Hash H, keyType key, int& p, int* hashsize) {
    int c = 0;
    p = key % hashsize[H.sizeindex];      // 计算要插入位置的索引

    // 0^2,1^2,-1^2,2^2,-2^2,3^2,-3^2,4^2,-4^2...,q^2,-q^2),q<=m/2,m为散列表的长度
    while (H.isfull[p] && H.data[p] != key && c < hashsize[H.sizeindex] - 1) {
        if (c == 0) {
            c = 1;
            p = (key + c) % hashsize[H.sizeindex];
        }
        else if (c > 0) {
            c = -c;
            p = (key - c * c) % hashsize[H.sizeindex];
        }
        else {  // (c < 0)
            c = -c + 1;
            p = (key + c * c) % hashsize[H.sizeindex];
        }
    }
    if (H.data[p] == key) {
        return true;    // 查找成功
    }
    return false;       // 查找失败 
}

bool insertHash(Hash& H, keyType key, int* hashsize) {
    int p;      // 保存插入时索引的位置
    if (searchHash(H, key, p, hashsize)) { // 此时key已存在,不需要再插入
        return false;
    }
    // 下面的if语句判断在处理平方探测的时候还是有些问题的!
    else if (H.count == hashsize[H.sizeindex] - 1) {// 此时哈希表已经满,得重新分配了
        // 首先是把之前的内容保存起来
        ++H.sizeindex;
        keyType* data = new keyType[hashsize[H.sizeindex]];
        bool* isfull = new bool[hashsize[H.sizeindex]];
        for (int i = 0; i < hashsize[H.sizeindex - 1]; ++i) {
            data[i] = H.data[i];
            isfull[i] = H.isfull[i];
        }
        // 给新增加的标志位 添加标志
        for (int j = hashsize[H.sizeindex - 1]; j < hashsize[H.sizeindex]; ++j) {
            H.isfull[j] = false;
        }
        delete[] H.data;
        delete[] H.isfull;

        H.data = data;
        H.isfull = isfull;
    }
    else {
        // 插入key
        H.data[p] = key;
        ++H.count;
        H.isfull[p] = true;
    }
}

bool DeleteHash(Hash& H, keyType key, int* hashsize) {
    int p;
    if (!searchHash(H, key, p, hashsize)) {//没找到要删除的元素
        return false;
    }
    else {
        H.isfull[p] = false;
        --H.count;
    }
}

void print(Hash H, int* hashsize) {
    cout << "当前的哈希表的长度:" << hashsize[H.sizeindex] << endl;
    cout << "哈希表存有元素的个数:" << H.count << endl;
    cout << "打印整个哈希表:" << endl;
    for (int i = 0; i < hashsize[H.sizeindex]; ++i) {
        if (H.isfull[i]) {
            cout << H.data[i] << " ";
        }
        else {
            cout << "空" << " ";
        }
    }
    cout << endl;
}
2.链地址法
/*!!!链地址法!!!*/
#include<iostream>
using namespace std;
typedef int keyType;
// 质数的集合
int hashsize[] = { 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107,
109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223,
227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337,
347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457,
461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593,
599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719,
727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857,
859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997 };

struct Node {
    keyType data; //每个结点的值
    Node* next;  //下一个结点
    Node() {
        next = nullptr;
    }
};

// 每次都是取质数最接近的质数
struct Hash {
    Node* elem;     // 头结点链表
    int sizeindex;
};

// cur指向key所在的当前结点, pre指向~前一结点
bool searchHash(Hash H, keyType key, Node*& cur, Node*& pre, int* hashsize) {

    int index = key % hashsize[H.sizeindex];
    Node* head = H.elem[index].next;
    pre = nullptr;
    while (head) {  //遍历该链表
        cur = head;
        if (head->data == key) {
            return true;
        }
        pre = head;
        head = head->next;
    }
    return false;
}

// 插入
bool insertHash(Hash& H, keyType key, int* hashsize) {

    Node* cur = nullptr;
    Node* pre = nullptr;
    if (searchHash(H, key, cur, pre, hashsize)) {
        return false;
    }
    else {
        Node* s = new Node;
        s->data = key;
        if (pre == nullptr) {   
            int index = key % hashsize[H.sizeindex];
            H.elem[index].next = s;
            return true;
        }
        cur->next = s;
    }
    return true;
}

//删除结点
bool deleteHash(Hash& H, keyType key, int* hashsize) {
    Node* cur = nullptr;
    Node* pre = nullptr;
    if (!searchHash(H, key, cur, pre, hashsize)) {
        return false;
    }
    else {
        if (pre == nullptr) {
            int index = key % hashsize[H.sizeindex];
            H.elem[index].next = cur->next;
        }
        else {
            pre->next = cur->next;
        }
        delete cur;
        return true;
    }
}

void print(Hash H, int* hashsize) {
    Node* p = nullptr;
    for (int i = 0; i < hashsize[H.sizeindex]; ++i) {
        cout << i << " : ";
        p = H.elem[i].next;
        while (p) {
            cout << p->data << " ";
            p = p->next;
        }
        cout << "空" << endl;
    }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ClimberCoding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值