我的理解...
优点:
- 在末尾插入的是O(1)而不是O(N)。
- 如果列表是双链表,则从末尾删除也是O(1)而不是O(N)。
坏处:
- 占用很少的额外内存:4-8个字节。
- 实施者必须跟踪尾巴。
从这些优点和缺点来看,我看不出为什么链表会避免使用尾部指针。有什么我想念的吗?
我的理解...
优点:
坏处:
从这些优点和缺点来看,我看不出为什么链表会避免使用尾部指针。有什么我想念的吗?
Answers:
您是正确的,尾巴指针永远不会伤害您,只会提供帮助。但是,有一种情况根本不需要尾巴指针。
如果使用链表实现堆栈,则不需要尾指针,因为可以保证所有访问,插入和删除都在头部进行。话虽这么说,但无论如何可能会使用带有尾指针的双向链接列表,因为这是库或平台中的标准实现,并且内存很便宜,但是不需要它。
链接列表通常非常持久且不可变。实际上,在函数式编程语言中,这种用法无处不在。尾指针将破坏这两个属性。但是,如果您不关心不变性或持久性,那么包含尾部指针的缺点就很小。
在链接列表中,我很少使用尾部指针,而在插入和删除(或从中间的线性时间删除)的堆栈式推/弹出模式足以满足需要的情况下,我更倾向于使用单链接列表。这是因为在我的常见用例中,尾指针实际上是昂贵的,就像将单链接列表转换为双链接列表一样昂贵。
通常,我对单链接列表的用法可能会存储成千上万个链接列表,每个链接列表仅包含几个列表节点。我通常也不会对链接列表使用指针。我将索引改为数组,因为索引可以是32位的,例如占用64位指针一半的空间。我通常也不会一次分配一个列表节点,而是再次使用一个大数组存储所有节点,然后使用32位索引将这些节点链接在一起。
例如,假设有一个视频游戏使用400x400网格来划分一百万个粒子,这些粒子四处移动并彼此反弹以加速碰撞检测。在那种情况下,一种非常有效的存储方式是存储160,000个单链接列表,在我的情况下,它转换为160,000个32位整数(〜640 KB),每个粒子转换一个32位整数的开销。现在,当粒子在屏幕上移动时,我们要做的就是更新一些32位整数,以将粒子从一个单元格移动到另一个单元格,如下所示:
...带有next
粒子节点的索引(“指针”),用作该单元中下一个粒子的索引或如果该粒子已死亡则要回收的下一个自由粒子(基本上是使用索引的自由列表分配器实现):
从单元中线性时间删除实际上并不是开销,因为我们通过迭代单元中的粒子来处理粒子逻辑,因此,双链表只会增加这种开销,而这种开销对就我而言,就像一条尾巴也根本不会使我受益。
尾指针将使网格的内存使用量增加一倍,并增加高速缓存未命中的数量。它还需要插入,以要求分支检查列表是否为空而不是无分支。使其成为双向链接列表将使每个粒子的列表开销增加一倍。我有90%的时间使用链表,这种情况适用于此类情况,因此尾部指针的存储实际上相对而言是相当昂贵的。
因此,在我首先使用链接列表的大多数情况下,4-8个字节实际上并不是一件容易的事。只是想插入那里,因为如果您使用数据结构存储大量元素,那么4-8字节可能并不总是那么微不足道。实际上,我使用链表来减少内存分配的数量和所需的内存量,而不是说存储160,000个为网格增长的动态数组,这些数组将具有爆炸性的内存使用(通常每个网格单元至少一个指针加两个整数)以及每个网格单元的堆分配,而不是每个单元只有一个整数和零堆分配)。
我经常发现许多人因其与前/中间删除和前/中间插入相关的恒定时间复杂性而到达链接列表,而在这种情况下,由于LL通常缺乏连续性,因此它们通常不是一个好的选择。从性能的角度来看,LL对我而言最美丽的地方是能够通过操作几个指针而将一个元素从一个列表移到另一个列表,并且能够在不使用可变大小的内存分配器的情况下实现可变大小的数据结构(因为每个节点都有一个统一的大小,例如,我们可以使用自由列表。如果每个列表节点都是针对通用分配器进行单独分配的,那么通常情况下,链表的价格要比其他方法差得多,
相反,我建议在大多数情况下,链表是对直接替代方法的非常有效的优化,最有用的形式通常是单链的,只需要一个头指针,并且不需要为每个对象分配通用内存节点通常可以仅池已为每个节点分配的内存(例如,从预先已分配的大数组中)。同样,在这种情况下,每个SLL通常会存储很少数量的元素,例如连接到图节点的边(许多微小的链表,而不是一个庞大的链表)。
还要记住,这些天我们有大量的DRAM,但这是第二慢的内存类型。对于带有64字节高速缓存行的L1高速缓存,我们每个内核仍然只有64 KB。结果,如果这意味着在性能至关重要的区域(如上面的粒子模拟)倍增数百万倍,那么节省少量的字节就真的很重要,如果这意味着在高速缓存行中存储或不存储两倍的节点之间的差异,例如