一、链表(LinkedList)
一种链式存储的线性表,所有元素的内存地址不一定是连续的
在 java.utils.LiskedList 的是使用双向链表
二、单向链表(SingleLinkedList)
单向链表只能通过头节点中的 next 节点进行遍历
1、构造方法
单向链表元素内存是临时创建的,内存地址是不连续的,使用默认构造方法即可
2、节点设计
private static class Node<E> {
E element;
Node<E> next;
public Node(E element, Node<E> next) {
this.element = element;
this.next = next;
}
}
设置虚拟头结点,设置虚拟头结点有助于元素的操作,leetcode删除链表倒数第N个节点
Node<E> first;
3、插入元素
- 指定元素的插入位置,获得元素的前一个节点 prev,创建新的节点 node ,使新插入 node 节点的next指针指向要插入位置的节点,之后修改 prev 节点指向将要插入的节点。
- 特殊情况处理,如果要插入的节点是链表的第一个元素的节点此时的 prve 便是虚拟头结点
public void add(int index, E element) {
if (index < 0 || index > size){
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
if (index == 0) {
first = new Node<>(element,first);
} else {
Node<E> prev = node(index - 1);
prev.next = new Node<>(element,prev.next);
}
size++ ; // 成员变量,存储链表中元素的个数
}
3.1、根据链表下标获取节点
private Node<E> node(int index) {
if (index < 0 || index > size){
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
4、增加节点
调用插入节点在末尾插入即可
public void add(E element) {
add(size,element);
}
5、查找元素
- 即遍历元素
private Node<E> node(int index) {
rangeCheck(index);
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
public E get(int index) {
return node(index).element;
}
6、删除元素
-
删除节点的关键在于获得将要删除节点的前一个 prev 节点和 后一个 nextNode 节点,prev 节点的next指针指向nextNode即可
-
另一种删除节点的思路 leetcode删除节点:将该元素的值换成后一个元素的值,将后一个元素删除
public E remove(int index) {
if (index < 0 || index > size){
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
Node<E> node = node(index);
E oldElement = node.element;
if (index == 0){
first = first.next;
}else {
Node<E> prev = node(index - 1);
prev.next = prev.next.next;
}
size --; // 成员变量,存储链表中元素的个数
return oldElement;
}
7、修改元素
- 先遍历根据下标获得该节点,然后在进行修改
private Node<E> node(int index) {
rangeCheck(index);
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
public E set(int index, E element) {
Node<E> node = node(index);
E oldElement = node.element;
node.element = element;
return oldElement;
}
三、双向链表(LinkedList)
相比单向链表,在节点设计中增设指向前一个节点的 prev 之中,进而又添加 last 虚拟尾节点指向链表的尾结点。
1、节点设计
//指向头节点的指针
Node<E> first;
//指向尾节点的指针
Node<E> last;
// 构造节点
private static class Node<E> {
E element;
Node<E> prev;
Node<E> next;
public Node(Node<E> prev, E element, Node<E> next) {
this.prev = prev;
this.element = element;
this.next = next;
}
}
2、查找元素
双向链表的查找与单向链表有所不同,单项链表只能通过开头开始查找,双向链表也可以从尾部进行查找。所以可以通过先判断索引所在的位置,选择从头 first 开始或从尾 last 开始查找,进而优化查找。
public E get(int index) {
return node(index).element;
}
/**
* 根据节点的index获得该节点
*/
private Node<E> node(int index) {
rangeCheck(index); // 自定义对index进行是否非法判断
if (index < (size >> 2)) {
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
} else {
Node<E> node = last;
for (int i = size - 1; i > index; i--) {
node = node.prev;
}
return node;
}
}
3、增加元素
双向链表的增加元素相比单向链表更加复杂,插入位置节点是nextNode,插入节点为node,插入位置前一个节点是 prevNode,核心是维护个个节点的 prev 和 next 属性
-
prevNode 的 next 是 node
- 特殊情况:prevNode 是 first ,即在首部插入元素,此时需要修改 first 指针的指向
-
node 的 prev 是 prevNode
-
nextNode 的 prev 是 node
- 特殊情况:nextNode 是 last,即在尾部插入元素,此时需要修改 last 指针的指向
-
node 的 next 是 nextNode
-
如果链表是空,即链表插入第一个元素,需要同时修改 first 和 last 指针
public void add(int index, E element) {
rangeCheckForAdd(index);
if (index == size) {
Node<E> oldLast = last;
last = new Node<>(oldLast, element, null);
if (oldLast == null) { // 这是链表添加的第一个元素
first = last;
} else {
oldLast.next = last;
}
}else {
Node<E> next = node(index);
Node<E> prev = next.prev;
Node<E> node = new Node<>(prev, element, next);
next.prev = node;
if (prev == null) { // index == 0
first = node;
} else {
prev.next = node;
}
}
size++;
}
4、删除元素
删除节点和添加节点类似,主要是处理维护 prev 和 next 指针,特殊情况主要是删除头结点和尾结点
public E remove(int index) {
rangeCheck(index);
Node<E> node = node(index);
Node<E> prev = node.prev;
Node<E> next = node.next;
if (prev == null) { // index == 0
first = next;
} else {
prev.next = next;
}
if (next == null) { // index == size - 1
last = prev;
} else {
next.prev = prev;
}
size--;
return node.element;
}
5、清空链表
单向链表与双向链表类似
public void clear() {
size = 0;
first = null;
last = null;
}
四、链表的复杂度
单看链表的操作是 O(1)级别的时间复杂度,但是还有遍历查找的过程,所以最好情况下时间复杂度是 o(1), 最坏是 O(n),平均复杂度是 O(n)
五、双向链表和动态数组的比较
- 双向链表相比动态数组浪费的内存更少,但是开辟、销毁空间的操作比较多
- 使用情形
- 如果频繁在任意位置进行增加、删除操作,建议使用双向链表
- 如果频繁使用查询功能,建议使用数组