# 二、链表

## 概述

我们将看到的第一个数据结构是链表，这是有充分理由的。除了是一个几乎无处不在的结构，用于从操作系统到视频游戏的一切，它也是一个构建块，许多其他数据结构可以用它来创建。

在非常一般的意义上，链表的目的是提供一种一致的机制来存储和访问任意数量的数据。顾名思义，它通过将数据链接到一个列表中来实现这一点。

在深入探讨这意味着什么之前，让我们先来回顾一下数据是如何存储在数组中的。

![](img/image001.png)

图 1:存储在数组中的整数数据

如图所示，数组数据存储为逻辑分段的单个连续分配的内存块。存储在数组中的数据被放在其中一个段中，并通过其在数组中的位置或索引进行引用。

这是存储数据的好方法。大多数编程语言使得分配数组和操作数组内容变得非常容易。连续数据存储提供了性能优势(即数据局部性)，对数据进行迭代很简单，并且可以在恒定时间内通过索引(随机访问)直接访问数据。

然而，有时数组并不是理想的解决方案。

考虑一个具有以下要求的程序:

1.  从输入源(`NextValue`方法)读取未知数量的整数，直到遇到数字 0xFFFF。
2.  将所有已读取的整数(在一次调用中)传递给`ProcessItems`方法。

由于需求表明需要在一次调用中将多个值传递给`ProcessItems`方法，一个显而易见的解决方案是使用整数数组。例如:

```cs
    void LoadData()
    {
        // Assume that 20 is enough to hold the values.
        int[] values = new int[20];
        for (int i = 0; i < values.Length; i++)
        {
            if (values[i] == 0xFFFF)
            {
                break;
            }

            values[i] = NextValue();
        }

        ProcessItems(values);
    }

    void ProcessItems(int[] values)
    {
        // ... Process data.
    }

```

这个解决方案有几个问题，但最明显的是当读取超过 20 个值时。就像现在的程序一样，从 21 到 *n* 的值被忽略了。这可以通过分配超过 20 个值来缓解，可能是 200 或 2000 个。也许大小可以由用户配置，或者如果数组已满，可以分配一个更大的数组，并将所有现有数据复制到其中。最终，这些解决方案会造成复杂性和内存浪费。

我们需要的是一个集合，它允许我们添加任意数量的整数值，然后按照它们被添加的顺序枚举这些整数。该集合不应具有固定的最大大小，也不需要随机访问索引。我们需要的是一个链表。

在我们继续学习如何设计和实现链表数据结构之前，让我们来预览一下我们的最终解决方案可能是什么样子。

```cs
    static void LoadItems()
    {
        LinkedList<int> list = new LinkedList<int>();
        while (true)
        {
            int value = NextValue();
            if (value != 0xFFFF)
            {
                list.Add(value);
            }
            else
            {
                break;
            }
        }

        ProcessItems(list);
    }

    static void ProcessItems(LinkedList<int> list)
    {
            // ... Process data.
    }

```

请注意，数组解决方案的所有问题都已不复存在。不再存在数组不够大或分配的容量超出需要的问题。

您还应该注意到，该解决方案通知了我们稍后将做出的一些设计决策，即`LinkedList`类接受泛型类型参数并实现`IEnumerable`接口。

## 实现链接列表类

### 节点

链表数据结构的核心是`Node`类。节点是一个容器，它提供了存储数据和连接其他节点的能力。

![](img/image002.png)

图 2:链表节点包含数据和指向下一个节点的属性

最简单的形式是，一个包含整数的`Node`类可能是这样的:

```cs
    public class Node
    {
        public int Value { get; set; }
        public Node Next { get; set; }
    }

```

有了这个，我们现在可以创建一个非常原始的链表。在下面的示例中，我们将分配三个节点(第一个、中间的和最后一个)，然后将它们链接到一个列表中。

