glibc 知:手册09:搜索和排序

1. 前言

The GNU C Library Reference Manual for version 2.35

2. 搜索和排序

Searching and Sorting

本章介绍用于搜索和排序任意对象数组的函数。您传递要作为参数应用的适当比较函数,以及数组中对象的大小和元素总数。

2.1. 定义比较函数

Defining the Comparison Function

为了使用排序数组库函数,您必须描述如何比较数组的元素。

为此,您需要提供一个比较函数来比较数组的两个元素。库将调用此函数,将指针作为参数传递给要比较的两个数组元素。您的比较函数应该像 strcmp(请参阅字符串/数组比较)那样返回一个值:如果第一个参数“小于”第二个参数,则返回负数;如果它们“相等”,则返回零;如果第一个参数“大于”,则返回正数.

下面是一个比较函数的示例,它适用于 double 类型的数字数组:

int
compare_doubles (const void *a, const void *b)
{
  const double *da = (const double *) a;
  const double *db = (const double *) b;

  return (*da > *db) - (*da < *db);
}

头文件 stdlib.h 定义了比较函数的数据类型的名称。这种类型是 GNU 扩展。

int compare_fn_t (const void *, const void *);

2.2. 数组搜索函数

Array Search Function

通常在数组中搜索特定元素意味着可能必须检查所有元素。GNU C 库包含执行线性搜索的函数。以下两个函数的原型可以在 search.h 中找到。

函数:void * lfind (const void *key, const void *base, size_t *nmemb, size_t size, comparison_fn_t compar)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

lfind 函数在数组中搜索由 base 指向的大小为字节的 *nmemb 元素,以查找与 key 指向的元素匹配的元素。compar 指向的函数用于判断两个元素是否匹配。

返回值是指向数组中匹配元素的指针,如果找到,则从 base 开始。如果没有可用的匹配元素,则返回 NULL。

此函数的平均运行时间为 *nmemb/2。仅当元素经常被添加到数组中或从数组中删除时才应使用此函数,在这种情况下,在搜索之前对数组进行排序可能没有用。

函数:void * lsearch (const void *key, void *base, size_t *nmemb, size_t size, comparison_fn_t compar)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

lsearch 函数类似于 lfind 函数。它在给定的数组中搜索一个元素,如果找到则返回它。不同之处在于,如果没有找到匹配的元素,lsearch 函数将 key 指向的对象(大小为 size 字节)添加到数组的末尾,并增加 *nmemb 的值以反映此添加。

这意味着对于调用者来说,如果不确定数组是否包含元素,则一个正在搜索为从 base 开始的数组分配的内存必须有至少 size 更多字节的空间。如果确定元素在数组中,最好使用 lfind,因此在调用 lsearch 时总是需要在数组中留出更多空间。

要在排序数组中搜索与键匹配的元素,请使用 bsearch 函数。此函数的原型位于头文件 stdlib.h 中。

函数:void * bsearch (const void *key, const void *array, size_t count, size_t size, comparison_fn_t compare)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

bsearch 函数在排序后的数组中搜索与 key 等效的对象。该数组包含计数元素,每个元素的大小为大小字节。

比较函数用于执行比较。此函数使用两个指针参数调用,并且应返回一个小于、等于或大于零的整数,对应于其第一个参数是否被认为小于、等于或大于其第二个参数。数组的元素必须已经根据此比较函数按升序排序。

返回值是指向匹配数组元素的指针,如果未找到匹配项,则返回空指针。如果数组包含多个匹配的元素,则返回的元素未指定。

这个函数的名字来源于它是使用二分搜索算法实现的。

2.3. 数组排序函数

Array Sort Function

要使用任意比较函数对数组进行排序,请使用 qsort 函数。此函数的原型位于 stdlib.h 中。

函数:void qsort(void *array, size_t count, size_t size, comparison_fn_t compare)

Preliminary: | MT-Safe | AS-Safe | AC-Unsafe corrupt | See POSIX Safety Concepts.

qsort 函数对数组数组进行排序。该数组包含计数元素,每个元素的大小为大小。

compare 函数用于对数组元素进行比较。此函数使用两个指针参数调用,并且应返回一个小于、等于或大于零的整数,对应于其第一个参数是否被认为小于、等于或大于其第二个参数。

