概述
鉴于党项链表添加、修改等操作的复杂度,我们可以使用双向链表提升链表的综合性能
双向链表的实现
添加元素
图解过程
常规插入
插入过程如下图所示,整个过程大致为:
- 新结点的prev指针指向原索引为1的结点的前驱节点
- 新节点的next指针指向原索引为1的结点
- 此时,新结点的前驱结点next指针指向新结点
- 此时,新结点的后继节点的prev指针指向新结点
注:这种操作衍生出一种特殊情况,即在表头插入,第三步的操作改为first指向新结点即可
插入到链表末尾
操作过程如下图所示,插入到链表尾巴只要操作四个指针即可,具体过程为:
- 新结点prev指向原先的last结点,next不指向任何结点,置为null
- 原last的next指针指向新的尾结点
- last指针指向新尾结点
注:这种操作衍生出一种特殊情况,即链表为空,操作只需将上述的第二步改为first指向新结点即可
代码
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
Node<E> newNode;
if (index == size) {
Node<E> oldLast = this.last;
last = new Node<E>(last, element, null);
if (oldLast == null) {
first = last;
} else {
oldLast.next = last;
}
} else {
Node<E> newNextNode = findNode(index);
Node<E> newPrevNode = newNextNode.prev;
newNode = new Node<E>(newPrevNode, element, newNextNode);
newNextNode.prev = newNode;
if (newPrevNode == null) {
first = newNode;
} else {
newPrevNode.next = newNode;
}
}
size++;
}
修改元素
代码
@Override
public E set(int index, E element) {
Node<E> node = findNode(index);
E oldElement = node.element;
node.element = element;
return oldElement;
}
删除元素
图解过程
如下所示,操作步骤大致为:
- 找到要删除的元素
- 待删元素的前驱结点next指针指向待删元素的后继结点
- 待删元素的后继结点指向待删元素的前驱结点
注: 这里的删除的结点若是first结点和last结点需特殊处理:
- 若删除的是first,则将第2步改为first指向first结点的前一个
- 若删除的是last,则将第3步改为last指向last结点的前一个
ps:看到下图操作,可能会有读者问为什么待删元素的prev和next不需要手动处理嘛?其实是不必的,java的gc root会回收那些没有被栈区(局部变量)指向的对象,这就是用java写数据结构比C语言方便的地方。
代码
@Override
public E remove(int index) {
Node<E> delNode = findNode(index);
Node<E> delPrevNode =delNode.prev;
Node<E> delNextNode = delNode.next;
if (delPrevNode == null) {
first = delNextNode;
} else {
delPrevNode.next = delNextNode;
}
if (delNextNode == null) {
last = delPrevNode;
} else {
delNextNode.prev = delPrevNode;
}
size--;
return delNode.element;
}
查找元素
代码
这里查找相比单向链表快很多,因为有了前后双指针,所以若要找的元素在前半部分,就用first去遍历。若要找后半部分,则用last指针往前遍历
public Node<E> findNode(int index) {
rangeCheck(index);
Node<E> node = null;
if (index < size >> 1) {
node = this.first;
for (int i = 0; i < index; i++) {
node = node.next;
}
} else {
node = this.last;
for (int i = size - 1; i > index; i--) {
node = node.prev;
}
}
return node;
}
清空元素
代码
@Override
public void clear() {
first = null;
last = null;
size = 0;
}
完整代码
package com.study.doubleLinkListDemo;
import com.study.singlelinkDemo.AbstractList;
import com.study.singlelinkDemo.List;
/**
* Created by Zsy on 2020/8/4.
*/
public class DoubleLinkList<E> extends AbstractList<E> {
private Node<E> first;
private 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;
}
}
@Override
public void clear() {
first = null;
last = null;
size = 0;
}
@Override
public int size() {
return size;
}
public Node<E> findNode(int index) {
rangeCheck(index);
Node<E> node = null;
if (index < size >> 1) {
node = this.first;
for (int i = 0; i < index; i++) {
node = node.next;
}
} else {
node = this.last;
for (int i = size - 1; i > index; i--) {
node = node.prev;
}
}
return node;
}
@Override
public E get(int index) {
return findNode(index).element;
}
@Override
public E set(int index, E element) {
Node<E> node = findNode(index);
E oldElement = node.element;
node.element = element;
return oldElement;
}
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
Node<E> newNode;
if (index == size) {
Node<E> oldLast = this.last;
last = new Node<E>(last, element, null);
if (oldLast == null) {
first = last;
} else {
oldLast.next = last;
}
} else {
Node<E> newNextNode = findNode(index);
Node<E> newPrevNode = newNextNode.prev;
newNode = new Node<E>(newPrevNode, element, newNextNode);
newNextNode.prev = newNode;
if (newPrevNode == null) {
first = newNode;
} else {
newPrevNode.next = newNode;
}
}
size++;
}
@Override
public E remove(int index) {
Node<E> delNode = findNode(index);
Node<E> delPrevNode =delNode.prev;
Node<E> delNextNode = delNode.next;
if (delPrevNode == null) {
first = delNextNode;
} else {
delPrevNode.next = delNextNode;
}
if (delNextNode == null) {
last = delPrevNode;
} else {
delNextNode.prev = delPrevNode;
}
size--;
return delNode.element;
}
@Override
public int indexOf(E element) {
if (element == null) {
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (node.element == null) return i;
node = node.next;
}
} else {
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (element.equals(node.element)) return i;
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(", [");
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (i != 0) {
string.append(", ");
}
string.append(node.element);
node = node.next;
}
string.append("]");
return string.toString();
}
}
小结
与单向链表的比较
删除
单向链表的复杂度为:
总规模:1+2+3+4+…n=(n+1)*n/2=n/2+n^2/2
除n求平均复杂度=(n+1)/2=>1/2+n/2
双向链表的复杂度为:
总规模:(1+2+3+4+…n/2)*2=n/2+n^2/4
除n求平均复杂度=1/2+n/4
综上所述:
双向链表操作过程将近缩减一半
与动态数组的比较
动态数组:开辟、销毁内存空间相对较小,但可能出现内存空间浪费的情况
双向链表:开辟、销毁内存空间相对较多,但不会出现内存空间浪费的情况
使用场景分析
如果频繁在尾结点add、remove操作,动态数组和双向链表均可
如果频繁在头结点add、remove操作,双向链表合适
如果需要频繁查询指定位置的元素,动态数组合适