• 【C语言】·算法学习·哈希算法全解


     

    目录

    C中的哈希

    它能做什么?

    快吗?

    是图书馆吗?

    C/C++ 和平台

    BSD 许可

    下载 uthash

    获得帮助

    贡献

    包括的额外内容

    历史

    可以直接从此处开始阅读

    哈希结构

    钥匙

    哈希句柄

    关于记忆的一句话

    哈希运算

    声明哈希

    添加项目

    更换项目

    查找项目

    删除项目

    计数项目

    迭代和排序

    一个完整的例子

    标准键类型

    整数键

    字符串键

    指针键

    结构键

    高级主题

    复合键

    多级哈希表

    多个哈希表中的项目

    具有多个键的项目

    新项目的排序插入

    几个排序顺序

    布隆过滤器(更快的未命中)

    选择

    指定备用键比较函数

    内置哈希函数

    哈希扫描

    扩展内件

    挂钩

    调试模式

    线程安全

    宏参考

    便利宏

    通用宏


    C中的哈希

    本文档是为 C 程序员编写的。由于您正在阅读本文,您很可能知道哈希用于使用键查找项目。在脚本语言中,哈希或“字典”一直在使用。在 C 语言中,哈希值不存在于语言本身中。该软件为 C 结构提供了一个哈希表。

    它能做什么?

    该软件支持对哈希表中的项目进行以下操作:

    1. 添加/替换

    2. 寻找

    3. 删除

    4. 数数

    5. 迭代

    6. 种类

    快吗?

    添加、查找和删除通常是恒定时间操作。这受您的关键域和散列函数的影响。

    此哈希旨在简约和高效。它大约有 1000 行 C。它自动内联,因为它是作为宏实现的。只要哈希函数适合您的密钥,它就会很快。您可以使用默认散列函数,或轻松比较性能并从其他几个 内置散列函数中进行选择。

    是图书馆吗?

    不,它只是一个头文件:uthash.h. 您需要做的就是将头文件复制到您的项目中,并且:

    #include "uthash.h"

    由于 uthash 只是一个头文件,因此没有可链接的库代码。

    C/C++ 和平台

    该软件可用于 C 和 C++ 程序。它已经过测试:

    • Linux

    • 使用 Visual Studio 2008 和 2010 的 Windows

    • 索拉里斯

    • OpenBSD

    • 自由BSD

    • 安卓

    测试套件

    要运行测试套件,请输入tests目录。然后,

    • 在 Unix 平台上,运行make

    • 在 Windows 上,运行“do_tests_win32.cmd”批处理文件。(如果您的 Visual Studio 安装在非标准位置,您可以编辑批处理文件)。

    BSD 许可

    该软件是在 修订后的 BSD 许可下提供的。它是免费和开源的。

    下载 uthash

    按照GitHub - troydhanson/uthash: C macros for hash tables and more上的链接克隆 uthash 或获取 zip 文件。

    获得帮助

    请使用uthash Google Group提问。您可以通过utash@googlegroups.com发送电子邮件。

    贡献

    您可以通过 GitHub 提交拉取请求。然而,uthash 的维护者重视保持不变,而不是添加花里胡哨。

    包括的额外内容

    uthash 附带了三个“附加功能”。这些提供列表、动态数组和字符串:

    历史

    为了我自己的目的,我在 2004-2006 年写了 utash。最初它托管在 SourceForge 上。Uthash 在 2006-2013 年间被下载了大约 30,000 次,然后转移到 GitHub。它已被纳入商业软件、学术研究和其他开源软件。它还被添加到许多 Unix-y 发行版的本机软件包存储库中。

    编写 utash 时,用 C 语言编写通用哈希表的选项比现在少。现在有更快的哈希表,内存效率更高的哈希表,以及非常不同的 API。但是,就像驾驶小型货车一样,uthash 很方便,并且可以为多种目的完成工作。

    可以直接从此处开始阅读

    哈希结构

    在 utash 中,哈希表由结构组成。每个结构代表一个键值关联。一个或多个结构字段构成密钥。结构指针本身就是值。

    定义可以散列的结构
    1. #include "uthash.h"
    2. struct my_struct {
    3. int id; /* key */
    4. char name[10];
    5. UT_hash_handle hh; /* makes this structure hashable */
    6. };

    请注意,在 uthash 中,当您将其添加到哈希表时,您的结构将永远不会被移动或复制到另一个位置。这意味着您可以保留其他安全指向您的结构的数据结构——无论您是在程序的生命周期中从哈希表中添加还是删除它。

    钥匙

    对关键字段的数据类型或名称没有限制。该键还可以包括多个连续的字段,具有任何名称和数据类型。

    任何数据类型……真的吗?

    是的,您的键和结构可以具有任何数据类型。与具有固定原型的函数调用不同,uthash 由宏组成——其参数是无类型的——因此能够处理任何类型的结构或键。

    唯一键

    与任何散列一样,每个项目都必须有一个唯一的键。您的应用程序必须强制执行密钥唯一性。在将项目添加到哈希表之前,您必须首先知道(如果有疑问,请检查!)该密钥尚未使用。您可以使用 . 检查哈希表中是否已存在键HASH_FIND

    哈希句柄

    UT_hash_handle字段必须存在于您的结构中。它用于使散列起作用的内部簿记。它不需要初始化。它可以命名任何东西,但您可以通过命名来简化问题hh。这允许您使用更简单的“便利”宏来添加、查找和删除项目。

    关于记忆的一句话

    高架

    散列句柄在 32 位系统上每个项目消耗大约 32 个字节,或在 64 位系统上每个项目消耗 56 个字节。相比之下,其他间接成本——存储桶和桌子——可以忽略不计。您可以使用HASH_OVERHEAD来获取哈希表的开销大小(以字节为单位)。请参阅宏参考

    清理是如何发生的

    有人问 uthash 如何清理其内部存储器。答案很简单: 当您从哈希表中删除最后一项时,uthash 会释放与该哈希表关联的所有内部内存,并将其指针设置为 NULL。

    哈希运算

    本节通过示例介绍 uthash 宏。有关更简洁的列表,请参阅宏参考

    方便与一般宏:

    uthash 宏分为两类。便利宏可以与整数、指针或字符串键一起使用(并要求您为字段选择常规名称hhUT_hash_handle。与通用宏相比,便利宏使用的参数更少,这使得它们对这些常见类型的键的使用更加简单。

    通用宏可用于任何类型的键,或用于多字段键,或者当.UT_hash_handle被命名为hh. 这些宏接受更多参数并提供更大的灵活性作为回报。但是,如果便利宏满足您的需要,请使用它们——您的代码将更具可读性。

    声明哈希

    您的哈希必须声明为NULL指向您的结构的 -initialized 指针。

    struct my_struct *users = NULL;    /* important! initialize to NULL */

    添加项目

    按照您认为合适的方式分配和初始化您的结构。对 uthash 重要的唯一方面是您的密钥必须初始化为唯一值。然后调用HASH_ADD。(这里我们使用便利宏 HASH_ADD_INT,它为 type 的键提供了简化的用法int)。

    将项目添加到哈希
    1. void add_user(int user_id, char *name) {
    2. struct my_struct *s;
    3. s = malloc(sizeof *s);
    4. s->id = user_id;
    5. strcpy(s->name, name);
    6. HASH_ADD_INT(users, id, s); /* id: name of key field */
    7. }

    第一个参数HASH_ADD_INT是哈希表,第二个参数是关键字段的名称。在这里,这是id。最后一个参数是指向要添加的结构的指针。

    等等..参数是字段名?

    如果您觉得奇怪的是id,结构中的字段名称可以作为参数传递……欢迎来到宏的世界。不用担心; C 预处理器将其扩展为有效的 C 代码。

    密钥在使用时不得修改

    将结构添加到散列后,不要更改其键的值。相反,从哈希中删除该项目,更改密钥,然后重新添加它。

    检查唯一性

    在上面的示例中,我们没有检查是否user_id已经是哈希中某个现有项的键。如果您的程序有可能生成重复键,则必须 在将键添加到散列之前明确检查唯一性。如果键已经在散列中,您可以简单地修改散列中的现有结构,而不是添加项。 将具有相同键的两个项目添加到哈希表是错误的

    让我们重写add_user函数来检查 id 是否在哈希中。只有当 id 不存在于散列中时,我们才创建项目并添加它。否则我们只是修改已经存在的结构。

    1. void add_user(int user_id, char *name) {
    2. struct my_struct *s;
    1. HASH_FIND_INT(users, &user_id, s); /* id already in the hash? */
    2. if (s == NULL) {
    3. s = (struct my_struct *)malloc(sizeof *s);
    4. s->id = user_id;
    5. HASH_ADD_INT(users, id, s); /* id: name of key field */
    6. }
    7. strcpy(s->name, name);
    8. }

    为什么 uthash 不为您检查密钥的唯一性?它为那些不需要它的程序节省了哈希查找的成本 - 例如,其密钥由递增的非重复计数器生成的程序。

    但是,如果替换是常见操作,则可以使用 HASH_REPLACE宏。这个宏,在添加item之前,会先尝试找到一个key相同的item,然后先删除。它还返回一个指向被替换项的指针,因此用户有机会释放其内存。

    将哈希指针传递给函数

    在上面的例子users中是一个全局变量,但是如果调用者想要将哈希指针传递给函数add_user?乍一看,您似乎可以简单地将users其作为参数传递,但这是行不通的。

    1. /* bad */
    2. void add_user(struct my_struct *users, int user_id, char *name) {
    3. ...
    4. HASH_ADD_INT(users, id, s);
    5. }

    您确实需要将指针传递给哈希指针:

    1. /* good */
    2. void add_user(struct my_struct **users, int user_id, char *name) { ...
    3. ...
    4. HASH_ADD_INT(*users, id, s);
    5. }

    请注意,我们在HASH_ADD也取消引用了指针。

    必须处理指向散列指针的指针的原因很简单:散列宏修改它(换句话说,它们修改指针本身而不仅仅是它指向的内容)。

    更换项目

    HASH_REPLACE宏等效于 HASH_ADD 宏,只是它们首先尝试查找和删除项目。如果它找到并删除一个项目,它还将返回该项目指针作为输出参数。

    查找项目

    要在哈希中查找结构,您需要它的键。然后调用HASH_FIND。(这里我们HASH_FIND_INT对 type 的键使用便利宏int)。

    使用其键查找结构
    1. struct my_struct *find_user(int user_id) {
    2. struct my_struct *s;
    3. HASH_FIND_INT(users, &user_id, s); /* s: output pointer */
    4. return s;
    5. }

    在这里,哈希表是users,并且&user_id指向键(在这种情况下是整数)。最后,s是 的输出变量HASH_FIND_INT。最终结果是s指向具有给定键的结构,或者NULL如果在散列中找不到键。

    笔记
    中间参数是指向键的指针。您不能将文字键值传递给HASH_FIND. 而是将文字值分配给变量,并将指针传递给变量。

    删除项目

    要从哈希中删除结构,您必须有一个指向它的指针。(如果您只有密钥,请先执行 aHASH_FIND以获取结构指针)。

    从哈希中删除项目
    1. void delete_user(struct my_struct *user) {
    2. HASH_DEL(users, user); /* user: pointer to deletee */
    3. free(user); /* optional; it's up to you! */
    4. }

    这里又users是哈希表,user是指向我们要从哈希中删除的结构的指针。

    uthash 永远不会释放你的结构

    删除一个结构只是将它从哈希表中删除——它不是free 。何时释放结构完全取决于您的选择;uthash 永远不会释放你的结构。例如,当使用HASH_REPLACE宏时,会返回一个替换的输出参数,以便用户可以取消分配它。

    删除可以改变指针

    哈希表指针(最初指向添加到哈希中的第一项)可以响应更改HASH_DEL(即,如果您删除哈希表中的第一项)。

    迭代删除

    HASH_ITER宏是一个删除安全的迭代构造,它扩展为一个简单的 for循环。

    从哈希中删除所有项目
    1. void delete_all() {
    2. struct my_struct *current_user, *tmp;
    3. HASH_ITER(hh, users, current_user, tmp) {
    4. HASH_DEL(users, current_user); /* delete; users advances to next */
    5. free(current_user); /* optional- if you want to free */
    6. }
    7. }

    一次性删除

    如果您只想删除所有项目,而不是释放它们或对每个元素进行任何清理,您可以在单个操作中更有效地执行此操作:

    HASH_CLEAR(hh, users);

    之后,列表头(此处为users)将设置为NULL

    计数项目

    可以使用以下方法获得哈希表中的项目数HASH_COUNT

    哈希表中的项目数
    1. unsigned int num_users;
    2. num_users = HASH_COUNT(users);
    3. printf("there are %u users\n", num_users);

    顺便说一句,即使列表头(这里,users)是NULL,这也有效,在这种情况下计数为 0。

    迭代和排序

    您可以通过从头开始并跟随hh.next指针来遍历散列中的项目。

    遍历哈希中的所有项目
    1. void print_users() {
    2. struct my_struct *s;
    3. for (s = users; s != NULL; s = s->hh.next) {
    4. printf("user id %d: name %s\n", s->id, s->name);
    5. }
    6. }

    还有一个hh.prev指针可用于从任何已知项目开始向后迭代散列。

    删除安全迭代

    在上面的示例中,在fors循环的主体中删除和释放是不安全的(因为每次循环迭代时都会取消引用)。这很容易正确重写(通过在释放之前将指针复制到临时变量),但它经常出现,包括删除安全迭代宏 , 。它扩展为 -loop 标头。以下是如何使用它来重写最后一个示例:ss->hh.nextsHASH_ITERfor

    struct my_struct *s, *tmp;
    1. HASH_ITER(hh, users, s, tmp) {
    2. printf("user id %d: name %s\n", s->id, s->name);
    3. /* ... it is safe to delete and free s here */
    4. }
    哈希也是一个双向链表。

    hh.prev由于andhh.next字段,可以在哈希中的项目中前后迭代。哈希中的所有项都可以通过重复跟随这些指针到达,因此哈希也是一个双向链表。

    如果您在 C++ 程序中使用 uthash,则需要对for 迭代器进行额外转换,例如s = static_cast(s->hh.next).

    排序

    当您跟随 hh.next指针时,以“插入顺序”访问散列中的项目。您可以使用 将项目排序为新顺序HASH_SORT

    HASH_SORT(users, name_sort);

    第二个参数是一个指向比较函数的指针。它必须接受两个指针参数(要比较的项目),并且int如果第一项分别排在第二项之前、等于或之后,则必须返回一个小于零、零或大于零的值。strcmp(这与qsort标准 C 库中使用的约定相同)。

    1. int sort_function(void *a, void *b) {
    2. /* compare a to b (cast a and b appropriately)
    3. * return (int) -1 if (a < b)
    4. * return (int) 0 if (a == b)
    5. * return (int) 1 if (a > b)
    6. */
    7. }

    下面是排序函数name_sortid_sort两个示例。

    对哈希中的项目进行排序
    1. int by_name(const struct my_struct *a, const struct my_struct *b) {
    2. return strcmp(a->name, b->name);
    3. }
    4. int by_id(const struct my_struct *a, const struct my_struct *b) {
    5. return (a->id - b->id);
    6. }
    7. void sort_by_name() {
    8. HASH_SORT(users, by_name);
    9. }
    10. void sort_by_id() {
    11. HASH_SORT(users, by_id);
    12. }

    当哈希中的项目被排序时,第一个项目可能会改变位置。在上面的例子中,users调用后可能会指向不同的结构HASH_SORT

    一个完整的例子

    我们将重复所有代码并用main()函数修饰它以形成一个工作示例。

    如果将此代码放在example.c与 相同目录 中的文件中uthash.h,则可以像这样编译和运行它:

    1. cc -o example example.c
    2. ./example

    按照提示尝试该程序。

    一个完整的程序
    1. #include /* printf */
    2. #include /* atoi, malloc */
    3. #include /* strcpy */
    4. #include "uthash.h"
    5. struct my_struct {
    6. int id; /* key */
    7. char name[21];
    8. UT_hash_handle hh; /* makes this structure hashable */
    9. };
    10. struct my_struct *users = NULL;
    11. void add_user(int user_id, const char *name)
    12. {
    13. struct my_struct *s;
    14. HASH_FIND_INT(users, &user_id, s); /* id already in the hash? */
    15. if (s == NULL) {
    16. s = (struct my_struct*)malloc(sizeof *s);
    17. s->id = user_id;
    18. HASH_ADD_INT(users, id, s); /* id is the key field */
    19. }
    20. strcpy(s->name, name);
    21. }
    22. struct my_struct *find_user(int user_id)
    23. {
    24. struct my_struct *s;
    25. HASH_FIND_INT(users, &user_id, s); /* s: output pointer */
    26. return s;
    27. }
    28. void delete_user(struct my_struct *user)
    29. {
    30. HASH_DEL(users, user); /* user: pointer to deletee */
    31. free(user);
    32. }
    33. void delete_all()
    34. {
    35. struct my_struct *current_user;
    36. struct my_struct *tmp;
    37. HASH_ITER(hh, users, current_user, tmp) {
    38. HASH_DEL(users, current_user); /* delete it (users advances to next) */
    39. free(current_user); /* free it */
    40. }
    41. }
    42. void print_users()
    43. {
    44. struct my_struct *s;
    45. for (s = users; s != NULL; s = (struct my_struct*)(s->hh.next)) {
    46. printf("user id %d: name %s\n", s->id, s->name);
    47. }
    48. }
    49. int by_name(const struct my_struct *a, const struct my_struct *b)
    50. {
    51. return strcmp(a->name, b->name);
    52. }
    53. int by_id(const struct my_struct *a, const struct my_struct *b)
    54. {
    55. return (a->id - b->id);
    56. }
    57. const char *getl(const char *prompt)
    58. {
    59. static char buf[21];
    60. char *p;
    61. printf("%s? ", prompt); fflush(stdout);
    62. p = fgets(buf, sizeof(buf), stdin);
    63. if (p == NULL || (p = strchr(buf, '\n')) == NULL) {
    64. puts("Invalid input!");
    65. exit(EXIT_FAILURE);
    66. }
    67. *p = '\0';
    68. return buf;
    69. }
    70. int main()
    71. {
    72. int id = 1;
    73. int running = 1;
    74. struct my_struct *s;
    75. int temp;
    76. while (running) {
    77. printf(" 1. add user\n");
    78. printf(" 2. add or rename user by id\n");
    79. printf(" 3. find user\n");
    80. printf(" 4. delete user\n");
    81. printf(" 5. delete all users\n");
    82. printf(" 6. sort items by name\n");
    83. printf(" 7. sort items by id\n");
    84. printf(" 8. print users\n");
    85. printf(" 9. count users\n");
    86. printf("10. quit\n");
    87. switch (atoi(getl("Command"))) {
    88. case 1:
    89. add_user(id++, getl("Name (20 char max)"));
    90. break;
    91. case 2:
    92. temp = atoi(getl("ID"));
    93. add_user(temp, getl("Name (20 char max)"));
    94. break;
    95. case 3:
    96. s = find_user(atoi(getl("ID to find")));
    97. printf("user: %s\n", s ? s->name : "unknown");
    98. break;
    99. case 4:
    100. s = find_user(atoi(getl("ID to delete")));
    101. if (s) {
    102. delete_user(s);
    103. } else {
    104. printf("id unknown\n");
    105. }
    106. break;
    107. case 5:
    108. delete_all();
    109. break;
    110. case 6:
    111. HASH_SORT(users, by_name);
    112. break;
    113. case 7:
    114. HASH_SORT(users, by_id);
    115. break;
    116. case 8:
    117. print_users();
    118. break;
    119. case 9:
    120. temp = HASH_COUNT(users);
    121. printf("there are %d users\n", temp);
    122. break;
    123. case 10:
    124. running = 0;
    125. break;
    126. }
    127. }
    128. delete_all(); /* free any structures */
    129. return 0;
    130. }

    该程序包含在tests/example.c. 您可以 make example在该目录中运行以轻松编译它。

    标准键类型

    本节详细介绍如何使用不同类型的键。您可以使用几乎任何类型的键——整数、字符串、指针、结构等。

    笔记
    关于浮动的说明

    您可以使用浮点键。这与任何测试浮点相等性的程序都有相同的警告。换句话说,即使是两个浮点数中最微小的差异也会使它们成为不同的键。

    整数键

    前面的示例演示了整数键的使用。回顾一下,使用便利宏HASH_ADD_INTHASH_FIND_INT带有整数键的结构。(其他操作如HASH_DELETEHASH_SORT对所有类型的键都是相同的)。

    字符串键

    如果您的结构具有字符串键,则要使用的操作取决于您的结构是指向键 ( char *) 还是字符串驻留within在结构 ( char a[10]) 中。 这种区别很重要HASH_ADD_KEYPTR正如我们将在下面看到的,当你的结构指向一个键(即键本身在结构之外)时,你需要使用;相反,HASH_ADD_STR 用于结构中包含的字符串

    笔记
    char[ ] 与 char*

    该字符串在下面第一个示例中的结构name——是一个 char[10]字段。在第二个例子中,键在结构之外name——是一个char *. 所以第一个示例使用HASH_ADD_STR但第二个示例使用HASH_ADD_KEYPTR. 有关此宏的信息,请参阅宏参考

    结构内的字符串

    字符串键控哈希(结构内的字符串)
    1. #include /* strcpy */
    2. #include /* malloc */
    3. #include /* printf */
    4. #include "uthash.h"
    5. struct my_struct {
    6. char name[10]; /* key (string is WITHIN the structure) */
    7. int id;
    8. UT_hash_handle hh; /* makes this structure hashable */
    9. };
    10. int main(int argc, char *argv[]) {
    11. const char *names[] = { "joe", "bob", "betty", NULL };
    12. struct my_struct *s, *tmp, *users = NULL;
    13. for (int i = 0; names[i]; ++i) {
    14. s = (struct my_struct *)malloc(sizeof *s);
    15. strcpy(s->name, names[i]);
    16. s->id = i;
    17. HASH_ADD_STR(users, name, s);
    18. }
    19. HASH_FIND_STR(users, "betty", s);
    20. if (s) printf("betty's id is %d\n", s->id);
    21. /* free the hash table contents */
    22. HASH_ITER(hh, users, s, tmp) {
    23. HASH_DEL(users, s);
    24. free(s);
    25. }
    26. return 0;
    27. }

    此示例包含在tests/test15.c. 它打印:

    betty's id is 2

    结构中的字符串指针

    现在,这是相同的示例,但使用的是char *键而不是char [ ]

    字符串键控哈希(结构指向字符串)
    1. #include /* strcpy */
    2. #include /* malloc */
    3. #include /* printf */
    4. #include "uthash.h"
    5. struct my_struct {
    6. const char *name; /* key */
    7. int id;
    8. UT_hash_handle hh; /* makes this structure hashable */
    9. };
    10. int main(int argc, char *argv[]) {
    11. const char *names[] = { "joe", "bob", "betty", NULL };
    12. struct my_struct *s, *tmp, *users = NULL;
    13. for (int i = 0; names[i]; ++i) {
    14. s = (struct my_struct *)malloc(sizeof *s);
    15. s->name = names[i];
    16. s->id = i;
    17. HASH_ADD_KEYPTR(hh, users, s->name, strlen(s->name), s);
    18. }
    19. HASH_FIND_STR(users, "betty", s);
    20. if (s) printf("betty's id is %d\n", s->id);
    21. /* free the hash table contents */
    22. HASH_ITER(hh, users, s, tmp) {
    23. HASH_DEL(users, s);
    24. free(s);
    25. }
    26. return 0;
    27. }

    此示例包含在tests/test40.c.

    指针键

    你的钥匙可以是一个指针。很清楚,这意味着指针本身 可以是键(相反,如果指向的东西是键,这是由 处理的不同用例HASH_ADD_KEYPTR)。

    这是一个简单的例子,其中结构有一个指针成员,称为key.

    一个指针键
    1. #include
    2. #include
    3. #include "uthash.h"
    4. typedef struct {
    5. void *key;
    6. int i;
    7. UT_hash_handle hh;
    8. } el_t;
    9. el_t *hash = NULL;
    10. char *someaddr = NULL;
    11. int main() {
    12. el_t *d;
    13. el_t *e = (el_t *)malloc(sizeof *e);
    14. if (!e) return -1;
    15. e->key = (void*)someaddr;
    16. e->i = 1;
    17. HASH_ADD_PTR(hash, key, e);
    18. HASH_FIND_PTR(hash, &someaddr, d);
    19. if (d) printf("found\n");
    20. /* release memory */
    21. HASH_DEL(hash, e);
    22. free(e);
    23. return 0;
    24. }

    此示例包含在tests/test57.c. 请注意,程序的结尾会从散列中删除元素,(并且由于散列中不再有元素),uthash 会释放其内部存储器。

    结构键

    您的关键字段可以具有任何数据类型。对于 uthash,它只是一个字节序列。因此,即使是嵌套结构也可以用作键。我们将使用通用宏HASH_ADDHASH_FIND进行演示。

    笔记
    结构包含填充(浪费的内部空间用于满足结构成员的对齐要求)。在将项目添加到散列或查找项目之前,必须将这些填充字节 清零。因此,在设置感兴趣的成员之前,始终将整个结构归零。下面的示例执行此操作 - 请参阅对memset.
    作为结构的键
    1. #include
    2. #include
    3. #include "uthash.h"
    4. typedef struct {
    5. char a;
    6. int b;
    7. } record_key_t;
    8. typedef struct {
    9. record_key_t key;
    10. /* ... other data ... */
    11. UT_hash_handle hh;
    12. } record_t;
    13. int main(int argc, char *argv[]) {
    14. record_t l, *p, *r, *tmp, *records = NULL;
    15. r = (record_t *)malloc(sizeof *r);
    16. memset(r, 0, sizeof *r);
    17. r->key.a = 'a';
    18. r->key.b = 1;
    19. HASH_ADD(hh, records, key, sizeof(record_key_t), r);
    20. memset(&l, 0, sizeof(record_t));
    21. l.key.a = 'a';
    22. l.key.b = 1;
    23. HASH_FIND(hh, records, &l.key, sizeof(record_key_t), p);
    24. if (p) printf("found %c %d\n", p->key.a, p->key.b);
    25. HASH_ITER(hh, records, p, tmp) {
    26. HASH_DEL(records, p);
    27. free(p);
    28. }
    29. return 0;
    30. }

    这种用法与下面解释的复合键的用法几乎相同。

    请注意,一般宏需要将 的名称UT_hash_handle作为第一个参数传递(这里是hh)。通用宏记录在宏参考中。

    高级主题

    复合键

    您的密钥甚至可以包含多个连续字段。

    多字段键
    1. #include /* malloc */
    2. #include /* offsetof */
    3. #include /* printf */
    4. #include /* memset */
    5. #include "uthash.h"
    6. #define UTF32 1
    7. typedef struct {
    8. UT_hash_handle hh;
    9. int len;
    10. char encoding; /* these two fields */
    11. int text[]; /* comprise the key */
    12. } msg_t;
    13. typedef struct {
    14. char encoding;
    15. int text[];
    16. } lookup_key_t;
    17. int main(int argc, char *argv[]) {
    18. unsigned keylen;
    19. msg_t *msg, *tmp, *msgs = NULL;
    20. lookup_key_t *lookup_key;
    21. int beijing[] = {0x5317, 0x4eac}; /* UTF-32LE for 北京 */
    22. /* allocate and initialize our structure */
    23. msg = (msg_t *)malloc(sizeof(msg_t) + sizeof(beijing));
    24. memset(msg, 0, sizeof(msg_t)+sizeof(beijing)); /* zero fill */
    25. msg->len = sizeof(beijing);
    26. msg->encoding = UTF32;
    27. memcpy(msg->text, beijing, sizeof(beijing));
    28. /* calculate the key length including padding, using formula */
    29. keylen = offsetof(msg_t, text) /* offset of last key field */
    30. + sizeof(beijing) /* size of last key field */
    31. - offsetof(msg_t, encoding); /* offset of first key field */
    32. /* add our structure to the hash table */
    33. HASH_ADD(hh, msgs, encoding, keylen, msg);
    34. /* look it up to prove that it worked :-) */
    35. msg = NULL;
    36. lookup_key = (lookup_key_t *)malloc(sizeof(*lookup_key) + sizeof(beijing));
    37. memset(lookup_key, 0, sizeof(*lookup_key) + sizeof(beijing));
    38. lookup_key->encoding = UTF32;
    39. memcpy(lookup_key->text, beijing, sizeof(beijing));
    40. HASH_FIND(hh, msgs, &lookup_key->encoding, keylen, msg);
    41. if (msg) printf("found \n");
    42. free(lookup_key);
    43. HASH_ITER(hh, msgs, msg, tmp) {
    44. HASH_DEL(msgs, msg);
    45. free(msg);
    46. }
    47. return 0;
    48. }

    此示例包含在tests/test22.c.

    如果您使用多字段键,请识别编译器填充相邻字段(通过在它们之间插入未使用的空间)以满足每个字段的对齐要求。例如,包含 achar后跟 an的结构int通常在 char 之后有 3 个“浪费”字节的填充,以使该int字段从 4 的倍数地址开始(4 是 int 的长度)。

    计算多字段键的长度:

    要在使用多字段键时确定键长度,您必须包括编译器为对齐目的添加的任何中间结构填充。

    计算密钥长度的一种简单方法是使用offsetof来自 . 公式为:

    1. key length = offsetof(last_key_field)
    2. + sizeof(last_key_field)
    3. - offsetof(first_key_field)

    在上面的示例中,keylen使用此公式设置变量。

    在处理多字段键时,您必须在将结构 添加到哈希表或在键HASH_ADD中使用其字段之前对其进行零填充。HASH_FIND

    在前面的示例中,memset用于通过零填充来初始化结构。这会将关键字段之间的任何填充清零。如果我们不对结构进行零填充,则此填充将包含随机值。随机值会导致HASH_FIND失败;因为如果它们的填充有任何差异,两个“相同”的键将看起来不匹配。

    或者,您可以自定义全局键比较函数 和键散列函数以忽略键中的填充。请参阅指定备用键比较函数

    多级哈希表

    当散列表的每个元素都包含自己的二级散列表时,就会出现多级散列表。可以有任意数量的级别。在脚本语言中,您可能会看到:

    $items{bob}{age}=37

    下面的 C 程序在 utash 中构建了这个示例:哈希表被称为 items. 它包含一个元素 ( bob),其自己的哈希表包含一个age值为 37 的元素 ( )。构建多级哈希表不需要特殊函数。

    虽然此示例使用相同的结构表示两个级别 (bobage),但也可以使用两个不同的结构定义。如果有三个或更多级别而不是两个级别也可以。

    多级哈希表
    1. #include
    2. #include
    3. #include
    4. #include "uthash.h"
    5. /* hash of hashes */
    6. typedef struct item {
    7. char name[10];
    8. struct item *sub;
    9. int val;
    10. UT_hash_handle hh;
    11. } item_t;
    12. item_t *items = NULL;
    13. int main(int argc, char *argvp[]) {
    14. item_t *item1, *item2, *tmp1, *tmp2;
    15. /* make initial element */
    16. item_t *i = malloc(sizeof(*i));
    17. strcpy(i->name, "bob");
    18. i->sub = NULL;
    19. i->val = 0;
    20. HASH_ADD_STR(items, name, i);
    21. /* add a sub hash table off this element */
    22. item_t *s = malloc(sizeof(*s));
    23. strcpy(s->name, "age");
    24. s->sub = NULL;
    25. s->val = 37;
    26. HASH_ADD_STR(i->sub, name, s);
    27. /* iterate over hash elements */
    28. HASH_ITER(hh, items, item1, tmp1) {
    29. HASH_ITER(hh, item1->sub, item2, tmp2) {
    30. printf("$items{%s}{%s} = %d\n", item1->name, item2->name, item2->val);
    31. }
    32. }
    33. /* clean up both hash tables */
    34. HASH_ITER(hh, items, item1, tmp1) {
    35. HASH_ITER(hh, item1->sub, item2, tmp2) {
    36. HASH_DEL(item1->sub, item2);
    37. free(item2);
    38. }
    39. HASH_DEL(items, item1);
    40. free(item1);
    41. }
    42. return 0;
    43. }

    上面的示例包含在tests/test59.c.

    多个哈希表中的项目

    一个结构可以添加到多个哈希表中。您可能会这样做的几个原因包括:

    • 每个哈希表可能使用不同的键;

    • 每个哈希表可能有自己的排序顺序;

    • 或者您可以简单地使用多个哈希表进行分组。例如,您可以在一个admin_users和一个users哈希表中拥有用户。

    您的结构需要UT_hash_handle为每个可能添加到其中的哈希表提供一个字段。你可以给它们起任何名字。例如,

    UT_hash_handle hh1, hh2;

    具有多个键的项目

    您可以创建一个以 ID 字段为键的哈希表,以及另一个以用户名为键的哈希表(如果用户名是唯一的)。您可以将相同的用户结构添加到两个哈希表(无需重复结构),允许通过用户名或 ID 查找用户结构。实现这一点的方法是UT_hash_handle为每个可以添加结构的散列设置一个单独的散列。

    具有两个不同键的结构
    1. struct my_struct {
    2. int id; /* first key */
    3. char username[10]; /* second key */
    4. UT_hash_handle hh1; /* handle for first hash table */
    5. UT_hash_handle hh2; /* handle for second hash table */
    6. };

    在上面的示例中,现在可以将结构添加到两个单独的哈希表中。在一个散列中,id是它的键,而在另一个散列中,username是它的键。(不要求两个哈希具有不同的键字段。它们都可以使用相同的键,例如id)。

    请注意,该结构有两个哈希句柄(hh1hh2)。在下面的代码中,请注意每个哈希句柄都专门用于特定的哈希表。(hh1始终与users_by_id哈希一起使用,而hh2始终与users_by_name哈希表一起使用)。

    结构上的两个键
    1. struct my_struct *users_by_id = NULL, *users_by_name = NULL, *s;
    2. int i;
    3. char *name;
    4. s = malloc(sizeof *s);
    5. s->id = 1;
    6. strcpy(s->username, "thanson");
    7. /* add the structure to both hash tables */
    8. HASH_ADD(hh1, users_by_id, id, sizeof(int), s);
    9. HASH_ADD(hh2, users_by_name, username, strlen(s->username), s);
    10. /* find user by ID in the "users_by_id" hash table */
    11. i = 1;
    12. HASH_FIND(hh1, users_by_id, &i, sizeof(int), s);
    13. if (s) printf("found id %d: %s\n", i, s->username);
    14. /* find user by username in the "users_by_name" hash table */
    15. name = "thanson";
    16. HASH_FIND(hh2, users_by_name, name, strlen(name), s);
    17. if (s) printf("found user %s: %d\n", name, s->id);

    新项目的排序插入

    如果您想维护已排序的散列,您有两个选择。第一个选项是使用 HASH_SRT() 宏,它将对 O(n log(n))中的任何无序列表进行排序。如果您只是在完成所有操作后使用单个最终 HASH_SRT() 操作以随机顺序填充哈希表,这是最佳策略。显然,如果您需要列表在插入项目之间有时处于有序状态,这不会满足您的要求。您可以在每次插入操作后使用 HASH_SRT() ,但这会产生O(n^2 log n)的计算复杂度。

    您可以采取的第二条路线是通过按顺序添加和替换宏。HASH_ADD_INORDER*宏的工作方式与对应的宏一样,HASH_ADD*但带有一个额外的比较函数参数:

    1. int name_sort(struct my_struct *a, struct my_struct *b) {
    2. return strcmp(a->name, b->name);
    3. }
    HASH_ADD_KEYPTR_INORDER(hh, items, &item->name, strlen(item->name), item, name_sort);

    新项目在插入时在O(n)中排序,因此创建包含所有项目的哈希表的总计算复杂度为O(n^2)。为了按顺序添加工作,列表必须在插入新项目之前处于有序状态。

    几个排序顺序

    毫不奇怪,两个哈希表可以有不同的排序顺序,但是这一事实也可以有利地用于以多种方式对相同的项目进行排序。这是基于将结构存储在多个哈希表中的能力。

    扩展前面的例子,假设我们有很多用户。我们已将每个用户结构添加到users_by_id哈希表和users_by_name哈希表中。(重申一下,这样做不需要每个结构有两个副本。)现在我们可以定义两个排序函数,然后使用HASH_SRT.

    1. int sort_by_id(struct my_struct *a, struct my_struct *b) {
    2. if (a->id == b->id) return 0;
    3. return (a->id < b->id) ? -1 : 1;
    4. }
    1. int sort_by_name(struct my_struct *a, struct my_struct *b) {
    2. return strcmp(a->username, b->username);
    3. }
    1. HASH_SRT(hh1, users_by_id, sort_by_id);
    2. HASH_SRT(hh2, users_by_name, sort_by_name);

    现在迭代 in 中的项目users_by_id将按 id-order 遍历它们,而自然地,迭代users_by_name将按 name-order 遍历它们。这些项目在每个订单中都是完全向前和向后链接的。因此,即使对于一组用户,我们也可以将它们存储在两个散列表中,以便以两种不同的排序顺序进行迭代。

    布隆过滤器(更快的未命中)

    产生公平未命中率(HASH_FIND导致NULL)的程序可能会受益于内置的布隆过滤器支持。默认情况下这是禁用的,因为只生成命中的程序会受到轻微的惩罚。此外,执行删除的程序不应使用布隆过滤器。虽然程序可以正常运行,但删除会降低过滤器的好处。要启用 Bloom 过滤器,只需编译-DHASH_BLOOM=n如下:

    -DHASH_BLOOM=27

    其中数字可以是不超过 32 的任何值,它决定了过滤器使用的内存量,如下所示。使用更多内存可使过滤器更准确,并有可能通过更快地消除未命中来加速您的程序。

    表 1. 选定 n 值的布隆过滤器大小
    n布隆过滤器大小(每个哈希表)

    16

    8 KB

    20

    128 KB

    24

    2 兆字节

    28

    32 兆字节

    32

    512 兆字节

    布隆过滤器只是一种性能特征;它们不会以任何方式改变哈希运算的结果。衡量布隆过滤器是否适合您的程序的唯一方法是对其进行测试。布隆过滤器大小的合理值是 16-32 位。

    选择

    提供了一个实验性的选择操作,它将源散列中满足给定条件的那些项插入到目标散列中。与使用 相比,此插入的效率要高一些 HASH_ADD,即因为不会为所选项目的键重新计算散列函数。此操作不会从源哈希中删除任何项目。相反,所选项目在两个哈希中都获得双重存在。目标哈希中可能已经有项目;所选项目被添加到其中。为了使结构可以与 一起使用HASH_SELECT,它必须具有两个或多个散列句柄。(如此所述,一个结构可以同时存在于多个哈希表中;每个哈希表必须有一个单独的哈希句柄)。

    1. user_t *users = NULL; /* hash table of users */
    2. user_t *admins = NULL; /* hash table of admins */
    1. typedef struct {
    2. int id;
    3. UT_hash_handle hh; /* handle for users hash */
    4. UT_hash_handle ah; /* handle for admins hash */
    5. } user_t;

    现在假设我们添加了一些用户,并且只想选择 id 小于 1024 的管理员用户。

    1. #define is_admin(x) (((user_t*)x)->id < 1024)
    2. HASH_SELECT(ah, admins, hh, users, is_admin);

    前两个参数是目标哈希句柄和哈希表,后两个参数是哈希句柄和哈希表,最后一个参数是选择条件。这里我们使用了一个宏is_admin(x),但我们也可以使用一个函数。

    1. int is_admin(const void *userv) {
    2. user_t *user = (const user_t*)userv;
    3. return (user->id < 1024) ? 1 : 0;
    4. }

    如果选择条件始终为真,则此操作本质上是将源散列合并到目标散列。

    HASH_SELECT将项目添加到目标而不将它们从源中删除;源哈希表保持不变。目标哈希表不能与源哈希表相同。

    使用示例HASH_SELECT包含在tests/test36.c.

    指定备用键比较函数

    当您调用 时HASH_FIND(hh, head, intfield, sizeof(int), out),uthash 将首先调用以确定要在其中搜索的存储桶,然后,对于 存储桶的每个元素,uthash 将评估 。 应该返回来表明这是一个匹配并且应该被返回,并且任何非零值表明应该继续搜索匹配的元素。HASH_FUNCTION(intfield, sizeof(int), hashvalue)beltbelt->hh.hashv == hashvalue && elt.hh.keylen == sizeof(int) && HASH_KEYCMP(intfield, elt->hh.key, sizeof(int)) == 0HASH_KEYCMP0elt

    默认情况下,uthash 定义HASH_KEYCMPmemcmp. 在不提供的平台上memcmp,您可以替换自己的实现。

    1. #undef HASH_KEYCMP
    2. #define HASH_KEYCMP(a,b,len) bcmp(a, b, len)

    替换您自己的密钥比较函数的另一个原因是,如果您的“密钥”不可比较。在这种情况下,您还需要替换自己的HASH_FUNCTION.

    1. struct Key {
    2. short s;
    3. /* 2 bytes of padding */
    4. float f;
    5. };
    6. /* do not compare the padding bytes; do not use memcmp on floats */
    7. unsigned key_hash(struct Key *s) { return s + (unsigned)f; }
    8. bool key_equal(struct Key *a, struct Key *b) { return a.s == b.s && a.f == b.f; }
    9. #define HASH_FUNCTION(s,len,hashv) (hashv) = key_hash((struct Key *)s)
    10. #define HASH_KEYCMP(a,b,len) (!key_equal((struct Key *)a, (struct Key *)b))

    替换您自己的密钥比较函数的另一个原因是为了权衡原始速度的正确性。在它对存储桶的线性搜索期间,uthash 总是首先比较 32 位,并且只有在 比较相等时才hashv调用。这意味着每次成功查找至少调用一次。给定一个好的散列函数,我们预计比较只会在 40 亿次中产生一次“误报”相等。因此,我们预计大部分时间都会生产。如果我们期望有很多成功的发现,并且我们的应用程序不介意偶尔的误报,我们可能会替换一个无操作比较函数:HASH_KEYCMPhashvHASH_KEYCMPhashvHASH_KEYCMP0

    1. #undef HASH_KEYCMP
    2. #define HASH_KEYCMP(a,b,len) 0 /* occasionally wrong, but very fast */

    注意:全局相等比较函数HASH_KEYCMP与作为参数传递给的小于比较函数完全没有关系HASH_ADD_INORDER

    内置哈希函数

    在内部,散列函数将键转换为桶号。您无需执行任何操作即可使用默认散列函数,目前是 Jenkins。

    某些程序可能会受益于使用另一个内置散列函数。uthash 包含一个简单的分析实用程序,可帮助您确定另一个哈希函数是否会为您提供更好的性能。

    您可以通过使用下面列出的符号名称之一来编译程序-DHASH_FUNCTION=HASH_xyz来使用不同的哈希函数 。xyz例如,

    cc -DHASH_FUNCTION=HASH_BER -o program program.c
    表 2. 内置哈希函数
    象征姓名

    JEN

    詹金斯(默认)

    BER

    伯恩斯坦

    SAX

    Shift-Add-Xor

    OAT

    一次一个

    FNV

    福勒/诺尔/Vo

    SFH

    保罗·谢

    哪个哈希函数最好?

    您可以轻松确定关键域的最佳哈希函数。为此,您需要在数据收集过程中运行一次程序,然后通过包含的分析实用程序运行收集的数据。

    首先,您必须构建分析实用程序。从顶级目录,

    1. cd tests/
    2. make

    我们将用于test14.c演示数据收集和分析步骤(这里使用sh语法将文件描述符 3 重定向到文件):

    使用 keystats
    1. % cc -DHASH_EMIT_KEYS=3 -I../src -o test14 test14.c
    2. % ./test14 3>test14.keys
    3. % ./keystats test14.keys
    4. fcn ideal% #items #buckets dup% fl add_usec find_usec del-all usec
    5. --- ------ ---------- ---------- ----- -- ---------- ---------- ------------
    6. SFH 91.6% 1219 256 0% ok 92 131 25
    7. FNV 90.3% 1219 512 0% ok 107 97 31
    8. SAX 88.7% 1219 512 0% ok 111 109 32
    9. OAT 87.2% 1219 256 0% ok 99 138 26
    10. JEN 86.7% 1219 256 0% ok 87 130 27
    11. BER 86.2% 1219 256 0% ok 121 129 27
    笔记
    中的数字 3-DHASH_EMIT_KEYS=3是文件描述符。您的程序不用于其自身目的的任何文件描述符都可以使用而不是 3。 启用的数据收集模式-DHASH_EMIT_KEYS=x不应在生产代码中使用。

    通常,您应该只选择列出的第一个哈希函数。在这里,这是SFH。这是为您的密钥提供最均匀分布的功能。如果有几个相同ideal%,则根据find_usec列选择最快的一个。

    keystats 列参考

    fcn

    哈希函数的符号名

    理想的%

    哈希表中可以在理想步数内查找的项目的百分比。(下面进一步解释)。

    #项目

    从发出的密钥文件中读取的密钥数

    #buckets

    添加所有键后哈希中的桶数

    重复%

    在发出的密钥文件中遇到的重复密钥的百分比。过滤掉重复的键以保持键的唯一性。(重复是正常的。例如,如果应用程序将一个项目添加到哈希中,将其删除,然后重新添加,则密钥将两次写入发出的文件。)

    旗帜

    如果设置了扩展禁止标志,则为oknx(noexpand),如扩展内部中所述。不建议使用noexpand设置了标志的散列函数。

    add_usec

    将所有键添加到哈希所需的时钟时间(以微秒为单位)

    find_usec

    查找哈希中的每个键所需的时钟时间(以微秒为单位)

    删除所有 usec

    删除哈希中的每个项目所需的时钟时间(以微秒为单位)

    理想的%

    什么是理想百分比?

    哈希中的n 个项目被分配到k个桶中。理想情况下,每个桶将包含相等份额(n/k)的项目。换句话说,如果每个桶都被平等地使用,桶链中任何项目的最大线性位置将为n/k 。如果一些桶被过度使用而其他桶未被充分利用,过度使用的桶将包含线性位置超过n/k的项目。这样的项目被认为是不理想的。

    正如您可能猜到的那样,ideal%是散列中理想项目的百分比。这些项目在其桶链中具有有利的线性位置。随着ideal% 接近 100%,哈希表接近恒定时间查找性能。

    哈希扫描

    笔记
    此实用程序仅在 Linux 和 FreeBSD(8.1 及更高版本)上可用。

    hashscan目录中包含一个名为的实用程序tests/make当您在该目录中运行时,它会自动构建。该工具检查正在运行的进程并报告它在该程序的内存中找到的 uthash 表。它还可以以可以输入的格式保存每个表中的键keystats

    下面是一个使用的例子hashscan。首先确保它已构建:

    1. cd tests/
    2. make

    由于hashscan需要一个正在运行的程序来检查,我们将启动一个简单的程序来创建一个哈希表,然后作为我们的测试对象休眠:

    1. ./test_sleep &
    2. pid: 9711

    现在我们有了一个测试程序,让我们运行hashscan它:

    1. ./hashscan 9711
    2. Address ideal items buckets mc fl bloom/sat fcn keys saved to
    3. ------------------ ----- -------- -------- -- -- --------- --- -------------
    4. 0x862e038 81% 10000 4096 11 ok 16 14% JEN

    如果我们想使用 复制其所有键以进行外部分析keystats,请添加-k标志:

    1. ./hashscan -k 9711
    2. Address ideal items buckets mc fl bloom/sat fcn keys saved to
    3. ------------------ ----- -------- -------- -- -- --------- --- -------------
    4. 0x862e038 81% 10000 4096 11 ok 16 14% JEN /tmp/9711-0.key

    现在我们可以运行./keystats /tmp/9711-0.key来分析哪个散列函数在这组键上具有最佳特性。

    hashscan 列参考

    地址

    哈希表的虚拟地址

    理想的

    可以在理想步数内查找的表中项目的百分比。请参阅本节中的[理想]keystats

    项目

    哈希表中的项目数

    水桶

    哈希表中的桶数

    麦克

    在哈希表中找到的最大链长度(uthash 通常会尝试在每个存储桶中保留少于 10 个项目,或者在某些情况下是 10 的倍数)

    佛罗里达州

    标志(或者ok,或者NX如果设置了扩展禁止标志)

    绽放/星期六

    如果哈希表使用布隆过滤器,这是过滤器的大小(作为 2 的幂)(例如,16 表示过滤器的大小为 2^16 位)。第二个数字是以百分比表示的位的“饱和度”。百分比越低,快速识别缓存未命中的潜在好处就越大。

    fcn

    哈希函数的符号名

    键保存到

    保存密钥的文件(如果有)

    哈希扫描的工作原理

    当 hashscan 运行时,它会将自己附加到目标进程,从而暂时挂起目标进程。在这个短暂的暂停期间,它会扫描目标的虚拟内存以查找 uthash 哈希表的签名。然后它检查签名是否附带有效的哈希表结构并报告它发现的内容。当它分离时,目标进程恢复正常运行。哈希扫描是“只读”执行的——目标进程没有被修改。由于 hashscan 正在分析正在运行的进程的瞬时快照,因此它可能会从一次运行返回不同的结果。

    扩展内件

    在内部,此哈希管理存储桶的数量,目标是拥有足够的存储桶,以便每个存储桶仅包含少量项目。

    为什么桶的数量很重要?

    当通过其键查找项目时,此哈希会线性扫描相应存储桶中的项目。为了使线性扫描在恒定时间内运行,每个桶中的项目数必须是有界的。这是通过根据需要增加存储桶的数量来实现的。

    正常展开

    此哈希尝试在每个存储桶中保留少于 10 个项目。当添加一个会导致存储桶超过此数量的项目时,哈希中的存储桶数量会加倍,并且项目会重新分配到新存储桶中。在理想的世界中,每个桶将包含以前一半的项目。

    存储桶扩展会根据需要自动且不可见地发生。应用程序无需知道它何时发生。

    每桶扩展阈值

    通常,所有存储桶共享相同的阈值(10 个项目),此时存储桶扩展被触发。在桶扩容过程中,如果发现某些桶被过度使用,uthash可以逐桶调整这个扩容触发阈值。

    调整此阈值后,它会从 10 变为 10 的倍数(对于该特定存储桶)。倍数基于实际链长度比理想长度大多少倍。在散列函数过度使用几个桶但总体分布良好的情况下,减少桶的过度扩展是一种实用的措施。但是,如果整体分布变得太糟糕,uthash 就会改变策略。

    抑制膨胀

    您通常不需要知道或担心这一点,特别是如果您keystats在开发期间使用该实用程序为您的密钥选择一个好的散列。

    散列函数可能会在桶中产生不均匀的项目分布。适度这不是问题。随着链条长度的增长,正常的铲斗膨胀发生。但是当出现严重的不平衡时(因为散列函数不太适合关键域),桶扩展可能无法有效减少链长度。

    想象一个非常糟糕的哈希函数,它总是将每个项目放在桶 0 中。无论桶的数量翻倍多少倍,桶 0 的链长度保持不变。在这种情况下,最好的行为是停止扩展,并接受O(n)的查找性能。这就是 uthash 所做的。如果散列函数不适合键,它会优雅地降级。

    如果两个连续的桶扩展产生ideal%的值低于 50%,则 uthash 禁止该哈希表的扩展。一旦设置,桶扩展禁止标志保持有效,只要散列中有项目。抑制膨胀可能会导致HASH_FIND表现出比恒定时间性能更差的情况。

    诊断挂钩

    如果 uthash 正在扩展存储桶或设置存储桶扩展禁止标志,则会执行两个“通知”挂钩。应用程序无需设置这些挂钩或采取行动来响应这些事件。它们主要用于诊断目的。通常这两个钩子都是未定义的,因此编译为空。

    uthash_expand_fyi挂钩可以定义为在 utash 执行存储桶扩展时执行代码。

    1. #undef uthash_expand_fyi
    2. #define uthash_expand_fyi(tbl) printf("expanded to %u buckets\n", tbl->num_buckets)

    每当 uthash 设置存储桶扩展禁止标志时,uthash_noexpand_fyi可以定义挂钩以执行代码。

    1. #undef uthash_noexpand_fyi
    2. #define uthash_noexpand_fyi(tbl) printf("warning: bucket expansion inhibited\n")

    挂钩

    你不需要使用这些钩子——它们只有在你想修改 uthash 的行为时才会出现。Hooks 可用于替换某些平台上可能不可用的标准库函数,更改 uthash 分配内存的方式,或运行代码以响应某些内部事件。

    uthash.h头会将这些钩子定义为默认值,除非它们已经定义。#undef在include 之后重新定义它们是安全的uthash.h,或者在包含之前定义它们是安全的;例如,通过-Duthash_malloc=my_malloc命令行传递。

    指定备用内存管理功能

    默认情况下,uthash 使用mallocandfree来管理内存。如果您的应用程序使用自己的自定义分配器,uthash 也可以使用它们。

    1. #include "uthash.h"
    2. /* undefine the defaults */
    3. #undef uthash_malloc
    4. #undef uthash_free
    5. /* re-define, specifying alternate functions */
    6. #define uthash_malloc(sz) my_malloc(sz)
    7. #define uthash_free(ptr, sz) my_free(ptr)
    8. ...

    注意uthash_free接收两个参数。该sz参数是为了方便管理自己的内存的嵌入式平台。

    指定备用标准库函数

    Uthash 还使用strlen(例如在HASH_FIND_STR便利宏中)和memset(仅用于归零内存)。在不提供这些功能的平台上,您可以替换自己的实现。

    1. #undef uthash_bzero
    2. #define uthash_bzero(a, len) my_bzero(a, len)
    3. #undef uthash_strlen
    4. #define uthash_strlen(s) my_strlen(s)

    记不清

    如果内存分配失败(即uthash_malloc函数返回NULL),默认行为是通过调用来终止进程exit(-1)。这可以通过重新定义uthash_fatal宏来修改。

    1. #undef uthash_fatal
    2. #define uthash_fatal(msg) my_fatal_function(msg)

    致命功能应终止进程或longjmp返回安全位置。请注意,分配失败可能会导致分配的内存无法恢复。之后uthash_fatal,哈希表对象应该被认为是不可用的;HASH_CLEAR当哈希表处于这种状态时,即使在哈希表上运行也可能不安全。

    要在无法分配内存时启用“返回失败”,请HASH_NONFATAL_OOM在包含uthash.h头文件之前定义宏。在这种情况下,uthash_fatal不使用;相反,每次分配失败都会导致一次调用uthash_nonfatal_oom(elt)whereelt是其插入触发失败的元素的地址。的默认行为 uthash_nonfatal_oom是无操作。

    1. #undef uthash_nonfatal_oom
    2. #define uthash_nonfatal_oom(elt) perhaps_recover((element_t *) elt)

    在调用 之前uthash_nonfatal_oom,哈希表回滚到有问题的插入之前的状态;没有内存泄漏。throw进出处理程序是安全longjmputhash_nonfatal_oom 。

    参数将elt是正确的指向元素的类型,除非 uthash_nonfatal_oom从 调用HASH_SELECT,在这种情况下,它将是void*类型并且必须在使用之前进行强制转换。无论如何,elt->hh.tbl 都会NULL

    ADD只有在向哈希表添加元素时(包括、REPLACESELECT操作) ,分配失败才可能发生。uthash_free不允许失败。

    调试模式

    如果使用此哈希的程序使用 编译-DHASH_DEBUG=1,则会激活特殊的内部一致性检查模式。在这种模式下,每次添加或删除操作后都会检查整个哈希的完整性。这仅用于调试 uthash 软件,不用于生产代码。

    tests/目录中,runningmake debug将运行此模式下的所有测试。

    在这种模式下,散列数据结构中的任何内部错误都会导致打印消息stderr并退出程序。

    UT_hash_handle数据结构next包括、prevhh_next字段 hh_prev。前两个字段决定了“应用程序”的顺序(即插入顺序——添加项目的顺序)。后两个字段确定“桶链”顺序。它们UT_hash_handles 在一个双向链表中链接在一起,这是一个桶链。

    在模式下执行的检查-DHASH_DEBUG=1

    • 哈希被完整遍历两次:一次按顺序,第二次按应用程序顺序

    • 将两次行走中遇到的项目总数与存储的数量进行检查

    • 在遍历顺序期间,将每个项目的hh_prev指针与最后访问的项目进行比较

    • 在遍历应用程序顺序期间,将每个项目的prev指针与最后访问的项目进行比较

    宏调试:

    有时很难解释包含宏调用的行上的编译器警告。在 uthash 的情况下,一个宏可以扩展到几十行。在这种情况下,扩展宏然后重新编译会很有帮助。通过这样做,警告消息将引用宏中的确切行。

    这是一个如何扩展宏然后重新编译的示例。这将使用 子目录test1.c中的程序。tests/

    1. gcc -E -I../src test1.c > /tmp/a.c
    2. egrep -v '^#' /tmp/a.c > /tmp/b.c
    3. indent /tmp/b.c
    4. gcc -o /tmp/b /tmp/b.c

    最后一行编译原始程序 (test1.c) 并展开所有宏。如果有警告,可以签入引用的行号/tmp/b.c

    线程安全

    您可以在线程程序中使用 uthash。但是您必须进行锁定。使用读写锁来防止并发写入。可以有并发阅读器(从 uthash 1.5 开始)。

    例如,使用 pthreads 你可以像这样创建一个 rwlock:

    1. pthread_rwlock_t lock;
    2. if (pthread_rwlock_init(&lock, NULL) != 0) fatal("can't create rwlock");

    然后,读者必须在进行任何HASH_FIND调用或迭代散列元素之前获取读锁:

    1. if (pthread_rwlock_rdlock(&lock) != 0) fatal("can't get rdlock");
    2. HASH_FIND_INT(elts, &i, e);
    3. pthread_rwlock_unlock(&lock);

    写入者在进行任何更新之前必须获得排他写入锁。添加、删除和排序都是必须锁定的更新。

    1. if (pthread_rwlock_wrlock(&lock) != 0) fatal("can't get wrlock");
    2. HASH_DEL(elts, e);
    3. pthread_rwlock_unlock(&lock);

    如果您愿意,您可以使用互斥锁而不是读写锁,但这会将读取器的并发性减少为一次单个线程。

    使用带有读写锁的 uthash 的示例程序包含在 tests/threads/test1.c.

    宏参考

    便利宏

    便利宏与通用宏做同样的事情,但需要更少的参数。

    为了使用方便的宏,

    1. 结构的UT_hash_handle字段必须命名为hh,并且

    2. 对于添加或查找,关键字段必须是类型intchar[]或指针

    表 3. 便利宏
    论据

    HASH_ADD_INT

    (head, keyfield_name, item_ptr)

    HASH_REPLACE_INT

    (head, keyfield_name, item_ptr, replaced_item_ptr)

    HASH_FIND_INT

    (head, key_ptr, item_ptr)

    HASH_ADD_STR

    (head, keyfield_name, item_ptr)

    HASH_REPLACE_STR

    (head, keyfield_name, item_ptr, replaced_item_ptr)

    HASH_FIND_STR

    (head, key_ptr, item_ptr)

    HASH_ADD_PTR

    (head, keyfield_name, item_ptr)

    HASH_REPLACE_PTR

    (head, keyfield_name, item_ptr, replaced_item_ptr)

    HASH_FIND_PTR

    (head, key_ptr, item_ptr)

    HASH_DEL

    (head, item_ptr)

    HASH_SORT

    (head, cmp)

    HASH_COUNT

    (head)

    通用宏

    这些宏在散列中添加、查找、删除和排序项目。如果您UT_hash_handle的名称不是hh,或者您的键的数据类型不是intor ,则需要使用通用宏char[]

    表 4. 通用宏
    论据

    HASH_ADD

    (hh_name, head, keyfield_name, key_len, item_ptr)

    HASH_ADD_BYHASHVALUE

    (hh_name, head, keyfield_name, key_len, hashv, item_ptr)

    HASH_ADD_KEYPTR

    (hh_name, head, key_ptr, key_len, item_ptr)

    HASH_ADD_KEYPTR_BYHASHVALUE

    (hh_name, head, key_ptr, key_len, hashv, item_ptr)

    HASH_ADD_INORDER

    (hh_name, head, keyfield_name, key_len, item_ptr, cmp)

    HASH_ADD_BYHASHVALUE_INORDER

    (hh_name, head, keyfield_name, key_len, hashv, item_ptr, cmp)

    HASH_ADD_KEYPTR_INORDER

    (hh_name, head, key_ptr, key_len, item_ptr, cmp)

    HASH_ADD_KEYPTR_BYHASHVALUE_INORDER

    (hh_name, head, key_ptr, key_len, hashv, item_ptr, cmp)

    HASH_REPLACE

    (hh_name, head, keyfield_name, key_len, item_ptr, replaced_item_ptr)

    HASH_REPLACE_BYHASHVALUE

    (hh_name, head, keyfield_name, key_len, hashv, item_ptr, replaced_item_ptr)

    HASH_REPLACE_INORDER

    (hh_name, head, keyfield_name, key_len, item_ptr, replaced_item_ptr, cmp)

    HASH_REPLACE_BYHASHVALUE_INORDER

    (hh_name, head, keyfield_name, key_len, hashv, item_ptr, replaced_item_ptr, cmp)

    HASH_FIND

    (hh_name, head, key_ptr, key_len, item_ptr)

    HASH_FIND_BYHASHVALUE

    (hh_name, head, key_ptr, key_len, hashv, item_ptr)

    HASH_DELETE

    (hh_name, head, item_ptr)

    HASH_VALUE

    (key_ptr, key_len, hashv)

    HASH_SRT

    (hh_name, head, cmp)

    HASH_CNT

    (hh_name, head)

    HASH_CLEAR

    (hh_name, head)

    HASH_SELECT

    (dst_hh_name, dst_head, src_hh_name, src_head, condition)

    HASH_ITER

    (hh_name, head, item_ptr, tmp_item_ptr)

    HASH_OVERHEAD

    (hh_name, head)

    笔记
    HASH_ADD_KEYPTR当结构包含指向键的指针而不是键本身时使用。

    和宏是一种性能机制HASH_VALUE..._BYHASHVALUE主要针对具有不同结构、不同哈希表、具有相同键的特殊情况。它允许一次获取哈希值,然后将其传递给..._BYHASHVALUE宏,从而节省了重新计算哈希值的费用。

  • 相关阅读:
    ssm框架
    qt 怎么实现在子窗体通知mainwindow界面发生改变
    Java实现图片转文字!(OCR实现)
    Linux企业应用——Docker(一)之初步了解Docker以及Docker的安装
    Mysql——》decimal
    prompt learning——你需要掌握的基础知识以及离散型 prompt 的代码
    使用Springboot开发前后端分离校园智能出行拼车系统
    Android init.rc 解析
    Codeforces Round 592 (Div 2)(A - G)
    手把手教你使用Java生成助记词、私钥、地址|Java区块链钱包生成助记词、地址
  • 原文地址:https://blog.csdn.net/m0_64560763/article/details/125992373