链表粗解

1. 链表简介

链表是一种线性数据结构,由一系列节点组成。每个节点包含数据和指向下一个节点的引用,形成节点之间的链式连接。与数组相比,链表具有以下特点:

  • 动态内存管理:
    可根据需要动态分配内存,避免预先分配固定大小。
  • 高效插入删除:
    在任意位置(已知前驱节点)插入或删除节点时,只需要更新指针,操作复杂度为 O(1)。
  • 顺序访问:
    由于节点不连续存储,访问链表中任意位置的数据需从头开始遍历,时间复杂度为 O(n)。

常见链表类型:

  • 单向链表 (Singly Linked List)
  • 双向链表 (Doubly Linked List)
  • 循环链表 (Circular Linked List)

2. 链表的数据结构原理

  1. 基本概念
  • 节点(Node): 链表的最基本的元素,每个节点包含数据部分(如代码中的 Val)和指向下一个节点的引用(Next)。
  • 头结点(Head): 链表的开始节点。部分实现中使用哨兵节点(dummy node),如本示例中 _head 始终存在但实际数据存放在 _head.Next 中。
  • 尾结点(Tail): 指向链表中最后一个节点。便于在尾部添加新节点时快速定位,无需从头开始遍历链表。
  1. 优缺点分析
    优点:
  • 动态内存分配: 插入和删除不需要移动其他元素,仅需调整指针,操作复杂度为 O(1)(在已知位置时)。
  • 节省空间: 不必预先定义固定大小,能根据数据量动态增长。

缺点:

  • 顺序存储: 链表不支持高效的随机访问。例如,获取第 i 个元素需要从头顺序查找,时间复杂度为 O(n)。
  • 额外空间: 每个节点需要额外存储一个或多个指针,增加内存开销。
  • 内存碎片化: 链表节点在内存中不是连续分配的,可能导致内存局部性较差。

3. 使用C#实现链表

  1. 单向链表(Singly Linked List)
    1.1 类结构
  • 节点Node:
public class Node
{
    public T Val { get; set; } //存储节点数据
    public Node? Next { get; set; } //指向下一个节点

    public Node(T val)
    {
        Val = val;
        Next = null;
    }
}
  • 私有成员变量
    private readonly Node _head; //哨兵头节点,不直接存储数据,实际数据存放在 _head.Next
    private Node _tail; //指向最后一个节点,便于尾部添加新节点
    private int _size; //维护链表中节点的数量
  • 接口实现: 实现了 IEnumerable 接口,用于支持迭代器模式,使得链表可以使用 foreach 进行遍历。

1.2 主要操作解析:

  • 头部插入:
    ①. 新节点的 Next 指向当前 _head.Next(原首节点)。
    ②. 哨兵节点 _head 的 Next 更新为新节点。
    ③. 处理空链表,更新 _tail 指向新节点。
public void AddFirst(T val) {
    var newNode = new Node(val);
    newNode.Next = _head.Next; // ① 新节点指向原首节点
    _head.Next = newNode;      // ② 哨兵指向新节点
    
    if (_size == 0)            // ③ 处理空链表
        _tail = newNode;
    
    _size++;
}
  • 任意位置插入
    a. 检查索引是否合法(索引值允许在 [0, _size] 之间)。
    b. 若索引为 0,则直接头部插入;若等于 _size,则直接尾部插入。
    c. 否则,定位到目标位置前一个节点,再将新节点插入到正确位置。
public void Insert(int index, T val) {
    CheckPositionIndex(index);
	// 头部插入
    if (index == 0) {
        AddFirst(val);
        return;
    }
	// 尾部插入
    if (index == _size) {
        AddLast(val);
        return;
    }

    Node prev = GetNode(index - 1);  // ① 找到前驱节点
    var newNode = new Node(val);
    newNode.Next = prev.Next;       // ② 新节点指向后续节点
    prev.Next = newNode;            // ③ 前驱节点指向新节点
    _size++;
}
  • 尾部删除
    a. 验证链表是否为空,否则抛出异常。
    b. 通过遍历找到倒数第二个节点。
    c. 将倒数第二个节点的 Next 设为 null,并更新 _tail 指向该节点。
    d. 更新链表节点计数并返回被删除节点的数据。
public T RemoveLast() {
    if (IsEmpty)
        throw new InvalidOperationException();
    
    Node prev = _head;
    while (prev.Next != _tail)  // ① 定位到倒数第二节点
        prev = prev.Next;
    
    T val = _tail.Val;
    prev.Next = null;           // ② 断开尾节点
    _tail = prev;               // ③ 更新尾指针
    _size--;
    return val;
}
  • 索引器访问
    a. 允许直接通过 list[index] 语法获取或设置指定位置的节点数据
public T this[int index] {
    get {
        Node node = GetNode(index);
        return node.Val;
    }
    set {
        Node node = GetNode(index);
        node.Val = value;
    }
}

