拼写错误的单词期间出现CS50拼写器细分错误问题

时间:2020-08-31 22:47:49

标签: c segmentation-fault cs50

我的代码在某处导致分段错误。我不确定如何。我不认为这是负载问题,因为程序在突然停止并给我段错误错误之前开始列出拼写错误的单词。

// Implements a dictionary's functionality

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "dictionary.h"
#define HASHTABLE_SIZE 80000
unsigned int count = 0;

// Represents a node in a hash table
typedef struct node
{
    char word[LENGTH + 1];
    struct node *next;
}
node;

// Number of buckets in hash table
const unsigned int N = HASHTABLE_SIZE;

// Hash table
node *table[N];

// Returns true if word is in dictionary else false
bool check(const char *word)
{
    node *tmp = NULL;
    int ch = hash(word);
    int len = strlen(word);
  char w[len+1];
  
  for(int i = 0; i<len; i++)
  {
      w[i] = tolower(word[i]);
  }
  w[len] = '\0';
        tmp = table[ch];
        while(tmp->next != NULL)
        {
            if(strcmp(tmp->word, w) == 0)
            {
                return true;
            }
            if(tmp->next != NULL)
            {
             tmp = tmp->next;   
            }
            
        }
        return false;
  }



// Hashes word to a number
unsigned int hash(const char *word)
{
    int len = strlen(word);
    char key[len+1];
    for(int p = 0; p < len; p++)
    {
        key[p] = tolower(word[p]);
    }
    key[len] = '\0';
    
    unsigned int hash = 0;
    for (int i = 0, n = strlen(key); i < n; i++)
        hash = (hash << 2) ^ key[i];

    return hash % HASHTABLE_SIZE;
}

// Loads dictionary into memory, returning true if successful else false
bool load(const char *dictionary)
{
    
    FILE *file = fopen(dictionary, "r");
    if(file == NULL)
    {
        printf("could not open file.\n");
        fclose(file);
        return false;
    }
    char temp[LENGTH + 1];
    while(fscanf(file, "%s", temp) != EOF)
    {
        node *tmp = malloc(sizeof(node));
        strcpy(tmp->word, temp);
        unsigned int code = hash(temp);
        count++;
        if(table[code] == NULL)
        {
            table[code] = tmp;
        }
        else if(table[code] != NULL)
        {
            
            node *pointer = table[code];
            while(pointer->next != NULL)
            {
              tmp->next = table[code];
              table[code] = tmp;
            }
                //YOU ARE HERE
            
        }
    }
    
    return true;
}

// Returns number of words in dictionary if loaded else 0 if not yet loaded
unsigned int size(void)
{
  
    node* tmp = NULL;
    for(int i=0; i< N; i++ )
    {
        if(table[i]!=NULL)
        {
            tmp = table[i];
            while(tmp->next != NULL)
            {
                tmp = tmp->next;
                count++;
            }
        }
    }
   
    return count;
}

// Unloads dictionary from memory, returning true if successful else false
bool unload(void)
{
    node *tmp = NULL;
    node *del;
    for(int i = 0; i < N; i++)
    {
        tmp = table[i];
        while(tmp->next != NULL)
        {
            del = tmp;
            if(tmp->next != NULL)
            {
             tmp = tmp->next; 
            }
            free(del);
        }
        return true;
    }
    return false;
}

运行该程序时,我收到以下消息:

~/pset5/speller/ $ ./speller dictionaries/large keys/her.txt

MISSPELLED WORDS

MISSPELLED
WORDS
Jonze
INT
Segmentation fault

因此,似乎正确加载了词典和文本。

1 个答案:

答案 0 :(得分:0)

您对CS50 Speller有一些误解。具体来说,要求:

您的check实现必须不区分大小写。换一种说法, 如果foo在字典中,则给定任何条件,检查应返回true 大写; foofoOfOofOOfOOFooFoOFOoFOO应该被认为是拼写错误的。

这意味着当您将字典加载到哈希表中时,必须在计算哈希值之前将字典单词转换为小写。否则,当您check(word)并将单词的副本转换为小写字母时,如果原始字典单词在哈希之前没有转换为小写字母,则永远不会计算相同的哈希值。

您的check(word)函数在计算哈希值之前也不会转换为小写。这将导致您错过用字典词的小写形式进行哈希处理的字典词。您也进行段错误,因为在取消引用tmp之前无法检查NULL是否不是tmp->next。但是,您对正确地检查哈希表的基础了解正确。

由于在散列和存储字典单词之前以及在散列要检查的单词副本之前都将转换为小写,因此使用简单的字符串到小写的函数是有意义的。然后,您可以将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;
}

接下来,尽管未在问题集中进行讨论,但您不应跳过哈希表的大小。 143091中有dictionaries/large个单词。理想情况下,您希望哈希表的负载率保持小于0.6(不超过60%的存储桶被填充,以最大程度地减少冲突)我没有测试过表的实际负载率,但是我不会想要少于N == 8000

更新:我确实进行了检查,并使用N == 131072,使用large加载lh_strhash()字典的负载因子为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(),因此只有在进行散列和存储之前进行转换(转换为上限或下限以保持一致)才有意义。

此外,在将该存储桶的列表中插入新条目之前,无需迭代该存储桶的最后一个节点。 (这是非常低效的),而是简单地使用一种名为“ 正向链接”的方法在存储桶地址处将新节点移动到->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/目录包含“ staff”代码的输出。此实现与键的输出匹配,但还包括定时信息(您不能更改此信息-它在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()函数,因此您可以计算{{ 1}},如果您有兴趣:

N