Java集合二:LinkedList和Queue

本文详细探讨了Java中的LinkedList,讲解了它的基础属性、构造方法、添加和删除节点的具体实现,并与ArrayList进行了对比。LinkedList作为List接口和Deque接口的实现,其在插入和删除操作上的性能优于ArrayList。同时,文章还介绍了Queue接口,特别是PriorityQueue,讨论了其添加、出队和删除元素的方法以及优先级队列的内部堆结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LinkedList

实现List接口与Deque接口的双向链表,实现了列表的所有操作,并且允许包括null值的所有元素。
在这里插入图片描述
先简单介绍一下上图的基础类:
1.Collection 接口: Collection接口是所有集合类的根节点,Collection表示一种规则,所有实现了Collection接口的类遵循这种规则

2.List 接口: List是Collection的子接口,它是一个元素有序(按照插入的顺序维护元素顺序)、可重复、可以为null的集合

3.AbstractCollection 类: Collection接口的骨架实现类,最小化实现了Collection接口所需要实现的工作量

4.AbstractList 类: List接口的骨架实现类,最小化实现了List接口所需要实现的工作量

5.Cloneable 接口: 实现了该接口的类可以显示的调用Object.clone()方法,合法的对该类实例进行字段复制,如果没有实现Cloneable接口的实例上调用Obejct.clone()方法,会抛出CloneNotSupportException异常。正常情况下,实现了Cloneable接口的类会以公共方法重写Object.clone()

6.Serializable 接口: 实现了该接口标示了类可以被序列化和反序列化

7.Deque 接口: Deque定义了一个线性Collection,支持在两端插入和删除元素,Deque实际是“double ended queue(双端队列)”的简称,大多数Deque接口的实现都不会限制元素的数量,但是这个队列既支持有容量限制的实现,也支持没有容量限制的实现,比如LinkedList就是有容量限制的实现,其最大的容量为Integer.MAX_VALUE

8.AbstractSequentialList 类: 提供了List接口的骨干实现,最大限度地减少了实现受“连续访问”数据存储(如链表)支持的此接口所需的工作,对于随机访问数据(如数组),应该优先使用 AbstractList,而不是使用AbstractSequentailList类

基础属性及构造方法
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    //长度
    transient int size = 0;
    //指向头结点
    transient Node<E> first;
    //指向尾结点
    transient Node<E> last;
}
具体实现
添加节点
//在链表的最后添加元素
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++;
}
删除节点

LinkedList中提供了两个方法删除节点,如下源码所示

//方法1.删除指定索引上的节点
public E remove(int index) {
    //检查索引是否正确
    checkElementIndex(index);
    //这里分为两步,第一通过索引定位到节点,第二删除节点
    return unlink(node(index));
}

