【数据结构】线性表 ⑥ ( 双循环链表 | 双循环链表插入操作 | 双循环链表删除操作 | LinkedList 双循环链表源码分析 )

2023-10-11 16:51:58 浏览数 (1)

一、双循环链表插入操作处理

双循环链表 中 , 需要对 插入 / 删除 / 遍历 操作 进行特殊处理 , 因为需要调节 前驱指针 和 后继指针 两个指针 ;

如 : 双循环链表 中 , 如果要插入元素 , 将 c 节点 插入到 a 节点 和 b 节点 之间 ,

当前的状态是 a 的后继指针 指向 b , b 的前驱指针指向 a ;

如果要实现插入 c 元素 , 则需要

  • 将 a 的 后继指针 指向 c ,
  • 将 c 的 前驱指针 指向 a ,
  • 将 c 的 后继指针 指向 b ,
  • 将 b 的 前驱指针 指向 c ;

插入节点操作 需要执行四个步骤 :

  • ① 将 c 的 前驱指针 指向 a
  • ② 将 a 的 后继指针 指向 c
  • ③ 将 c 的 后继指针 指向 b
  • ④ 将 b 的 前驱指针 指向 c

二、双循环链表删除操作处理


下面的链表插入成功 , 顺序为 a , c , b ,

如果要删除双循环链表中的 c 元素 , 只需要将 a 元素的 后继指针 指向 b , 将 b 元素的 前驱指针 指向 a 即可 ;

c 元素没有指针指向后 , 会自动被内存回收 ;

三、LinkedList 双循环链表源码分析


LinkedList 源码地址 : https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/ojluni/src/main/java/java/util/LinkedList.java

1、链表节点

LinkedList 链表是一个 双循环链表 , 下面的 Node 类 , 就是双循环链表的 节点 ;

代码语言:javascript复制
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

源码参考 : https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/ojluni/src/main/java/java/util/LinkedList.java#1021

2、LinkedList 链表中收尾元素指针

在 LinkedList 双循环链表中 , 维护了 首元素节点指针 transient Node<E> first , 尾元素节点指针 transient Node<E> last , 分别指向 首尾元素 ;

代码语言:javascript复制
    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

3、链表插入操作

LinkedList 双循环链表 调用 add 方法 添加元素 , 在其中调用了 linkLast 函数 , 将元素插入到了队尾 ;

代码语言:javascript复制
    /**
     * 将指定的元素追加到此列表的末尾。
     *
     * <p>这个方法等价于 {@link #addLast}.
     *
     * @param e 元素添加到此列表
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        linkLast(e);
        return true;
    }

源码参考 : https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/ojluni/src/main/java/java/util/LinkedList.java#354

在 linkLast 函数中 , 创建了新的节点 , 将数据设置到了新节点中 , 最后将新节点设置为 尾部节点 ;

注意 , 设置新的尾部节点时 ,

  • 首先 , 保存原来的尾部节点指针 ( 现在不保存 , 之后访问不到了 ) ;
  • 然后 , 将新的节点设置为 尾部节点 ;
  • 最后 , 将原来的 尾部节点 的后继指针 指向新插入的节点 ;
代码语言:javascript复制
    /**
     * 链接作为最后一个元素。
     */
    void linkLast(E e) {
    	// 先保存尾结点的指针
        final Node<E> l = last;
        // 创建一个新节点 , 将数据插入到新节点中
        final Node<E> newNode = new Node<>(l, e, null);
        // 将新节点赋值给 尾部元素节点指针 
        last = newNode;
        if (l == null)
        	// 链表是空的 , 该节点是插入的第一个节点
            first = newNode;
        else
        	// 链表不为空 , 该节点是插入的除第一个节点之外的后续节点
            l.next = newNode;
        size  ;
        modCount  ;
    }

源码参考 : https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/ojluni/src/main/java/java/util/LinkedList.java#147

4、链表向指定位置插入操作

调用 LinkedList 的 public void add(int index, E element) 函数 , 可以向指定索引添加元素 ,

如果添加的非末尾元素 , 则调用 linkBefore 函数 向 链表 中插入数据 ;

