什么是哈希表以及如何在 C 中创建它? [关闭]

Posted

技术标签:

【中文标题】什么是哈希表以及如何在 C 中创建它? [关闭]【英文标题】:What is a hash table and how do you make it in C? [closed] 【发布时间】:2015-11-02 23:40:42 【问题描述】:

我有几个关于称为哈希表(也称为关联数组)的数据结构以及它是如何在 C 中实现的问题。

如何在 C 中制作哈希表? 什么是哈希表以及如何实现它? 为什么我要使用哈希表而不是数组?

注意: 我知道这是一个非常广泛的问题,需要一个很大的答案,但是,我这样做是因为有人问我这一切是什么。所以我把它放在这里是为了充分解释它并帮助其他人。

【问题讨论】:

@doublesharp 我有一些朋友想知道它是什么,我想在这里发布它,以便将来对其他人有所帮助 是的,很抱歉这个问题很笼统。它最初是一个问题,询问如何实现哈希表以在 C 中存储名称,但是当我开始编写答案时,我想更彻底地解释它,结果有点变成了这个 【参考方案1】:

先决条件

对于这个答案,我假设您知道如何使用指针、结构,并且对 C 语言有基本的了解。

如果你不知道。在谈论算法和数据结构的速度时,您应该知道这些术语:

O() =(读作“Big-oh”)Big-oh 或 O() 指的是“最坏情况”运行时。同样,在数学中,它是大 O 表示法,描述了函数的限制行为。如果某些东西 O(1) 那是恒定的时间“真的很好”。如果某些东西是 O(n),这意味着列表是否有一百万长。最坏的情况是运行一百万次。 O() 通常用于确定某事物运行的速度,因为这是在最坏情况下运行的速度。

Ω =(希腊字母 Omega)指的是最好的情况。它的使用不如 O() 多,所以我不会详细介绍它。但只要知道,如果是 Ω(1),在最好的情况下,它只需要一次。

Θ = (希腊字母 theta) 的独特之处在于它仅在 O() 和 Ω() 运行时相同时使用。就像在递归排序算法merge sort 的情况下一样。它的运行时间是 Θ(n(log(n)))。这意味着它是 O(n(log(n))) 并且是 Ω(n(log(n)))。

什么是哈希表?

哈希表或关联数组是编程中常用的数据结构。哈希表只是一个带有哈希函数的链表(稍后我会谈到链表是什么)。哈希函数基本上只是将事物放入不同的“篮子”中。每个“篮子”只是另一个链接列表或其他东西,具体取决于您如何实现它。当我向您展示如何实现哈希表时,我将解释有关哈希表的更多细节。

为什么要使用哈希表而不是数组?

数组非常易于使用且制作简单,但它也有其缺点。对于这个例子,假设我们有一个程序,我们希望将它的所有用户保存在一个数组中。

这很简单。假设我们计划让这个程序不超过 100 个用户并用我们的用户填充该数组

char* users[100];

// iterate over every user and "store" their name
for (int i = 0; i < userCount; i++)

    users[i] = "New username here";

所以这一切都很好,很好,也很快。那是 O(1) 就在那里。我们可以在恒定时间内访问任何用户。

但是现在让我们假设我们的程序非常受欢迎。它现在有 80 多个用户。哦哦!我们最好增加该数组的大小,否则我们将获得缓冲区溢出。

那么我们该怎么做呢?好吧,我们将不得不创建一个更大的新数组,并将旧数组的内容复制到新数组中。

这是非常昂贵的,我们不想这样做。我们想聪明地思考,而不是使用固定大小的东西。好吧,我们已经知道如何使用指向我们优势的指针,如果我们愿意,我们可以将信息捆绑到 struct 中。

所以我们可以创建一个struct 来存储用户名,然后让它(通过指针)指向一个新的struct!我们现在有一个可扩展的数据结构。它是通过指针链接在一起的捆绑信息列表。因而得名链表。

链接列表

让我们创建那个链表。首先我们需要一个struct

typedef struct node

    char* name;
    struct node* next;

node;

好的,所以我们有一个字符串name 和...等一下...我从未听说过称为struct node 的数据类型。好吧,为了我们的方便,我typedef 一个新的“数据类型”称为node,它也恰好是我们的struct,称为node

现在我们已经有了列表的节点,接下来我们需要什么?好吧,我们需要为我们的列表创建一个“根”,以便我们可以traverse 它(稍后我将解释traverse 的意思)。所以让我们分配一个根。 (记住之前node数据类型我typdefed)

node* first = NULL;

所以现在我们有了根目录,我们需要做的就是创建一个函数来将新用户名插入到我们的列表中。

/*
 * inserts a name called buffer into
 * our linked list
 */
void insert(char* buffer)
     
    // try to instantiate node for number
    node* newptr = malloc(sizeof(node));
    if (newptr == NULL)
    
        return;
    

    // make a new ponter
    newptr->name = buffer;
    newptr->next = NULL;

    // check for empty list
    if (first == NULL)
    
        first = newptr;
    
    // check for insertion at tail
    else
    
        // keep track of the previous spot in list
        node* predptr = first;

        // because we don't know how long this list is
        // we must induce a forever loop until we find the end
        while (true)
        
            // check if it is the end of the list
            if (predptr->next == NULL)
            
                // add new node to end of list
                predptr->next = newptr;

                // break out of forever loop
                break;
            

            // update pointer
            predptr = predptr->next;
        
             

