线性结构简介
数据是散落在内存荒原上的沙砾,人类文明用线性结构在数字世界中铺就了第一条道路。在算法世界的版图里,线性结构是所有复杂架构的基石。它的定义看似朴素:数据元素之间存在着 “一对一” 的关系,除了首尾,每个节点都有且仅有一个前驱和后继。然而,在这种极简的几何关系背后,隐藏着计算机科学中最深刻的博弈——物理连续性与逻辑灵活性之间的较量。这种线性结构的演化,本质上是人类在访问速度与组织成本之间寻找平衡点的过程。无论是栈的内敛、队列的规整,还是哈希表的奔放,其底层逻辑都逃不开数组与链表这两大元思想的纠缠。
物理的刚性:数组(Array)
数组是线性结构的 “守旧派”。它在内存中圈定一块连续的领土,赋予每个元素一个永恒的编号(索引)。这种对物理空间的极致榨取,换来了 $O(1)$ 级别的随机访问速度。它是追求极致读取效率时的首选,但也必须承受 “牵一发而动全身” 的插入成本。
逻辑的韧性:链表 (Linked List)
链表则是线性结构的 “自由派”。它不要求领土的连贯,而是通过指针(Pointer)在散乱的内存间拉起一条若隐若现的纤绳。它放弃了瞬间定位的能力,却换取了在任何位置“随进随出”的潇洒。它是动态内存管理的灵魂,也是实现高级数据结构的原材料。
常见链表
单链表
为了让 addLast 不再遍历链表,我增加了对 tail 指针的维护(用空间换时间),这也是链表优化的必经之路。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
|
public class MyLinkedList { private static class Node { int data; Node next;
public Node(int data) { this.data = data; } }
private Node head; private Node tail; private int size;
public void addFirst(int data) { Node node = new Node(data); if (head == null) { head = tail = node; } else { node.next = head; head = node; } size++; }
public void addLast(int data) { if (head == null){ addFirst(data); return; } Node node = new Node(data); tail.next = node; tail = node; size++; }
public void remove(int data) { if (head == null) { return; }
if (head.data == data) { head = head.next; if (head == null) tail = null; size--; return; }
Node cur = head; while (cur.next != null) { if (cur.next.data == data) { if (cur.next == tail) tail = cur; cur.next = cur.next.next; size--; return; } cur = cur.next; } }
public void removeAll(int data) { if (head == null) { return; }
Node h = new Node(0); h.next = head; Node cur = h; while (cur.next != null) { if (cur.next.data == data) { if (cur.next== tail) tail = cur; cur.next = cur.next.next; size--; } else { cur = cur.next; } }
head = h.next; if (head== null) tail = null; }
public void clean() { head = tail = null; size = 0; }
public void print() { Node cur = head; while (cur != null) { System.out.print(cur.data + "->"); cur = cur.next; } System.out.println("null"); }
public static void main(String[] args) { MyLinkedList list = new MyLinkedList(); list.addFirst(20); list.addFirst(20); list.addFirst(10); list.addLast(30); list.addLast(30); list.print();
list.remove(20); list.print();
list.removeAll(30); list.print();
list.clean(); list.print(); } }
|
双链表
在单链表中,我们最痛苦的莫过于 “无法回头”。即使有了 tail 指针,想删除最后一个节点,由于找不到前驱节点,我们还是得从头跑一遍。双向链表通过多出一个 prev 指针,彻底打破了这个僵局。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
|
public class MyDoubleLinkedList { private static class Node { int data; Node prev; Node next;
public Node(int data) { this.data = data; } }
private Node head; private Node tail; private int size;
public void addFirst(int data) { Node node = new Node(data); if (head == null) { head = tail = node; } else { node.next = head; head.prev = node; head = node; } size++; }
public void addLast(int data) { if (head == null) { addFirst(data); return; } Node node = new Node(data); tail.next = node; node.prev = tail; tail = node; size++; }
public void removeFirst() { if (head == null) { return; } if (head == tail) { head = tail = null; } else { head = head.next; head.prev = null; } size--; }
public void removeLast() { if (tail == null) return;
if (head == tail) { head = tail = null; } else { tail = tail.prev; tail.next = null; } size--; }
public void remove(int data) { Node cur = head; while (cur != null) { if (cur.data == data) { unlink(cur); return; } cur = cur.next; } }
public void removeAll(int data) { Node cur = head; while (cur != null) { if (cur.data == data) { Node nextNode = cur.next; unlink(cur); cur = nextNode; } else { cur = cur.next; } } }
private void unlink(Node node) { Node prevNode = node.prev; Node nextNode = node.next;
if (prevNode == null) { head = nextNode; } else { prevNode.next = nextNode; node.prev = null; }
if (nextNode == null) { tail = prevNode; } else { nextNode.prev = prevNode; node.next = null; }
size--; }
public void clean() { Node cur = head; while (cur != null) { Node next = cur.next; cur.prev = null; cur.next = null; cur = next; } head = tail = null; size = 0; }
public void print() { Node cur = head; while (cur != null) { System.out.print(cur.data + " <-> "); cur = cur.next; } System.out.println("null"); }
public static void main(String[] args) { MyDoubleLinkedList list = new MyDoubleLinkedList(); list.addLast(10); list.addLast(20); list.addLast(20); list.addLast(30); list.print();
list.remove(20); list.print();
list.removeAll(20); list.addFirst(5); list.print();
list.clean(); list.print(); } }
|
循环链表
循环链表是将线性表的终点重新连接到起点,使数据流成了一个永无止境的环。在实际开发中,循环链表最著名的应用场景就是操作系统的时间片轮转调度(Round Robin),以及经典的数学问题:约瑟夫环。
假设有 n 个人围成一圈,从第一个人开始报数,报到 m 的人出列。 循环链表不需要像数组那样不断地搬运数据,它只需要像时钟拨针一样转动指针,剔除节点即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
|
public class MyCircularLinkedList { private static class Node { int data; Node prev; Node next;
public Node(int data) { this.data = data; } }
private Node head; private int size;
public void addNode(int data) { Node node = new Node(data); if (head == null) { head = node; node.next = head; node.prev = head; } else { Node tail = head.prev; node.next = head; node.prev = tail; head.prev = node; tail.next = node; } size++; }
private void removeNode(Node targetNode) { if (size == 1) { head = null; } else { targetNode.prev.next = targetNode.next; targetNode.next.prev = targetNode.prev; if (targetNode == head) { head = targetNode.next; } } size--; }
public void print() { if (head == null) return; Node cur = head; do { System.out.print(cur.data + " <-> "); cur = cur.next; } while (cur != head); System.out.println("(Back to Head: " + head.data + ")"); }
public void createCircle(int n) { for (int i = 1; i <= n; i++) { addNode(i); } }
public void josephusSolve(int k, int m) { if (head == null || m < 1) return;
Node cur = head; for (int i = 1; i < k; i++) { cur = cur.next; } System.out.println("--- 约瑟夫环出列顺序 ---");
while (size > 1) { for (int i = 1; i < m; i++) { cur = cur.next; }
System.out.print(cur.data + " -> "); removeNode(cur);
cur = cur.next; } System.out.println("\n最后留下的幸运儿是: " + cur.data); }
public static void main(String[] args) { MyCircularLinkedList cl = new MyCircularLinkedList(); cl.createCircle(5); cl.josephusSolve(1,4); } }
|