```cs
    // +-----+------+
    // |  3  | null +
    // +-----+------+
    Node first = new Node { Value = 3 };

    // +-----+------+    +-----+------+
    // |  3  | null +    |  5  | null +
    // +-----+------+    +-----+------+
    Node middle = new Node { Value = 5 };

    // +-----+------+    +-----+------+
    // |  3  |  *---+--->|  5  | null +
    // +-----+------+    +-----+------+
    first.Next = middle;

    // +-----+------+    +-----+------+   +-----+------+
    // |  3  |  *---+--->|  5  | null +   |  7  | null +
    // +-----+------+    +-----+------+   +-----+------+
    Node last = new Node { Value = 7 };

    // +-----+------+    +-----+------+   +-----+------+
    // |  3  |  *---+--->|  5  |  *---+-->|  7  | null +
    // +-----+------+    +-----+------+   +-----+------+
    middle.Next = last;

```

我们现在有一个链表，以节点`first`开始，以节点`last`结束。最后一个节点的`Next`属性指向空值，这是列表结束指示符。给定这个列表，我们可以执行一些基本操作。例如，每个节点的`Data`属性的值:

```cs
    private static void PrintList(Node node)
    {
        while (node != null)
        {
            Console.WriteLine(node.Value);
            node = node.Next;
        }
    }

```

`PrintList`方法的工作原理是迭代列表中的每个节点，打印当前节点的值，然后移动到`Next`属性指向的节点。

现在我们已经了解了链表节点可能是什么样子，让我们看看实际的`LinkedListNode`类。

```cs
    public class LinkedListNode<T>
    {
        /// <summary>
        /// Constructs a new node with the specified value.
        /// </summary>
        public LinkedListNode(T value)
        {
            Value = value;
        }

        /// <summary>
        /// The node value.
        /// </summary>
        public T Value { get;  internal set; }

        /// <summary>
        /// The next node in the linked list (null if last node).
        /// </summary>
        public LinkedListNode<T> Next { get;  internal set; }
    }

```

### 链接列表类

在实现我们的`LinkedList`类之前，我们需要考虑我们希望能够用这个列表做什么。

前面我们看到集合需要支持强类型数据，所以我们知道我们想要创建一个通用接口。

因为我们用的是。NET 框架来实现这个列表，我们希望这个类能够像其他内置集合类型一样工作是有意义的。最简单的方法就是实现`ICollection<T>`接口。注意我选择的是`ICollection<T>`而不是`IList<T>`。这是因为`IList<T>`接口增加了通过索引访问值的能力。虽然直接索引通常很有用，但它不能在链表中有效地实现。

考虑到这些要求，我们可以创建一个基本的类存根，然后通过本章的其余部分，我们可以填写这些方法。

```cs
    public class LinkedList<T> :
        System.Collections.Generic.ICollection<T>
    {
        public void Add(T item)
        {
            throw new System.NotImplementedException();
        }

        public void Clear()
        {
            throw new System.NotImplementedException();
        }

        public bool Contains(T item)
        {
            throw new System.NotImplementedException();
        }

        public void CopyTo(T[] array, int arrayIndex)
        {
            throw new System.NotImplementedException();
        }

        public int Count
        {
            get;
            private set;
        }

        public bool IsReadOnly
        {
            get { throw new System.NotImplementedException(); }
        }

        public bool Remove(T item)
        {
            throw new System.NotImplementedException();
        }

        public System.Collections.Generic.IEnumerator<T> GetEnumerator()
        {
            throw new System.NotImplementedException();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            throw new System.NotImplementedException();
        }
    }

```

### 添加

| 行为 | 将提供的值添加到链表的末尾。 |
| 表演 | *O* (1) |

将项目添加到链接列表包括三个步骤:

1.  分配新的`LinkedListNode`实例。
2.  查找现有列表的最后一个节点。
3.  将最后一个节点的`Next`属性指向新节点。

关键是要知道哪个节点是列表中的最后一个节点。我们有两种方法可以知道这一点。第一种方法是跟踪第一个节点(“头”节点)并遍历列表，直到找到最后一个节点。这种方法不需要我们跟踪最后一个节点，这样可以节省一个引用的内存(无论您的平台指针大小如何)，但是需要我们在每次添加节点时遍历列表。这将使`Add`成为 *O* ( *n* )操作。