警告:如果两个对象比较相等,排序后的顺序是不可预测的。也就是说,排序是不稳定的。当比较仅考虑部分元素时,这可能会有所不同。具有相同排序键的两个元素可能在其他方面有所不同。

尽管传递给比较函数的对象地址位于数组中,但它们不需要与这些对象的原始位置相对应,因为排序算法可能会在进行一些比较之前交换数组中的对象。使用 qsort 执行稳定排序的唯一方法是首先使用某种单调计数器来扩充对象。

这是一个使用上面定义的比较函数按数字顺序对双精度数组进行排序的简单示例(请参阅定义比较函数):

{
  double *array;
  int size;qsort (array, size, sizeof (double), compare_doubles);
}

qsort 函数的名称源于它最初是使用“快速排序”算法实现的。

此库中 qsort 的实现可能不是就地排序,因此可能会使用额外的内存量来存储数组。

2.4. 搜索和排序示例

Searching and Sorting Example

这是一个示例,展示了 qsort 和 bsearch 与结构数组的使用。数组中的对象通过将它们的名称字段与 strcmp 函数进行比较来排序。然后,我们可以根据名称查找单个对象。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/* Define an array of critters to sort. */

struct critter
  {
    const char *name;
    const char *species;
  };

struct critter muppets[] =
  {
    {"Kermit", "frog"},
    {"Piggy", "pig"},
    {"Gonzo", "whatever"},
    {"Fozzie", "bear"},
    {"Sam", "eagle"},
    {"Robin", "frog"},
    {"Animal", "animal"},
    {"Camilla", "chicken"},
    {"Sweetums", "monster"},
    {"Dr. Strangepork", "pig"},
    {"Link Hogthrob", "pig"},
    {"Zoot", "human"},
    {"Dr. Bunsen Honeydew", "human"},
    {"Beaker", "human"},
    {"Swedish Chef", "human"}
  };

int count = sizeof (muppets) / sizeof (struct critter);



/* This is the comparison function used for sorting and searching. */

int
critter_cmp (const void *v1, const void *v2)
{
  const struct critter *c1 = v1;
  const struct critter *c2 = v2;

  return strcmp (c1->name, c2->name);
}


/* Print information about a critter. */

void
print_critter (const struct critter *c)
{
  printf ("%s, the %s\n", c->name, c->species);
}


/* Do the lookup into the sorted array. */

void
find_critter (const char *name)
{
  struct critter target, *result;
  target.name = name;
  result = bsearch (&target, muppets, count, sizeof (struct critter),
                    critter_cmp);
  if (result)
    print_critter (result);
  else
    printf ("Couldn't find %s.\n", name);
}

/* Main program. */

int
main (void)
{
  int i;

  for (i = 0; i < count; i++)
    print_critter (&muppets[i]);
  printf ("\n");

  qsort (muppets, count, sizeof (struct critter), critter_cmp);

  for (i = 0; i < count; i++)
    print_critter (&muppets[i]);
  printf ("\n");

  find_critter ("Kermit");
  find_critter ("Gonzo");
  find_critter ("Janice");

  return 0;
}

该程序的输出如下所示:

Kermit, the frog
Piggy, the pig
Gonzo, the whatever
Fozzie, the bear
Sam, the eagle
Robin, the frog
Animal, the animal
Camilla, the chicken
Sweetums, the monster
Dr. Strangepork, the pig
Link Hogthrob, the pig
Zoot, the human
Dr. Bunsen Honeydew, the human
Beaker, the human
Swedish Chef, the human

Animal, the animal
Beaker, the human
Camilla, the chicken
Dr. Bunsen Honeydew, the human
Dr. Strangepork, the pig
Fozzie, the bear
Gonzo, the whatever
Kermit, the frog
Link Hogthrob, the pig
Piggy, the pig
Robin, the frog
Sam, the eagle
Swedish Chef, the human
Sweetums, the monster
Zoot, the human

Kermit, the frog
Gonzo, the whatever
Couldn't find Janice.

2.5. hsearch 函数

The hsearch function

本章到目前为止提到的函数用于在已排序或未排序的数组中进行搜索。还有其他方法可以组织信息,以后应该搜索这些信息。插入、删除和搜索的成本不同。一种可能的实现是使用散列表。以下函数在头文件 search.h 中声明。

函数:int hcreate (size_t nel)

Preliminary: | MT-Unsafe race:hsearch | AS-Unsafe heap | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

