链表 LinkedList
写在开头
- 许久之前文章提到过的:
动态数组
、栈
、队列
,其底层均依托于静态数组
,通过resize()
进行动态扩容操作。 - 而
链表
,则为真正的动态数据结构
,同样也是最简单的动态数据结构
。 链表
这种数据结构可以帮助我们了解计算机中指针(引用)
、递归
等概念。
节点 Node
-
数据存储在
节点
中,需要多少节点就生产多少节点进行挂接,但失去了随机访问的能力,适合索引无语义的情况。class node { E e; Node next; }
链表数据结构创建 LinkedList,为保证节点信息安全性,采用内部类方式进行构造
/**
* @author by Jiangyf
* @classname LinkedList
* @description 链表
* @date 2019/9/28 13:08
*/
public class LinkedList<E> {
/**
* 节点内部类
*/
private class Node {
public E e;
public Node next;
public Node(E e, Node next) {
this.e = e;
this.next = next;
}
public Node(E e) {
this(e, null);
}
public Node() {
this(null, null);
}
@Override
public String toString() {
return "Node{" +
"e=" + e +
", next=" + next +
'}';
}
}
private Node head;
int size;
public LinkedList() {
head = null;
size = 0;
}
// 获取链表容量
public int getSize() {
return size;
}
// 判断链表是否为空
public boolean isEmpty() {
return size == 0;
}
}
添加操作方法
-
从链表头部添加元素
public void addFirst(E e) { head = new Node(e, head); size ++; }
-
从链表中间位置
index
处添加元素,注意:先连后断
public void add(int index, E e) throws IllegalAccessException { // 索引校验 if (index < 0 || index > size) { throw new IllegalAccessException("Add failed. Illegal index."); } // 判断是操作是否为头部添加 if (index == 0) { addFirst(e); } else { // 创建前置节点 Node prev = head; // 定位到待插入节点前一个节点 for (int i = 0; i < index -1 ; i++) { prev = prev.next; } prev.next = new Node(e, prev.next); size ++; } }
-
在链表尾部位置添加元素
public void addLast(E e) throws IllegalAccessException { add(size, e); }
-
为链表设立
虚拟头结点(dummyHead)
,解决从头部添加和其他位置添加的逻辑不一致情况虚拟头结点
作为链表内部机制进行设置,在原有head
节点改进为dummyHead.next = head
,适配节点添加逻辑。- 修改代码
-
添加
虚拟头结点dummyHead
,不存放任何内容private Node dummyHead; public LinkedList() { dummyHead = new Node(null, null); size = 0; }
-
修改
add(index, e)
方法// 从链表中间添加元素 先连后断 public void add(int index, E e) throws IllegalAccessException { // 索引校验 if (index < 0 || index > size) { throw new IllegalAccessException("Add failed. Illegal index."); } // 创建前置节点 Node prev = dummyHead; // 定位到待插入节点前一个节点,遍历index次原因为 dummyHead为head节点前一个节点 for (int i = 0; i < index ; i++) { prev = prev.next; } prev.next = new Node(e, prev.next); size ++; }
-
修改
addFirst(e)
方法public void addFirst(E e) throws IllegalAccessException { add(0, e); }
-
-
获取指定位置
index
的节点元素public E get(int index) throws IllegalAccessException { // 索引校验 if (index < 0 || index > size) { throw new IllegalAccessException("Get failed. Illegal index."); } // 定位到head节点 Node cur = dummyHead.next; for (int i = 0; i < index; i++) cur = cur.next; return cur.e; }
-
获取头结点、尾结点
public E getFirst() throws IllegalAccessException { return get(0); } public E getLast() throws IllegalAccessException { return get(size - 1); }
-
更新指定位置元素
public void set(int index, E e) throws IllegalAccessException { // 索引校验 if (index < 0 || index > size) { throw new IllegalAccessException("Set failed. Illegal index."); } Node cur = dummyHead.next; for (int i = 0; i < index ; i++) cur = cur.next; cur.e = e; }
-
查找链表中是否存在元素
public boolean contains(E e) { Node cur = dummyHead.next; while(cur != null) { if (cur.e.equals(e)) { return true; } cur = cur.next; } return false; }
-
删除链表元素节点
public E remove(int index) throws IllegalAccessException { // 索引校验 if (index < 0 || index > size) { throw new IllegalAccessException("Remove failed. Illegal index."); } // 定位到待删除节点的前一节点 Node prev = dummyHead; for (int i = 0; i < index - 1 ; i++) prev = prev.next; // 保存待删除节点 Node retNode = prev.next; // 跨过待删除节点进行连接 prev.next = retNode.next; // 待删除节点next置空 retNode.next = null; size --; return retNode.e; } public E removeFirst() throws IllegalAccessException { return remove(0); } public E removeLast() throws IllegalAccessException { return remove(size - 1); }
-
通过上述方法,我们可以分析得出:链表的CURD操作的平均时间复杂度均为O(n),链表的操作均要进行遍历。
仔细想想,如果对链表的操作仅限于头部
呢? 细思极恐,是不是复杂度就降为O(1)啦?又由于链表是动态的,不会造成空间的浪费,所以当且仅当头部
操作下,优势是很明显的!
-
基于
头部操作
,用链表实现栈
,关于Stack接口
,可以查看 数据结构_2:栈public class LinkedListStack<E> implements Stack<E> { private LinkedList<E> list; public LinkedListStack(LinkedList<E> list) { this.list = new LinkedList<>(); } @Override public int getSize() { return list.getSize(); } @Override public boolean isEmpty() { return list.isEmpty(); } @Override public void push(E e) throws IllegalAccessException { list.addFirst(e); } @Override public E pop() throws IllegalAccessException { return list.removeFirst(); } @Override public E peek() throws IllegalAccessException { return list.getFirst(); } }
-
既然有了实现,那就拿
链表栈
和数组栈
比较下吧,创建测试函数private static double testStack(Stack<Integer> stack, int opCount) throws IllegalAccessException { long startTime = System.nanoTime(); Random random = new Random(); for (int i = 0; i < opCount; i ++) stack.push(random.nextInt(Integer.MAX_VALUE)); for (int i = 0; i < opCount; i ++) stack.pop(); long endTime = System.nanoTime(); return (endTime - startTime) / 1000000000.0; }
-
分别创建
链表栈
和数组栈
进行一百万次的入栈和出站操作,比较两者用时,貌似好像链表栈
好一点。
-
继续加大数据量到一千万次的入栈和出栈操作,此时链表栈的表现就不佳了。
原因大致是:数组栈
的pop和push操作基于数组尾部进行处理;而链表栈
的pop和push操作基于链表头部操作,且投保操作含有创建新节点的操作(new Node),因此比较耗时。 -
栈
结构已经创建,那么队列
也是必不可少的,前文中的数组队列
的构建是从头部和尾部均进行了操作,由于出列操作的复杂度为O(n),入列操作的复杂度为O(1),进行了队列结构的优化,于是产生了数组实现的循环队列
,且性能要远高于普通的数组队列
。于是我们对链表
这一结构进行分析:-
由于存在
head
头指针,头部操作的复杂度为O(1)【dummyHead的设定】 -
那么基于这个原理,添加上
tail
尾指针,记录链表尾部(索引),是否可以将尾部的操作复杂度降低呢?head
指针的定位是依赖于虚拟头指针的结构设定,而tail
指针无此设定,若要进行尾部元素删除操作,还需要定位到待删除元素的前一元素,仍需要进行遍历。 -
基于上述,
链表节点Node的next
设定,更有利于我们从链表首部进行出队操作
,链表尾部进行入队操作
。 -
采用
head
+tail
改造我们的LinkedListQueue
/** * @author by Jiangyf * @classname LinkedListQueue * @description 链表队列 * @date 2019/9/28 16:35 */ public class LinkedListQueue<E> implements Queue<E> { /** * 节点内部类 */ private class Node { public E e; public Node next; public Node(E e, Node next) { this.e = e; this.next = next; } public Node(E e) { this(e, null); } public Node() { this(null, null); } @Override public String toString() { return "Node{" + "e=" + e + ", next=" + next + '}'; } } private Node head, tail; private int size; public LinkedListQueue() { this.head = null; this.tail = null; this.size = 0; } @Override public int getSize() { return size; } @Override public boolean isEmpty() { return size == 0; } @Override public void enqueue(E e) { // 入队 从链表尾部进行 if (tail == null) { // 表示链表为空 tail = new Node(e); head = tail; } else { // 不为空,指向新创建的元素,尾指针后移 tail.next = new Node(e); tail = tail.next; } size ++; } @Override public E dequeue() { // 出队 从链表头部进行 if (isEmpty()) { throw new IllegalArgumentException("Queue is empty"); } // 获取待出队元素 Node retNode = head; // 头指针后移 head = head.next; // 待删除元素与链表断开 retNode.next = null; if (head == null) { // 链表中仅有一个元素的情况,头指针移动后变为空链表 tail = null; } size --; return retNode.e; } @Override public E getFront() { if (isEmpty()) { throw new IllegalArgumentException("Queue is empty"); } return head.e; } }
-
同样,与之前的
数组队列
、循环队列
、链表队列
进行下性能测试(10万数量级)
可见,循环队列和链表队列的性能远高于数组队列,其原因就是头尾指针动态控制数据结构,而数组队列出列时要反复的进行数据复制,因此消耗时间较长。
-