LOADFACTOR/README.md

665 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 散列表的设计与实现
# 一、需求分析
本系统为电话号码查找系统,本系统最频繁的操作为查询功能,查询速度的快慢对此系统有至关重要的影响,因此应该选择合适的数据结构来进行设计。散列表可以实现 O(1)的快速查找,用 Hash 数据结构作为底层存储结构较为合适。本系统应首先实现Hash表的基本结构和操作在此基础上构建电话号码查找系统。电话号码查找系统包括若干数据项电话号码、用户名、地址可以键盘输入或文件批量导入记录既可以使用电话号码作为索引建立Hash表也可以使用姓名作为索引建立Hash表并通过电话号码和姓名进行查找记录。更进一步在设计Hash数据结构时可设计不同的Hash函数及采用不同的冲突解决算法来比较性能的差异。具体功能如下
## 1.1 Hash表设计
设计 Hash 表的 ADT设计 Hash 表的存储结构以及基本操作,设计不同的 Hash函数对字符串进行散列设计不同的冲突解决策略如链地址法、线性探测法等。Hash表的结构由key-value组成基本操作有添加元素、查找元素、删除元素、遍历元素、建表、销毁表等并且设计可以计算平均查找长度ASL的函数以此来比较不同的Hash函数使用相同的冲突解决算法以及相同的Hash 函数使用不同的冲突解决算法的优劣。为方便Hash表的使用Hash表可自动扩容。
## 1.2 添加与导入记录
系统可采用两种方式导入信息:手动添加与文件批量导入。手动添加方式用户通过交互性界面一步一步输入待添加记录的电话号码、姓名、地址,可连续进行添加。文件批量导入方式,预先将记录以指定格式写入文件,系统再从指定文件中批量导入。
## 1.3 查询记录
查询记录功能为此系统主要功能,可采用两种查询方式:按电话号码查询与按姓名查询。如果是按照电话号码建立索引,按电话号码查询将迅速完成,如果是按照姓名建立索引,按姓名查询将迅速完成。
## 1.4 不同Hash函数比较
设计多种 Hash 函数,使用同一冲突解决方法比较其 ASL研究不同 Hash 函数对于冲突率的影响。
## 1.5 不同冲突解决方法比较
设计多种冲突解决方法,使用同一 Hash 函数比较其 ASL研究不同的冲突解决方法对于 ASL 的影响。
# 二、概要设计
## 2.1 数据类型的定义
使用链地址法的 Hash ADT 设计
```c++
# define MAXCAPACITY 1 << 30
# define LOADFACTOR 0.75f
# define MAXKEYLEN 255
typedef AddList ElemType;
struct Node {
char key[MAXKEYLEN];
ElemType value;
Node *next;
}
;
struct HashTable {
int capacity;
int size;
Node *table;
}
;
int NextPrime(int n);
void InitHashTable(HashTable &hash_table, int init_capacity);
unsigned int SumHash(const char *key, int table_size);
unsigned int ShiftHash(const char *key, int table_size);
unsigned int ELFHash(const char *key, int table_size);
bool Put(HashTable &hash_table, const char *key, ElemType value, ElemType
&old_value, unsigned int (*Hash)(const char *key, int table_size));
bool Get(HashTable &hash_table, const char *key, ElemType &value, unsigned int (*Hash)(const char *key, int table_size));
bool Remove(HashTable &hash_table, const char *key, ElemType &value, unsigned int (*Hash)(const char *key, int table_size));
void DelHashTable(HashTable &hash_table);
void TraverseHashTable(HashTable &hash_table, void (*visit)(ElemType v));
double GetASL(HashTable &hash_table, unsigned int (*Hash)(const char *key, int table_size));
```
使用线性探测法的 Hash ADT 设计
```c++
typedef AddList ArrElemType;
// 散列单元状态类型,分别对应:有合法元素、空单元、有已删除元素
typedef enum {
Legitimate, Empty, Deleted
}
NodeType;
struct HashNode {
char key[MAXKEYLEN];
ArrElemType value;
NodeType type;
}
;
struct ArrayHashTable {
int size;
int capacity;
HashNode *table;
}
;
void InitArrayHashTable(ArrayHashTable &hash_table, int init_capacity);
void DelArrayHashTable(ArrayHashTable &hash_table);
bool IsFull(ArrayHashTable &hash_table);
bool LinearDelete(ArrayHashTable &hash_table, const char *key);
bool LinearGet(ArrayHashTable &hash_table, const char *key, ArrElemType
&value);
bool LinearGetNum(ArrayHashTable &hash_table, const char *key,
ArrElemType &value, int &num);
int LinearPut(ArrayHashTable &hash_table, const char *key, ArrElemType value, ArrElemType &old_value);
```
电话查找系统数据类型定义
```c++
struct AddList {
char phone_num[MAXPHONENUM];
char name[MAXNAME];
char address[MAXADDRESS];
AddList() {
}
AddList(const char *phone_num, const char *name, const char *address) {
strcpy(this->phone_num, phone_num);
strcpy(this->name, name);
strcpy(this->address, address);
}
}
;
```
## 2.2 功能模块结构图
根据需求分析为了满足用户的功能需求将系统划分为以下几个模块Hash 表模块、查询记录模块、添加记录模块、导入记录模块、Hash 函数比较模块、冲突解决办法比较模块。其中 Hash 表模块包括链表实现的 Hash 表子模块和线性探测法实现的 Hash 表子模块,查询记录模块包括电话号码查询子模块和姓名查
询子模块。
![](https://www.writebug.com/myres/static/uploads/2023/2/7/18d75fc8db1623cc56c29a04468c279f.writebug)
图 1 模块结构图
## 2.3 Hash模块概要设计
链地址法 Hash 表设计
链地址法 Hash 表使用链表来解决冲突Hash 表维护一定长度的链表数组,对于 Hash 值相同的元素,将其插入 Hash 值对应的数组上的链表中。结构如下图所示,为长度为 7 的 Hash 表:
![](https://www.writebug.com/myres/static/uploads/2023/2/7/b184afc4777427d9782c3d3049e1830f.writebug)
- 图 2 链地址法 Hash 表结构图
- 线性探测法 Hash 表设计
- 使用数组保存元素,插入元素时,先算出 Hash 值,将其放入 Hash 值所对应的位置,如果有冲突,使用线性探测法,地址公式为:
```c++
Hi = (Hash(key) + i) % size i = 1, 2, 3, 4…… size-1 其中 size 为 Hash 数组的总大小,结构图如下:
```
![](https://www.writebug.com/myres/static/uploads/2023/2/7/18068f0334cf5368edd7bc87cfcd7c88.writebug)
图 3 线性探测法 Hash 表结构图
# 三、运行环境
硬件环境:
cpuIntel(R) Core(TM) i7-6500U CPU @ 2.50GHz × 4
内存8G
软件环境:
OSUbuntu 18.04.1 LTS 64位
Linux内核Linux version 4.4.0-17134-Microsoft
# 四、开发工具和编程语言
编辑软件:
```c++
Visual Studio Code 1.30.1
```
编译软件:
```c++
GNU G++ version 7.3.0
```
构建工具:
```c++
GNU Make 4.1
```
编程语言:
C++语言
# 五、详细设计
在概要设计的基础上,对每个模块进行详细设计。下面分别列出各个模块的
详细设计。
## 5.1 Hash函数设计
Hash 表的基本原理为:给出一个 Key调用 Hash 函数算出该元素所在的位置。Hash 函数的设计有多种方案,这里给出三种对字符串进行散列的函数。
字符串各位相加 基本思想为将字符串的各位值累加,再求余得到最终位置,代码如下:
```c++
unsigned int SumHash(const char *key, int table_size) {
unsigned int h = 0;
while (*key) h += *key++;
return h % table_size;
}
```
字符串循环移位相加
公式为:
```c++
key) = key[n i 1] × 32 %table_size
```
此散列方法涉及到所有的 n 个字符,且散列分布较好,代码如下:
```c++
unsigned int ShiftHash(const char *key, int table_size) {
unsigned int h = 0;
while (*key) h = (h << 5) + *key++;
return h % table_size;
}
```
ELF Hash 方法
该散列方法同样为对字符的 ASCII 编码值进行计算ELFhash 函数能够比较均匀地把字符串分布在散列表中,代码如下:
```c++
unsigned int ELFHash(const char *key, int table_size) {
unsigned long h = 0;
unsigned long x = 0;
while (*key) {
h = (h << 4) + (*key++);
if ((x = h & 0xF0000000L) != 0) {
h ^= (x >> 24);
h &= ~x;
}
}
return h % table_size;
}
```
## 5.2 链地址法Hash设计
Hash 表初始化
通过初始化函数可实现对 Hash 表的初始化,用户可自定义初始 Hash 表 容量大小,初始化函数保证哈希表容量为一个素数,这样可使得散列均匀。
int NextPrime(int n) 函数返回大于 n 的下一个素数。流程图及代码如下所示:
![](https://www.writebug.com/myres/static/uploads/2023/2/7/da4ac543b845d6d1aa2437b57edaaf82.writebug)
图 4 初始化链地址法 Hash 表流程图
```c++
void InitHashTable(HashTable &hash_table, int init_capacity) {
if (init_capacity > 0 && init_capacity < MAXCAPACITY) hash_table.capacity = NextPrime(init_capacity); else hash_table.capacity = NextPrime(32);
hash_table.table = new Node[hash_table.capacity];
hash_table.size = 0;
for (int i = 0; i < hash_table.capacity; i++) {
hash_table.table[i].next = NULL;
}
}
```
Hash 表扩容机制
当 Hash 表实际存储的元素个数即将等于 Hash 表容量*装填因子时,对 Hash 表进行扩容,使其容量变为当前容量的 2 倍,再取素数。流程图及代码
如下图所示:
![](https://www.writebug.com/myres/static/uploads/2023/2/7/4eaf25ea7c8d2d3a79786b5b02cc6763.writebug)
图 5 链地址法 Hash 表扩容机制流程图
```c++
void ReCapacity(HashTable &hash_table, unsigned int (*Hash)(const char
*key, int table_size)) {
const int ori_capacity = hash_table.capacity;
hash_table.capacity = NextPrime(hash_table.capacity * 2);
Node *ori_table = hash_table.table;
hash_table.table = new Node[hash_table.capacity];
for (int i = 0; i < hash_table.capacity; i++) {
hash_table.table[i].next = NULL;
}
for (int i = 0; i < ori_capacity; i++) {
Node *p = ori_table[i].next, *q;
while (p) {
q = p;
p = p->next;
int new_pos = Hash(q->key, hash_table.capacity);
q->next = hash_table.table[new_pos].next;
hash_table.table[new_pos].next = q;
}
}
delete[] ori_table;
}
```
Hash 表 Put 操作
向 Hash 表添加元素时,调用 Put 操作。若添加新元素后使得元素个数等于哈希表容量*装填因子,进行扩容。将以 key 为键的元素 value 放入 Hash 表,返回是否有旧元素,有返回 true使用 value 进行替换,否则返回 false old_value 为旧元素。流程图及代码如下图所示:
![](https://www.writebug.com/myres/static/uploads/2023/2/7/64eb6646bc3bc7a8797e9856eef6c47b.writebug)
图 6 链地址法 Hash 表 Put 操作流程图
```c++
bool Put(HashTable &hash_table, const char *key, ElemType value, ElemType
&old_value, unsigned int (*Hash)(const char *key, int table_size)) {
if (hash_table.size + 1 >= hash_table.capacity * LOADFACTOR)
ReCapacity(hash_table, Hash);
unsigned int pos = Hash(key, hash_table.capacity);
Node *p = hash_table.table[pos].next;
while (p) {
if (!strcmp(p->key, key)) {
old_value = p->value;
p->value = value;
return true;
}
p = p->next;
}
Node *node = new Node;
strcpy(node->key, key);
node->value = value;
node->next = hash_table.table[pos].next;
hash_table.table[pos].next = node;
hash_table.size++;
return false;
}
```
Hash 表 Get 操作
获取 Hash 表为 key 的元素时,调用 Get 元素,返回是否存在该元素。流程图和代码如下:
![](https://www.writebug.com/myres/static/uploads/2023/2/7/d22d5d0993b18bfc727aa67e7a8faaa9.writebug)
图 7 链地址法 Hash 表 Get 操作流程图
```c++
bool Get(HashTable &hash_table, const char *key, ElemType &value, unsigned int (*Hash)(const char *key, int table_size)) {
int pos = Hash(key, hash_table.capacity);
Node *p = hash_table.table[pos].next;
while (p) {
if (!strcmp(key, p->key)) {
value = p->value;
return true;
}
p = p->next;
}
return false;
}
```
Hash 表 Remove 操作
当删除 Hash 表中为 Key 的元素时,调用 Remove 函数。代码如下:
```c++
bool Remove(HashTable &hash_table, const char *key, ElemType &value, unsigned int (*Hash)(const char *key, int table_size)) {
int pos = Hash(key, hash_table.capacity);
Node *p = &hash_table.table[pos];
while (p ->next) {
if (!strcmp(p->next->key, key)) {
value = p->next->value;
Node *t = p->next;
p->next = t->next;
delete t;
return true;
}
p = p->next;
}
return false;
}
```
Hash 表销毁操作不需要此 Hash 表时,需要释放空间,调用 DelHashTable 函数,代码如
下:
```c++
void DelHashTable(HashTable &hash_table) {
for (int i = 0; i < hash_table.capacity; i++) {
Node *p = hash_table.table[i].next;
while (p) {
Node *t = p;
p = p->next;
delete t;
}
}
delete[] hash_table.table;
}
```
Hash 表遍历操作遍历 Hash 表时,调用 TraverseHashTable 函数,需指定访问函数 Visit从而实现对 Hash 表内每个元素的访问。代码如下:
```c++
void TraverseHashTable(HashTable &hash_table,
void (*visit)(ElemType v)) {
for (int i = 0; i < hash_table.capacity; i++) {
Node *p = hash_table.table[i].next;
while (p) {
visit(p->value);
p = p->next;
}
}
}
```
## 5.3 线性探测法Hash表设计
Hash 表初始化
线性探测法实现的 Hash 表使用数组存储数据,初始化需要申请数组空间,并为每一元素标记 Empty。代码如下
```c++
void InitArrayHashTable(ArrayHashTable &hash_table, int init_capacity) {
if (init_capacity > 0 && init_capacity < MAXCAPACITY) hash_table.capacity = NextPrime(init_capacity); else hash_table.capacity = NextPrime(32);
hash_table.table = new HashNode[hash_table.capacity];
hash_table.size = 0;
for (int i = 0; i < hash_table.capacity; i++) hash_table.table[i].type = Empty;
}
```
Hash 表 Put 操作
Put 方法使用线性探测解决冲突,如果算出的 Hash 位置已经有元素或者被删除,则位置不断+1 寻找下一个 Empty 位置,代码如下:
```c++
//返回值:哈希表已满:-1 存在 key 相同元素替换1 新元素插入哈希表0
int LinearPut(ArrayHashTable &hash_table, const char *key,
ArrElemType value, ArrElemType &old_value) {
if (IsFull(hash_table)) return -1;
unsigned int curpos, newpos;
curpos = newpos = ShiftHash(key, hash_table.capacity);
while (!(hash_table.table[newpos].type == Empty || (!strcmp(hash_table.table[newpos].key, key) && hash_table.table[newpos].type == Legitimate))) {
newpos = (newpos + 1) % hash_table.capacity;
}
if (hash_table.table[newpos].type != Empty) {
old_value = hash_table.table[newpos].value;
hash_table.table[newpos].value = value;
return 1;
}
strcpy(hash_table.table[newpos].key, key);
hash_table.table[newpos].type = Legitimate;
hash_table.table[newpos].value = value;
hash_table.size++;
return 0;
}
```
Hash Get 操作
Get 方法通过算出的 Hash 位置不断寻找 Legitimate 元素代码如下
```c++
bool LinearGet(ArrayHashTable &hash_table, const char *key,
ArrElemType &value) {
unsigned int curpos, newpos;
curpos = newpos = ShiftHash(key, hash_table.capacity);
int cnt = 1;
while (!(hash_table.table[newpos].type == Empty || (strcmp(hash_table.table[newpos].key, key) == 0 && hash_table.table[newpos].type == Legitimate))) {
newpos = (newpos + 1) % hash_table.capacity;
if (++cnt == hash_table.capacity) return false;
}
if (hash_table.table[newpos].type == Empty) return false;
value = hash_table.table[newpos].value;
return true;
}
```
Hash 表 Delete 操作
Delete 方法用于删除为 Key 的元素,添加 Deleted 标记,防止之后查找和插入时出现错误。代码如下:
```c++
bool LinearDelete(ArrayHashTable &hash_table, const char *key) {
unsigned int curpos, newpos;
curpos = newpos = ShiftHash(key, hash_table.capacity);
int cnt = 1;
while (!(hash_table.table[newpos].type == Empty || (strcmp(hash_table.table[newpos].key, key) == 0 && hash_table.table[newpos].type == Legitimate))) {
newpos = (newpos + 1) % hash_table.capacity;
if (++cnt == hash_table.capacity) return false;
}
if (hash_table.table[newpos].type == Empty) return false;
hash_table.table[newpos].type = Deleted;
return true;
}
```
## 5.4 电话号码查询系统设计
系统首先选择建立索引方式以电话号码为 Key 建立 Hash 表或以姓名为 Key 建立 Hash 包括以下函数以下记录表示一条电话号码姓名地址的记录添加记录
```c++
bool PutRecord(HashTable &hash_tabe, const char *key, AddList add, AddList &old_add, unsigned int (*Hash)(const char *key, int table_size) = ShiftHash)
```
获取记录:
```c++
bool GetRecord(HashTable &hash_table, const char *key, AddList &add, unsigned int (*Hash)(const char *key, int table_size) = ShiftHash)
```
移除记录
```c++
bool RemoveRecord(HashTable &hash_table, const char *key, AddList &add,
unsigned int (*Hash)(const char *key, int table_size) = ShiftHash)
```
输出记录:
```c++
void PrintRecord(AddList add)
```
添加记录界面
```c++
void AddRecordForm()
```
查询记录界面:
```c++
void QueryRecordForm()
```
按电话号码查找记录
```c++
void QueryRecordByPhoneNum()
```
按姓名查找记录:
```c++
void QueryRecordByName()
```
显示所有记录
```c++
void DisplayAllRecord()
```
从文件导入记录:
```c++
void ImportRecordsFromFile()
```
随机生成记录
```c++
void GenerateRandomRecords(int total_num, AddList Records[])
```
哈希函数对比:
```c++
void HashFuncCompare()
```
冲突解决策略对比
```c++
void ConflictMethodCompare()
```
主界面:
```c++
void MainForm()
```
Hash 类型选择
```c++
void ChooseHashType()
```
# 六、运行结果
以下为系统运行示例
![](https://www.writebug.com/myres/static/uploads/2023/2/7/56cdbf50b177d6a3bd0ecdd3fce8a32e.writebug)
8 选择建立的 Hash 类型
![](https://www.writebug.com/myres/static/uploads/2023/2/7/c5f54af744f7b30fadfc00a10153310f.writebug)
9 系统主界面
![](https://www.writebug.com/myres/static/uploads/2023/2/7/e4754556150ed0d619cf75fd4cab29fa.writebug)
10 批量导入记录后查看全部记录
![](https://www.writebug.com/myres/static/uploads/2023/2/7/ec3045533cfdf040aea6ba8dcbf5952b.writebug)
11 手动添加一条新纪录
![](https://www.writebug.com/myres/static/uploads/2023/2/7/77d4fa235dc1abab1a871792bf986fbe.writebug)
12 再次查看全部记录
![](https://www.writebug.com/myres/static/uploads/2023/2/7/23af6397c57d308ee76175a4c5a21388.writebug)
13 按电话号码查询记录
![](https://www.writebug.com/myres/static/uploads/2023/2/7/9fbd875a0f7575abe6c679faeddc4282.writebug)
14 按姓名查询记录
![](https://www.writebug.com/myres/static/uploads/2023/2/7/413f57be43eaf198a9e2979e98fb6450.writebug)
15 不同 Hash 函数比较 ASL
![](https://www.writebug.com/myres/static/uploads/2023/2/7/659c7466cefb851b33c638d409fe0f2a.writebug)
16 不同 Hash 函数比较 ASL大数据
![](https://www.writebug.com/myres/static/uploads/2023/2/7/e00cb058add8ce24d685e06f900464d9.writebug)
17 不同冲突解决策略比较 ASL
![](https://www.writebug.com/myres/static/uploads/2023/2/7/4fa079228c20dee60e0a9d4a0e133ba6.writebug)
18 不同冲突解决策略比较 ASL大数据
# 七、调试分析
在测试大量数据的 Hash 函数比较时Linux 操作系统会报 Segment Fault如先随机生成数据测试之后再次测试就会出现错误经过仔细分析发现链表操作存在问题在进行扩容操作时对于新创建的链表数组忘记将 next 初始化为 NULL可见对于链表的相关操作需要格外细心指针申请使用完应释放
下图 78-80 行为忘记初始化的代码
![](https://www.writebug.com/myres/static/uploads/2023/2/7/570101593aeee6d2bac1e25eeb818cbf.writebug)
19 出现问题的代码段78-80
# 八、心得体会
本次课程设计我选择了 Hash 表的设计与实现这个题目因为在 C++、Java 等语言中经常使用这种数据结构故对于 Hash 表有一定了解而且 Hash 的思想用处极为广泛如数据库数据加密等所以希望自己可以实现其基本结构和操作并分别以链表和数组的方式来实现
编写 Hash 算法的过程并不很容易我参考了 JDK HashMap 中的源码实现了解到其基本的设计思想并将其应用到自己的 Hash 算法中在自己的链表实现 Hash 使用了 HashMap 所使用的链表来解决冲突的方法即相同 Hash 值的元素被放在同一个链表中查找时转变为线性查找同时应用了 HashMap 中的扩容思想 Hash 表中装填因子达到某一阈值时 Hash 表进行自动扩容此方式可增强 Hash 表的易用度和性能在编写完成进行大容量数据测试时发现了链表式 Hash Bug经过仔细查看代码发现是在扩容时忘记将新申请的空间的每个 next 域初始化以后在编写链表的程序时要格外细心
对于数组的 Hash 算法最开始以为就是简单的数组查找或添加找不到直接线性探测就可以了但是当删除元素时如果简单的进行删除操作就会出现问题下次操作可能无法找到正确的位置因为之后插入元素的位置依赖与之前插入元素的位置所以对于数组的 Hash 算法需要涉及标记用于标记某个元素是否有效是否被删除某个位置是否为空加入标记后 Hash 表的 GetPutDelete 操作也变得复杂起来
实现过 Hash 表之后开始实现电话号码查询系统由于主要是查询操作所以使用 Hash 表作为数据结构非常合适在实现过 Hash 表之后以此为基本数据结构再实现系统比较容易
在比较模块我对均使用链表式 Hash 但采用不同 Hash 函数以及均使用移位 Hash 但一个采用链表式 Hash另一个采用数组线性探测的 Hash 进行了比较测试通过 ASL 发现对于自动扩容的链表式 Hash 优势较为明显当然如果要获得较好的散列性能需要选择合适的 Hash 函数
总之通过这次的数据结构课程设计让我对数据结构有了更进一步的理解自己编写算法不仅收获了知识而且收获了编写算法程序的快乐数据结构的学习应该是一个长期实践的过程之后我要继续努力学习和实践
# 九、参考文献
- 严蔚敏, 吴伟民. 数据结构C 语言版[M]. 北京:清华大学出版社, 2012.
- 陈越, 何钦铭等. 数据结构第2版[M]. 北京:高等教育出版社,2016.
- Thomas H.Cormen . Introduction to Algorithms, third edition [M]. 北京:机械
- 工业出版社, 2012. https://docs.oracle.com/javase/7/docs/api/java/util/HashMap.html [OL].