第二种方法要求我们跟踪列表中的最后一个节点(“尾部”节点)，当我们添加新节点时，我们只需直接访问存储的引用。这是一个 *O* (1)算法，因此是首选方法。

我们需要做的第一件事是向`LinkedList`类添加两个私有字段:对第一个(头)和最后一个(尾)节点的引用。

```cs
    private LinkedListNode<T> _head;
    private LinkedListNode<T> _tail;

```

接下来，我们需要添加执行这三个步骤的方法。

```cs
    public void Add(T value)
    {
        LinkedListNode<T> node = new LinkedListNode<T>(value);

        if (_head == null)
        {
            _head = node;
            _tail = node;
        }
        else
        {
            _tail.Next = node;
            _tail = node;
        }

        Count++;
    }

```

首先，它分配新的`LinkedListNode`实例。接下来，它检查列表是否为空。如果列表为空，只需将`_head`和`_tail`引用分配给新节点，即可添加新节点。新节点现在是列表中的第一个和最后一个节点。如果列表不为空，则该节点被添加到列表的末尾，并且`_tail`引用被更新以指向列表的新末尾。

添加节点时，`Count`属性会递增，以确保`ICollection<T>.Count`属性返回准确的值。

### 移除

| 行为 | 移除列表中值等于所提供值的第一个节点。如果某个值被删除，该方法返回`true`。否则返回`false`。 |
| 表演 | *O* ( *n* ) |

在谈论`Remove`算法之前，我们先来看看它试图实现什么。在下图中，列表中有四个节点。我们希望移除值为 3 的节点。

![](img/image003.png)

图 3:一个包含四个值的链表

移除完成后，列表将被修改，使得值为 2 的节点上的`Next`属性指向值为 4 的节点。

![](img/image004.png)

图 4:移除了 3 个节点的链表

节点移除的基本算法是:

1.  找到要移除的节点。
2.  更新被移除节点之前的节点的`Next`属性，以指向被移除节点之后的节点。

一如既往，细节决定成败。移除节点时，我们需要考虑以下几种情况:

*   列表可能为空，或者我们试图删除的值可能不在列表中。在这种情况下，列表将保持不变。
*   正在删除的节点可能是列表中唯一的节点。在这种情况下，我们只需将`_head`和`_tail`字段设置为`null`。
*   要移除的节点可能是第一个节点。在这种情况下，没有前面的节点，因此我们需要更新`_head`字段来指向新的头节点。
*   该节点可能在列表的中间。这就是图 3 和图 4 所示的情况。
*   该节点可能是列表中的最后一个节点。在这种情况下，我们更新`_tail`字段以引用列表中倒数第二个节点，并将其`Next`属性设置为`null`。

```cs
    public bool Remove(T item)
    {
        LinkedListNode<T> previous = null;
        LinkedListNode<T> current = _head;

        // 1: Empty list: Do nothing.
        // 2: Single node: Previous is null.
        // 3: Many nodes:
        //    a: Node to remove is the first node.
        //    b: Node to remove is the middle or last.

        while (current != null)
        {
            if (current.Value.Equals(item))
            {
                // It's a node in the middle or end.
                if (previous != null)
                {
                    // Case 3b.

                    // Before: Head -> 3 -> 5 -> null
                    // After:  Head -> 3 ------> null
                    previous.Next = current.Next;

                    // It was the end, so update _tail.
                    if (current.Next == null)
                    {
                        _tail = previous;
                    }
                }
                else
                {
                    // Case 2 or 3a.

                    // Before: Head -> 3 -> 5
                    // After:  Head ------> 5

                    // Head -> 3 -> null
                    // Head ------> null
                    _head = _head.Next;

                    // Is the list now empty?
                    if (_head == null)
                    {
                        _tail = null;
                    }
                }

                Count--;

                return true;
            }

            previous = current;
            current = current.Next;
        }

        return false;
    }

```

