目录
实验目的及内容
卫星成像技术可以识别每一棵树的种类,输入观测到的每树的英文名称,请编写程序输出每种树的数量和所占比例。(存储采用二叉排序树,AVL树或建立哈希表结构)
解题思路
这种 根据 key(树种名称)寻找 value(树种的比例和数量)的问题,非常适合用哈希表结构来完成;
首先,可以基于每一棵树的英文名称来计算哈希表中的key值,C语言使用char[]数组来存储英文字符串,并在结尾自动添加‘ \0 ’字符。
哈希函数的构建:除留取余法
用关键字k除以某个不大于哈希表长度m的整数p所得的余数作为哈希地址,即 h(k)= k mod p(mod 为求余运算,p≤m) ,这种方法的关键是选好 p, p 取奇数比取偶数好,最好是能够去最接近表长的质数!(或者不含小于20的质因子的合数)
因此,我选取了97作为除数,同时加入一个技巧:
// 哈希函数:根据键值计算哈希值
int hashFunction(const char *key)
{
int hashValue = 0;
for (int i = 0; key[i] != '\0'; i++)
{
hashValue += i*i*(int)key[i]; // 将每个字符的ASCII码值乘以其在串中的位置后相加
}
return (hashValue) % 97; // 对其取模得到哈希值(在哈希表中的索引)
}
原型:extern void *realloc(void *mem_address, unsigned int newsize);
语法:指针名=(数据类型*)realloc(要改变内存大小的指针名,新的大小)。
//新的大小若小于原来的大小,原数据的末尾可能丢失(被其他使用内存的数据覆盖等)
头文件:#include <stdlib.h> 有些编译器需要#include <malloc.h>,在TC2.0中可以使用alloc.h头文件
功能:先判断当前的指针是否有足够的连续空间,如果有,扩大mem_address指向的地址,并且将mem_address返回,如果空间不够,先按照newsize指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来mem_address所指内存区域(注意:原来指针是自动释放,不需要使用free),同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。
返回值:如果重新分配成功则返回指向被分配内存的指针,否则返回空指针NULL。
注意:这里原始内存中数据还是保持不变的。当内存不再使用时,应使用free()函数将内存块释放。原来的内存不改变,不会释放也不会移动,(所以使用的时候应该保留原指针,避免分配失败产生内存泄漏)
假如原来的内存后面还有足够多剩余内存的话,realloc的内存=原来的内存+剩余内存,realloc还是返回原来内存的地址; 假如原来的内存后面没有足够多剩余内存的话,realloc将申请新的内存,然后把原来的内存数据拷贝到新内存里,原来的内存将被自动free掉,realloc返回新内存的地址
实验代码及注释
时间复杂度为: O(n+e), n 为所有树的总数,e为解决哈希冲突所用的链节点指针数量,即查找每一个单独树种的时间开销为O(1+e);
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <string.h>
#define TABLE_SIZE 101
// 全局,统计哈希表的元素个数
int hashelem = 0;
// 统计哈希表装填因子,内联函数,减少频繁调用造成的额外时间开销
inline float Getloadfactor()
{
return hashelem / TABLE_SIZE;
}
// 哈希表节点结构体定义
typedef struct HashNode
{
char name[100]; // 存储树的名称
int count; // 存储树的数量
struct HashNode *next; // 链表指针,用于解决哈希冲突
} hnode;
// 哈希函数:根据键值计算哈希值
int hashFunction(const char *key)
{
int hashValue = 0;
// 先对key进行一波处理:将每个字符的ASCII码值乘以其在串中的位置的平方后,相加
for (int i = 0; key[i] != '\0'; i++)
{
hashValue += i * i * (int)key[i];
}
return (hashValue) % 97; // 对处理后的值取模得到哈希值(在哈希表中的索引)
}
// 向哈希表中插入树的信息,要注意哈希冲突
void insertTree(hnode *hashMap, const char *treeName)
{
float fac = Getloadfactor();
if (fac > 0.6)
{ /*为哈希表扩容*/
hashMap = (hnode *)realloc(hashMap, 2 * TABLE_SIZE * sizeof(hnode));
};
int hashValue = hashFunction(treeName); // 计算树的哈希值
hnode *current = &hashMap[hashValue]; // 指向哈希表中的相应位置
// 若映射到哈希表的对应位置为空,则直接添加
if (current->count == 0)
{
strcpy(current->name, treeName); // 将树的名称复制到哈希表中
current->count = 1; // 树的数量设为1
hashelem++; // 哈希表的元素个数加一
}
// 映射到的哈希表中的相应位置不空,则应该比较具体的key值(字符串值)
else
{
int flag = strcmp(treeName, current->name); // 检测是否是一样的字符串
// 若字符串完全匹配,则相应的树的数量+1
if (flag == 0)
{
current->count++;
}
// 不同则继续遍历链表直到末尾
else
{
while (current->next != NULL)
{
current = current->next; // 链表指针后移
flag = strcmp(treeName, current->name); // 比较key值(字符串),检测哈希冲突
// 如果扫描到一个节点发现key值匹配,则把对应树的数量加一
if (flag == 0)
{
current->count++;
}
}
// 扫完所有的链表结点,都不同,这说明发生了新的哈希冲突,创建新节点并添加到链表末尾
if (flag != 0)
{
hnode *newTree = (hnode *)malloc(sizeof(hnode));
strcpy(newTree->name, treeName); // 拷贝字符串
newTree->count = 1;
newTree->next = NULL;
current->next = newTree;
}
}
}
}
// 检查输入是否为结束标记
int isEndMarker(const char *input, const char *endMarker)
{
return strcmp(input, endMarker) == 0; // 比较输入和结束标记是否相同
}
/*主函数*/
int main()
{
hnode hashMap[TABLE_SIZE] = {0}; // 创建哈希表,初始化为空
char treeName[100]; // 存储输入的树种名称
const char *endMarker = "END"; // 结束标记,输入为该标记时程序停止
const char *msg1 = "该树在哈希表中的索引为:"; // 提示1
printf("输入每棵树的英文:\n");
while (scanf("%s", treeName) == 1)
{
if (isEndMarker(treeName, endMarker))
{
break; // 如果输入为结束标记,退出循环
}
insertTree(hashMap, treeName); // 将对应名称的树种插入哈希表
}
int totalCount = 0; // 所有树的数量
for (register int i = 0; i < TABLE_SIZE; i++)
{
hnode *current = &hashMap[i];
// 遍历链表输出树种信息并计算总数量
while (current != NULL)
{
if (current->count > 0)
{
totalCount += current->count; // 更新总体树的数量
}
current = current->next; // 移动到链表的下一个节点
}
}
// 输出总体树的数量
printf("\n******观测到所有树的总数量为:%d******\n\n", totalCount);
// 输出树种名称、数量和总体占比
// 个人认为格式化输出数字时, C++ 的cout没有printf好用,printf代码漂亮一些
for (register int i = 0; i < TABLE_SIZE; i++)
{
hnode *current = &hashMap[i];
if (current->count > 0)
{
while (current != NULL)
{
printf("树种名称:");
printf("%*s", 8, current->name); // 左对齐输出名称
printf(",数量:%3d,", current->count); // 左对齐输出数量
float proportion = (float)current->count / totalCount * 100.0; // 计算树种占比,转换为百分比形式
printf("所占比例为:%4.2f%%", proportion); // 输出占比,保留百分号后两位
printf(" %*s", 20, msg1); // 左对齐输出
printf("%2d\n", i);
current = current->next; // 移动到链表的下一个节点
}
}
}
return 0;
}
输入输出说明及结果截图
输入:模拟卫星成像图输入每棵树的英文名称,其实就是这些树的英文名所组成的矩阵
输出:每种树的数量和所占比例
(8*8)
由于哈希函数构造的不错,所以很难产生哈希冲突,比如遇到:koa & oak,如果单纯把字符串的字符值相加,那肯定会产生哈希冲突,但是我改进后的函数就不会↓:
接下来用更多真正的英文树名看一下实际应用的效果如何,容不容易产生哈希冲突:
不妨用一个由15种不同的树的英文名组成的15*15的矩阵做测试样例,要求这些英文名随机分布在矩阵中。
先用Python生成定义一个包含15种不同树的英文名的列tree_names
。然后通过两个嵌套的循环生成一个15x15的矩阵,并在每个位置随机选择一个树的英文名填充。最后,使用循环打印出生成的矩阵。
然后把这个矩阵用作测试样例,结果如下:
由最右边这一列可以看出,这个样例没有产生哈希冲突,并且分散的蛮不错,说明这个哈希函数对真实的英文树名还是OK的
心得体会
哈希表是一个非常高效的查找结构,本次采用除留余数法作为哈希函数,这种方法的关键是选好除数p, 例如, p 取奇数比取偶数好,最好是能够去最接近表长的质数!(或者不含小于20的质因子的合数),同时,在取余数之前,完全可以灵活地对已知的key值做一些有利于避免冲突的操作。本次实验中,这种方法应用于真实的英文树名表现良好,不太容易产生哈希冲突。所以对于哈希表来说,哈希函数的选取比处理哈希冲突的方法要重要,例如MD5信息摘要算法就是一种几乎不会产生冲突的哈希函数