如何为不同单词分配唯一索引以实现准确的词频统计?
如何为不同单词分配唯一索引以实现准确的词频统计?
兄弟,我看你为了词频统计试了好几种野路子,先是用rand()瞎分配索引(这肯定不行啊,同一个单词每次rand出来的索引都不一样,计数全乱),后来又搞了个ASCII加减的哈希函数,结果还是踩了哈希冲突的大坑——like和keep明明只出现一次,却被算成了两次,太闹心了对吧?
先给你拆解下问题:你自己写的freq函数逻辑太简单了,不同的字符串很容易算出同一个结果(也就是哈希冲突),而且你完全没处理冲突的情况,直接把结果当数组索引用,自然会统计错误。下面给你几个实用的解决方案,从简单到进阶,按需选就行:
解决方案1:最省心的“笨办法”——直接字符串对比统计
如果你的文本规模不大(比如你当前测试的只有30多个单词),完全没必要搞什么索引,直接用字符串对比就够了,完全不会有冲突,逻辑还简单。
思路:
维护两个数组:
- 一个存已经出现过的唯一单词
- 一个存对应单词的出现次数
每拿到一个新单词,就遍历已存的单词列表: - 找到相同的单词,就把对应计数+1
- 没找到,就把这个单词加入列表,计数设为1
解决方案2:进阶版——靠谱哈希函数+冲突处理
如果以后要处理超大文本(比如几万几十万单词),字符串对比的O(n)速度就不够用了,这时候就得用哈希表,但得注意两点:
- 用专门为字符串设计的低冲突哈希函数,别自己瞎搞ASCII加减
- 必须处理哈希冲突(再好的哈希函数也会有冲突)
第一步:换个靠谱的字符串哈希函数
比如经典的DJB2哈希函数,专门为字符串优化,冲突率极低,C实现如下:
unsigned long djb2_hash(const char *str) { unsigned long hash = 5381; int c; while ((c = *str++)) { // 只保留字母,统一转小写 if (c >= 'A' && c <= 'Z') c += 32; if ((c >= 'a' && c <= 'z')) { hash = ((hash << 5) + hash) + c; // 等价于 hash * 33 + c,33是经验值,冲突率低 } } return hash; }
第二步:处理哈希冲突
就算用DJB2,也可能出现不同单词算出同一个哈希值的情况,这时候得用链地址法(最常用的冲突处理方式):
- 把哈希表的每个“桶”做成一个链表
- 计算单词的哈希值后,对桶的数量取模得到桶的索引
- 遍历这个桶的链表:找到相同单词就计数+1,没找到就新增一个节点加入链表
给你改好的代码示例(用字符串对比法,适合当前场景)
我把你的代码重构了下,解决了标点处理、大小写统一、哈希冲突的问题,结果绝对准确:
#include <stdio.h> #include <string.h> #include <ctype.h> int main() { char arr[] = "Figurines that fall like leaves then disappear, keep calling Is it real? Is it real? Dark machines that wheeze and breathe then mock the air, appalling What is real? What is real?"; int str_len = strlen(arr); // 先把整个字符串转成小写,避免大小写不同被当成不同单词 for (int d = 0; d < str_len; d++) { arr[d] = tolower(arr[d]); } // 维护唯一单词列表和对应计数 char *unique_words[50]; // 存出现过的唯一单词 int word_counts[50] = {0}; // 对应单词的出现次数 int unique_num = 0; // 已记录的唯一单词数量 // 分割单词:把空格、逗号、问号都当成分隔符,避免把"disappear,"当成独立单词 char *tok = strtok(arr, " ,?!"); while (tok != NULL) { // 跳过空串(比如连续多个分隔符的情况) if (strlen(tok) == 0) { tok = strtok(NULL, " ,?!"); continue; } // 检查当前单词是否已经存在 int is_found = 0; for (int j = 0; j < unique_num; j++) { if (strcmp(unique_words[j], tok) == 0) { word_counts[j]++; is_found = 1; break; } } // 没找到,就新增到列表里 if (!is_found) { unique_words[unique_num] = tok; word_counts[unique_num] = 1; unique_num++; } tok = strtok(NULL, " ,?!"); } // 计算总单词数 int total_words = 0; for (int j = 0; j < unique_num; j++) { total_words += word_counts[j]; } // 输出最终结果 printf("=== 词频统计结果 ===\n"); for (int j = 0; j < unique_num; j++) { double percentage = (double)word_counts[j] / total_words * 100; printf("单词: %-12s 出现次数: %d 占比: %.2f%%\n", unique_words[j], word_counts[j], percentage); } return 0; }
最后再提几个你原代码的细节坑
- 大小写处理时机错了:你原代码是先
strtok分割单词,再转小写,这会导致分割后的单词可能还带着大写字母,同一个单词的大小写版本被当成不同单词 - 分隔符没处理标点:你只用空格分割,导致
disappear,、real?这种带标点的被当成独立单词,和正常的real分开统计 freq函数的判断逻辑反了:你写的if(arr[i + 1] < 'a' && arr[i + 1] > 'z'),这条件永远为假(一个字符不可能同时小于a又大于z),应该改成if(arr[i + 1] < 'a' || arr[i + 1] > 'z')
按照上面的代码跑,你就能得到准确的统计结果,like和keep都会显示出现1次,终于正常了 😎




