List 集合 —— LinkedList


概述

LinkedList继承体系图

图中蓝色实线箭头是指继承关系 ,绿色虚线箭头是指接口实现关系。

  1. LinkedList 继承自 AbstractSequentialList 并实现了 List 接口 和 Deque 双向队列接口,因此 LinkedList 不但拥有 List 的相关操作方法,也拥有队列的相关操作方法。
  2. LinkedList 和 ArrayList 一样实现了 CloneableSerializable 标志接口,因此 LinkedList 拥有克隆和序列化的特性。

LinkedList 一些主要特性:

  • LinkedList 底层实现的数据结构为双向链表
  • LinkedList 元素存取有序,允许存储重复的元素,允许存储 null
  • LinkedList 是非线程安全的,如果想保证线程安全的前提下操作 LinkedList,可以使用 List list = Collections.synchronizedList(new LinkedList(…)); 来生成一个线程安全的 LinkedList

LinkedList 双向链表实现 & 成员变量

双向链表也叫双链表,是链表的一种子数据结构,它具有以下的特点:

每个节点上有三个字段:当前节点的数据字段(data),指向上一个节点的字段(prev),和指向下一个节点的字段(next)
在这里插入图片描述

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

LinkedList 的节点实现完全符合双向链表的数据结构要求,构造方法的参数分别为前驱节点,当前节点的元素,后继节点。

LinkedList 的主要成员变量如下:

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; // 链表的最后一个节点

我们都知道,链表数据结构相对于数组结构,优点在于增删,缺点在于查找。而 LinkedList 通过保存第一个节点和最后一个节点,,当我们需要根据索引查找节点时,可以根据 indexsize / 2的大小,来决定从头部查还是从尾部查,这也算是一定程度上弥补了单链表数据结构的缺点。


LinkedList 构造方法

LinkedList 有两个构造方法:

/**
 * 生成一个空链表 first = last = null
 */
public LinkedList() {
}

/**
 * 根据传入的集合,生成一个包含该集合元素的 LinkedList 集合
 * @param  其内部的元素将按顺序作为 LinkedList 节点
 * @throws NullPointerException 如果传入的集合为 null 将抛出空指针异常
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

带参数的构造方法,内部调用了 addAll(c) 方法,实际上这方法调用了 addAll(size, c) 方法,addAll(c) 在外部单独调用时,将指定集合的元素作为节点,添加到 LinkedList 链表尾部,而 addAll(size, c) 可以将集合元素插入到指定索引位置。

public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

/**
 * 在 index 节点前插入所有 c 集合的元素
 * 返回值表示是否成功添加了对应的元素
 */
public boolean addAll(int index, Collection<? extends E> c) {
	// 检查索引是否满足 0 =< index =< size 的要求
    checkPositionIndex(index); 
	// 调用对应 Collection 实现类的 toArray 方法将集合转为数组
    Object[] a = c.toArray();
    int numNew = a.length;
    // 检查数组长度,如果为 0 则直接返回 false 表示没有添加任何元素
    if (numNew == 0)
        return false;
	// 保存 index 当前的节点为 succ,当前节点的上一个节点为 pred
    Node<E> pred, succ;
    // 如果 index = size 表示在链表尾部插入
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        succ = node(index);
        pred = succ.prev;
    }
 	// 遍历数组将对应的元素包装成节点添加到链表中
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        Node<E> newNode = new Node<>(pred, e, null);
        // 如果 pred 为空表示 LinkedList 集合中还没有元素,生成的第一个节点将作为头节点 赋值给 first 成员变量
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        pred = newNode;
    }
	// 如果 index 位置的元素为 null 则遍历数组后 pred 所指向的节点即为新链表的末节点,赋值给 last 成员变量
    if (succ == null) {
        last = pred;
    } else {
    	// 否则将 pred 的 next 索引指向 succ ,succ 的 prev 索引指向 pred
        pred.next = succ;
        succ.prev = pred;
    }
    // 更新当前链表的长度 size 并返回 true 表示添加成功
    size += numNew;
    modCount++;
    return true;
}