代码语言:javascript复制
    /**
     * 将指定元素插入此列表中的指定位置。
     * 将当前在该位置的元素(如果有的话)
     * 和任何后续元素向右移动(在它们的索引上加1)。
     *
     * @param index 要插入指定元素的索引
     * @param element 要插入的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
    	// 检查索引合法性
        checkPositionIndex(index);

        if (index == size)
        	// 如果是添加到末尾
            linkLast(element);
        else
        	// 如果是添加到非末尾的元素
            linkBefore(element, node(index));
    }

源码参考 : https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/ojluni/src/main/java/java/util/LinkedList.java#532

void linkBefore(E e, Node<E> succ) 函数 , 是将 E 数据对应的节点插入到 Node<E> succ 数据之前 ;

代码语言:javascript复制
    /**
     * 在非空节点 succ 前插入元素 e
     */
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        // 获取 succ 节点 前驱节点
        final Node<E> pred = succ.prev;
        // 新建要插入的节点 
        final Node<E> newNode = new Node<>(pred, e, succ);
        // 前驱节点 的 后继指针 指向 新节点
        succ.prev = newNode;
        if (pred == null)
        	// 如果插入的 是 首元素节点
            first = newNode;
        else
        	// 后继节点 的 前驱指针 指向新节点
            pred.next = newNode;
        size  ;
        modCount  ;
    }

源码参考 : https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/ojluni/src/main/java/java/util/LinkedList.java#163

5、获取指定索引的元素

LinkedList 中 , 调用 public E get(int index) 函数 , 获取指定索引的元素 ;

checkElementIndex 函数的作用是 检查该索引是否合法 ;

node 函数就是获取 双循环链表 元素的方法 ;

代码语言:javascript复制
    /**
     * 返回列表中指定位置的元素。
     *
     * @param index 要返回的元素的索引
     * @return 在此列表中指定位置的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

源码参考 : https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/ojluni/src/main/java/java/util/LinkedList.java#500

Node<E> node(int index) 函数的核心操作 , 就是执行 index - 1 次 循环 , 找到对应的节点并返回 ;

在执行前 判定 index 靠近 首元素 还是 尾部元素 , 如果 index < (size >> 1) 可以判定为 index 处于前半部分 ;

  • 如果 index 靠近 首部元素 , 则正向遍历 ;
  • 如果 index 靠近 尾部元素 , 则逆向遍历 ;
代码语言:javascript复制
    /**
     * 返回指定元素索引处的(非空)节点。
     */
    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            // 从前往后遍历
            for (int i = 0; i < index; i  )
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            // 从后往前遍历
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

源码参考 : https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/ojluni/src/main/java/java/util/LinkedList.java#596

6、删除指定索引的元素

LinkedList 双循环链表 中 , 调用 public E remove(int index) 函数 , 删除指定索引的元素 ;

删除的核心操作 , 就是 unlink 函数 , 将指定节点从 双循环链表 中脱离 ;

代码语言:javascript复制
    /**
     * 移除此列表中指定位置的元素。
     * 将所有后续元素向左移动(从它们的索引中减去1)。
     * 返回从列表中删除的元素。
     *
     * @param index 要删除的索引
     * @return the 元素先前在指定位置
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

源码参考 : https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/ojluni/src/main/java/java/util/LinkedList.java#551

unlink 函数中 , 先获取要删除节点的 前驱节点 和 后继节点 , 然后 执行下面两个操作 :

  • 前驱节点 的 后继指针 指向 后继节点 ;
  • 后继节点 的 前驱指针 指向 前驱节点 ;
代码语言:javascript复制
    /**
     * Unlinks non-null node x.
     */
    E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        // 获取节点的 前驱节点
        final Node<E> next = x.next;
        // 获取节点的 后继节点 
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
        	// 前驱节点 的 后继指针 指向 后继节点
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
        	// 后继节点 的 前驱指针 指向 前驱节点 
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount  ;
        return element;
    }

源码参考 : https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/ojluni/src/main/java/java/util/LinkedList.java#220

0 人点赞