目录
1.双向链表
我们之前学习的链表,也叫做单向链表,它有一些缺点:
- 无论访问哪个节点,即使是最后一个节点,也永远都是从头结点向后查找访问。
使用双向链表可以提升链表的综合性能。
1.1双向链表的设计
1.双向链表除了指向头结点的first指针,还多多了一个last指针,指向最后一个节点。
- 这样如果找的是比较靠后的位置的节点,那么就可以从last指针开始向前扫描。
2.双向链表的每个节点中多了一个prev前指针,指向它的上一个节点。
2.实现双向链表
在原来的LinkedList基础上实现双向链表。保留原来的LinkedList并改名为SingleLinkedList。
2.1.属性和构造
public class LinkedList<E> extends AbstractList<E>{
private Node<E> first;
private Node<E> last;
//内部类:Node节点
private static class Node<E>{
E element;
Node<E> next;
Node<E> prev;
public Node(Node<E> prev, E element, Node<E> next){
this.prev = prev;
this.element = element;
this.next = next;
}
}
}
2.2.node(int index)
1.返回索引位置处的节点:原来都是从头结点向后找,现在不能这样写了,因为还可以从后向前找。
靠近前半部分,从前面开始找;靠近后半部分,从后半部分开始找。
/**
* 返回索引位置处的节点
* @param index
* @return
*/
private Node<E> node(int index) {
rangeCheck(index);
Node<E> node = null;
//如果索引靠近左侧
if(index < (size >> 1)) {
node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
}else {//否则索引靠近右侧
node = last;
for (int i = size-1; i > index; i--) {
node = node.prev;
}
}
return node;
}
2.get/set:改(存)查(取)方法调用node()方法,既然node()方法改过了,它们就不用改了
2.3.clear()
1.清空链表的话,first和last指针都要清空
@Override
public void clear() {
size = 0;
first = null;
last = null;
}
2.疑惑:之前我们说,如果一个对象,没有引用指向的话,那么它就会成功死掉。所以我们之前清空单向链表,直接将first置为null即可。
但是现在即使我们将first和last指针都清空,但是每个节点都还有引用/指针指向,那么这些节点还能被成功的清空吗?
3.gc root:能够成功清理
Java里面有一个gc root对象,如果这些节点没有被gc root引用的话,就会被干掉。
比如栈指针(局部变量)就是gc root变量:这个机制会判断,这些节点有没有被栈指针(局部变量)指向包括间接指向,如果没有就会被回收清理。
4.所以clear()这样写即可,我们可以用finalize()方法测试。
2.4.add()方法
- 具体代码
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
//1.当插入到链尾时index=size
if(index == size) {
Node<E> beforeNode = last;
Node<E> newNode = new Node<E>(beforeNode, element, null);
if(beforeNode == null) {
//2.当链表为空没有元素时,size=0
first = newNode;
}else {
beforeNode.next = newNode;
}
last = newNode;
}else {
//找到插入位置的节点:这个节点之后会变成新节点的下一个节点
Node<E> nextNode = node(index);
//得到新节点的前一个节点
Node<E> beforeNode = nextNode.prev;
//新节点
Node<E> newNode = new Node<E>(beforeNode, element, nextNode);
//连线
if(beforeNode == null) {
//3.index = 0时
first = newNode;
}else {
beforeNode.next = newNode;
}
nextNode.prev = newNode;
}
size++;
}
- 注意size=0,添加第一个元素的情况
- 注意当插入到链尾时index=size
- 注意:index = 0时
2.5.remove()方法
代码:
@Override
public E remove(int index) {
rangeCheck(index);
Node<E> oldNode = node(index);
Node<E> prevNode = oldNode.prev;
Node<E> nextNode = oldNode.next;
//如果删除的是0结点,那么prevNode是null
if(prevNode == null) {
first = nextNode;
}else {
prevNode.next = nextNode;
}
//如果删除的是size-1结点,那么nextNode是null
if(nextNode == null) {
last = prevNode;
}else {
nextNode.prev = prevNode;
}
size--;
return oldNode.element;
}
2.6.toString()方法改进
public class LinkedList<E> extends AbstractList<E>{
private Node<E> first;
private Node<E> last;
//内部类:Node节点
private static class Node<E>{
E element;
Node<E> next;
Node<E> prev;
public Node(Node<E> prev, E element, Node<E> next){
this.prev = prev;
this.element = element;
this.next = next;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if(prev != null) {
sb.append(prev.element);
}else {
sb.append("null");
}
sb.append("-").append(element).append("-");
if(next != null) {
sb.append(next.element);
}else {
sb.append("null");
}
return sb.toString();
}
}
@Override
public String toString() {
StringBuilder string = new StringBuilder();
Node<E> tmpNode = first;
string.append("[");
for (int i = 0; i < size; i++) {
if(i != 0) string.append(", ");
string.append(tmpNode);
tmpNode = tmpNode.next;
}
string.append("]");
return string.toString();
}
}
3.双向链表小结
3.1.双向链表VS单向链表
1.粗略对比一下删除的操作数量:需要查找到要删除的位置
- 单向链表平均时间复杂度:(1+2+3+…+n)/n=1/2+n/2
- 双向链表,单次最多查找n/2,平均时间复杂度:(1+2+…+n/2)/n,1/2+n/4
- 复杂度虽然还是O(n):但是删除操作的效率提高了近一半。
3.2.双向链表VS动态数组
动态数组:开辟,销毁内存空间的次数相对较少,但可能造成内存空间的浪费。(可以通过动态缩容机制解决)
双向链表:开辟,销毁内存很频繁。每次添加,删除节点都要开辟销毁内存,但不会造成内存空间的浪费,需要多少用多少。
小结
1.如果频繁在尾部进行添加,删除操作:动态数组,双向链表均可选择。因为数组直接在尾部添加元素不需要移动其他元素,复杂度是O(1);双向链表由于有last指针,能直接找到尾节点,不用从头遍历了,复杂度也是O(1);
2.如果频繁的在头部进行添加,删除操作,建议选择双向链表:首先肯定不能选择动态数组,动态数组此时是最坏的复杂度O(n)。单向链表和双向链表此时差不多,只不过LinkedList在Java中的实现本来就是双向链表。
3.如果有频繁的在任意位置添加,删除操作,建议选择使用双向链表:双向链表的查找效率比单向链表高一倍,所以就会导致删除和添加操作比单向链表更高。
4.如果有频繁的查询操作(随机访问操作),建议选择使用动态数组。
5.有了双向链表,单向链表是否就没有任何用处了呢?
并非如此,在哈希表的设计中就用到了单链表。至于原因,后续再讲。
4.源码分析
1.Java官方的LinkedList也是一个双向链表
2.对比一下源码和我们的实现
- clear:
在之前我们分析过,只要将first
和last
指针置位null
,那么剩下的节点由于没有被gc root
引用,仍然会被清空回收,所以我们没有对剩下的节点在做单独的处理。
但是我们发现JDK中的clear()
方法,除了将first
和last
指针置位null
外,还分别对单独的每个节点都做了清空处理:这是因为JDK中实现的LinkedList有一个迭代器,这个迭代器主要是用来遍历链表的。这就可能存在一个情况,虽然first
和last
指针不再指向这些节点了,但是迭代器对象还可能指向这些节点,正在使用着这些节点,那么就会导致被指向的节点不会销毁。由于LinkedList是双向链表,节点相互指向,就又会导致所有节点都不会被清空。
所以JDK源码将节点的next和prev指针主动置为null,那么只有被迭代器引用的节点还存在,其他的节点就会被回收,方便GC回收。