当移除节点时，`Count`属性递减，以确保`ICollection<T>.Count`属性返回准确的值。

### 包含

| 行为 | 返回一个布尔值，该值指示所提供的值是否存在于链接列表中。 |
| 表演 | *O* ( *n* ) |

`Contains`方法相当简单。它会从第一个节点到最后一个节点查看列表中的每个节点，并在找到匹配该参数的节点后立即返回 true。如果到达列表末尾，没有找到该节点，则该方法返回`false.`

```cs
    public bool Contains(T item)
    {
        LinkedListNode<T> current = _head;
        while (current != null)
        {
            if (current.Value.Equals(item))
            {
                return true;
            }

            current = current.Next;
        }

        return false;
    }

```

### 获取分子

| 行为 | 返回一个`IEnumerator<T>`实例，允许从第一个到最后一个枚举链表值。 |
| 表演 | 返回枚举器实例是一个 *O* (1)操作。枚举每一项都是一个 *O* ( *n* )操作。 |

`GetEnumerator`通过从第一个节点到最后一个节点枚举列表来实现，并使用 C# `yield`关键字将当前节点的值返回给调用者。

注意`LinkedList`在`GetEnumerator`方法的`IEnumerable<T>`版本中实现迭代行为，并在`IEnumerable`版本中遵从该行为。

```cs
    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
        LinkedListNode<T> current = _head;
        while (current != null)
        {
            yield return current.Value;
            current = current.Next;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable<T>)this).GetEnumerator();
    }

```

### 晴

| 行为 | 从列表中删除所有项目。 |
| 表演 | *O* (1) |

`Clear`方法只需将`_head`和`_tail`字段设置为`null`即可清除列表。因为。NET 是一种垃圾收集语言，节点不需要显式移除。调用者，而不是链表，有责任确保如果节点包含`IDisposable`引用，它们将被正确处理。

```cs
    public void Clear()
    {
        _head = null;
        _tail = null;
        Count = 0;
    }

```

### 抄

| 行为 | 从指定的数组索引开始，将链表的内容从头到尾复制到提供的数组中。 |
| 表演 | *O* ( *n* ) |

`CopyTo`方法简单地遍历列表项，并使用简单的赋值将项复制到数组中。调用者有责任确保目标数组包含适当的可用空间来容纳列表中的所有项目。

```cs
    public void CopyTo(T[] array, int arrayIndex)
    {
        LinkedListNode<T> current = _head;
        while (current != null)
        {
            array[arrayIndex++] = current.Value;
            current = current.Next;
        }
    }

```

### 计数

| 行为 | 返回一个整数，指示列表中当前的项数。当列表为空时，返回值为`0`。 |
| 表演 | *O* (1) |

`Count`只是一个带有公共 getter 和私有 setter 的自动实现的属性。真正的行为发生在`Add`、`Remove`和`Clear`方法中。

```cs
    public int Count
    {
        get;
        private set;
    }

```

### 只读

| 行为 | 如果列表不是只读的，返回`false`。 |
| 表演 | *O* (1) |

```cs
    public bool IsReadOnly
    {
        get { return false; }
    }

```

## 双向链表

我们刚刚创建的`LinkedList`类被称为单链表。这意味着在一个节点和列表中的下一个节点之间只存在一条单向链路。链表有一个常见的变体，它允许调用者从两端访问该列表。这种变化被称为双向链表。

为了创建一个双向链表，我们需要首先修改我们的`LinkedListNode`类，以获得一个名为`Previous`的新属性。`Previous`会像`Next`一样行动，只是它会指向列表中的前一个节点。

![](img/image005.png)

图 5:使用上一个节点属性的双向链表

以下部分将仅描述单链表和新的双链表之间的变化。

### 节点类别

将在`LinkedListNode`类中进行的唯一更改是添加一个名为`Previous`的新属性，该属性指向链表中的前一个`LinkedListNode`，如果它是链表中的第一个节点，则返回`null`。