经过上边的代码注释可以了解到,LinkedList 批量添加节点的方法大致分为如下几个步骤:

  1. 检查索引值是否合法,不合法将抛出角标越界异常
  2. 保存 index 位置的节点,和 index-1 位置的节点
  3. 将参数集合转化为数组,循环将数组中的元素封装为节点添加到链表中
  4. 更新链表长度并返回 true 表示添加成功

LinkedList 的增删改查

LinkedList 添加节点的方法

LinkedList 作为链表数据结构的实现,不同于数组,它可以方便的在头尾插入一个节点,而 add 方法默认在链表尾部添加节点:

/**
 * Inserts the specified element at the beginning of this list.
 *
 * @param e the element to add
 */
 public void addFirst(E e) {
    linkFirst(e);
 }

/**
 * Appends the specified element to the end of this list.
 *
 * <p>This method is equivalent to {@link #add}.
 *
 * @param e the element to add
 */
 public void addLast(E e) {
    linkLast(e);
 }
    
/**
 * Appends the specified element to the end of this list.
 *
 * <p>This method is equivalent to {@link #addLast}.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
 public boolean add(E e) {
    linkLast(e);
    return true;
 }

上述英文太过简单不翻译了,我们可以看到 add 方法是有返回值的,这个可以注意下。可以看出最终调用的方法为 linkFirstlinkLast 方法:

/**
 * 在链表头部添加一个节点
 */
private void linkFirst(E e) {
   // 添加元素之前的头节点
   final Node<E> f = first;
   // 以添加的元素为节点值构建新的头节点 并将 next 指针指向 之前的头节点
   final Node<E> newNode = new Node<>(null, e, f);
   // first 索引指向将新的节点
   first = newNode;
   // 如果添加之前链表空则新的节点也作为未节点
   if (f == null)
       last = newNode;
   else
       f.prev = newNode;//否则之前头节点的 prev 指针指向新节点
   size++;
   modCount++;//操作数++
}

/**
 * 在链表末尾添加一个节点
 */
 void linkLast(E e) {
   final Node<E> l = last; // 保存之前的尾节点
   // 构建新的尾节点,并将新节点 prev 指针指向 之前的尾节点
   final Node<E> newNode = new Node<>(l, e, null);
   // last 索引指向尾节点
   last = newNode;
   if (l == null) // 如果之前链表为空则新节点也作为头节点
       first = newNode;
   else // 否则将之前的尾节点的 next 指针指向新节点
       l.next = newNode;
   size++;
   modCount++;//操作数++
}

除了上述几种添加元素的方法,以及在讲构造方法时说明的 addAll 方法,LinkedList 还提供了 add(int index, E element) 方法,下面我们来看看这个方法:

/**
 * 在指定 index 位置插入节点
 */
public void add(int index, E element) {
   // 检查索引是否越界
   checkPositionIndex(index);
   // 如果 index = size 代表是在尾部插入节点
   if (index == size)
       linkLast(element);
   else
       linkBefore(element, node(index));
}

可以先看到当 0 =< index <size 的时候调用了 linkBefore(element, node(index))方法,我们先来看下 node(index) 方法的实现:

/**
 * 返回一个非空节点,这个非空节点位于 index 位置
 */
 Node<E> node(int index) {
   // assert isElementIndex(index);
    // 如果 index < size/2 则从0开始寻找指定索引的节点
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
         // 如果 index >= size/2 则从 size-1 开始寻找指定索引的节点
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
 }

大家可能会疑惑为什么这里注释为返回一个非空节点?其实仔细想下就明白了,这里的节点一定不为 null,如果一开始链表为空的时候,index 为 0 的位置肯定为 null,为什么不会产生异常情况呢?其实如果一开始链表中没有元素 size = 0,如果我们向 index = 0 的位置添加元素是不会走到 else 中的,而是会调用 linkLast(element) 方法去添加元素。 因此 node 方法可以用于根据指定 index 去以 size/2 为界限搜索 index 位置的 Node;

我们再看回 linkBefore 方法,为什么要叫做 linkBefore 呢,因为在链表的中间位置添加节点,其实就是将 index 原来的节点前添加一个节点,添加节点我们需要知道该节点的前一个节点和当前节点:

  1. 将构造的新节点的 prev 指针指向 index 的前一个节点
  2. 新节点的 next 指针指向 index 位置的节点
  3. index 位置节点 prev 指针指向新节点
  4. index 位置前节点(pred)的 next 指针指向新节点

