【哈希表】嘻哈表:“数据快递员”

1 概述

1.1 案例介绍

在现代软件开发中,数据结构的选择对程序的性能和可维护性有着至关重要的影响。

哈希表Hash Table)是C语言中实现高效查找、插入和删除的核心数据结构之一。其核心思想是利用哈希函数将键(Key)映射到数组中的特定位置(索引),从而实现接近O(1)平均时间复杂度的操作。

本案例相关实验将在华为云开发者空间云主机进行,开发者空间云主机为开发者提供了高效稳定的云资源,确保用户的数据安全。云主机当前已适配完整的C/C++开发环境,支持

VS Code等多种IDE工具安装调测。

1.2 适用对象

  • 个人开发者
  • 高校学生

1.3 案例时间

本案例总时长预计60分钟。

1.4 案例流程

d02d852664143a87e40baf08da0a054f.png

说明:

  1. 开通开发者空间,搭建C/C++开发环境。
  2. 打开VS Code,编写代码运行程序。

1.5 资源总览

本案例预计花费总计0元。

资源名称规格单价(元)时长(分钟)
开发者空间-云主机鲲鹏通用计算增强型 kC2 | 4vCPUs | 8G | Ubuntu免费60
VS Code1.97.2免费60

2 配置实验环境

2.1 开发者空间配置

面向广大开发者群体,华为开发者空间提供一个随时访问的“开发桌面云主机”、丰富的“预配置工具集合”和灵活使用的“场景化资源池”,开发者开箱即用,快速体验华为根技术和资源。

如果还没有领取开发者空间云主机,可以参考免费领取云主机文档领取。

领取云主机后可以直接进入华为开发者空间工作台界面,点击打开云主机 > 进入桌面连接云主机。

a1aae6ff53aac98855ef597dd6899967.png

552fc96c3b58a06e294e4a760ae719e3.PNG

2.2 配置实验环境

参考案例中心《基于开发者空间,定制C&C++开发环境云主机镜像》“2. 实验环境搭建”、“3. VS Code安装部署”章节完成开发环境、VS code及插件安装。

ce130591abe73e48832079b65fec477e.PNG

3 哈希表的基本操作

3.1 哈希表核心概念

键(Key):要存储或查找的数据标识(如姓名、ID)。

值(Value):与键关联存储的数据。

哈希函数(Hash Function):将任意大小的键key转换为固定范围的整数(哈希值hash),尽量均匀地将不同的键分布到数组的不同位置。

哈希冲突(Collision):不可避免的现象。指两个不同的键key1 != key2经过哈希函数计算后得到相同的索引 (hash(key1) == hash(key2))。

3.2 哈希函数

常见简单哈希函数(需根据实际数据特性选择或设计):

  • 除法取余法:hash = key % table_size (最常用,table_size最好为质数)
  • 乘法取整法:hash = floor(table_size * (key * A mod 1)) (A为小数如0.618)
  • 字符串哈希:如 DJB2, sdbm (hash = hash * 31 + char 是常见简化版)

1)首先是整数类型的除法取余法乘法取整法,代码如下:

#include <stdio.h>
#include <string.h>

// 1. 除法取余法(整数键)
unsigned int division_hash(int key, int table_size) {
    // 使用质数作为表大小可减少冲突
    return key % table_size;
}

// 2. 乘法取整法(整数键)
unsigned int multiplication_hash(int key, int table_size) {
    // Knuth推荐的常数 A ≈ (√5-1)/2 ≈ 0.6180339887
    const double A = 0.6180339887;

    // 计算 key*A 的小数部分
    double product = key * A;
    double fractional = product - (int)product;
    // 映射到表范围
    return (unsigned int)(table_size * fractional);
}

int main() {
    // 测试整数键
    int int_keys[] = {42, 123, 789, 1024, 2048, 4096};
    int table_size = 20; // 哈希表大小

    printf("整数键哈希测试 (表大小: %d):\n", table_size);
    printf("键值\t除法取余\t乘法取整\n");
    printf("----------------------------------\n");

    for (int i = 0; i < sizeof(int_keys)/sizeof(int_keys[0]); i++) {
        int key = int_keys[i];
        printf("%d\t%u\t\t%u\n", 
               key, 
               division_hash(key, table_size),
               multiplication_hash(key, table_size));
    }    
    return 0;
} 

点击编辑器左上角运行按钮直接运行,Terminal窗口可以看到打印内容。

b196f64b8f1726fd287c4e0163f2900e.png

88ba8bc1d166465c8b5d2a3240ce132c.png

2)字符串哈希,代码如下:

#include <stdio.h>
#include <string.h>

// 1. 字符串哈希(sdbm算法)
unsigned int string_hash(const char* key, int table_size) {
    unsigned int hash_val = 0;
    while (*key) hash_val = (hash_val * 31) + (*key++);
    return hash_val % table_size;
}

