您对CS50 Speller 有一些误解。具体要求:
您的检查实现必须不区分大小写。换一种说法,
如果foo 在字典中,那么 check 应该返回 true 给定任何
其资本化; foo、foO、fOo、fOO、fOO、
Foo、FoO、FOo 和 FOO 应视为拼写错误。
这意味着当您将字典加载到哈希表中时,您必须在计算哈希之前将字典单词转换为小写。否则,当您check(word) 并将单词的副本转换为小写时,如果原始字典单词在散列之前未转换为小写,您将永远不会计算相同的散列。
您的check(word) 函数在计算哈希之前也不会转换为小写字母。这将导致您错过使用字典单词的小写形式散列的字典单词。您也出现了段错误,因为您在取消引用 tmp->next 之前未能检查 tmp 不是 NULL。但是,您在其他情况下如何检查哈希表的基础知识是正确的。
由于在散列和存储字典单词之前以及散列要检查的单词副本之前都将转换为小写,因此使用简单的 string-to-lower 函数是有意义的。然后你可以将你的 check() 函数减少为:
// string to lower
char *str2lower (char *str)
{
if (!str) return NULL;
char *p = str;
for (; *p; p++)
if (isupper((unsigned char)*p))
*p ^= ('A' ^ 'a');
return str;
}
// Returns true if word is in dictionary else false
bool check(const char *word)
{
char lcword[LENGTH+1]; /* make a copy of word from txt to convert to lc */
size_t len = strlen (word); /* get length of word */
unsigned h;
if (len > LENGTH) { /* validate word will fit */
fprintf (stderr, "error: check() '%s' exceeds LENGTH.\n", word);
return false;
}
memcpy (lcword, word, len+1); /* copy word to lower-case word */
h = hash (str2lower(lcword)); /* convert to lower-case then hash */
for (node *n = table[h]; n; n = n->next) /* now loop over list nodes */
if (strcmp (lcword, n->word) == 0) /* compare lower-case words */
return true;
return false;
}
接下来,虽然没有在问题集中讨论,但您不应该吝啬哈希表的大小。 dictionaries/large 中有 143091 字。理想情况下,您希望保持哈希表的负载因子小于0.6(不超过 60% 的存储桶被填充以最大程度地减少冲突)我尚未测试您的表的实际负载因子,但我不会不想要任何小于N == 8000
更新:我确实检查了,使用N == 131072,您的负载因子使用lh_strhash() 加载large 字典将是0.665,这已经达到了您想要的程度重新散列,但出于您的目的应该没问题。 (值得注意的是,所有额外的存储都不会将检查时间的负载改善超过百分之一秒(这表明即使处理额外的冲突,它们也相当有效)
哈希函数
您可以尝试几个,但使用/usr/share/dict/words(这是large 的来源)我发现openSSL lh_strhash() 哈希函数提供了最少的冲突次数,同时非常有效。您可以将 hash() 函数实现为包装器,并以这种方式快速尝试多种不同的哈希值,例如
// openSSL lh_strhash
uint32_t lh_strhash (const char *s)
{
uint64_t ret = 0, v;
int64_t n = 0x100;
int32_t r;
if (!s || !*s) return (ret);
for (; *s; s++) {
v = n | (*s);
n += 0x100;
r = (int32_t)((v >> 2) ^ v) & 0x0f;
ret = (ret << r) | (ret >> (32 - r));
ret &= 0xFFFFFFFFL;
ret ^= v * v;
}
return ((ret >> 16) ^ ret);
}
// Hashes word to a number
unsigned int hash (const char *word)
{
return lh_strhash (word) % N;
}
您的 load() 函数在散列之前无法转换为小写字母。您不可能在哈希表中排列和存储字典中每个单词的每个大写排列。由于您必须执行不区分大小写的check(),因此只有在散列和存储之前进行转换(转换为大写或小写——保持一致)才有意义。
此外,在将新条目插入存储桶列表之前,无需迭代到存储桶的最后一个节点。 (这是非常低效的)而是简单地使用一种名为“forward-chaining”的方法在存储桶地址处插入新节点,将那里的内容移动到->next指针,然后再将存储桶设置为地址的新节点。这给出了 O(1) 时间插入。例如:
// Loads dictionary into memory, returning true if successful else false
bool load (const char *dictionary)
{
char word[MAXC];
FILE *fp = fopen (dictionary, "r");
if (!fp) {
perror ("fopen-dictionary");
return false;
}
while (fgets (word, MAXC, fp)) {
unsigned h;
size_t len;
node *htnode = NULL;
word[(len = strcspn(word, " \r\n"))] = 0; /* trim \n or terminate at ' ' */
if (len > LENGTH) {
fprintf (stderr, "error: word '%s' exceeds LENGTH.\n", word);
continue;
}
if (!(htnode = malloc (sizeof *htnode))) {
perror ("malloc-htnode");
return false;
}
h = hash (str2lower(word));
memcpy (htnode->word, word, len+1); /* copy word to htnode->word */
htnode->next = table[h]; /* insert node at table[h] */
table[h] = htnode; /* use fowrard-chaining for list */
htsize++; /* increment table size */
}
fclose (fp);
return htsize > 0;
}
至于哈希表大小,只需向dictionary.c 添加一个全局变量,然后像上面load() 中所做的那样递增该全局变量(即htsize 变量)。这使得表格size() 的功能很简单:
// Hash table size
unsigned htsize;
...
// Returns number of words in dictionary if loaded else 0 if not yet loaded
unsigned int size (void)
{
return htsize;
}
你的unload() 有点复杂,如果table[i] 有一个节点,将无法释放分配的内存。相反,您实际上可以缩短逻辑并完成您所需要的:
// Unloads dictionary from memory, returning true if successful else false
bool unload(void)
{
for (int i = 0; i < N; i++) {
node *n = table[i];
while (n) {
node *victim = n;
n = n->next;
free (victim);
}
}
htsize = 0;
return true;
}
键的使用/区别示例
创建一个test/ 目录,然后将输出重定向到test/ 目录中的文件,您可以将结果与预期结果进行比较:
$ ./bin/speller texts/bible.txt > test/bible.txt
keys/ 目录包含“员工”代码的输出。此实现与键的输出相匹配,但也包括时间信息(这不是您可以更改的——它在speller.c 中硬编码,您无法根据练习的限制进行修改):
$ diff -uNb keys/bible.txt test/bible.txt
--- keys/bible.txt 2019-10-08 22:35:16.000000000 -0500
+++ test/bible.txt 2020-09-01 02:09:31.559728835 -0500
@@ -33446,3 +33446,9 @@
WORDS MISSPELLED: 33441
WORDS IN DICTIONARY: 143091
WORDS IN TEXT: 799460
+TIME IN load: 0.03
+TIME IN check: 0.51
+TIME IN size: 0.00
+TIME IN unload: 0.01
+TIME IN TOTAL: 0.55
+
(注意:-b 选项允许 diff 到 "ignore changes in the amount of white space",因此它将忽略行尾的变化,例如 DOS "\r\n" 与 Linux '\n' 行尾)
代码输出和keys/ 目录中的文件之间的唯一区别是在第一列(最后6 行)中标有'+' 符号的那些行显示了时序信息是唯一的区别。
内存使用/错误检查
所有内存都已正确释放:
$ valgrind ./bin/speller texts/lalaland.txt > test/lalaland.txt
==10174== Memcheck, a memory error detector
==10174== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==10174== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==10174== Command: ./bin/speller texts/lalaland.txt
==10174==
==10174==
==10174== HEAP SUMMARY:
==10174== in use at exit: 0 bytes in 0 blocks
==10174== total heap usage: 143,096 allocs, 143,096 frees, 8,026,488 bytes allocated
==10174==
==10174== All heap blocks were freed -- no leaks are possible
==10174==
==10174== For counts of detected and suppressed errors, rerun with: -v
==10174== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
检查一下,如果您还有其他问题,请告诉我。
如果您在细节上苦苦挣扎,这是使用的完整 dictionary.c,我在末尾添加了 loadfactor() 函数,因此您可以计算 N 上不同值的负载因子有兴趣:
// Implements a dictionary's functionality
#include "dictionary.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <ctype.h>
// Represents a node in a hash table
typedef struct node
{
char word[LENGTH + 1];
struct node *next;
}
node;
// Number of buckets in hash table
#define N 131072
// Max Characters Per-Line of Input
#define MAXC 1024
// Hash table
node *table[N];
// Hash table size
unsigned htsize;
// string to lower
char *str2lower (char *str)
{
if (!str) return NULL;
char *p = str;
for (; *p; p++)
if (isupper((unsigned char)*p))
*p ^= ('A' ^ 'a');
return str;
}
// Returns true if word is in dictionary else false
bool check(const char *word)
{
char lcword[LENGTH+1]; /* make a copy of word from txt to convert to lc */
size_t len = strlen (word); /* get length of word */
unsigned h;
if (len > LENGTH) { /* validate word will fit */
fprintf (stderr, "error: check() '%s' exceeds LENGTH.\n", word);
return false;
}
memcpy (lcword, word, len+1); /* copy word to lower-case word */
h = hash (str2lower(lcword)); /* convert to lower-case then hash */
for (node *n = table[h]; n; n = n->next) /* now loop over list nodes */
if (strcmp (lcword, n->word) == 0) /* compare lower-case words */
return true;
return false;
}
// openSSL lh_strhash
uint32_t lh_strhash (const char *s)
{
uint64_t ret = 0, v;
int64_t n = 0x100;
int32_t r;
if (!s || !*s) return (ret);
for (; *s; s++) {
v = n | (*s);
n += 0x100;
r = (int32_t)((v >> 2) ^ v) & 0x0f;
ret = (ret << r) | (ret >> (32 - r));
ret &= 0xFFFFFFFFL;
ret ^= v * v;
}
return ((ret >> 16) ^ ret);
}
// Hashes word to a number
unsigned int hash (const char *word)
{
return lh_strhash (word) % N;
}
// Loads dictionary into memory, returning true if successful else false
bool load (const char *dictionary)
{
char word[MAXC];
FILE *fp = fopen (dictionary, "r");
if (!fp) {
perror ("fopen-dictionary");
return false;
}
while (fgets (word, MAXC, fp)) {
unsigned h;
size_t len;
node *htnode = NULL;
word[(len = strcspn(word, " \r\n"))] = 0; /* trim \n or terminate at ' ' */
if (len > LENGTH) {
fprintf (stderr, "error: word '%s' exceeds LENGTH.\n", word);
continue;
}
if (!(htnode = malloc (sizeof *htnode))) {
perror ("malloc-htnode");
return false;
}
h = hash (str2lower(word));
memcpy (htnode->word, word, len+1); /* copy word to htnode->word */
htnode->next = table[h]; /* insert node at table[h] */
table[h] = htnode; /* use fowrard-chaining for list */
htsize++; /* increment table size */
}
fclose (fp);
return htsize > 0;
}
// Returns number of words in dictionary if loaded else 0 if not yet loaded
unsigned int size (void)
{
return htsize;
}
// Unloads dictionary from memory, returning true if successful else false
bool unload(void)
{
for (int i = 0; i < N; i++) {
node *n = table[i];
while (n) {
node *victim = n;
n = n->next;
free (victim);
}
}
htsize = 0;
return true;
}
float loadfactor (void)
{
unsigned filled = 0;
for (int i = 0; i < N; i++)
if (table[i])
filled++;
return (float)filled / N;
}