所以你去。我们有一个基本的链接列表,现在我们可以继续添加我们想要的所有用户,而且我们不必担心空间不足。但这确实有不利的一面。最大的问题是我们列表中的每个节点或“用户”都是“匿名的”。我们不知道他们是否有,甚至我们有多少用户。 (当然有一些方法可以做得更好——我只是想展示一个非常基本的链表)我们必须遍历整个链表才能添加用户,因为我们无法直接访问末尾。

就像我们身处一场巨大的沙尘暴中,你什么也看不见,我们需要去我们的谷仓。我们看不到我们的谷仓在哪里,但我们有一个解决方案。有人站在我们那里(我们的nodes),他们都拿着两条绳子(我们的指针)。每个人只有一根绳子,但那根绳子的另一端被其他人握着。就像我们的struct 一样,绳索充当指向它们所在位置的指针。那么我们怎么去我们的谷仓呢? (对于这个例子,谷仓是列表中的最后一个“人”)。好吧,我们不知道我们的人员有多大,也不知道他们去了哪里。事实上,我们所看到的只是一根系着绳子的栅栏柱。 (我们的根!)栅栏柱永远不会改变,所以我们可以抓住柱子并开始移动,直到我们看到我们的第一个人。那个人拿着两条绳子(帖子的指针和他们的指针)。

所以我们一直沿着绳索前进,直到遇到一个新人并抓住他们的绳索。最终,我们走到了尽头,找到了我们的谷仓!

简而言之,这就是一个链表。它的好处是它可以随心所欲地扩展,但它的运行时间取决于列表的大小,即 O(n)。因此,如果有 100 万用户,则必须运行 100 万次才能插入新名称!哇,插入 1 个名字似乎真的很浪费。

幸运的是,我们很聪明,可以创建更好的解决方案。我们为什么不拥有几个链表,而不是只有一个链表。一个链表数组,如果你愿意的话。我们为什么不创建一个大小为 26 的数组。这样我们就可以为字母表中的每个字母创建一个唯一的链表。现在代替 n 的运行时间。我们可以合理地说,我们的新运行时间将是 n/26。现在,如果您有一个 100 万大的列表,那根本不会有太大的不同。但是对于这个例子,我们只是保持简单。

所以我们有一个链表数组,但是我们要如何将我们的用户排序到数组中。嗯……我们为什么不做一个函数来决定哪个用户应该去哪里。如果您将进入数组或“表”,此函数将“散列”用户。所以让我们创建这个“散列”链表。因此名称哈希表

哈希表

正如我刚才所说,我们的哈希表将是一个链表数组,并将按用户名的第一个字母进行哈希处理。 A 会转到位置 0,B 会转到位置 1,以此类推。

此哈希表的struct 将与我们之前的链表的结构相同

typedef struct node

    char* name;
    struct node* next;

node;

现在就像我们的链表一样,我们需要一个哈希表的根

node* first[26] = NULL;

根将是一个字母大小的数组,其中的所有位置都将初始化为NULL。 (记住:链表中的最后一个元素必须指向NULL,否则我们不会知道它是结束)

让我们做一个主函数。它需要一个我们要散列然后插入的用户名。

int main(char* name)

    // hash the name into a spot
    int hashedValue = hash(name);

    // insert the name in table with hashed value
    insert(hashedValue, name);

这是我们的哈希函数。这很简单。我们要做的就是查看单词中的第一个字母,并根据它的字母给出一个 0 到 25 之间的值

/*
 * takes a string and hashes it into the correct bucket
 */
int hash(const char* buffer)

    // assign a number to the first char of buffer from 0-25
    return tolower(buffer[0]) - 'a';

所以现在我们只需要创建我们的插入函数。它看起来就像我们之前的插入函数,除了每次我们引用我们的根时,我们都会将它作为一个数组来引用。

/*
 * takes a string and inserts it into a linked list at a part of the hash table
 */
void insert(int key, const char* buffer)

    // try to instantiate node to insert word
    node* newptr = malloc(sizeof(node));
    if (newptr == NULL)
    
        return;
    

    // make a new pointer
    strcpy(newptr->name, buffer);
    newptr->next = NULL;

    // check for empty list
    if (first[key] == NULL)
    
       first[key] = newptr;
    
    // check for insertion at tail
    else
    
        node* predptr = first[key];
        while (true)
        
            // insert at tail
            if (predptr->next == NULL)
            
                predptr->next = newptr;
                break;
            

            // update pointer
            predptr = predptr->next;
        
    

这就是哈希表的基础知识。如果您知道如何使用指针和结构,那就很简单了。我知道这是一个非常简单的哈希表示例,它只有一个插入函数,但你可以让它变得更好,并通过你的哈希函数变得更有创意。您还可以根据需要使数组变大,甚至可以使用多维数组。

【讨论】:

执行 strcpy 时会崩溃,因为您没有为字符串分配内存。 “好吧,我们将不得不创建一个更大的新数组,并将旧数组的内容复制到新数组中”这有点误导。使用realloc 的动态数组被广泛使用。

以上是关于什么是哈希表以及如何在 C 中创建它? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

在哈希表中创建字符串的哈希值的时间复杂度

你将如何用语言 x 实现哈希表? [关闭]

hash表以及处理冲突的方法

从文件加载 sql 并在 oracle 中创建它的视图

哈希表基础知识

超高性能 C/C++ 哈希映射(表、字典)[关闭]