linkBefore 也是做了上述四件事:

void linkBefore(E e, Node<E> succ) {
   // assert succ != null;
   // 由于 succ 一定不为空,所以可以直接获取 prev 节点
   final Node<E> pred = succ.prev;
   // 新节点 prev 节点为 pred,next 节点为 succ
   final Node<E> newNode = new Node<>(pred, e, succ);
   // 原节点的 prev 指向新节点
   succ.prev = newNode;
   // 如果 pred 为空即头节点处插入了一个节点,则将新的节点赋值给 first 索引
   if (pred == null)
       first = newNode;
   else
       pred.next = newNode;// 否则 pred 的下一个节点改为新节点
   size++;
   modCount++;
}

LinkedList 删除节点的方法

与添加节点方法对应的就是删除节点方法:

/**
 *  删除头节点
 * @return 删除的节点的值 即 节点的 element
 * @throws NoSuchElementException 如果链表为空则抛出异常
 */
 public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
 }

/**
 *  删除尾节点
 *
 * @return  删除的节点的值 即 节点的 element
 * @throws NoSuchElementException  如果链表为空则抛出异常
 */
 public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
 }

可以看出最终调用的方法为 unlinkFirstunlinkLast 方法:

/**
 * 移除头节点
 */
 private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    // 头节点的 element 这里作为返回值使用
    final E element = f.item;
    // 头节点下个节点
    final Node<E> next = f.next;
    // 释放头节点的 next 指针,和 element 下次 gc 的时候回收这个内部类
    f.item = null;
    f.next = null; // help GC
    // 将 first 索引指向新的节点
    first = next;
    // 如果 next 节点为空,即链表只有一个节点的时候,last 指向 null
    if (next == null)
        last = null;
    else
        next.prev = null; // 否则 next 的 prev 指针指向 null
    size--;// 改变链表长度
    modCount++;// 修改操作数
    return element;// 返回删除节点的值
 }

/**
 * 移除未节点
 */
 private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    // 未节点的前一个节点,
    final Node<E> prev = l.prev;
    // 释放未节点的内容
    l.item = null;
    l.prev = null; // help GC
    // 将 last 索引指向新的未节点
    last = prev;
    // 链表只有一个节点的时候,first 指向 null
    if (prev == null)
       first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
 }

前面我们说过在指定位置添加节点的时候是4个步骤,移除头尾节点是两个特殊的节点,但是总体来说还是一样的。下面看到 unlink(node(index)) 就是这样的:
在这里插入图片描述

/**
 * Unlinks non-null node x.
 */
