【数据结构】散列表(哈希表)的学习知识总结

目录

1、散列表

2、散列函数

2.1 定义

2.2 散列函数的构造 

2.2.1 除留余数法

2.2.2 直接定址法

2.2.3 数字分析法

2.2.4 平方取中法

3、冲突(碰撞)

4、处理冲突的方法 

4.1 拉链法(链接法)

4.2 开放定址法

5、C语言实现散列表(哈希表)

方式一:数组

方式二:指针和动态内存分配


1、散列表

        散列表(Hash Table)是一种数据结构,它通过使用散列函数将关键字映射到表中的一个位置,从而实现快速查找。散列表通常被用于实现字典、数据库索引等数据结构,它可以提供关键字的高效插入、查找和删除操作。散列表的实现基于数组,每个元素包含一个键值对(key-value pair)。其中,键通常是一个字符串或整数,值可以是任何类型的数据,例如字符串、整数、对象等。在查找元素时,散列表使用散列函数将关键字转换为数组索引,并在此位置上查找键值对。如果存在,则返回其对应的值;如果不存在,则返回空值。

【数据结构】散列表(哈希表)的学习知识总结_第1张图片

2、散列函数

2.1 定义

        散列函数是一种将输入数据映射到固定大小输出数据的函数。它将不同大小的输入数据转换成相同长度的散列值(也称为哈希值、摘要或指纹),通常用作数据加密、数字签名、消息认证码(MAC)等领域的基础。

散列函数应该满足以下要求:

1. 均匀性:散列函数应该将不同输入数据均匀地映射到输出空间的不同位置,以减少散列冲突的发生。

2. 独立性:输入数据的微小变化应该能够引起输出散列值的大幅度变化,以增强散列函数的安全性。

3. 不可逆性:根据输出散列值无法推导出输入数据,即难以通过散列值反向计算出输入数据。

常见的散列函数算法包括MD5、SHA-1、SHA-2、SHA-3等。但是,由于计算机计算能力的提高和攻击技术的发展,目前常用的算法已不够安全,需要不断更新和升级。

 

2.2 散列函数的构造 

2.2.1 除留余数法

        将关键字除以某个数m,取余数作为散列地址,即h(k)=k%m。但是如果m的取值不合适,可能会导致散列地址分布不均匀。m的取值一般是不大于散列表表长的最大质数。

2.2.2 直接定址法

        将关键字直接映射为地址,即h(k)=a*k+b(a,b是常数)。但是如果关键字取值范围较大,则需要大量的内存空间。 

2.2.3 数字分析法

        选取数码分布较为均匀的若干位最为散列地址。

2.2.4 平方取中法

        先将关键字平方,然后取中间几位作为散列地址,即h(k)=取中间几位( k^2 )。这种方法能够减小散列地址的值的尺寸,但是也可能出现分布不均匀的问题。

【数据结构】散列表(哈希表)的学习知识总结_第2张图片

3、冲突(碰撞)

        散列表的冲突指的是将两个或多个不同的键映射到了散列表的同一个位置上。这种情况称为冲突,因为两个不同的键无法在同一个位置上存储。

        散列表的冲突可以通过散列函数的优化和冲突解决方法来减少或避免。以下是几种常见的冲突解决方法:

  1. 链接法 (Chaining):将相同位置上的键值对通过链表等数据结构链接在一起。
  2. 开放地址法 (Open Addressing):在发生冲突时,按照一定规则去寻找数组内的下一个空闲位置存储。
  3. 双散列法 (Double Hashing):当发生冲突时,使用另一个散列函数对键进行再次散列,再根据新的散列值去找到空位置存储。

 

4、处理冲突的方法 

4.1 拉链法(链接法)

        散列表解决冲突的链接法又被称为“拉链法”,其主要思想是将散列到同一个位置的元素存储在一个链表中。当散列到同一个位置的元素出现冲突时,只需要将新的元素插入到链表的末尾即可。

具体实现方式如下:

1. 创建一个散列表,其中每个位置都是一个链表的头结点。

2. 对于要插入的元素,先计算它的散列值,并找到对应的链表头结点。

