05-双向链表

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()方法

  1. 具体代码
	@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++;
	}
  1. 注意size=0,添加第一个元素的情况

在这里插入图片描述

在这里插入图片描述

  1. 注意当插入到链尾时index=size
    在这里插入图片描述
  2. 注意: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.对比一下源码和我们的实现

  1. clear:

在这里插入图片描述
在之前我们分析过,只要将firstlast指针置位null,那么剩下的节点由于没有被gc root引用,仍然会被清空回收,所以我们没有对剩下的节点在做单独的处理。

但是我们发现JDK中的clear()方法,除了将firstlast指针置位null外,还分别对单独的每个节点都做了清空处理:这是因为JDK中实现的LinkedList有一个迭代器,这个迭代器主要是用来遍历链表的。这就可能存在一个情况,虽然firstlast指针不再指向这些节点了,但是迭代器对象还可能指向这些节点,正在使用着这些节点,那么就会导致被指向的节点不会销毁。由于LinkedList是双向链表,节点相互指向,就又会导致所有节点都不会被清空。

所以JDK源码将节点的next和prev指针主动置为null,那么只有被迭代器引用的节点还存在,其他的节点就会被回收,方便GC回收。

猜你喜欢

转载自blog.csdn.net/tttxxl/article/details/115243158