[...] 并想了解更多使用链表的原因。我不是全职程序员,但我可以编写代码并且一直在努力学习。我确实了解基本的 C/C++,但从来没有经验或不得不使用链表。
链表实际上是一种极其简单的数据结构。它们有几种类型,但总体概念只是分配节点并通过索引或指针将它们链接在一起,如下所示:
为什么在这种情况下使用链表?
链表有一些有趣的属性,上图中提到了其中一个属性,例如从/到中间的恒定时间删除和插入。
为什么不只是创建结构并将其存储在数组中?
他们实际上可能是。在上图中,节点可以直接存储在数组中。然后链接节点的目的是允许快速插入和删除。元素数组不提供这种灵活性,但如果您存储一个节点数组,其中存储索引或指向next 和可能previous 元素的指针,那么您可以开始重新排列结构并删除内容并将内容插入到只需使用链接,就可以在恒定时间内完成所有操作。
链表最有效的用法通常是连续或部分连续地存储节点(例如:使用空闲链表)并将它们链接在一起以允许快速插入和删除。您可以将节点存储在一个大数组中,例如vector,然后将它们链接起来并通过索引取消链接。链表的另一个有趣特性是,您只需更改几个指针即可将元素从一个链表的中间快速转移到另一个链表。
它们还有一个属性,当每个节点都具有相同的大小时,它们在注意分配时可以非常有效地连续存储。例如,如果它们都使用自己的类似数组的容器,那么有效地表示一堆可变大小的存储桶可能会很棘手,因为每个存储桶都希望分配不同数量的内存。但是,如果他们只存储一个指向列表节点的索引/指针,他们可以轻松地将所有节点存储在一个巨大的数组中,用于所有存储桶。
也就是说,在 C++ 中,链表经常被误用。尽管它们在算法上有好处,但如果节点没有以提供空间局部性的方式分配,其中很多实际上并不能转化为卓越的性能。否则,您可能会导致缓存未命中,并且可能会在访问每个节点时出现一些页面错误。
尽管如此,小心使用节点在内存中的位置,它们可能非常有用。这是一个示例用法:
在这种情况下,我们可能有一个粒子模拟,其中每个粒子都在每个帧周围移动,并带有碰撞检测,我们将屏幕划分为网格单元。这使我们能够避免二次复杂性碰撞检测,因为一个粒子只需要检查与同一单元中的其他粒子的碰撞。实际版本可能存储 100x100 个网格单元(10,000 个网格单元)。
但是,如果我们对所有 10,000 个网格单元使用像 std::vector 这样的基于数组的数据结构,那将在内存中爆炸。最重要的是,将每个粒子从一个细胞转移到另一个细胞将是一项代价高昂的线性时间操作。通过在此处使用链表(以及仅将整数用于链接的数组),我们可以在这里和那里更改一些索引(指针),以便在粒子移动时将粒子从一个单元格转移到另一个单元格,而内存使用非常便宜(10,000 个网格单元意味着 10,000 个 32 位整数,转换为大约 39 KB,每个粒子的链接开销为 4 字节)。
谨慎使用,链表是一种非常有用的结构。但是,它们经常会被滥用,因为想要针对通用内存分配器分别分配每个单个节点的幼稚实现往往会导致大量缓存未命中,因为节点在内存中会非常碎片化。链表的用处往往是最近被遗忘的一个细节,尤其是在 C++ 中,因为 std::list 实现,除非与自定义分配器一起使用,否则属于那种幼稚的缓存未命中类别。但是,它们在操作系统中的使用方式往往非常高效,在获得上述算法优势的同时又不会失去参考的局部性。