C语言字典实现:check函数拼写检测异常及性能优化咨询
嘿,我来帮你搞清楚这两个问题,都是C语言字符串和哈希表优化里的常见坑!
tmp[n] = '\0'?垃圾值真的有影响! 首先得回忆C语言里字符串的本质:C语言没有原生的字符串类型,所谓的字符串就是一段字符数组,必须以\0(空字符)作为结束标记。所有字符串操作函数(比如strlen、strcasecmp,还有你用的hash函数里的循环)都是靠这个\0来判断字符串什么时候结束的。
你原来的代码里:
char tmp[strlen(word)]; int n = strlen(word);
这里strlen(word)返回的是单词里实际字符的数量,不包括结尾的\0。所以你声明的tmp数组大小刚好只能装下所有字符,没有多余的空间放\0。然后你循环把每个字符转成小写存到tmp[0]到tmp[n-1],这时候tmp数组的最后一个元素是tmp[n-1],后面的内存是未初始化的垃圾值。
如果不加tmp[n] = '\0',当你把tmp传给hash函数或者其他字符串函数时,这些函数会从tmp[0]开始读,一直读到内存里某个随机出现的\0为止——这就意味着它会把tmp后面的垃圾值当成字符串的一部分!
举个例子:假设word是"Cat",strlen(word)是3,tmp数组大小是3,存了'c'、'a'、't'。如果后面的垃圾值是'x'、'\0',那hash函数会把"catx"当成输入来计算哈希值,这和字典里"cat"的哈希值完全不一样,自然找不到正确的节点,统计错误拼写的数量就乱了。
另外还要提醒你:你现在写的tmp[n] = '\0'其实是数组越界了,因为tmp的大小是n,索引范围是0到n-1,tmp[n]已经超出了数组的边界,属于未定义行为(只是刚好在你的环境里内存后面的位置可写,没出问题)。正确的写法应该是把tmp的大小声明为strlen(word)+1,这样才有空间放\0:
int n = strlen(word); char tmp[n + 1]; // 多留一个位置给'\0' for (int i = 0; i < n; i++) { tmp[i] = tolower(word[i]); } tmp[n] = '\0';
check函数的运行速度? 你的check函数现在的流程是:转小写→哈希→遍历链表比较,我们可以从这几个环节入手优化:
1. 去掉临时数组tmp,让哈希函数直接忽略大小写
现在你要先把单词转成小写存到tmp,再传给hash函数,这一步既占内存又耗时间。可以修改hash函数,让它在计算哈希值的时候自动把字符转成小写,这样直接传原word给hash就行,省掉创建tmp的步骤:
unsigned int hash(const char *word) { unsigned int hashValue = 0; for (int count = 0; word[count] != '\0'; count++) { char c = tolower(word[count]); // 这里直接转小写 hashValue = c + (hashValue << 6) + (hashValue << 16) - hashValue; } return (hashValue % N); }
修改后check函数可以简化成:
bool check(const char *word) { int index = hash(word); node *head = table[index]; int word_len = strlen(word); // 只计算一次长度 while (head != NULL) { // 先比较长度,长度不一样直接跳过,不用调用strcasecmp if (strlen(head->word) != word_len) { head = head->next; continue; } if (strcasecmp(head->word, word) == 0) { return true; } head = head->next; } return false; }
2. 优化哈希函数,减少链表冲突
哈希表的性能很大程度取决于哈希函数的冲突率——冲突越少,每个桶里的链表就越短,遍历的时间就越少。你现在用的哈希函数虽然能用,但可以试试更经典的字符串哈希函数,比如djb2:
unsigned int hash(const char *word) { unsigned long hash = 5381; int c; while ((c = tolower(*word++)) != '\0') { hash = ((hash << 5) + hash) + c; // hash * 33 + c } return hash % N; }
这个函数的冲突率通常比你现在的更低,能有效缩短链表长度。另外,你设置的桶数量N=11230尽量用质数,也能减少冲突。
3. 提前存储单词长度,避免重复计算strlen
strlen是O(n)的操作,每次遍历链表都调用strlen(head->word)会浪费时间。可以修改node结构体,在加载字典的时候就把单词长度存进去:
typedef struct node { char word[LENGTH + 1]; int length; // 新增:存储单词长度 struct node *next; } node;
然后在load函数里:
strcpy(newnode->word, buffer); newnode->length = strlen(buffer); // 存储长度 newnode->next = NULL;
这样check函数里可以直接比较长度,不用再调用strlen:
bool check(const char *word) { int word_len = strlen(word); int index = hash(word); node *head = table[index]; while (head != NULL) { if (head->length != word_len) { head = head->next; continue; } if (strcasecmp(head->word, word) == 0) { return true; } head = head->next; } return false; }
这能节省不少重复计算长度的时间,尤其是长单词多的时候。
4. 进阶:把链表改成更高效的数据结构
如果你的课程允许,还可以把每个桶里的链表改成平衡二叉搜索树(比如红黑树)或者跳表,这样查找的时间复杂度从O(k)(k是链表长度)降到O(logk),在字典很大的时候提升非常明显。不过这会增加代码复杂度,适合追求极致性能的时候用。
内容的提问来源于stack exchange,提问作者rndnewbiee22

