链表
链表是什么
在计算机科学中,链表是数据元素的线性集合,元素的线性顺序不是由它们在内
存中的物理地址给出的。
它是由一组节点组成的数据结构,每个元素指向下一个
元素,这些节点一起,表示线性序列。
链表特点
- 链表是通过节点来存储数据的。每个节点包含两个部分:
- 数据域:用于储存数据
- 引用域:用于储存下一个节点的引用(指针、引用),也是链表的关键
- 链表是一个分散存储的结构,它的节点分散在内存的任意位置。必须使用链将其连接起来。
- 链表通过引用域指向下一个节点,因此它可以很灵活地改变节点间的关系。
- 链表支持动态扩展(不需要像数组一样初始化时确定长度),可以很方便地在任意位置添加和删除节点。
- 链表的访问速度较慢,因为链表会造成空间跳跃。访问下一个节点需要反复地读取下一个引用。
链表分类
- 单链表:
单链表就是只有一个指向子节点的指针,Head为头指针。访问元素只能从头到尾。
- 双向链表:
双链表与单链表的不同之处在于双链表多了一个指向前驱节点的指针,这样它就可以从前往后和从后往前遍历了。
- 循环链表
循环链表是单链表的结尾指向链表头,形成一个环状的链表。
-
这个结构对遍历很友好,可以从任何节点开始遍历,因为从任何地方开始都可以完整遍历整个结构,但是循环结束条件应该特殊一点。
链表操作分析
以单链表为例:
链表查询的时间复杂度为O(n), 因为它查询需要从头部到尾部去遍历,看是否是要查询的元素。
链表增加和删除的时间复杂度为O(1), 因为它是原地操作,不需要重新调整结构,它只需要调整指针指向就行
比如删除: 它只要改变要删除的目标的指针指向就行。
如图,将删除的节点的前一个指向改为指向自己的next节点,然后自己的next指向改为null。
使用场景
- 实现队列:链表的先进先出(FIFO)特性很适合实现队列。
- 实现栈:链表的后进先出(LIFO)特性可以实现栈。
- 保存动态数据:当数据量不确定时,链表能够很容易添加和删除节点,适合存储动态的数据。
- 频繁进行增删的结构。(因为它增删很快,不会对原有结构造成影响)
手写一个Java的LinkedList
在Java中,LinkedList是双向链表在Java中的实现。它实现了List接口, 内部是一个双向链表的结构。
为了加深理解,我们可以手写一个简单的LinkedList(FakeLinkedList),当然不可能有源码写的好,我这里只是为了演示双向链表的使用。
最终完成的结构如下图所示:
从上到下依次是:
- 当前头节点
- 当前尾节点
- 当前大小
- 头插法
- 尾插法
- 拆开节点
- 遍历方法
- 节点对象
因为Java中的LinkedList是支持泛型的,所以我这个盗版也支持。
Node节点
首先肯定是需要写出Node节点用于存储数据。
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
从上到下依次是:
- 存储值
- 后继节点
- 前驱节点
- 构造函数
完成头插法
头插法就是在链表头部插入一个新节点,将新节点作为头结点。
private void linkFirst(E value){
Node<E> tempNode = first;
Node<E> newNode = new Node<>(null, value, first);
first = newNode;
if(tempNode == null)tail = newNode;
else tempNode.prev = newNode;
size++;
}
从上到下依次是:
- 存储当前头结点的引用
- 创建新节点(前驱为null,后继节点设为当前头节点)
- 头结点指向新节点
- 判断之前存的tempNode是否为空,为空则把尾节点指向当前节点。不然尾节点一直无指向。
- 不为空,则让tempNode的前驱节点指向新节点
完成尾差法
尾插法就是在链表尾部插入一个新节点,将新节点作为尾结点。
private void linkLast(E value){
Node<E> tempNode = tail;
Node<E> newNode = new Node<>(tail, value, null);
tail = newNode;
if(tempNode == null)first = newNode;
else tempNode.next = newNode;
}
操作与头插法差不多,只是是从后面插入,就不赘述了。
完成拆节点操作
拆开节点是你传入任何一个节点,然后这个函数可以帮你把节点从原结构中剥离出来。下面是我去偷的一张示意图。
private E unlink(Node<E> x) {
Node<E> next = x.next;
E item = x.item;
Node<E> prev = x.prev;
// 这里是拆掉前驱节点与自己的关系
if (prev==null) first = next;
else {
// 将前驱节点指向自己的后驱节点
prev.next = next;
// 将自己的前驱节点设为null
x.prev = null;
}
// 这里是拆掉后驱节点与自己的关系
if(next == null) tail = prev;
else {
// 将后驱节点指向自己的前驱节点
next.prev = prev;
// 自己后驱节点设为null
x.next = null;
}
return item;
}
上面就是链表的基本操作了,当然没有写的很完善(你需要暴露出头差法和尾插法等操作的方法,我这里写的只是内部操作,并没有暴露到外面)。
代码测试
为了测试方便,又写了一个遍历方法。
public void lists() {
Node<E> temp = first;
while (temp!=null){
System.out.print(temp.item + " ");
temp = temp.next;
}
}
public static void main(String[] args) {
FakeLinkedList<String> linkedList = new FakeLinkedList<>();
linkedList.linkFirst("2");
linkedList.linkFirst("1");
linkedList.linkLast("3");
linkedList.lists();
}
输出
我是先把2加到头部,然后将1加到头部,再将3加到尾部,这个简单的步骤:
预期输出1 2 3
最后输出1 2 3
输出结果与预期结果一致,然后呢,我们的拆链操作需要手写一个查询方法,找到对应节点再调用我们的unlink()操作,这里就不演示了,逻辑也比较简单。
总结
- 链表是一个线性结构
- 链表有单链表,双向链表,循环链表
- Java里的LinkedList是双链表
- 链表可以用于实现队列,栈
- 链表长度是动态改变的
- 链表增删很快,查询较数组慢
- 链表你自己也可以手写!!!