```cs
    public class LinkedListNode<T>
    {
        /// <summary>
        /// Constructs a new node with the specified value.
        /// </summary>
        /// <param name="value"></param>
        public LinkedListNode(T value)
        {
            Value = value;
        }

        /// <summary>
        /// The node value.
        /// </summary>
        public T Value { get;  internal set; }

        /// <summary>
        /// The next node in the linked list (null if last node).
        /// </summary>
        public LinkedListNode<T> Next { get; internal set; }

        /// <summary>
        /// The previous node in the linked list (null if first node).
        /// </summary>
        public LinkedListNode<T> Previous { get;  internal set; }
    }

```

### 添加

而单链表只将节点添加到列表的末尾，而双链表将允许分别使用`AddFirst`和`AddLast`将节点添加到列表的开始和末尾。`ICollection<T>.Add`方法将遵从`AddLast`方法，以保持与单键`List`类的兼容性。

#### AddFirst(第一个)

| 行为 | 将提供的值添加到列表的前面。 |
| 表演 | *O* (1) |

将节点添加到列表前面时，操作与添加到单链表非常相似。

1.  将新节点的`Next`属性设置为旧的头节点。
2.  将旧头节点的`Previous`属性设置为新节点。
3.  更新`_tail`字段(如有必要)并增加`Count`。

```cs
    public void AddFirst(T value)
    {
        LinkedListNode<T> node = new LinkedListNode<T>(value);

        // Save off the head node so we don't lose it.
        LinkedListNode<T> temp = _head;

        // Point head to the new node.
        _head = node;

        // Insert the rest of the list behind head.
        _head.Next = temp;

        if (Count == 0)
        {
            // If the list was empty then head and tail should
            // both point to the new node.
            _tail = _head;
        }
        else
        {
            // Before: head -------> 5 <-> 7 -> null
            // After:  head  -> 3 <-> 5 <-> 7 -> null
            temp.Previous = _head;
        }

        Count++;
    }

```

#### add last！add last！add last！add last！add last

| 行为 | 将提供的值添加到列表的末尾。 |
| 表演 | *O* (1) |

在列表的末尾添加一个节点甚至比在开头添加一个节点更容易。

新节点简单地附加到列表的末尾，适当地更新`_tail`和`_head`的状态，并且`Count`递增。

```cs
    public void AddLast(T value)
    {
        LinkedListNode<T> node = new LinkedListNode<T>(value);

        if (Count == 0)
        {
            _head = node;
        }
        else
        {
            _tail.Next = node;

            // Before: Head -> 3 <-> 5 -> null
            // After: Head -> 3 <-> 5 <-> 7 -> null
            // 7.Previous = 5
            node.Previous = _tail;
        }

        _tail = node;
        Count++;
    }

```

如前所述，`ICollection<T>.Add`现在将简称为`AddLast`。

```cs
    public void Add(T value)
    {
        AddLast(value);
    }

```

### 移除

像`Add`一样，`Remove`方法将被扩展以支持从列表的开始或结尾移除节点。`ICollection<T>.Remove`方法将继续从一开始就移除项目，唯一的改变是更新适当的`Previous`属性。

#### 移除第一个

| 行为 | 从列表中移除第一个值。如果列表为空，则不采取任何操作。 |
| 表演 | *O* (1) |

`RemoveFirst`通过将链表的`head`属性设置为列表中的第二个节点并将其`Previous`属性更新为`null`来更新列表。这将删除对前一个头节点的所有引用，并将其从列表中删除。如果列表只包含一个单例，或者是空的，列表将是空的(`head`和`tail`属性将是`null`)。

```cs
    public void RemoveFirst()
    {
        if (Count != 0)
        {
            // Before: Head -> 3 <-> 5
            // After:  Head -------> 5

            // Head -> 3 -> null
            // Head ------> null
            _head = _head.Next;

            Count--;

            if (Count == 0)
            {
                _tail = null;
            }
            else
            {
                // 5.Previous was 3; now it is null.
                _head.Previous = null;
            }
        }
    }

```