3. 遍历该链表,查找是否有相同的元素,如果有,则更新它的值;否则,在链表末尾插入新元素。

4. 如果插入操作导致链表长度超过一定阈值,可以考虑进行动态扩容或者重新散列操作。

5. 对于查询和删除操作,也需要先计算元素的散列值,找到对应的链表,然后再进行操作。

        在实际使用中,为了避免链表过长影响性能,可以设置一个阈值,当链表长度超过该阈值时,可以考虑进行扩容或者重新散列操作。【数据结构】散列表(哈希表)的学习知识总结_第3张图片

4.2 开放定址法

        开放定址法是一种常用的解决冲突的方法。具体来说,开放定址法的思想是当发现散列表中的某个位置已经被占用时,就去寻找下一个可用的位置,直到找到一个空槽或者遍历完整个散列表。

开放定址法中,有四种基本的探测方法:

        1. 线性探测:逐个查看表中的下一个空单元,直到找到一个空的单元为止。

        2. 平方探测:访问下一个位置的时候,是按照下一个位置的平方进行访问的。

        3. 双重散列:访问下一个位置的时候,是根据第二个哈希函数得到的值来访问的。

        4.伪随机序列法:访问下一个位置的时候,是根据伪随机序列数得到的值来访问的。

利用开放定址法解决冲突的散列表的具体实现步骤如下:

        1. 对于给定的键值,计算其散列值。

        2. 如果在散列表中的该位置为空,则将该键值存储在该位置上。

        3. 如果在散列表中的该位置已经被占用,则使用开放定址法寻找下一个可用的位置,直到找到一个空槽或者遍历完整个散列表。

        4. 在寻找到的空槽上存储该键值。

        5. 当需要查找某个键值时,首先计算该键值的散列值,并在散列表中查找该键值。如果已经找到,则返回该键值的值。如果未找到,则表示该键值不存在于散列表中。

        开放定址法可能会造成散列表中某些位置被长时间占用,导致散列表性能下降。因此,在实现散列表时需要进行合理的设计和优化。【数据结构】散列表(哈希表)的学习知识总结_第4张图片

5、C语言实现散列表(哈希表)

方式一:数组

        哈希表是一种常用的数据结构,它可以在平均情况下实现常数时间的插入、删除和查找操作。下面是用C语言数组实现哈希表的代码:

#include
#include
#include

#define MAX_SIZE 100 //哈希表的最大长度

//定义哈希表结构体
typedef struct HashTable{
    int size;
    int* keys;
    char** values;
}HashTable;

//哈希函数
int hashFunction(int key, int size){
    return key % size;
}

//创建哈希表
HashTable* createHashTable(int size){
    HashTable* hashtable = (HashTable*)malloc(sizeof(HashTable));
    hashtable->size = size;
    hashtable->keys = (int*)calloc(size, sizeof(int));
    hashtable->values = (char**)calloc(size, sizeof(char*));
    for(int i=0; ikeys[i] = -1;
    }
    return hashtable;
}

//插入键值对
void insert(HashTable* hashtable, int key, char* value){
    int index = hashFunction(key, hashtable->size);
    while(hashtable->keys[index] != -1){ //如果该位置已经被占用,则线性探测到下一个位置
        index++;
        index %= MAX_SIZE;
    }
    hashtable->keys[index] = key;
    hashtable->values[index] = (char*)malloc(sizeof(char) * (strlen(value)+1));
    strcpy(hashtable->values[index], value);
}

//获取键对应的值
char* get(HashTable* hashtable, int key){
    int index = hashFunction(key, hashtable->size);
    while(hashtable->keys[index] != key){
        index++;
        index %= MAX_SIZE;
    }
    return hashtable->values[index];
}

//删除键值对
void delete(HashTable* hashtable, int key){
    int index = hashFunction(key, hashtable->size);
    while(hashtable->keys[index] != key){
        index++;
        index %= MAX_SIZE;
    }
    hashtable->keys[index] = -1;
    free(hashtable->values[index]);
}

