【问题标题】:If a dynamic data structure like this is possible, why do we need a linked-list?如果像这样的动态数据结构是可能的,为什么我们需要一个链表?
【发布时间】:2014-05-07 07:38:20
【问题描述】:

如果我们有 std::setstd::vector 可以动态增长和收缩,为什么我们需要链表?

注意我真的不明白,为什么会有这么多反对票。 投反对票的,请离开 cmets。

【问题讨论】:

标签: c++ arrays dynamic data-structures linked-list


【解决方案1】:

为什么我们需要链表?

链表是 O(1) 的插入、删除、连接等。链表变大了吗?那又怎样,系统保持正常运行。只是计划不需要随机访问。

向量以(可能)二进制块的形式增长,因此时间命中增长到相当大且相当快的东西。这是一个公认的罕见的时间点击。

在 Ubuntu-15.04 上的旧戴尔 g++ 4.9.2 的以下输出中,最后一列是自上次容量更改以来的毫秒时间。 (最后一列匆忙添加,以便为您重复使用示例;)

     element         vector     bytes stack use         bytes heap use
      count       capacity    'sizeof(vector)'     (element count * element size)
          0              0                24                       0         0
          1              1                24                       4         0
          2              2                24                       8         0
          3              4                24                      12         0
          5              8                24                      20         0
          9             16                24                      36         0
         17             32                24                      68         0
         33             64                24                     132         0
         65            128                24                     260         1
        129            256                24                     516         0
        257            512                24                   1,028         0
        513          1,024                24                   2,052         0
      1,025          2,048                24                   4,100         0
      2,049          4,096                24                   8,196         0
      4,097          8,192                24                  16,388         1
      8,193         16,384                24                  32,772         2
     16,385         32,768                24                  65,540         3
     32,769         65,536                24                 131,076         8
     65,537        131,072                24                 262,148        12
    131,073        262,144                24                 524,292        28
    262,145        524,288                24               1,048,580        50
    524,289      1,048,576                24               2,097,156        38
  1,048,577      2,097,152                24               4,194,308        76
  2,097,153      4,194,304                24               8,388,612       149
  4,194,305      8,388,608                24              16,777,220       300
  8,388,609     16,777,216                24              33,554,436       601
 16,777,217     33,554,432                24              67,108,868      1215
 33,554,433     67,108,864                24             134,217,732      2475
 67,108,865    134,217,728                24             268,435,460      4982
134,217,729    268,435,456                24             536,870,916     10247
268,435,457    536,870,912                24           1,073,741,828     21387

这是足够的理由吗?我不知道。

我将向量用于我的列表(它几乎永远不会增长到 Gigabyte 大小,当我找到最大大小时我添加了 Reserve 的使用)、我的 fifo(处理文件和目录创建了数百万个字符串),并且通常忽略这个列表,因为我使用向量的速度足够快(哎呀,不敢相信我刚刚大声说出来)。

此表中的最后一个条目是当向量容量增加到 512 K 元素时(从 512 K - 1)。分配、数据复制和擦除(使用小对象的 noop dtors)但没有更多的 push_back()s,导致堆栈上的 1 G 字节,耗时 21 秒。

(我想我不再希望在这台旧机器上安装 8 Gig。)


编辑 - 2015 年 7 月 1 日

我重新运行此代码以重新生成表格的最后 2 行并捕获挂钟信息。

134,217,729    268,435,456                24             536,870,916      9769
268,435,457    536,870,912                24           1,073,741,828     20983

real 0m41.147s
user 0m39.928s
sys  0m1.164s

挂钟时间报告为 41 秒。

产生最后一条输出线需要 21 秒(以毫秒为单位的实时时钟测量),并且

20 秒产生第 1 29 行...这不只是指数!


编辑 - 2015 年 7 月 3 日 - 认为 OP 需要一些关于集合与链表的 cmets。

