数据结构与算法-双向链表

486 阅读3分钟

1,什么是双向链表?

  • 双向链表的节点会多一个指向前一个节点的指向prev

  • 双向链表的头节点的prev指向null,尾节点的next指向null

  • 链表的设计LinkedList会有一个指向头节点的first,指向为节点的last

1.1,双向链表的初始化

  • 相比较于单项链表,多了一个first指向和节点的指向prev

    public class LinkedList3 extends AbstractList {

    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;
    
    	}
    }
    

    }

2,查询方法的实现

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--) {//元素从index到size-1进行查询
    	    node = node.prev;
    	}
    	return node;
    }
}
  • 双向链表的查询和之前的单项链表有很大的不同,查询的效率更高
  • 在查询时,可以判断索引是在size的前1/2,还是size的后1/2。
  • 在size的前1/2: 元素在index为0到传入索引开始查询,node.next赋值给node
  • 在size的后1/2:元素从index到size-1进行查询,倒叙查询,node.prev赋值给node

3,插入元素

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;
}
  • 插入元素就是在,当前的index的位置插入新的元素

  • 保存index位置的节点node为next

  • 保存index位置之前index-1位的节点node为prev

  • 在index位置插入新的节点newNode,newNode的属性prev应该指向index-1位置的节点prev,newNode的属性next应该指向原来index位置的节点next

  • 原来index位置的节点next的prev指向newNode

  • index-1节点位置的next指向newNode

当新插入的节点索引为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;
    }
}
  • 当传入节点索引为0时,也就是当前节点的prev属性为null
  • 此时只需要first指向当前的属性newNode

当插入的节点位置是链表的最后时, 因为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++;
}
  • 当传入的index==size的时候,表示在最后一个位置插入元素
  • 先保存最后位置的节点为oldLast
  • last属性指向插入的新的节点newNode,newNode的prev属性指向oldLast节点,newNode的next属性指向null
  • oldLast的next属性指向插入的新节点,也就是last

4,删除节点

public E remove(int index) {
    // 需要删除的节点
    Node<E> node = node(index);
    // 删除节点的前一个节点
    Node<E> prev = node.prev;
    // 删除节点的后一个节点
    Node<E> next = node.next;	
    // 删除节点, 只需要去掉对这个节点的引用即可
    // 如果prev == null, 说明删除的是第一个节点,index== 0
    if (prev == null) {
    	first = next;
    }else {
    	prev.next = next;
    }
    // 如果next == null, 说明删除的是最后一个节点,index==size-1
    if (next == null) {
    	last = prev;
    }else {
    	next.prev = prev;
    }
    size--;
    return node.element;
}
  • 删除节点:就是让被删除的当前的节点失去前后节点的指向
  • 同样需要特殊处理,删除第一个位置节点和最后位置的节点
  • 最后要让size--

5,清空链表

public void clear() {
    size = 0;
    first = null;
    last = null;
}
  • 需要size为0,first和last的指向全部为

6,双向链表和动态数组的比较

  • 动态数组:开辟,销毁内存空间的次数相对较少,但是可能造成内存空间的浪费(可以通过缩容来解决)
  • 双向链表:开辟,销毁内存空间的次数相对较多,但是不会造成内存空间的浪费
  • 如果频繁的在尾部进行添加,删除操作,两者均可选择
  • 如果频繁的在头部进行添加,删除操作,建议选择双向链表
  • 如果有频繁的查询操作,建议使用动态数组