//测试代码
int main(){
    HashTable* hashtable = createHashTable(MAX_SIZE);
    insert(hashtable, 1, "张三");
    insert(hashtable, 2, "李四");
    insert(hashtable, 3, "王五");

    printf("%s\n", get(hashtable, 2)); //输出"李四"

    delete(hashtable, 1);
    printf("%s\n", get(hashtable, 1)); //输出空字符串

    return 0;
}

        在该实现中,我们使用了线性探测解决了哈希冲突的问题。对于哈希冲突,我们首先根据哈希函数计算键的索引,如果该位置已经被占用,则继续向下线性探测,直到找到空闲位置为止。当我们要查找或删除键值对时,我们也需要进行线性探测,直到找到对应的键为止。

方式二:指针和动态内存分配

哈希表是一种常见的数据结构,它可以支持快速的查询和插入操作。在C语言中,我们可以使用指针和动态内存分配来实现一个哈希表。

下面是一个简单的哈希表实现示例:

#include 
#include 
#include 

#define TABLE_SIZE 10000

// 哈希表节点结构体
typedef struct HashNode {
    int key;
    int value;
    struct HashNode* next;
} HashNode;

// 哈希表结构体
typedef struct HashTable {
    HashNode* table[TABLE_SIZE];
} HashTable;

// 哈希函数,将key映射成一个索引
int hash(int key) {
    return key % TABLE_SIZE;
}

// 初始化一个哈希表
HashTable* initHashTable() {
    HashTable* ht = (HashTable*)malloc(sizeof(HashTable));
    memset(ht->table, 0, sizeof(ht->table));
    return ht;
}

// 插入一个键值对到哈希表中
void insert(HashTable* ht, int key, int value) {
    int index = hash(key);
    HashNode* node = (HashNode*)malloc(sizeof(HashNode));
    node->key = key;
    node->value = value;
    node->next = NULL;

    if (ht->table[index] == NULL) {
        ht->table[index] = node;
    } else {
        HashNode* cur = ht->table[index];
        while (cur->next != NULL) {
            cur = cur->next;
        }
        cur->next = node;
    }
}

// 查找一个key对应的值
int find(HashTable* ht, int key) {
    int index = hash(key);
    HashNode* cur = ht->table[index];
    while (cur != NULL) {
        if (cur->key == key) {
            return cur->value;
        }
        cur = cur->next;
    }
    return -1;
}

// 从哈希表中删除一个键值对
void delete(HashTable* ht, int key) {
    int index = hash(key);
    HashNode* cur = ht->table[index];
    if (cur == NULL) {
        return;
    }
    if (cur->key == key) {
        ht->table[index] = cur->next;
        free(cur);
        return;
    }
    HashNode* prev = cur;
    cur = cur->next;
    while (cur != NULL) {
        if (cur->key == key) {
            prev->next = cur->next;
            free(cur);
            return;
        }
        prev = cur;
        cur = cur->next;
    }
}

int main() {
    HashTable* ht = initHashTable();
    insert(ht, 1, 10);
    insert(ht, 2, 20);
    insert(ht, 3, 30);
    printf("%d\n", find(ht, 1));
    printf("%d\n", find(ht, 2));
    printf("%d\n", find(ht, 3));
    delete(ht, 2);
    printf("%d\n", find(ht, 2));
    return 0;
}

        在上面的代码中,我们定义了一个哈希表节点结构体HashNode,包含了一个键值对和指向下一个节点的指针;还定义了一个哈希表结构体HashTable,包含了一个指针数组,每个指针指向一个节点链表。

        哈希函数hash将键映射成一个索引,使用求余运算实现。初始化函数initHashTable动态分配内存,并将指针数组初始化为空。插入函数insert根据哈希函数计算出索引,再将节点插入到指针数组对应位置的链表中。查找函数find根据哈希函数计算出索引,再在对应链表中遍历查找键值对。删除函数delete根据哈希函数计算出索引,再在对应链表中遍历查找要删除的节点,并释放对应的内存。

        在主函数中,我们创建一个哈希表并插入三个键值对。然后分别查找这三个键,并删除第二个键。最后再次查找第二个键,由于已经被删除,输出-1。

 

你可能感兴趣的:(数据结构,数据结构,散列表(哈希表))