#### remove last-移除负载

| 行为 | 从列表中移除最后一个节点。如果列表为空，则不执行任何操作。 |
| 表演 | *O* (1) |

`RemoveLast`通过将列表的`tail`属性设置为当前尾部节点之前的节点来工作。这将从列表中删除最后一个节点。如果列表为空或只有一个节点，当方法返回`head`和`tail`属性时，它们都将是`null`。

```cs
    public void RemoveLast()
    {
        if (Count != 0)
        {
            if (Count == 1)
            {
                _head = null;
                _tail = null;
            }
            else
            {
                // Before: Head --> 3 --> 5 --> 7
                //         Tail = 7
                // After:  Head --> 3 --> 5 --> null
                //         Tail = 5
                // Null out 5's Next property.
                _tail.Previous.Next = null;
                _tail = _tail.Previous;
            }

            Count--;
        }
    }

```

#### 去除

| 行为 | 移除列表中值等于所提供值的第一个节点。如果某个值被删除，该方法返回`true`。否则返回`false`。 |
| 表演 | *O* ( *n* ) |

`ICollection<T>.Remove`方法与单链接版本几乎相同，除了`Previous`属性现在在移除操作期间被更新。为了避免重复代码，当确定被移除的节点是列表中的第一个节点时，该方法调用`RemoveFirst`。

```cs
    public bool Remove(T item)
    {
        LinkedListNode<T> previous = null;
        LinkedListNode<T> current = _head;

        // 1: Empty list: Do nothing.
        // 2: Single node: Previous is null.
        // 3: Many nodes:
        //    a: Node to remove is the first node.
        //    b: Node to remove is the middle or last.

        while (current != null)
        {
            // Head -> 3 -> 5 -> 7 -> null
            // Head -> 3 ------> 7 -> null
            if (current.Value.Equals(item))
            {
                // It's a node in the middle or end.
                if (previous != null)
                {
                    // Case 3b.
                    previous.Next = current.Next;

                    // It was the end, so update _tail.
                    if (current.Next == null)
                    {
                        _tail = previous;
                    }
                    else
                    {
                        // Before: Head -> 3 <-> 5 <-> 7 -> null
                        // After:  Head -> 3 <-------> 7 -> null

                        // previous = 3
                        // current = 5
                        // current.Next = 7
                        // So... 7.Previous = 3
                        current.Next.Previous = previous;
                    }

                    Count--;
                }
                else
                {
                    // Case 2 or 3a.
                    RemoveFirst();
                }

                return true;
            }

            previous = current;
            current = current.Next;
        }

        return false;
    }

```

### 但是为什么呢？

我们可以在列表的前端和后端添加节点，那又怎样？我们为什么在乎？就目前的情况来看，双链接`List`类并不比单链表更强大。但是只需一个小小的修改，我们就可以打开各种可能的行为。通过将`head`和`tail`属性公开为只读公共属性，链表消费者将能够实现各种新的行为。

```cs
    public LinkedListNode<T> Head
    {
        get
        {
            return _head;
        }
    }

    public LinkedListNode<T> Tail
    {
        get
        {
            return _tail;
        }
    }

```

通过这个简单的更改，我们可以手动枚举列表，这允许我们执行反向(首尾相连)枚举和搜索。

例如，下面的代码示例显示了如何使用列表的`Tail`和`Previous`属性反向枚举列表，并在每个节点上执行一些处理。

```cs
    public void ProcessListBackwards()
    {
        LinkedList<int> list = new LinkedList<int>();
        PopulateList(list);

        LinkedListNode<int> current = list.Tail;
        while (current != null)
        {
            ProcessNode(current);
            current = current.Previous;
        }
    }

```

此外，双链接`List`类允许我们轻松创建[德格](4.html#_Deque_Double_Ended)类，它本身就是其他类的构建模块。我们将在[第 4 章](4.html#_Chapter_4_)中讨论这堂课。