hcreate 函数创建一个至少可以包含 nel 元素的哈希表。没有可能扩大该表,因此有必要明智地选择 nel 的值。用于实现此功能的方法可能需要使散列表中的元素数量大于预期的最大元素数量。如果填充 80% 或更多,散列表通常会效率低下。只有在很少存在冲突的情况下,才能实现由散列保证的恒定访问时间。有关更多信息,请参阅 Knuth 的“计算机编程的艺术,第 3 部分:搜索和排序”。

这个函数最薄弱的地方是整个程序最多只能使用一个哈希表。该表分配在本地内存中,不受程序员的控制。作为扩展,GNU C 库提供了一组带有可重入接口的附加函数,该接口提供了类似的接口,但允许保留任意多个哈希表。

如果前一个表首先通过调用 hdestroy 被销毁,则可以在程序运行中使用多个哈希表。

如果成功,该函数将返回一个非零值。如果它返回零,则说明出现问题。这可能意味着已经有一个哈希表在使用中,或者程序内存不足。

函数:void hdestroy (void)

Preliminary: | MT-Unsafe race:hsearch | AS-Unsafe heap | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

hdestroy 函数可用于释放在先前调用 hcreate 中分配的所有资源。调用此函数后,可以再次调用 hcreate 并分配一个大小可能不同的新表。

重要的是要记住,在调用 hdestroy 时哈希表中包含的元素不会被此函数释放。程序代码有责任释放这些字符串(如果有必要的话)。如果没有额外的、单独保存的信息,就不可能释放所有元素内存,因为没有函数可以遍历散列表中的所有可用元素。如果确实需要释放一个表和所有元素,程序员必须保留所有表元素的列表,并且在调用 hdestroy 之前,他/她必须使用此列表释放所有元素的数据。这是一个非常令人不快的机制,它也表明这种散列表主要用于创建一次并使用到程序运行结束的表。

哈希表的条目和搜索的键是使用这种类型定义的:

数据类型: ENTRY

char *key

指向以零结尾的字符串的指针,该字符串描述搜索的键或散列表中的元素。

这是对 hsearch 函数功能的限制:它们只能用于始终使用 NUL 字符且仅用于终止键的数据集。无法处理密钥的一般二进制数据。

void *data

供应用程序使用的通用指针。哈希表实现在条目中保留这个指针,但不以任何方式使用它。

数据类型: struct entry

ENTRY 的基础类型。

函数:ENTRY * hsearch (ENTRY item, ACTION action)

Preliminary: | MT-Unsafe race:hsearch | AS-Unsafe | AC-Unsafe corrupt/action==ENTER | See POSIX Safety Concepts.

要在使用 hcreate 创建的散列表中进行搜索,必须使用 hsearch 函数。这个函数可以对一个元素执行简单的搜索(如果 action 的值为 FIND),或者它也可以将关键元素插入到散列表中。条目永远不会被替换。

键由指向 ENTRY 类型对象的指针表示。为了在散列表中定位相应位置,仅使用结构的关键元素。

如果找到具有匹配键的条目,则操作参数无关紧要。返回找到的条目。如果没有找到匹配的条目并且 action 参数的值为 FIND,则函数返回一个 NULL 指针。如果未找到条目并且操作参数的值为 ENTER,则将新条目添加到使用参数项初始化的散列表中。返回指向新添加条目的指针。

如前所述,到目前为止所描述的函数所使用的哈希表是全局的,程序中任何时候最多可以有一个哈希表。一种解决方案是使用以下作为 GNU 扩展的函数。所有这些都有一个共同点,即它们对哈希表进行操作,该哈希表由 struct hsearch_data 类型的对象的内容描述。这种类型应该被视为不透明的,它的任何成员都不应该直接改变。

函数:int hcreate_r (size_t nel, struct hsearch_data *htab)

Preliminary: | MT-Safe race:htab | AS-Unsafe heap | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

hcreate_r 函数将 htab 指向的对象初始化为包含至少具有 nel 元素的散列表。所以这个函数等价于hcreate函数,只是初始化的数据结构是由用户控制的。

这允许一次拥有多个哈希表。struct hsearch_data 对象所需的内存可以动态分配。在调用此函数之前,必须将其初始化为零。

如果操作成功,则返回值非零。如果返回值为零,则说明出现问题,这可能意味着程序内存不足。

