手撕LinkedList

LinkedList 底层数据结构是双向链表,可以存储任何元素(包括 null)。它也实现了 Deque 接口,可以将它当做双向队列使用。


初始化

它并没有任何初始化。双向链表依靠 firstlast 两个节点维护:

	/**
     * 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;

对比 LinkedHashMap 发现,双向链表使用头尾节点维护,LinkedList 的节点类型为 Node :

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

整个数据结构大概如此了。


存取操作

来看看新增操作:

add(E)

	public boolean add(E e) {
        linkLast(e);
        return true;
    }

	// 将新增节点作为最后一个节点
	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++;
    }

add(int,E)

	public void add(int index, E element) {
        checkPositionIndex(index);

        // 作为最后一个节点
        if (index == size)
            linkLast(element);
        else
            // 插入 index 节点之前
            linkBefore(element, node(index));
    }

	private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

	// 对于 add 或者 iterator 操作,index 是否是一个有效的参数 
	private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }

	// 返回特殊元素索引的节点
	Node<E> node(int index) {
        // assert isElementIndex(index);
		
        // 基于 size 对半查找索引的节点
        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;
        }
    }

这里需要注意,使用带索引的 add 新增操作会有一个查找过程,并且会对索引进行校验,索引应当大于等于0且小于等于 size,这与 set 操作略有不同。

**set(E) **:

 	public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

	private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
	
	private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }

严格来说,set 属于替换值操作,并不属于新增操作,所以它在检查索引时,索引必须存在,即大于等于0,小于 size 。

对于底层数据结构是双向链表的 LinkedList 来说,它在按索引新增或者替换值时,需要先查找到节点(size 大小一般的循环),而对于底层数据结构是数组的 ArrayList 来说,在按索引新增时,会导致数组复制操作,在替换的时候,则非常快,不会有任何额外的操作。两者的共同点就是新增的索引可以等于 size ,而替换操作的索引则不能。


删除操作

remove 操作:

	// 循环链表,查找到待移除对象,则移除
	public boolean remove(Object o) {
        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;
    }
	
	// 查找到当前索引所在节点,移除该节点
	public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

相对于 ArrayList 来说,两者在移除对象时,都有一个循环操作,需要注意,这里的移除仅仅是移除所找到的第一个元素。在基于索引的移除时,ArrayList 会多一个数组的复制操作,而 LinkedList 则多一个查找操作,并且由于 LinkedList 还实现了 Deque 接口,所以它额外支持 removeFirstremoveLast 快捷操作。


迭代器

在了解底层数据结构之后,对于迭代器的操作,就变为对于底层数据结构的操作。LinkedListArrayList 的迭代器都是 “快速失败” 的。在遇到并发修改集合结构时,会尽最大努力抛出 并发修改异常。这依赖于它们内部维护的 modCount 计数,在创建迭代器的时候,会将该值赋值给迭代器的 expectedModCount ,在迭代器的后续操作中会比较 modCountexpectedModCount (集合的新增,或者删除等会修改 modCount),如果不等于,则抛出 ConcurrentModificationException

注意:并发修改时,只是尽最大努力抛出,我们并不能依赖该异常,下面这种情况则不会抛出异常:

		List<String> linkedList = new LinkedList<>();
        linkedList.add("11s");
        linkedList.add("123");
        linkedList.add("13s");
        Iterator<String> iterator = linkedList.iterator();
        while(iterator.hasNext()){
            String value = iterator.next();
            System.out.println(value);
            if(value.equals("123")){
                linkedList.remove("11s");
            }
        }

这是一种很典型的情况,在遍历到倒数第二个元素以后,进行更改容器结构的操作,这并不会导致并发修改异常抛出。这是因为并发修改异常会在 next 方法抛出,但按如上操作,在下一次进行 hasNext 判断时,循环就结束了,也就无法执行到 next 方法了。

这是一种错误的示范,并不代表你就可以这样去进行修改操作。使用迭代器的 remove 可以移除元素,如果还想进行新增操作,则可以使用 list 的 ListIterator 迭代器。


总结

最近看了几篇集合或者 Map ,它们的很多特性都是由于底层数据结构决定的,所以了解底层数据结构之后,对于在什么场景下,使用什么容器就更加清楚了。

LinkerList 相比 ArrayList 更加适用于删除或者修改操作比较多的场景,它会比 ArrayList 少一些数组复制的操作。但对于查找某个特定索引位置的元素,ArrayList 则拥有更快的速度。不过对于遍历,两者应该都可以。


推荐博文


手撕ArrayList


我与风来


认认真真学习,做思想的产出者,而不是文字的搬运工
错误之处,还望指出

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LinkedListJava中的一个类,它实现了List接口和Deque接口,可以被看作是一个顺序容器、队列和栈。LinkedList的遍历过程和查找过程类似,可以从头节点开始往后遍历。然而,LinkedList不擅长随机位置访问,如果使用随机访问遍历LinkedList,效率会很低。通常情况下,我们会使用foreach循环来遍历LinkedList,因为foreach最终会转换成迭代器形式。LinkedList的遍历核心就是它的迭代器实现。[1] LinkedList的继承体系较为复杂,它继承自AbstractSequentialList类,并实现了List和Deque接口。AbstractSequentialList是一个基于顺序访问的接口,通过继承此类,子类只需实现部分代码即可拥有完整的一套访问某种序列表的接口。LinkedList还实现了Deque接口,Deque又继承自Queue接口,因此LinkedList具备了队列的功能。[2][3] LinkedList的实现方式决定了所有与下标有关的操作都是线性时间复杂度,而在首段或末尾删除元素只需要常数时间复杂度。LinkedList没有实现同步(synchronized),如果需要多个线程并发访问,可以使用Collections.synchronizedList()方法对其进行包装。[2] 总结来说,LinkedList是一个灵活的数据结构,可以用作顺序容器、队列和栈。它的遍历过程需要注意效率问题,不适合随机位置访问。LinkedList的继承体系较为复杂,继承自AbstractSequentialList类,并实现了List和Deque接口。LinkedList的实现方式决定了与下标有关的操作是线性时间复杂度,而在首段或末尾删除元素只需要常数时间复杂度。[1][2][3]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值