You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET中‘双指针列表’是什么?如何实现并使用该O(1)删除的List<T>?

Hey there! Great question about this lesser-known List<T> variant—double-pointer lists are super handy when you need fast deletions without the overhead of shifting elements like a regular List<T> does. Let's walk through exactly what they are, how to implement one, and how to use it.

什么是双指针列表?

A double-pointer list (often called a doubly linked list with a List<T>-like interface) is a data structure where each element (node) keeps track of both its previous and next element in the list. Unlike a standard List<T>—which stores elements in a contiguous array and requires O(n) time to delete elements (since all elements after the deleted one have to be shifted left)—a double-pointer list lets you delete any node in O(1) time. You just adjust the pointers of the neighboring nodes to skip over the deleted one.

核心实现思路
  • Each node contains three parts: T Value (the actual data), Node<T> Previous (pointer to the prior node), Node<T> Next (pointer to the next node)
  • The list itself tracks the Head (first node) and Tail (last node) to make adding elements to either end fast
  • To unlock O(1) deletions, you need direct access to the node (not just its index or value). If you delete by index/value, it's still O(n) because you have to traverse to find the node first—but with a node reference, deletion is instant.
完整C#实现示例

Here's a production-ready implementation that mirrors common List<T> methods while leveraging double-pointer logic:

public class DoublePointerList<T>
{
    // Internal node class to hold data and pointers
    private class Node<TNode>
    {
        public TNode Value { get; set; }
        public Node<TNode> Previous { get; set; }
        public Node<TNode> Next { get; set; }

        public Node(TNode value)
        {
            Value = value;
            Previous = null;
            Next = null;
        }
    }

    private Node<T> _head;
    private Node<T> _tail;
    public int Count { get; private set; }

    // Add an element to the end of the list
    public void Add(T value)
    {
        var newNode = new Node<T>(value);
        if (_tail == null)
        {
            // List is empty, head and tail are the same
            _head = newNode;
            _tail = newNode;
        }
        else
        {
            _tail.Next = newNode;
            newNode.Previous = _tail;
            _tail = newNode;
        }
        Count++;
    }

    // Add an element to the start of the list
    public void AddFirst(T value)
    {
        var newNode = new Node<T>(value);
        if (_head == null)
        {
            _head = newNode;
            _tail = newNode;
        }
        else
        {
            _head.Previous = newNode;
            newNode.Next = _head;
            _head = newNode;
        }
        Count++;
    }

    // Delete a node by reference (O(1) time)
    public bool Remove(Node<T> nodeToRemove)
    {
        if (nodeToRemove == null)
            return false;

        // Update head if we're deleting the first node
        if (nodeToRemove == _head)
            _head = nodeToRemove.Next;

        // Update tail if we're deleting the last node
        if (nodeToRemove == _tail)
            _tail = nodeToRemove.Previous;

        // Adjust neighboring pointers to skip the deleted node
        if (nodeToRemove.Previous != null)
            nodeToRemove.Previous.Next = nodeToRemove.Next;

        if (nodeToRemove.Next != null)
            nodeToRemove.Next.Previous = nodeToRemove.Previous;

        Count--;
        return true;
    }

    // Delete by value (O(n) time—we have to find the node first)
    public bool Remove(T value)
    {
        var current = _head;
        while (current != null)
        {
            if (EqualityComparer<T>.Default.Equals(current.Value, value))
            {
                return Remove(current);
            }
            current = current.Next;
        }
        return false;
    }

    // Get node by index (O(n) time—optimized to start from head/tail)
    public Node<T> GetNode(int index)
    {
        if (index < 0 || index >= Count)
            throw new ArgumentOutOfRangeException(nameof(index));

        Node<T> current;
        // Traverse from the closer end to save time
        if (index < Count / 2)
        {
            current = _head;
            for (int i = 0; i < index; i++)
                current = current.Next;
        }
        else
        {
            current = _tail;
            for (int i = Count - 1; i > index; i--)
                current = current.Previous;
        }
        return current;
    }

    // Get value by index
    public T Get(int index)
    {
        return GetNode(index).Value;
    }

    // Traverse the list forward
    public IEnumerable<T> TraverseForward()
    {
        var current = _head;
        while (current != null)
        {
            yield return current.Value;
            current = current.Next;
        }
    }

    // Traverse the list backward
    public IEnumerable<T> TraverseBackward()
    {
        var current = _tail;
        while (current != null)
        {
            yield return current.Value;
            current = current.Previous;
        }
    }
}
如何使用双指针列表

Here's a quick example showing common operations, including the O(1) deletion magic:

// Initialize a new list
var myList = new DoublePointerList<string>();

// Add elements
myList.Add("Apple");
myList.Add("Banana");
myList.AddFirst("Orange");

// Get the second node (Banana)
var bananaNode = myList.GetNode(1);

// Remove it in O(1) time—no element shifting needed!
myList.Remove(bananaNode);

// Traverse forward to see the result
foreach (var item in myList.TraverseForward())
{
    Console.WriteLine(item); // Output: Orange, Apple
}

// Remove by value (O(n) time since we search first)
myList.Remove("Orange");

// Traverse backward
foreach (var item in myList.TraverseBackward())
{
    Console.WriteLine(item); // Output: Apple
}
Key Tradeoffs to Remember
  • O(1) deletion only works if you have a node reference: Deleting by index/value is still O(n) because you need to find the node first.
  • No O(1) random access: Unlike List<T>, getting an element by index requires traversal (though we optimized it to start from the closer end).
  • Perfect for dynamic collections: This shines in scenarios where you frequently delete elements you're currently iterating over (like event handlers or managing active objects).

内容的提问来源于stack exchange,提问作者Jimmy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 10:20:37