小码哥数据结构与算法(三): 双向链表

740 阅读4分钟

本篇是恋上数据结构与算法(第一季)的学习笔记, 使用JAVA语言

一、双向链表

  • 与单向链表不同的是, 双向链表多了从后向前的指向
  • 使用双向链表可以加快节点的查询速度

二、设计双向链表

  • 与单项链表不同的是, 双向链表增加了last指针, 同时节点增加了向前的指向prev属性

public class LinkedList3<E> extends AbstractList<E> {
    private Node<E> first;
    private Node<E> last;
    private static class Node<E> {
    	E element;
    	Node<E> next;
    	Node<E> prev;
    	public Node(Node<E> prev, E element, Node<E> next) {
    	    this.element = element;
    	    this.next = next;
    	    this.prev = prev;
    	}
    }
}

另外, 与单项链表不同的方法实现是查询节点插入节点删除节点清空节点这四个方法

1、查询节点

  • 因为是双向链表, 所以查询节点可以从后向前查询
  • 查询节点的方向可以根据节点总数的一半, 即size >> 1为分隔, 索引小于一半的从前向后查询, 索引大于等于一半的从后向前查询
private Node<E> node(int index) {
    rangeCheck(index);
    // 根据节点数量的一半进行区分
    if (index < (size >> 1)) {
    	Node<E> node = first;
    	// 小于size >> 1的节点从前向后查询
    	for (int i = 0; i < index; i++) {
    	    node = node.next;
    	}
    	return node;
    }else {
    	Node<E> node = last;
    	// 大于等于 size >> 1 的节点从后向前查询
    	for (int i = size - 1; i > index; i--) {
    	    node = node.prev;
    	}
    	return node;
    }
}

2、插入节点

  • 插入节点, 就是在两个节点之间加入新的节点
  • 首先要找到需要插入节点位置的原节点, 原节点要成为新节点的下一个节点
  • 原节点的上一个节点成为新节点的上一个节点
public void addNode(int index, E element) {
    // 新节点后的下一个节点, 就是原链表 index 位置的节点
    Node<E> next = node(index);
    // 新节点后的上一个节点, 就是原链表 index-1 位置的节点
    Node<E> prev = next.prev;
    // 创建新节点, 新节点的上一个节点时prev, 新节点的下一个节点是next
    Node<E> node = new Node<>(prev, element, next);
    // next的上一个节点是新节点
    next.prev = node;
    // prev的下一个节点是新节点
    prev.next = node;
}
  • 当新插入的节点索引为0时, 需要做特殊处理, 因为first引用着0节点
public void addNode(int index, E element) {
    // 新节点后的下一个节点, 就是原链表 index 位置的节点
    Node<E> next = node(index);
    // 新节点后的上一个节点, 就是原链表 index-1 位置的节点
    Node<E> prev = next.prev;
    // 创建新节点, 新节点的上一个节点时prev, 新节点的下一个节点是next
    Node<E> node = new Node<>(prev, element, next);
    // next的上一个节点是新节点
    next.prev = node;
    // 当next.prev == null时, 说明新添加节点的索引是0
    if (next.prev == null) { 
        // 此时first应该指向引得索引
        first = node;
    }else {
        // prev的下一个节点是新节点
        prev.next = node;
    }
}
  • 当插入的节点位置是链表的最后时, 因为last属性引用最后一个节点, 所以也需要做特殊处理
  • 最终的插入方法如下
public void add(int index, E element) {
    rangeCheckForAdd(index);
    // 如果 index == size, 说明添加的索引是最后位置
    if (index == size) {
    	Node<E> oldLast = last;
    	// 新创建节点, 新节点的next = null, prev = 之前的最后节点
    	last = new Node<>(oldLast, element, null);
    	// 旧链表的最后节点的下一个节点是 新创建的节点
    	oldLast.next = last;
    }else {
    	// 添加新节点后的下一个节点
    	Node<E> next = node(index);
    	// 添加新节点后的上一个节点
    	Node<E> prev = next.prev;
    	// 创建新节点, 新节点的上一个节点时prev, 新节点的下一个节点是next
        Node<E> node = new Node<>(prev, element, next);
    	// next的上一个节点是新节点
    	next.prev = node;
    	// 当next.prev == null时, 说明新添加节点的索引是0
    	if (next.prev == null) { 
            first = node;
        }else {
            // prev的下一个节点是新节点
            prev.next = node;
        }
    }
    size++;
}

3、删除节点

  • 删除节点, 只需要让被删除节点的前一个节点与后一个节点之间相互联系, 同时去掉所有对被删除节点引用即可
  • 需要注意的是, 查出第0个节点最后一个节点要特殊处理
public E remove(int index) {
    // 需要删除的节点
    Node<E> node = node(index);
    // 删除节点的前一个节点
    Node<E> prev = node.prev;
    // 删除节点的后一个节点
    Node<E> next = node.next;	
    // 删除节点, 只需要去掉对这个节点的引用即可
    // 如果prev == null, 说明删除的是第一个节点
    if (prev == null) {
    	first = next;
    }else {
    	prev.next = next;
    }
    // 如果next == null, 说明删除的是最后一个节点
    if (next == null) {
    	last = prev;
    }else {
    	next.prev = prev;
    }
    size--;
    return node.element;
}

4、清空节点

  • 清空节点, 需要将firstlast的引用全部断开
public void clear() {
    size = 0;
    first = null;
    last = null;
}