int main() {
    // 测试字符串键
    const char* string_keys[] = {"apple", "banana", "orange", "grape", "kiwi", "mango"};
    int str_table_size = 15; // 字符串哈希表大小

    printf("\n字符串键哈希测试 (表大小: %d):\n", str_table_size);
    printf("字符串\t哈希值\n");
    printf("------------------\n");

    for (int i = 0; i < sizeof(string_keys)/sizeof(string_keys[0]); i++) {
        const char* key = string_keys[i];
        printf("%s\t%u\n", key, string_hash(key, str_table_size));
    }

    return 0;
} 

点击编辑器左上角运行按钮直接运行,Terminal窗口可以看到打印内容。

f8daf1b76dd8e5f3ad1c899480eedd4b.png

3.3 哈希冲突及解决方案

哈希冲突‌是指在使用哈希函数时,不同的键通过哈希函数计算得到相同的哈希值,导致它们被映射到哈希表的同一位置的现象。

由于哈希表的存储空间是有限的,而可能的输入值是无限的,因此总会存在冲突的可能性‌。

而解决哈希冲突,我们有以下几种方案,首先是链地址法。

1)链地址法:通过将哈希到同一位置的元素存储在链表中来处理冲突。‌该方法将散列地址相同的元素链接为同义词子表,各链表头结点组成向量结构,实现高效查找、插入和删除操作。‌‌

链地址法代码演示如下:

#include <iostream>
#include <list>
#include <vector>
using namespace std;

class HashTable {
    static const int SIZE = 5;
    vector<list<pair<int, string>>> table;

    int hash(int key) {
        return key % SIZE;
    }

public:
    HashTable() : table(SIZE) {}

    void insert(int key, string value) {
        int index = hash(key);
        cout << "插入 (" << key << ", " << value << ") -> 桶" << index;

        // 检查冲突
        if (!table[index].empty()) {
            cout << " [冲突! 已存在 " << table[index].size() << " 个元素]";
        }

        table[index].push_back({key, value});
        cout << endl;
    }

    void display() {
        cout << "\n哈希表内容 (链地址法):\n";
        for (int i = 0; i < SIZE; i++) {
            cout << "桶" << i << ": ";
            for (auto& item : table[i]) {
                cout << "(" << item.first << ", " << item.second << ") ";
            }
            cout << endl;
        }
    }
};

int main() {
    HashTable ht;
    ht.insert(10, "苹果");
    ht.insert(20, "香蕉");  // 冲突 (10 % 5 = 0, 20 % 5 = 0)
    ht.insert(15, "橙子");  // 冲突 (15 % 5 = 0)
    ht.insert(7, "葡萄");
    ht.insert(12, "柠檬");  // 冲突 (7 % 5 = 2, 12 % 5 = 2)
    ht.display();
    return 0;
}

点击编辑器左上角运行按钮直接运行,Terminal窗口可以看到打印内容。

9b978f2a23d91e2a7ed63ceaaf824bcc.png

2)线性探测法: 线性探测法是解决哈希表冲突的开放寻址策略‌,其核心机制是通过顺序探测下一个空闲位置实现冲突处理。当哈希冲突发生时,该方法依次检查哈希表中后续槽位(直到表尾后回到表头),直至找到空槽插入数据。该方法的优势在于实现简单,但可能因"堆积现象"降低查询效率。‌‌

线性探测法代码演示如下:

#include <iostream>
#include <vector>
using namespace std;

class HashTable {
    static const int SIZE = 7;
    vector<pair<int, string>> table;
    vector<bool> occupied;

    int hash(int key) {
        return key % SIZE;
    }

public:
    HashTable() : table(SIZE, {-1, ""}), occupied(SIZE, false) {}

    void insert(int key, string value) {
        int index = hash(key);
        cout << "插入 (" << key << ", " << value << ") -> 尝试位置: " << index;

        int probes = 0;
        while (occupied[index]) {
            probes++;
            cout << " [冲突! 探测新位置: " << index << "]";
            index = (index + 1) % SIZE;
        }

        table[index] = {key, value};
        occupied[index] = true;
        cout << " -> 最终位置: " << index;
        if (probes > 0) cout << " (探测次数: " << probes << ")";
        cout << endl;
    }

    void display() {
        cout << "\n哈希表内容 (线性探测):\n";
        for (int i = 0; i < SIZE; i++) {
            cout << "位置" << i << ": ";
            occupied[i] ? cout << "(" << table[i].first << ", " << table[i].second << ")" 
                         : cout << "空";
            cout << endl;
        }
    }
};

int main() {
    HashTable ht;
    ht.insert(10, "苹果");
    ht.insert(20, "香蕉");  // 20 % 7 = 6
    ht.insert(15, "橙子");  // 15 % 7 = 1
    ht.insert(8, "葡萄");   // 8 % 7 = 1 -> 冲突
    ht.insert(22, "柠檬");  // 22 % 7 = 1 -> 冲突
    ht.display();
    return 0;
}

