hash统计树 composed by: YHL

目录

实验目的及内容

解题思路

实验代码及注释

输入输出说明及结果截图

心得体会

实验目的及内容

卫星成像技术可以识别每一棵树的种类,输入观测到的每树的英文名称,请编写程序输出每种树的数量和所占比例。(存储采用二叉排序树,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; // 对其取模得到哈希值(在哈希表中的索引)
}
处理哈希冲突(采用链地址法,简单粗暴)
与开放地址相比链地址法有如下优点:
1,链地址法处理冲突 简单,且无堆积现象,即非同义词决不会发生冲突,因此 平均查找长度较短。
2,链地址法中链表的结点是动态申请的,故它 更适合造表前无法确定表长的情况
3,链地址法构造的散列表删除结点很方便,只需简单的删去链表上相应的结点即可。
哈希表的扩容: realloc()函数(略)

原型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返回新内存的地址

传递给realloc的指针必须是先前通过malloc(), calloc(), 或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信息摘要算法就是一种几乎不会产生冲突的哈希函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值