函数:void hdestroy_r (struct hsearch_data *htab)

Preliminary: | MT-Safe race:htab | AS-Unsafe heap | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

hdestroy_r 函数释放 hcreate_r 函数为这个相同的对象 htab 分配的所有资源。至于 hdestroy,程序负责释放表元素的字符串。

函数:int hsearch_r (ENTRY item, ACTION action, ENTRY **retval, struct hsearch_data *htab)

Preliminary: | MT-Safe race:htab | AS-Safe | AC-Unsafe corrupt/action==ENTER | See POSIX Safety Concepts.

hsearch_r 函数等效于 hsearch。前两个参数的含义是相同的。但是,该函数不是在单个全局哈希表上操作,而是在由 htab 指向的对象(通过调用 hcreate_r 进行初始化)所描述的表上工作。

与 hcreate 的另一个区别是指向表中找到的条目的指针不是函数的返回值。它通过将其存储在 retval 参数指向的指针变量中来返回。函数的返回值是一个整数值,如果它非零则表示成功,如果它为零则表示失败。在后一种情况下,全局变量 errno 表示失败的原因。

ENOMEM

该表已填满,并且使用迄今为止未知的键和设置为 ENTER 的操作调用了 hsearch_r。

ESRCH

action 参数是 FIND,并且在表中没有找到对应的元素。

2.6. tsearch 函数

The tsearch function

组织数据以进行有效搜索的另一种常见形式是使用树。tsearch 函数族通过提供与元素数量的对数成正比的平均访问时间,为函数提供了一个很好的接口来组织可能大量的数据。GNU C 库实现甚至保证永远不会超过这个界限,即使对于导致简单二叉树实现出现问题的输入数据也是如此。

本章中描述的功能都在 System V 和 X/Open 规范中进行了描述,因此具有很强的可移植性。

与 hsearch 函数相比,tsearch 函数可用于任意数据,而不仅仅是以零结尾的字符串。

tsearch 函数的优点是不需要初始化数据结构的函数。初始化为 NULL 的 void * 类型的简单指针是一棵有效的树,可以扩展或搜索。这些函数的原型可以在头文件 search.h 中找到。

函数:void * tsearch (const void *key, void **rootp, comparison_fn_t compar)

Preliminary: | MT-Safe race:rootp | AS-Unsafe heap | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

tsearch 函数在 *rootp 指向的树中搜索匹配键的元素。compar 指向的函数用于判断两个元素是否匹配。有关可用于比较参数的函数的规范,请参阅定义比较函数

如果树不包含匹配的条目,则键值将被添加到树中。tsearch 不会复制 key 指向的对象(因为大小未知,怎么可能)。相反,它添加了对该对象的引用,这意味着只要使用树数据结构,该对象就必须可用。

树由指向指针的指针表示,因为有时需要更改树的根节点。所以千万不能假设rootp指向的变量在调用后具有相同的值。这也表明使用同一棵树同时多次调用 tsearch 函数是不安全的。在不同的树上一次运行不止一次是没有问题的。

返回值是指向树中匹配元素的指针。如果创建了新元素,则指针指向新数据(实际上是键)。如果必须创建一个条目并且程序空间不足,则返回 NULL。

函数:void * tfind (const void *key, void *const *rootp, comparison_fn_t compar)

Preliminary: | MT-Safe race:rootp | AS-Safe | AC-Safe | See POSIX Safety Concepts.

tfind 函数类似于 tsearch 函数。它定位与 key 指向的元素匹配的元素,并返回指向该元素的指针。但是如果没有匹配元素可用,则不会输入新元素(注意 rootp 参数指向一个常量指针)。相反,该函数返回 NULL。

与 hsearch 函数相比,tsearch 函数的另一个优点是可以轻松删除元素。

函数:void * tdelete (const void *key, void **rootp, comparison_fn_t compar)

Preliminary: | MT-Safe race:rootp | AS-Unsafe heap | AC-Unsafe corrupt mem | See POSIX Safety Concepts.

要从树中删除特定元素匹配键,可以使用 tdelete。它使用与 tfind 相同的方法定位匹配元素。然后删除相应的元素,并且该函数返回指向已删除节点的父节点的指针。如果树中没有匹配的条目,则无法删除任何内容,并且该函数返回 NULL。如果树的根被删除,则 tdelete 返回一些不等于 NULL 的未指定值。