Step2:点击编辑器左上角运行按钮直接运行,Terminal窗口可以看到打印内容。

e542482d2728444403c5a1e65c6b232a.png

4 综合案例:学生成绩管理系统

4.1 功能需求分析

学生成绩管理系统设计功能如下:

  1. 添加学生信息;
  2. 查询学生信息;
  3. 删除学生信息;
  4. 计算平均分;
  5. 显示出所有学生;
  6. 退出。

4.2 代码实现及验证

下面案例使用标准库中unordered_map(哈希表实现)开发的学生成绩管理系统。

代码参考如下:

#include <iostream>
#include <unordered_map>
#include <string>
#include <vector>
#include <iomanip>
#include <numeric>

using namespace std;

// 学生结构体
struct Student {
    string name;
    int id;
    double score;
};

// 学生成绩管理系统类
class StudentGradeManager {
private:
    unordered_map<int, Student> studentMap; // 使用哈希表存储学生信息,学号为键

public:
    // 添加学生
    void addStudent(const string& name, int id, double score) {
        if (studentMap.find(id) != studentMap.end()) {
            cout << "学号 " << id << " 已存在!" << endl;
            return;
        }

        Student student;
        student.name = name;
        student.id = id;
        student.score = score;

        studentMap[id] = student;
        cout << "学生 " << name << " 添加成功!" << endl;
    }

    // 查询学生成绩
    void queryStudent(int id) {
        auto it = studentMap.find(id);
        if (it != studentMap.end()) {
            const Student& student = it->second;
            cout << "学号: " << student.id << ", 姓名: " << student.name 
                 << ", 成绩: " << fixed << setprecision(1) << student.score << endl;
        } else {
            cout << "未找到学号为 " << id << " 的学生!" << endl;
        }
    }

    // 删除学生记录
    void deleteStudent(int id) {
        if (studentMap.erase(id) > 0) {
            cout << "学号 " << id << " 的学生记录已删除!" << endl;
        } else {
            cout << "未找到学号为 " << id << " 的学生!" << endl;
        }
    }

    // 计算班级平均分
    void calculateAverage() {
        if (studentMap.empty()) {
            cout << "当前没有学生记录!" << endl;
            return;
        }

        double total = 0.0;
        for (const auto& pair : studentMap) {
            total += pair.second.score;
        }

        double average = total / studentMap.size();
        cout << "班级平均分: " << fixed << setprecision(1) << average << endl;
    }

    // 显示所有学生信息
    void displayAll() {
        if (studentMap.empty()) {
            cout << "当前没有学生记录!" << endl;
            return;
        }

        cout << "所有学生信息:" << endl;
        cout << "学号\t姓名\t成绩" << endl;
        for (const auto& pair : studentMap) {
            const Student& student = pair.second;
            cout << student.id << "\t" << student.name << "\t" 
                 << fixed << setprecision(1) << student.score << endl;
        }
    }

    // 显示菜单
    static void showMenu() {
        cout << "\n学生成绩管理系统" << endl;
        cout << "1. 添加学生" << endl;
        cout << "2. 查询学生" << endl;
        cout << "3. 删除学生" << endl;
        cout << "4. 计算平均分" << endl;
        cout << "5. 显示所有学生" << endl;
        cout << "0. 退出" << endl;
        cout << "请选择操作: ";
    }
};

int main() {
    StudentGradeManager manager;
    int choice;

    do {
        StudentGradeManager::showMenu();
        cin >> choice;

        switch (choice) {
            case 1: {
                string name;
                int id;
                double score;
                cout << "请输入学生姓名: ";
                cin >> name;
                cout << "请输入学号: ";
                cin >> id;
                cout << "请输入成绩: ";
                cin >> score;
                manager.addStudent(name, id, score);
                break;
            }
            case 2: {
                int id;
                cout << "请输入要查询的学号: ";
                cin >> id;
                manager.queryStudent(id);
                break;
            }
            case 3: {
                int id;
                cout << "请输入要删除的学号: ";
                cin >> id;
                manager.deleteStudent(id);
                break;
            }
            case 4:
                manager.calculateAverage();
                break;
            case 5:
                manager.displayAll();
                break;
            case 0:
                cout << "感谢使用,再见!" << endl;
                break;
            default:
                cout << "无效的选择,请重新输入!" << endl;
        }
    } while (choice != 0);

    return 0;
}

点击编辑器左上角运行按钮直接运行,Terminal窗口可以看到打印内容,可以手动添加学生信息、查询学生信息、删除学生信息、计算平均分、显示所有学生信息、退出操作等。

d727545849ed5f6362ba4c73a7c780e0.png

e0f6e895bc6767ae9424b09ae632ba78.png

开发者可以在此案例基础上做更多操作,增加对哈希表数据结构的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值