如果我们有可以动态增长和收缩的 std::set [and ...],为什么 我们需要链表吗?

来自 cppreference.com:

std::set 是一个关联容器,它包含一个排序的集合 Key 类型的唯一对象。排序是使用键比较完成的 功能比较。搜索、删除和插入操作 对数复杂度。集合通常实现为红黑 树。

一方面,集合(我很少使用)有点像链表,其中会有节点(当实现为红黑树时),每个节点都是独立分配的。当集合变大时,系统会保持相对正常的运行(始终如一?)(至少在节点涉入交换之前(我说的是交换,不是沼泽)。

恕我直言,一个集合(rbtree)没有“工作积压”。相反,树在每次插入/追加/删除工作上取得平衡。 (我无法通过一点点努力来挑逗一些证据......对不起,没有桌子可以看。)

当您查看红黑树时,您会发现 rbtree 不包含数据,只有键(和颜色)。

我可以想象一个链表,其中只有数据中的键和颜色(如集合)。软件非常灵活。但我的想象力现在失败了。

我非常欣赏关联容器的想法。我发现 std::map 非常有用,它影响了我对几种软件挑战的看法。

但是,我还没有研究过这张地图是如何发挥它的魔力的。多学习。

【讨论】:

    【解决方案2】:

    除了固定的插入/删除/拼接时间之外,链表还提供了另一个有用的属性——当列表被改变时迭代器不会失效(当然,指向已删除元素的迭代器除外。因此,如果你有一个链表,你可以保留一个迭代器作为指向列表的指针。

    虽然您可以通过存储元素的索引来获得与向量类似的效果,但使用列表并存储迭代器的优点是,无论列表发生什么,只要元素仍然存在,迭代器将始终指向同一个元素。如果您使用了向量,则如果向量中较早的元素被删除,则需要修复索引。

    【讨论】:

      【解决方案3】:

      虽然目前给出的所有答案都是正确的,但让我尝试用适合 C++ 初学者的语言来解释它。

      std::vector(或一般的数组)的优点是您可以在相对较短的时间内通过索引访问它的任何元素。然而,其中一个弱点是在靠近开始中间的位置插入元素的成本相对较高。在这种情况下,需要分配更多存储空间,并且必须将插入的元素之后的所有元素复制到它们的新位置。在最坏的情况下,无法分配额外的空间,必须将整个数组复制到新位置。

      相比之下,访问列表的第 1000 个元素相对较慢,因为我们必须跟随 999 个链接到列表的下一个元素。但是,在该位置插入新元素相对容易且快速。我们只需要为新元素分配空间并修改几个指针。

      您会看到,向量有其特定的优势,列表也是如此。

      【讨论】:

        【解决方案4】:

        因为有时我们在某些特定情况下需要 O(1) 的插入/擦除性能。我们无法通过setvector 实现目标。

        +========+=========+=========+=========+===============+
        |        | Insert  | Erase   | Lookup  | Iterator      |
        +========+=========+=========+=========+===============+
        | vector | O(N)    | O(N)    | O(1)    | Random Access |
        +--------+---------+---------+---------+---------------+
        | set    | O(logN) | O(logN) | O(logN) | Sequential    |
        +--------+---------+---------+---------+---------------+
        | list   | O(1)    | O(1)    | O(N)    | Sequential    |
        +--------+---------+---------+---------+---------------+
        

        【讨论】:

          【解决方案5】:

          两者都不是 - 它是一个带有长度注释的固定大小数组,以避免缓冲区溢出。

          它确实有办法调整它的大小,但是 Resize 方法破坏性地擦除了内部数组,这有点奇怪。它应该分配新的目的地,复制成员,然后释放原始数组。

          【讨论】:

            猜你喜欢
            • 2011-04-05
            • 1970-01-01
            • 2017-08-27
            • 1970-01-01
            • 1970-01-01
            • 2012-10-11
            • 2013-06-03
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多