双向链表是链式存储结构的一种,每个节点除了存储自身数据,还会保存指向前一个节点和后一个节点的引用,这种设计让链表在任意位置的插入和删除操作都可以避免大量元素的移动。.NET框架中的LinkedList就是基于这种双向链表结构实现的泛型集合,适合需要频繁修改集合内容的场景。

LinkedList的核心节点结构
LinkedList的底层核心是LinkedListNode<T>节点类,每个节点包含数据、前驱节点引用和后继节点引用三个核心部分,同时还会关联到所属的LinkedList实例,避免节点被随意插入到不匹配的链表中。下面是简化后的节点结构示意:
internal sealed class LinkedListNode<T>
{
// 节点存储的实际数据
internal T item;
// 指向前一个节点的引用
internal LinkedListNode<T> prev;
// 指向后一个节点的引用
internal LinkedListNode<T> next;
// 该节点所属的LinkedList实例
internal LinkedList<T> list;
// 构造函数,初始化节点数据和所属链表
internal LinkedListNode(T value, LinkedList<T> linkedList)
{
item = value;
list = linkedList;
}
// 对外暴露的属性,获取节点数据
public T Value
{
get { return item; }
set { item = value; }
}
// 获取前驱节点
public LinkedListNode<T> Previous
{
get { return prev; }
}
// 获取后继节点
public LinkedListNode<T> Next
{
get { return next; }
}
// 获取所属链表
public LinkedList<T> List
{
get { return list; }
}
}LinkedList的核心属性与字段
LinkedList类本身会维护链表的头节点、尾节点和节点数量,通过这些字段可以快速获取链表的基本信息,同时保证操作的正确性。核心字段和属性如下:
- head:链表头节点引用,第一个节点的prev为null
- tail:链表尾节点引用,最后一个节点的next为null
- count:链表中节点的总数量
- Count属性:对外暴露的节点数量,直接返回count字段的值
- First属性:返回头节点head
- Last属性:返回尾节点tail
核心操作实现逻辑
添加节点操作
LinkedList提供了在头部、尾部以及指定节点前后添加节点的方法,所有添加操作都会创建新的LinkedListNode实例,然后调整相关节点的前后引用关系,不需要移动其他节点。以在链表尾部添加节点为例,实现逻辑如下:
public LinkedListNode<T> AddLast(T value)
{
// 创建新的节点,关联到当前链表
LinkedListNode<T> newNode = new LinkedListNode<T>value, this);
if (head == null)
{
// 如果链表为空,新节点同时作为头节点和尾节点
head = newNode;
tail = newNode;
}
else
{
// 否则将新节点链接到尾节点之后,更新尾节点引用
tail.next = newNode;
newNode.prev = tail;
tail = newNode;
}
count++;
return newNode;
}删除节点操作
删除节点时,只需要调整被删除节点前后节点的引用关系,跳过被删除的节点即可,时间复杂度为O(1),前提是需要拿到要删除的节点引用。如果是按值删除,需要先遍历找到对应节点,时间复杂度为O(n)。下面是在任意位置删除节点的核心逻辑:
public void Remove(LinkedListNode<T> node)
{
// 校验节点是否属于当前链表
if (node == null || node.list != this)
{
throw new InvalidOperationException("节点不属于当前链表");
}
LinkedListNode<T> prevNode = node.prev;
LinkedListNode<T> nextNode = node.next;
if (prevNode != null)
{
// 调整前驱节点的后继引用
prevNode.next = nextNode;
}
else
{
// 如果被删除的是头节点,更新头节点引用
head = nextNode;
}
if (nextNode != null)
{
// 调整后继节点的前驱引用
nextNode.prev = prevNode;
}
else
{
// 如果被删除的是尾节点,更新尾节点引用
tail = prevNode;
}
// 清空被删除节点的关联信息
node.prev = null;
node.next = null;
node.list = null;
count--;
}查找节点操作
LinkedList没有像List那样支持索引访问,查找节点需要从头节点或者尾节点开始逐个遍历,直到找到匹配的节点或者遍历完整个链表。按值查找的实现逻辑如下:
public LinkedListNode<T> Find(T value)
{
// 从前往后遍历节点
LinkedListNode<T> current = head;
// 处理值可能为null的情况
if (value == null)
{
while (current != null)
{
if (current.item == null)
{
return current;
}
current = current.next;
}
}
else
{
// 使用默认比较器比较值
EqualityComparer<T> comparer = EqualityComparer<T>.Default;
while (current != null)
{
if (comparer.Equals(current.item, value))
{
return current;
}
current = current.next;
}
}
return null;
}LinkedList与List的对比
两种集合的设计思路不同,适用场景也有明显区别,具体对比如下:
| 对比维度 | LinkedList | List<T> |
|---|---|---|
| 内存结构 | 非连续内存,节点分散存储 | 连续内存,基于数组实现 |
| 插入删除效率 | 已知节点位置时O(1),按值查找后删除为O(n) | 非尾部位置操作需要移动元素,平均O(n) |
| 查找效率 | 只能遍历查找,O(n) | 支持索引访问O(1),二分查找O(log n) |
| 内存开销 | 每个节点额外存储前后引用,开销更大 | 仅存储数据和数组额外容量,开销更小 |
| 适用场景 | 频繁插入删除、不需要随机访问的场景 | 频繁查询、尾部添加为主的场景 |
使用注意事项
- LinkedList的节点不能跨链表使用,每个节点都绑定了所属的链表实例,强行插入其他链表的节点会抛出异常
- 遍历LinkedList时如果使用foreach,不要直接在遍历过程中修改链表结构,否则会触发迭代器异常,需要修改的话可以先记录要操作的节点,遍历完成后再处理
- 如果需要频繁按值查找然后操作节点,LinkedList的效率不如Dictionary或者HashSet配合List使用,要根据实际场景选择
LinkedList双向链表数据结构.NET集合修改时间:2026-06-02 21:20:19