E unlink(Node<E> x) {
   // assert x != null;
   final E element = x.item;
   //保存 index 节点的前后两个节点
   final Node<E> next = x.next;
   final Node<E> prev = x.prev;
    // 如果节点为头节点,则做 unlinkFirst 相同操作
   if (prev == null) {
       first = next;
   } else {//否则将上一个节点的 next 指针指向下个节点
       prev.next = next;
       // 释放 index 位置 prev 指针
       x.prev = null;
   }
    // 如果节点为尾节点,则将 last 索引指向上个节点
   if (next == null) {
       last = prev;
   } else {// 否则下个节点 prev 指针指向上个节点
       next.prev = prev;
       x.next = null;
   }

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

看完 unlink 操作结合之前说的 node(index),下边两种删除节点的操作,就很好理解了

/**
 * 删除指定索引位置的节点
 */
public E remove(int index) {
   checkElementIndex(index);
   return unlink(node(index));
}

/**
 *删除从头节点起第一个与 o 相同的节点
 */
public boolean remove(Object o) {
    // 区别对待 null 元素,比较元素时候使用 == 而不是 equals
   if (o == null) {
       for (Node<E> x = first; x != null; x = x.next) {
           if (x.item == null) {
               unlink(x);
               return true;
           }
       }
   } else {
       for (Node<E> x = first; x != null; x = x.next) {
           if (o.equals(x.item)) {
               unlink(x);
               return true;
           }
       }
   }
   return false;
}

看完单个删除节点的方法, LinkedList 实现了 List 接口的 clear 操作,用于删除链表所有的节点:

/**
* Removes all of the elements from this list.
* The list will be empty after this call returns.
*/
public void clear() {
   // 依次清除节点,帮助释放内存空间
   for (Node<E> x = first; x != null; ) {
       Node<E> next = x.next;
       x.item = null;
       x.next = null;
       x.prev = null;
       x = next;
   }
   first = last = null;
   size = 0;
   modCount++;
}

LinkedList 查询节点的方法

LinkedList 查询节点的方法,可分为根据指定的索引查询,获取头节点,获取未节点三种。值得注意的是,根据索引去获取节点内容的效率并不高,所以如果查询操作大于增删操作的时候建议用 ArrayList 去替代。

/**
* 根据索引查询
*
public E get(int index) {
   checkElementIndex(index);
   return node(index).item;
}

/**
* 返回 first 索引指向的节点的内容
*
* @return the first element in this list
* @throws NoSuchElementException 如果链表为空则抛出异常
*/
public E getFirst() {
   final Node<E> f = first;
   if (f == null)
       throw new NoSuchElementException();
   return f.item;
}

/**
* 返回 last 索引指向的节点的内容
*
* @return the last element in this list
* @throws NoSuchElementException 如果链表为空则抛出异常
*/
public E getLast() {
   final Node<E> l = last;
   if (l == null)
       throw new NoSuchElementException();
   return l.item;
}

LinkedList 修改节点的方法

修改节点也分为修改指定索引的节点内容和修改头节点内容,未节点内容的方法? 哈哈,理所当然了,其实 LinkedList 只提供了 set(int index, E element) 一个方法。

public E set(int index, E element) {
   // 判断索引是否越界
   checkElementIndex(index);
   // 采用 node 方法查找对应索引的节点
   Node<E> x = node(index);
   //保存节点原有的内容值
   E oldVal = x.item;
   // 设置新值
   x.item = element;
   // 返回旧的值
   return oldVal;
}

LinkedList 的元素查询方法

通过前面我们知道 LinkedList 提供根据索引查询节点的方法,LinkedList 还提供了一系列判断元素在链表中的位置的方法。

/** 
* 返回参数元素在链表的节点索引,如果有重复元素,那么返回值为从**头节点**起的第一相同的元素节点索引,
* 如果没有值为该元素的节点,则返回 -1;
* 
* @param o element to search for
* @return 
*/
public int indexOf(Object o) {
   int index = 0;
   // 区别对待 null 元素,用 == 判断,非空元素用 equels 方法判断 
   if (o == null) {
       for (Node<E> x = first; x != null; x = x.next) {
           if (x.item == null)
               return index;
           index++;
       }
   } else {
       for (Node<E> x = first; x != null; x = x.next) {
           if (o.equals(x.item))
               return index;
           index++;
       }
   }
   return -1;
}

/**
* 返回参数元素在链表的节点索引,如果有重复元素,那么返回值为从**尾节点起**的第一相同的元素节点索引,
* 如果没有值为该元素的节点,则返回 -1;
*
* @param o element to search for
* @return the index of the last occurrence of the specified element in
*         this list, or -1 if this list does not contain the element
*/
public int lastIndexOf(Object o) {
   int index = size;
   if (o == null) {
       for (Node<E> x = last; x != null; x = x.prev) {
           index--;
           if (x.item == null)
               return index;
       }
   } else {
       for (Node<E> x = last; x != null; x = x.prev) {
           index--;
           if (o.equals(x.item))
               return index;
       }
   }
   return -1;
}

两个方法分别返回从头节点起第一个与指定元素相同的节点索引,和从尾节点起第一个与参数元素相同的节点索引。

除了上述两个方法我们还可以调用 contains(Object o) 来判断链表中是否有该元素存在。调用 indexOf 从头结点开始查询元素位置,遍历完成后若 返回值 !=-1 则表示存在,反之不存在。

public boolean contains(Object o) {
    return indexOf(o) != -1;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值