//方法2.删除指定值的节点
public boolean remove(Object o) {
    //判断删除的元素是否为null
    if (o == null) {
        //若是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;
}

1.首先确定index的位置,是靠近first还是靠近last

2.若靠近first则从头开始查询,否则从尾部开始查询,可以看出这样避免极端情况的发生,也更好的利用了LinkedList双向链表的特征

和ArrayList的对比

相同点
1.接口实现:都实现了List接口,都是线性列表的实现
2.线程安全:都是线程不安全的

区别
1.底层实现:ArrayList内部是数组实现,而LinkedList内部实现是双向链表结构
2.接口实现:ArrayList实现了RandomAccess可以支持随机元素访问,而LinkedList实现了Deque可以当做队列使用
3.性能:新增、删除元素时ArrayList需要使用到拷贝原数组,而LinkedList只需移动指针,查找元素 ArrayList支持随机元素访问,而LinkedList只能一个节点的去遍历

性能比较

1.LinkedList下插入、删除是性能优于ArrayList,这是由于插入、删除元素时ArrayList中需要额外的开销去移动、拷贝元素(但是使用removeElements2方法所示去遍历删除是速度异常的快,这种方式的做法是从末尾开始删除,不存在移动、拷贝元素,从而速度非常快)
2.ArrayList在查询元素的性能上要由于LinkedList

Queue

这里主要介绍一下PriorityQueue(底层用数组实现堆的结构),其他的队列可参考我的另一篇博客(https://blog.youkuaiyun.com/qq_38311489/article/details/89290438

优先队列跟普通的队列不一样,普通队列是一种遵循FIFO规则的队列,拿数据的时候按照加入队列的顺序拿取。 而优先队列每次拿数据的时候都会拿出优先级最高的数据。

优先队列内部维护着一个堆,每次取数据的时候都从堆顶拿数据(堆顶的优先级最高),这就是优先队列的原理。

add,添加方法
public boolean add(E e) {
    return offer(e); // add方法内部调用offer方法
}
public boolean offer(E e) {
    if (e == null) // 元素为空的话,抛出NullPointerException异常
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length) // 如果当前用堆表示的数组已经满了,调用grow方法扩容
        grow(i + 1); // 扩容
    size = i + 1; // 元素个数+1
    if (i == 0) // 堆还没有元素的情况
        queue[0] = e; // 直接给堆顶赋值元素
    else // 堆中已有元素的情况
        siftUp(i, e); // 重新调整堆,从下往上调整,因为新增元素是加到最后一个叶子节点
    return true;
}
private void siftUp(int k, E x) {
    if (comparator != null)  // 比较器存在的情况下
        siftUpUsingComparator(k, x); // 使用比较器调整
    else // 比较器不存在的情况下
        siftUpComparable(k, x); // 使用元素自身的比较器调整
}
private void siftUpUsingComparator(int k, E x) {
    while (k > 0) { // 一直循环直到父节点还存在
        int parent = (k - 1) >>> 1; // 找到父节点索引,等同于(k - 1)/ 2
        Object e = queue[parent]; // 获得父节点元素
        // 新元素与父元素进行比较,如果满足比较器结果,直接跳出,否则进行调整
        if (comparator.compare(x, (E) e) >= 0) 
            break;
        queue[k] = e; // 进行调整,新位置的元素变成了父元素
        k = parent; // 新位置索引变成父元素索引,进行递归操作
    }
    queue[k] = x; // 新添加的元素添加到堆中
}

poll,出队方法
public E poll() {
    if (size == 0)
        return null;
    int s = --size; // 元素个数-1
    modCount++;
    E result = (E) queue[0]; // 得到堆顶元素
    E x = (E) queue[s]; // 最后一个叶子节点
    queue[s] = null; // 最后1个叶子节点置空
    if (s != 0)
        siftDown(0, x); // 从上往下调整,因为删除元素是删除堆顶的元素
    return result;
}
private void siftDown(int k, E x) {
    if (comparator != null) // 比较器存在的情况下
        siftDownUsingComparator(k, x); // 使用比较器调整
    else // 比较器不存在的情况下
        siftDownComparable(k, x); // 使用元素自身的比较器调整
}
private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1; // 只需循环节点个数的一般即可
    while (k < half) {
        int child = (k << 1) + 1; // 得到父节点的左子节点索引,即(k * 2)+ 1
        Object c = queue[child]; // 得到左子元素
        int right = child + 1; // 得到父节点的右子节点索引
        if (right < size &&
            comparator.compare((E) c, (E) queue[right]) > 0) // 左子节点跟右子节点比较,取更大的值
            c = queue[child = right];
        if (comparator.compare(x, (E) c) <= 0)  // 然后这个更大的值跟最后一个叶子节点比较
            break;
        queue[k] = c; // 新位置使用更大的值
        k = child; // 新位置索引变成子元素索引,进行递归操作
    }
    queue[k] = x; // 最后一个叶子节点添加到合适的位置
}

remove,删除队列元素
public boolean remove(Object o) {
    int i = indexOf(o); // 找到数据对应的索引
    if (i == -1) // 不存在的话返回false
        return false;
    else { // 存在的话调用removeAt方法,返回true
        removeAt(i);
        return true;
    }
}
private E removeAt(int i) {
    modCount++;
    int s = --size; // 元素个数-1
    if (s == i) // 如果是删除最后一个叶子节点
        queue[i] = null; // 直接置空,删除即可,堆还是保持特质,不需要调整
    else { // 如果是删除的不是最后一个叶子节点
        E moved = (E) queue[s]; // 获得最后1个叶子节点元素
        queue[s] = null; // 最后1个叶子节点置空
        siftDown(i, moved); // 从上往下调整
        if (queue[i] == moved) { // 如果从上往下调整完毕之后发现元素位置没变,从下往上调整
            siftUp(i, moved); // 从下往上调整
            if (queue[i] != moved)
                return moved;
        }
    }
    return null;
}

注意:
jdk内置的优先队列PriorityQueue内部使用一个堆维护数据,每当有数据add进来或者poll出去的时候会对堆做从下往上的调整和从上往下的调整。

2、PriorityQueue不是一个线程安全的类,如果要在多线程环境下使用,可以使用 PriorityBlockingQueue 这个优先阻塞队列。其中add、poll、remove方法都使用 ReentrantLock 锁来保持同步,take() 方法中如果元素为空,则会一直保持阻塞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值