/*
* 链表是以线性、单向序列排列的值的集合。与Dart List等连续存储选项相比,它具有几个理论优势:
* 1. 在列表前面插入和删除耗时是常量
* 2. 可靠的性能特性。
*/
/// 节点
class Node<T> {
// 链表拥有一个value和一个向下节点的索引
Node({required this.value, this.next});
T value;
Node<T>? next;
@override
String toString() {
if (next == null) return '$value';
// 使节点可递归打印输出
return '$value -> ${next.toString()}';
}
}
class LinkedList<E> extends Iterable<E> {
Node<E>? head;
Node<E>? tail;
// 在列表的前面添加一个值。时间复杂度是O(1)
void push(E value) {
// 您将创建一个新节点,并指向曾经是头的节点。然后将这个新节点设置为head。
// 在您推入空列表的情况下,新节点既是列表的头,也是列表的尾
head = Node(value: value, next: head);
tail ??= head;
}
// 在列表末尾添加一个值。时间复杂度是O(1)
void append(E value) {
// 和以前一样,如果列表为空,则需要将head和tail都更新到新节点。
// 由于在空列表上进行追加在功能上与推送相同,因此只需调用推送即可完成工作。
if (isEmpty) {
push(value);
return;
}
// 在所有其他情况下,都会在尾部节点之后创建一个新节点。
// 由于您在isEmpty的情况下进行了push,因此tail被保证为非null。
tail!.next = Node(value: value);
// 由于这是尾部插入,所以您的新节点也是列表的新尾部。
tail = tail!.next;
}
// 在列表中查找特定节点。nodeAt将尝试根据给定的索引检索列表中的节点。
// 由于您只能从head节点访问列表中的节点,因此必须进行迭代遍历。
// 时间复杂度是O(i)
Node<E>? nodeAt(int index) {
// 你创建了一个新的head引用,并设置了一个计数器来跟踪你在列表中的位置
var currentNode = head;
var currentIndex = 0;
// 使用while循环,您可以在列表中向下移动索引,直到达到所需的索引。
// 空列表或越界索引将导致null返回值。
while (currentNode != null && currentIndex < index) {
currentNode = currentNode.next;
currentIndex += 1;
}
return currentNode;
}
// 在特定节点后面插入新节点。时间复杂度是O(1)
Node<E> insertAfter(Node<E> node, E value) {
if (tail == node) {
append(value);
return tail!;
}
node.next = Node(value: value, next: node.next);
return node.next!;
}
// 删除列表前面的值。
// 通过将头部向下移动到一个节点,您已经有效地删除了列表中的第一个节点。
// 如果列表变为空,则将tail设置为null。
// pop返回从列表中删除的值。此值可以为null,因为列表可能为空。
// 时间复杂度是O(1)
E? pop() {
final value = head?.value;
head = head?.next;
if (isEmpty) {
tail = null;
}
return value;
}
// 删除列表末尾的值。
// 删除列表的最后一个节点有些不方便。
// 尽管您有一个对尾部节点的引用,但如果不获得对其之前节点的引用就无法将其截断。因此不得不遍历
// 时间复杂度是O(n)
E? removeLast() {
// 如果列表只包含一个节点,那么removeLast在功能上等同于pop。
// 由于pop将处理头引用和尾引用的更新,所以您只需要委托这项工作。
// pop还将处理空列表的情况
if (head?.next == null) return pop();
// 您从一开始搜索节点,直到current.next是tail。这意味着current是tail之前的节点。
var current = head;
while (current!.next != tail) {
current = current.next;
}
// 从尾部收集返回值,然后将尾部之前的节点重新布线为新的尾部。
final value = tail?.value;
tail = current;
tail?.next = null;
return value;
}
// 删除列表中特定节点之后的值。
// 最后一个删除操作是删除列表中特定点上的节点。这与insertAfter非常相似。
// 您将首先在要删除的节点之前找到该节点,然后取消链接。
// 时间复杂度是O(1)
E? removeAfter(Node<E> node) {
final value = node.next?.value;
// 如果删除的节点是尾部节点,则需要特别小心,因为尾部引用需要更新。
if (node.next == tail) {
tail = node;
}
// 通过重置给定节点到下一个节点的链接,可以从列表中删除下一节点,实际上跳过了后面的节点。
node.next = node.next?.next;
return value;
}
// 问题1: 反向打印
// 这个算法的时间复杂度是O(n),因为你必须遍历列表的每个节点。
// 由于隐式地使用函数调用堆栈来处理每个元素,因此空间复杂度也是O(n)。
void printNodesRecursively<T>(Node<T>? node) {
// 1 您从基本情况开始:终止递归的条件。若node为null,则表示您已到达列表的末尾。
if (node == null) return;
// 2 这是您的递归调用,对下一个节点调用相同的函数。
printNodesRecursively(node.next);
// 3 添加打印语句的位置将决定是否按相反顺序打印列表
// 递归调用之后的任何代码都只在基本情况触发之后调用,也就是说,在递归函数到达列表末尾之后。
// 随着递归语句的分解,节点数据被打印出来。
print(node.value);
}
void printListInReverse<E>(LinkedList<E> list) {
printNodesRecursively(list.head);
}
// 问题2: 查找中间节点
// 一种解决方案是让两个引用遍历列表的节点,其中一个的速度是另一个的两倍
// 一旦较快的参考到达终点,较慢的参考将位于在中间。将函数更新为以下内容:
// 时间复杂度是O(n)
Node<E>? getMiddle<E>(LinkedList<E> list) {
var slow = list.head;
var fast = list.head;
// 在while循环中,fast检查接下来的两个节点,而slow只检查一个。这就是众所周知的e runner’s technique.
while (fast?.next != null) {
fast = fast?.next?.next;
slow = slow?.next;
}
return slow;
}
@override
bool get isEmpty => head == null;
@override
Iterator<E> get iterator => _LinkedListIterator(this);
@override
String toString() {
if (isEmpty) return 'Empty list';
return head.toString();
}
}
// 问题3:反转链接列表
// O(n)时间的复杂性,两种实现方式
extension ReversibleLinkedList<E> on LinkedList<E> {
// 首先将列表中的当前值推送到一个新的临时列表中。
// 这将以相反的顺序创建一个列表。在该点之后,列表的头部指向反向节点。
// 尽管O(n)是反转列表的最佳时间复杂度,但这个解决方案中存在显著的资源成本
// 现在,reverse将不得不为临时列表上的每个push方法分配新的节点。
void reverse() {
final tempList = LinkedList<E>();
for (final value in this) {
tempList.push(value);
}
head = tempList.head;
}
// 您可以避免完全使用临时列表,并通过操纵每个节点的下一个指针来反转列表。
// 代码最终变得更加复杂,但在性能方面可以获得相当大的好处。
// 时间复杂度依然是 O(n)
void reverseHighPerformance() {
// 首先将head分配给tail。接下来,创建两个引用——previous一个和current引用——以跟踪遍历。
// 该策略相当简单:每个节点都指向列表中的下一个节点。您将遍历列表,并使每个节点指向上一个节点:
// 然而,您不需要使用临时列表或分配任何新的Node对象,这显著提高了该算法的性能。
tail = head;
var previous = head;
var current = head?.next;
previous?.next = null;
// more to come...
while (current != null) {
final next = current.next;
current.next = previous;
previous = current;
current = next;
}
// 一旦完成了所有指针的反转,就可以将头设置为该列表的最后一个节点。在反向方法的末尾添加以下内容:
head = previous;
}
}
// 问题4:删除所有重复的节点
// 该算法的时间复杂度为O(n),因为您需要遍历所有元素
extension RemovableLinkedList<E> on LinkedList {
void removeAll(E value) {
// 有几种情况你需要考虑。要考虑的第一种情况是,当列表的头部包含要删除的值时。
while (head != null && head!.value == value) {
head = head!.next;
}
// 取消节点链接
// 像许多与链表相关的算法一样,您将利用指针算术技能来取消节点的链接
var previous = head;
var current = head?.next;
while (current != null) {
if (current.value == value) {
previous?.next = current.next;
current = previous?.next;
continue;
}
// more to come
previous = current;
current = current.next;
}
// 当原始tail是包含要删除的值的节点时,这是必要的,原始tail可能已被删除,所以需要重新tail
tail = previous;
}
}
class _LinkedListIterator<E> implements Iterator<E> {
_LinkedListIterator(LinkedList<E> list) : _list = list;
final LinkedList<E> _list;
Node<E>? _currentNode;
@override
E get current => _currentNode!.value;
bool _firstPass = true;
@override
bool moveNext() {
if (_list.isEmpty) return false;
if (_firstPass) {
_currentNode = _list.head;
_firstPass = false;
} else {
_currentNode = _currentNode?.next;
}
return _currentNode != null;
}
}
// final node1 = Node(value: 1);
// final node2 = Node(value: 2);
// final node3 = Node(value: 3);
// node1.next = node2;
// node2.next = node3;
// print(node1);
// 1 -> 2 -> 3
/// 目前构建列表的方法还有很多不足之处。您可以很容易地看到,
/// 以这种方式构建长列表是不切实际的。缓解这个问题的一种常见方法是
/// 构建一个LinkedList来管理Node对象。
// final list = LinkedList<int>();
// list.push(3);
// list.push(2);
// list.push(1);
// print('Before: $list');
// var middleNode = list.nodeAt(1)!;
// list.insertAfter(middleNode, 42);
// print('After: $list');
// Before: 1 -> 2 -> 3
// After: 1 -> 2 -> 42 -> 3
// final list = LinkedList<int>();
// list.push(3);
// list.push(2);
// list.push(1);
// print('Before: $list');
// final poppedValue = list.pop();
// print('After: $list');
// print('Popped value: $poppedValue');
// Before: 1 -> 2 -> 3
// After: 2 -> 3
// Popped value: 1
// final list = LinkedList<int>();
// list.push(3);
// list.push(2);
// list.push(1);
// print('Before: $list');
// final removedValue = list.removeLast();
// print('After: $list');
// print('Removed value: $removedValue');
// Before: 1 -> 2 -> 3
// After: 1 -> 2
// Removed value: 3
// final list = LinkedList<int>();
// list.push(3);
// list.push(2);
// list.push(1);
// print('Before: $list');
// final firstNode = list.nodeAt(0);
// final removedValue = list.removeAfter(firstNode!);
// print('After: $list');
// print('Removed value: $removedValue');
// Before: 1 -> 2 -> 3
// After: 1 -> 3
// Removed value: 2
// var list = LinkedList<int>();
// list.push(3);
// list.push(2);
// list.push(1);
// print('Original list: $list');
// print("Printing in reverse:");
// printListInReverse(list);
// Original list: 1 -> 2 -> 3
// Printing in reverse:
// 3
// 2
// 1
// var list = LinkedList<int>();
// list.push(3);
// list.push(2);
// list.push(1);
// print(list);
// final middleNode = getMiddle(list);
// print('Middle: ${middleNode?.value}');
// 1 -> 2 -> 3
// Middle: 2
// var list = LinkedList<int>();
// list.push(3);
// list.push(2);
// list.push(1);
// print('Original list: $list');
// list.reverse();
// print('Reversed list: $list');
// Original list: 1 -> 2 -> 3
// Reversed list: 3 -> 2 -> 1
// var list = LinkedList<int>();
// list.push(3);
// list.push(2);
// list.push(2);
// list.push(1);
// list.push(1);
// list.removeAll(3);
// print(list);
// 1 -> 1 -> 2 -> 2
LinkedLists in Dart
2023-03-23