1.3 完整代码:

public class LinkedList<T> : IEnumerable<T>
{
    public class Node
    {
        public T Val { get; set; }
        public Node? Next { get; set; }

        public Node(T val)
        {
            Val = val;
            Next = null;
        }
    }

    private readonly Node _head; //哨兵头节点,不直接存储数据,实际数据存放在 _head.Next
    private Node _tail; //指向最后一个节点,便于尾部添加新节点
    private int _size; //维护链表中节点的数量

    public LinkedList()
    {
        _head = new Node(default);
        _tail = _head;
        _size = 0;
    }

    public void AddFirst(T val)
    {
        var newNode = new Node(val);
        newNode.Next = _head.Next;
        _head.Next = newNode;
        if (_size == 0)
        {
            _tail = newNode;
        }
        _size++;
    }

    public void AddLast(T val)
    {
        var newNode = new Node(val);
        _tail.Next = newNode;
        _tail = newNode;
        _size++;
    }

    public void Add(int index, T val)
    {
        CheckPositionIndex(index);
        if (index == 0)
        {
            AddFirst(val);
            return;
        }

        if (index == _size)
        {
            AddLast(val);
            return;
        }

        var prev = GetNode(index - 1);
        var newNode = new Node(val);
        newNode.Next = prev.Next;
        prev.Next = newNode;
        _size++;
    }

    public T RemoveFirst()
    {
        if (IsEmpty)
        {
            throw new IndexOutOfRangeException("No elements to remove");
        }

        var first = _head.Next;
        _head.Next = first.Next;
        if (_size == 1)
        {
            _tail = _head;
        }
        _size--;
        return first.Val;
    }

    public T RemoveLast()
    {
        if (IsEmpty)
        {
            throw new IndexOutOfRangeException("No elements to remove");
        }

        var prev = _head;
        while (prev.Next != _tail)
        {
            prev = prev.Next;
        }

        T val = _tail.Val;
        prev.Next = null;
        _tail = prev;
        _size--;
        return val;
    }

    public T Remove(int index)
    {
        CheckElementIndex(index);
        if (index == 0)
        {
            return RemoveFirst();
        }

        var prev = GetNode(index - 1);
        var nodeToRemove = prev.Next;
        prev.Next = nodeToRemove.Next;
        if (nodeToRemove == _tail)
        {
            _tail = prev;
        }

        _size--;
        return nodeToRemove.Val;
    }

    public T this[int index]
    {
        get => GetNode(index).Val;
        set => GetNode(index).Val = value;
    }

    public T GetFirst()
    {
        if (IsEmpty)
        {
            throw new IndexOutOfRangeException("No elements in the list");
        }
        return _head.Next.Val;
    }

    public T GetLast()
    {
        if (IsEmpty)
        {
            throw new IndexOutOfRangeException("No elements in the list");
        }
        return _tail.Val;
    }

    public T Get(int index)
    {
        CheckElementIndex(index);
        var p = GetNode(index);
        return p.Val;
    }

    public T Set(int index, T val)
    {
        CheckElementIndex(index);
        var p = GetNode(index);

        var oldVal = p.Val;
        p.Val = val;
        return oldVal;
    }

    public override string ToString()
    {
        if (_size == 0)
        {
            return "No elements in the list";
        }

        StringBuilder sb = new();
        sb.AppendLine($"Size = {_size}");
        var curr = _head.Next;
        while (curr != null)
        {
            sb.Append(curr.Val);
            if (curr.Next != null)
            {
                sb.Append(" -> ");
            }
            curr = curr.Next;
        }
        return sb.ToString();
    }

    public int Count => _size;

    public bool IsEmpty => _size == 0;

    private Node GetNode(int index)
    {
        CheckElementIndex(index);
        Node curr = _head.Next;
        for (int i = 0; i < index; i++)
        {
            curr = curr.Next;
        }
        return curr;
    }

    /// <summary>
    /// 检查用于获取、删除或更新节点时的索引是否在有效范围 [0, _size-1] 内。
    /// </summary>
    private void CheckElementIndex(int index)
    {
        if (index < 0 || index >= _size)
            throw new IndexOutOfRangeException($"Index: {index}, _size: {_size}");
    }

    /// <summary>
    /// 检查用于添加节点时的索引是否在有效范围 [0, _size] 内。
    /// </summary>
    private void CheckPositionIndex(int index)
    {
        if (index < 0 || index > _size)
            throw new IndexOutOfRangeException($"Index: {index}, _size: {_size}");
    }

    public IEnumerator<T> GetEnumerator()
    {
        Node curr = _head.Next;
        while (curr != null)
        {
            yield return curr.Val;
            curr = curr.Next;
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
posted @ 2025-04-08 17:04  Ar4te  阅读(25)  评论(0)    收藏  举报