链表
定义
在计算机科学中,链表是数据元素的线性集合,其每个元素都指向下一个元素,元素存储上并不连续
分类
- 单向链表:每个元素只知道下一个元素是谁
- 双向链表:每个元素知道其上一个元素和下一个元素
- 循环链表:通常的链表尾节点tail指向的都是null,而循环链表的尾tail指向的是头节点head
*链表内还有一种特殊的节点称为哨兵(Sentinel)节点,也叫做哑元(Dummy)节点,,它不存储数据,通常用作头尾,用来简化边界判断
性能
随机访问:
根据index查找,时间复杂度O(n),数据越多随机访问性能越差
插入和删除:
·起始位置:O(1),和链表数据规模无关
·结束位置:如果已知tail尾节点是O(1),不知道tail尾节点是O(n)
·中间位置:根据index查找时间+O(1)
单向链表
构建单向链表类
这里要注意的是这个类的创建过程,有单链表类内部还有节点类
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
| public class SinglyLinkedList implements Iterable<Integer> { private Node head = null;
@Override public Iterator<Integer> iterator() { return new Iterator<Integer>() { Node p = head;
@Override public boolean hasNext() { return p != null; }
@Override public Integer next() { int v = p.value; p = p.next; return v; } }; }
private static class Node { int value; Node next;
public Node(int value, Node next) { this.value = value; this.next = next; } } }
|
常见的单链表方法
有熟悉的增删改查方法,底层原理在代码中写的比较详细了
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
|
public void addFirst(int value) { head = new Node(value, head); }
public void loop1(Consumer<Integer> consumer) { Node p = head; while (p != null) { consumer.accept(p.value); p = p.next; } }
public void loop2(Consumer<Integer> consumer) { for (Node p = head; p != null; p = p.next) { consumer.accept(p.value); } }
private Node findLast() { if(head == null){ return null; } Node p; for (p = head; p.next != null; p = p.next) {
} return p; }
public void addLast(int value){ Node last = findLast(); if(last == null){ addFirst(value); return; } last.next = new Node(value,null); }
private Node findNode(int index){ int i = 0; for(Node p = head; p != null; p = p.next, i++){ if(i == index){ return p; } } return null; }
public int get(int index){ Node node = findNode(index); if(node == null){ throw illegalIndex(index); } return node.value; }
private static IllegalArgumentException illegalIndex(int index) { return new IllegalArgumentException(String.format("index [%d] 不合法%n", index)); }
public void insert(int index, int value){ if(index == 0){ addFirst(value); return; } Node prev = findNode(index - 1); if(prev == null){ throw illegalIndex(index); } prev.next = new Node(value, prev.next); }
public void removeFirst(){ if(head == null){ throw illegalIndex(0); } head = head.next; }
public void removeByIndex(int index){ if(index == 0){ removeFirst(); return; } Node prev = findNode(index - 1); if(prev == null){ throw illegalIndex(index); } Node removed = prev.next; if(removed == null){ throw illegalIndex(index); } prev.next = removed.next; }
|
带哨兵的单向链表
个人理解哨兵:哨兵节点实际上就是省去了考虑大量的边界问题,一开始链表就存在一个哨兵节点,占着索引0的位置,但我们书写代码的时候可以把哨兵的索引写成-1这样其它的代码都可以不需要改变。哨兵节点是只读,不可操作的隐藏节点,设计非常巧妙,省去了很多在头部的判断情况,充分的简化了代码。
*需要注意遍历的时候起始遍历点是哨兵.next 这样就可以正常遍历。
双向链表(带哨兵)
和单向链表大差不差,多了一个往前指的指针,好处在于在尾部可以直接根据尾哨兵从尾遍历
构建双向链表类
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
|
public class DoublyLinkedListSentinel implements Iterable<Integer>{ @Override public Iterator<Integer> iterator() { return new Iterator<Integer>() { Node p = head.next; @Override public boolean hasNext() { return p != tail; }
@Override public Integer next() { Integer value = p.value; p = p.next; return value; } }; }
static class Node { Node prev; Integer value; Node next;
public Node(Node prev, Integer value, Node next) { this.prev = prev; this.value = value; this.next = next; } }
private Node head; private Node tail;
public DoublyLinkedListSentinel() { head = new Node(null,666,null); tail = new Node(null,888,null); head.next = tail; tail.prev = head; } }
|
常用带哨兵方法
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
|
private Node findNode(int index){ int i = -1; for (Node p = head; p != tail; p = p.next,i++){ if(index == i){ return p; } } return null; }
public void insert(int index,int value){ Node prev = findNode(index - 1); if(prev == null){ throw illegalIndex(index); } Node next = prev.next; Node inserted = new Node(prev,value,next); prev.next = inserted; next.prev = inserted; }
private static IllegalArgumentException illegalIndex(int index) { return new IllegalArgumentException(String.format("index [%d] 不合法%n", index)); }
public void remove(int index){ Node prev = findNode(index - 1); if(prev == null){ throw illegalIndex(index); } Node removed = prev.next; if(removed == tail){ throw illegalIndex(index); } Node next = removed.next; prev.next = next; next.prev = prev; }
public void addLast(int value){ Node last = tail.prev; Node added = new Node(last,value,tail); last.next = added; tail.prev = added; }
public void removeLast(){ Node removed = tail.prev; if(removed == head){ throw illegalIndex(0); } Node prev = removed.prev; prev.next = tail; tail.prev = 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
| public class RingLinkedListSentinel implements Iterable<Integer>{ @Override public Iterator<Integer> iterator() { return new Iterator<Integer>() { Node p = sentinel.next; @Override public boolean hasNext() { return p != sentinel; }
@Override public Integer next() { int value = p.value; p = p.next; return value; } }; }
private static class Node { Node prev; int value; Node next;
public Node(Node prev, int value, Node next) { this.prev = prev; this.value = value; this.next = next; } }
private Node sentinel = new Node(null,-1,null);
public RingLinkedListSentinel() { sentinel.next = sentinel; sentinel.prev = sentinel; } }
|
常见的环形链表方法
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
|
public void addFirst(int value){ Node a = sentinel; Node b = sentinel.next; Node inserted = new Node(a,value,b); a.next = inserted; b.prev = inserted; }
public void addLast(int value){ Node a = sentinel.prev; Node b = sentinel;
Node inserted = new Node(a,value,b);
a.next = inserted; b.prev = inserted; }
public void removeFirst(){ Node removed = sentinel.next; if(removed == sentinel){ throw new IllegalArgumentException("非法"); } Node a = sentinel; Node b =removed.next; a.next = b; b.prev = a; }
public void removeLast(){ Node removed = sentinel.prev; if(removed == sentinel){ throw new IllegalArgumentException("非法"); } Node a = removed.prev; Node b = sentinel; a.next = b; b.prev = a; }
public void removeByValue(int value){ Node removed = findByValue(value); if(removed == null){ return; } Node a =removed.prev; Node b =removed.next; a.next = b; b.prev = a; }
private Node findByValue(int value){ Node p =sentinel.next; while(p != sentinel){ if(p.value == value){ return p; } p = p.next; } return null; }
|
链表递归遍历法
下面以单向链表(带哨兵)来展示递归遍历,下一节就会讲递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public void loop3(Consumer<Integer> before, Consumer<Integer> after){ recursion(head.next,before,after); }
private void recursion(Node curr, Consumer<Integer> before, Consumer<Integer> after){ if(curr == null){ return; } before.accept(curr.value); recursion(curr.next, before, after); after.accept(curr.value); }
|
实现的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| list.loop3(value -> { System.out.println("before" + value); }, value ->{ System.out.println("after" + value); });
|
由结果不难发现执行递归的上下遍历出来的链表顺序互为逆序
究其原因应该是发生了递归回溯,根据出入栈的规则导致结果的不同
跟spring特性中的AOP(面向切面编程)的原理很类似
详细的解释等下一章学递归就好解释了
解释:只有内层方法调用完了,外层才能继续运行,意思就是外层从一开始调用就不会停止,直到回到最外层走完。
将递归打开更加直观(没有用到的判断被我删去了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void f(Node node = 1) { System.out.println("before" + node.value); void f(Node node = 2) { System.out.println("before" + node.value); void f(Node node = 3) { System.out.println("before" + node.value); void f(Node node = null) { if(node == null) { return; } } System.out.println("after" + node.value); } System.out.println("after" + node.value); } System.out.println("after" + node.value); }
|