1 概述
1.1 案例介绍
在现代软件开发中,数据结构的选择对程序的性能和可维护性有着至关重要的影响。
哈希表(Hash Table)是C语言中实现高效查找、插入和删除的核心数据结构之一。其核心思想是利用哈希函数将键(Key)映射到数组中的特定位置(索引),从而实现接近O(1)平均时间复杂度的操作。
本案例相关实验将在华为云开发者空间云主机进行,开发者空间云主机为开发者提供了高效稳定的云资源,确保用户的数据安全。云主机当前已适配完整的C/C++开发环境,支持
VS Code等多种IDE工具安装调测。
1.2 适用对象
- 个人开发者
- 高校学生
1.3 案例时间
本案例总时长预计60分钟。
1.4 案例流程

说明:
- 开通开发者空间,搭建C/C++开发环境。
- 打开VS Code,编写代码运行程序。
1.5 资源总览
本案例预计花费总计0元。
| 资源名称 | 规格 | 单价(元) | 时长(分钟) |
|---|---|---|---|
| 开发者空间-云主机 | 鲲鹏通用计算增强型 kC2 | 4vCPUs | 8G | Ubuntu | 免费 | 60 |
| VS Code | 1.97.2 | 免费 | 60 |
2 配置实验环境
2.1 开发者空间配置
面向广大开发者群体,华为开发者空间提供一个随时访问的“开发桌面云主机”、丰富的“预配置工具集合”和灵活使用的“场景化资源池”,开发者开箱即用,快速体验华为根技术和资源。
如果还没有领取开发者空间云主机,可以参考免费领取云主机文档领取。
领取云主机后可以直接进入华为开发者空间工作台界面,点击打开云主机 > 进入桌面连接云主机。


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

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窗口可以看到打印内容。


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窗口可以看到打印内容。

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窗口可以看到打印内容。

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窗口可以看到打印内容。

4 综合案例:学生成绩管理系统
4.1 功能需求分析
学生成绩管理系统设计功能如下:
- 添加学生信息;
- 查询学生信息;
- 删除学生信息;
- 计算平均分;
- 显示出所有学生;
- 退出。
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窗口可以看到打印内容,可以手动添加学生信息、查询学生信息、删除学生信息、计算平均分、显示所有学生信息、退出操作等。


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

被折叠的 条评论
为什么被折叠?



