链表
存放一个数据的同时保存下一个数据在内存中的地址,通过地址找到下一个数据,存储地址是不连续的,散乱的。
单链表
单链表的结构
每个节点存放的是:val(值)和next(下一个节点的指向)。
查找
单向链表的查找操作通常是这样的:
- 从头节点进入,开始比对节点的值,如果不同则通过指针进入下一个节点
- 重复上面的动作,直到找到相同的值,或者节点的指针指向null 链表的查找性能与数组一样,都是时间复杂度为O(n).
插入删除
链表与数组最大的不同就在于删除、插入的性能优势,由于链表是非连续的内存,所以不用像数组一样在插入、删除操作的时候需要进行大面积的成员位移,比如在a、b节点之间插入一个新节点c,链表只需要:
- a断开指向b的指针,将指针指向c
- c节点将指针指向b,完毕 这个插入操作仅仅需要移动一下指针即可,插入、删除的时间复杂度只有O(1).
读取性能
链表相比之下也有劣势,那就是读取操作远不如数组,数组的读取操作之所以高效,是因为它是一块连续内存,数组的读取可以通过寻址公式快速定位,而链表由于非连续内存,所以必须通过指针一个一个节点遍历.
比如,对于一个数组,我们要读取第三个成员,我们仅需要arr[2]就能快速获取成员,而链表则需要从头部节点进入,在通过指针进入后续节点才能读取.
应用场景
由于双向链表的存在,单向链表的应用场景比较少,因为很多场景双向链表可以更出色地完成.
但是单向链表并非无用武之地,在以下场景中依然会有单向链表的身影:
撤销功能,这种操作最多见于各种文本、图形编辑器中,撤销重做在编辑器场景下属于家常便饭,单向链表由于良好的删除特性在这个场景很适用 实现图、hashMap等一些高级数据结构
代码实现
public class SinglyLinkedList<T> {
// 链表本身的头节点
Node<T> head;
// 链表的大小
int size;
// 初始化头节点
SinglyLinkedList() {
head = new Node<T>(null, null);
}
public class Node<T> {
T data;
Node<T> next;
Node(T data, Node next) {
this.data = data;
this.next = next;
}
}
/**
* 头插法
* @param t
*/
public void addFirstNode(T t) {
// 新节点,还没有指向
Node<T> tNode = new Node<>(t, null);
Node<T> first = head;
// 判断是否为空链表,如果为空,直接插入
if (first.next == null) {
first.next = tNode;
// 大小加一
size++;
return;
}
// 先让当前节点指向头节点的下一个节点,防止下一个节点丢失
tNode.next = first.next;
// 再让头节点指向当前节点
first.next = tNode;
size++;
}
public void addLastNode(T t) {
// 新节点,还没有指向
Node<T> tNode = new Node<>(t, null);
Node<T> first = head;
// 判断是否为空链表,如果为空,直接插入
if (first.next == null) {
first.next = tNode;
// 大小加一
size++;
return;
}
// 循环遍历链表,看是不是尾部
while (first.next != null) {
first = first.next; // 让当前节点变成下一个节点
}
first.next = tNode;
size++;
}
}
/**
* 指定位置插入
* @param index
* @param t
*/
public void addNode(int index, T t){
// 新节点,还没有指向
Node<T> tNode = new Node<>(t, null);
Node<T> first = head;
// 判断是否为空链表,如果为空,直接插入
if (first.next == null) {
first.next = tNode;
// 大小加一
size++;
return;
}
// 判断只要下标不是0那就证明,还没到应该插入的位置,index--,下标从0开始
while (index == 0) {
first = first.next;
index--;
}
Node<T> temp = new Node<>(null, null);
temp.next = first.next;
first.next = tNode;
tNode.next = temp.next;
temp.next = null;
size++;
}
/**
* 删除
* @param data
*/
public void remove(T data) {
// 新节点,还没有指向
Node<T> tNode = new Node<>(data, null);
Node<T> first = head;
// 判断是否为空链表
if (first.next == null) {
System.out.println("没有元素可删除");
return;
}
while (first.next != null) {
if (first.next.data.equals(data)) {
// 当前节点指向的节点已被删除,所以后移
first.next = first.next.next;
first.next.next = null;
size--;
}
first = first.next;
}
}
双向链表
我们上文已经提到,单向链表的应用场景并不多,而真正在生产环境中被广泛运用的正是双向链表.
双向链表与单向链表相比有何特殊之处?
我们看到双向链表多了一个新的指针prev指向节点的前一个节点,当然由于多了一个指针,所以双向链表要更占内存.
别小看双向链表多了一个前置指针,在很多场景里由于多了这个指针,它的效率更高,也更加实用.
比如编辑器的「undo/redo」操作,双向链表就更加适用,由于拥有前后指针,用户可以自由得进行前后操作,如果这个是一个单向链表,那么用户需要遍历链表这时的时间复杂度是O(n).
真正生产级应用中的编辑器采用的数据结构和设计模式更加复杂,比如Word就是采用Piece Table数据结构加上Command queue模式实现「undo/redo」的,不过这是后话了.
代码实现
public class DoubleLinkedList<T> {
Node<T> head;
int size = 0;
DoubleLinkedList() {
head = new Node<>(null,null,null);
}
class Node<T> {
T val;
Node<T> pre;
Node<T> next;
Node(T val, Node pre, Node next) {
this.val = val;
this.pre = pre;
this.next = next;
}
}
/**
* 头插法
* @param val
*/
public void addFirstNode(T val) {
Node<T> tNode = new Node<>(val, null, null);
Node<T> first = head;
if (first.next == null && first.pre == null) {
tNode.pre = first;
first.next = tNode;
size++;
return;
}
// 先让当前节点指向头节点的下一个节点,防止后续节点丢失
tNode.next = first.next;
// 让下一个节点pre指向当前节点
first.next.pre = tNode;
first.next = tNode;
// 当前节点的pre指向头
tNode.pre = first;
size++;
}
/**
* 尾插法
* @param val
*/
public void addLastNode(T val) {
Node<T> tNode = new Node<>(val, null, null);
Node<T> first = head;
if (first.next == null) {
tNode.pre = first;
first.next = tNode;
size++;
return;
}
while (first.next != null) {
first = first.next;
}
first.next = tNode;
tNode.pre = first;
size++;
}
/**
* 指定位置插入
* @param index
* @param val
*/
public void addNode(int index, T val) {
Node<T> tNode = new Node<>(val, null, null);
Node<T> first = head;
if (first.next == null) {
tNode.pre = first;
first.next = tNode;
size++;
return;
}
// 判断是否到这个位置了,如果到了index就为0,没到index就--,然后后移
while (index > 0) {
first = first.next;
index--;
}
Node<T> temp = new Node<>(null,null, null);
temp.next = first.next;
first.next = tNode;
temp.next.pre = tNode;
tNode.next = temp.next;
tNode.pre = first;
temp.next = null;
size++;
}
public void list() {
Node last = head;
if(last.next == null) {
//空链表
System.out.println("链表为空");
return ;
}
while(last.next!=null) {
System.out.println(last.next);
last = last.next;
}
}
}
循环链表
循环链表,顾名思义,他就是将单向链表的尾部指针指向了链表头节点:
循环链表一个应用场景就是操作系统的分时问题,比如有一台计算机,但是有多个用户使用,CPU要处理多个用户的请求很可能会出现抢占资源的情况,这个时候计算机会采取分时策略来保证每个用户的使用体验.
双向循环链表
双向循环链表就是循环链表和双向链表的结合。