函数:void tdestroy (void *vroot, __free_fn_t freefct)

Preliminary: | MT-Safe | AS-Unsafe heap | AC-Unsafe mem | See POSIX Safety Concepts.

如果必须删除完整的搜索树,可以使用 tdestroy。它释放 tsearch 函数分配的所有资源以生成 vroot 指向的树。

对于每个树节点中的数据,调用函数 freefct。指向数据的指针作为参数传递给函数。如果不需要这样的工作,freefct 必须指向一个什么都不做的函数。在任何情况下都会调用它。

此功能是 GNU 扩展,不包含在 System V 或 X/Open 规范中。

除了创建和销毁树数据结构的函数之外,还有另一个函数允许您将函数应用于树的所有元素。该函数必须具有以下类型:

void __action_fn_t (const void *nodep, VISIT value, int level);

nodep 是当前节点的数据值(曾经作为 tsearch 的关键参数给出)。level 是一个数值,对应于树中当前节点的深度。根节点的深度为 0,其子节点的深度为 1,依此类推。VISIT 类型是枚举类型。

数据类型:VISIT

VISIT 值指示树中当前节点的状态以及函数的调用方式。节点的状态是“叶”或“内部节点”。对于每个叶节点,该函数只被调用一次,对于每个内部节点,它被调用 3 次:在处理第一个子节点之前、处理第一个子节点之后以及处理两个子节点之后。这使得处理树遍历的所有三种方法(甚至它们的组合)成为可能。

preorder

当前节点是一个内部节点,并且在处理第一个子节点之前调用该函数。

postorder

当前节点是一个内部节点,在处理第一个子节点后调用该函数。

endorder

当前节点是内部节点,在处理第二个子节点后调用该函数。

leaf

当前节点是叶子。

函数:void twalk (const void *root, __action_fn_t action)

Preliminary: | MT-Safe race:root | AS-Safe | AC-Safe | See POSIX Safety Concepts.

对于树中每个有root指向的节点的节点,twalk函数调用参数action提供的函数。对于叶节点,该函数只被调用一次,其值设置为叶。对于内部节点,该函数被调用 3 次,将 value 参数或操作设置为适当的值。动作函数的级别参数是在下降树时计算的,方法是每次下降到一个子节点时将值加一,从根节点的值 0 开始。

由于用于 twalk 的 action 参数的函数不得修改树数据,因此在多个线程中同时运行 twalk 是安全的,在同一棵树上工作。并行调用 tfind 也是安全的。不得使用修改树的函数,否则行为未定义。但是,如果不借助全局变量(和线程安全问题),很难将树外部的数据传递给回调函数,因此请参见下面的 twalk_r 函数。

函数:void twalk_r (const void *root, void (*action) (const void *key, VISIT which, void *closure), void *closure)

Preliminary: | MT-Safe race:root | AS-Safe | AC-Safe | See POSIX Safety Concepts.

对于树中每个有root指向的节点的节点,twalk_r函数调用参数action提供的函数。对于叶子节点,该函数只调用一次,设置为叶子。对于内部节点,该函数被调用 3 次,将操作的 which 参数设置为适当的值。闭包参数未经修改地传递给动作函数的每次调用。

可以在 twalk_r 函数之上实现 twalk 函数,这就是为什么没有单独的 level 参数的原因。

#include <search.h>

struct twalk_with_twalk_r_closure
{
  void (*action) (const void *, VISIT, int);
  int depth;
};

static void
twalk_with_twalk_r_action (const void *nodep, VISIT which, void *closure0)
{
  struct twalk_with_twalk_r_closure *closure = closure0;

  switch (which)
    {
    case leaf:
      closure->action (nodep, which, closure->depth);
      break;
    case preorder:
      closure->action (nodep, which, closure->depth);
      ++closure->depth;
      break;
    case postorder:
      /* The preorder action incremented the depth. */
      closure->action (nodep, which, closure->depth - 1);
      break;
    case endorder:
      --closure->depth;
      closure->action (nodep, which, closure->depth);
      break;
    }
}

void
twalk (const void *root, void (*action) (const void *, VISIT, int))
{
  struct twalk_with_twalk_r_closure closure = { action, 0 };
  twalk_r (root, twalk_with_twalk_r_action, &closure);
}

3. 参考

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值