什么是哈希表以及如何在 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
数据类型我typdef
ed)
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;
所以你去。我们有一个基本的链接列表,现在我们可以继续添加我们想要的所有用户,而且我们不必担心空间不足。但这确实有不利的一面。最大的问题是我们列表中的每个节点或“用户”都是“匿名的”。我们不知道他们是否有,甚至我们有多少用户。 (当然有一些方法可以做得更好——我只是想展示一个非常基本的链表)我们必须遍历整个链表才能添加用户,因为我们无法直接访问末尾。
就像我们身处一场巨大的沙尘暴中,你什么也看不见,我们需要去我们的谷仓。我们看不到我们的谷仓在哪里,但我们有一个解决方案。有人站在我们那里(我们的node
s),他们都拿着两条绳子(我们的指针)。每个人只有一根绳子,但那根绳子的另一端被其他人握着。就像我们的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 中创建它? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章