Java面试专项——集合专题三(LinkedList)

本文详细探讨了LinkedList在Java集合框架中的作用和特性,指出其作为双向链表在增删操作上的优势,同时解释了其底层数据结构和实现细节。LinkedList不仅实现了链表功能,还支持栈和队列的操作。文章通过源码分析,展示了添加、删除和遍历等基本操作的实现,并提供了简单手写LinkedList的示例,最后提到了迭代器的fail-fast机制。
摘要由CSDN通过智能技术生成

目录

ArrayList引发的思考

LinkedList底层原理

LinkedList强大在哪里

手写LinkedList加深印象 

LinkedList集合中迭代器的fail-fast机制


ArrayList引发的思考

优点:查询快

缺点:

1、增删慢,消耗CPU性能(情况一:向指定索引上添加或删除元素,需要往前或往后移动索引后的元素  情况二:如果原数组中空间不够了,需要扩容,创建一个新的数组然后将原数组中的元素复制到新数组上)

2、比较浪费内存空间(申请的空间必须是连续的,并且并不是申请几个空间就用几个空间)

那么有没有一个集合能够弥补ArrayList的缺陷呢?这里我们就要提到双向链表了。

说起数据结构中的链表,我们不得不提这种数据结构的优缺点:

优点:添加和删除元素比较快,因为只是移动指针,并且不需要判断是否需要扩容

缺点:查询和遍历效率比较低,因为链表不能想顺序表那样随机存取,在第i个位置上执行存或者取的操作,顺序表仅需访问一次,而链表需要从头开始依次访问i次

而LinkedList就是基于双向循环链表实现的首先来看一个LinkedList的继承实现树:

LinkedList不仅是双向循环链表的实现,除了可以当做单链表、双链表、循环链表来操作外,它实现了Deque接口,它还可以当做栈、队列和双端队列来使用。

LinkedList底层原理

首先我们来重点说说双向链表这个数据结构

每一个节点都由3个部分组成,一个是数据本身item,一个是指向下一个节点的next指针,还有就是指向上一个节点的prev指针,另外,双向链表还有一个 first 指针,指向头节点,和 last 指针,指向尾节点。在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;
        }
    }

再看该类的成员变量

 成员变量size用来记录双向链表的大小,first节点用来指向链表的头,last用来指向链表的尾

再看类的构造方法

public LinkedList() {
    }

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param  c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

该类有两个构造方法,一个是空参数构造方法,另外一个是通过已有的集合进行构造。我们再来看看通过集合构造里面调用的addAll()方法做了什么

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

        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;

        Node<E> pred, succ;
        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);
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            pred = newNode;
        }

        if (succ == null) {
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

        size += numNew;
        modCount++;
        return true;
    }

 可见,调用集合调用方法,如果传入的集合不为空,就将集合中的元素遍历为每个值封装成一个Node节点,然后依次链接到链表上。

注意如果传入的集合为空,那么会报出空指针异常 

接下来看看LinkedList的基本操作:添加,删除,遍历,查询等

先看添加,从双向链表的结构来看,添加元素可以在链表的头、尾、以及中间的任意位置添加新的元素。因为 LinkedList 有头指针和尾指针,所以在表头或表尾进 行插入元素只需要 O(1) 的时间,而在指定位置插入元素则需要先遍历一下链表, 所以复杂度为 O(n)。首先看看在头部添加元素:

 从头部添加元素,只要把头指针first指向新的node节点,新的node节点的next指针指向原先first指针指向的node,再把原先first指针指向的node的prev指针指向新的node节点就可以了

private void linkFirst(E e) {
        final Node<E> f = first; //使用临时node
        final Node<E> newNode = new Node<>(null, e, f); 
        //封装新的node,并把新node的nex指向f
        first = newNode;
        if (f == null) //判断first是否为空
            last = newNode;
        else
            f.prev = newNode; //把f的prev指向新的node
        size++; //链表长度加1
        modCount++; //记录链表被修改的次数
    }

在尾部添加,其实和在头部添加一样,只是把first换成了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++;
    }

那么在中间位置添加元素呢?

void linkBefore(E e, Node<E> succ) { //表示在在succ节点前面添加e元素
        // assert succ != null;
        final Node<E> pred = succ.prev; //获取succ的前面节点
        final Node<E> newNode = new Node<>(pred, e, succ); //把e封装成节点,并把prev指向succ前面节点,把next指向succ节点
        succ.prev = newNode; //然后把succ的prev指向新的节点
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode; //把succ的前节点的next只想新的节点
        size++; //链表长度+1
        modCount++; //修改次数+1
    }
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强大在哪里

前面我们说过LinkedList实现了链表、栈、队列等诸多数据结构,那么对这些数据结构的操作都对应了LInkedList类中的那些方法呢?来看下面的一个表格

public boolean add(E e)
在链表尾部插入元素
public void add(int index, E element)
在链表指定索引位置插入元素
public void addFirst(E e)
在链表头部插入元素
public void addLast(E e)
在链表尾部插入元素
public boolean addAll(Collection<? extends E> c)
在尾部追加新集合元素
public boolean addAll(int index, Collection<? extends E> c)
在指定索引位置追加集合中的元素
public void push(E e)

对于栈:将元素入栈

对于链表:头部插入与元素

public E pop()

对于栈:将栈顶元素弹出返回

对于链表:返回并删除链表头元素

public E pollFirst()
检索返回并删除链表第一个元素,如果链表为空返回null
public E pollLast()
检索返回并删除链表最后一个元素,如果链表为空返回null
public E peek()
检索返回但不删除链表第一个元素,如果链表为空则返回null
public E peekFirst()
检索返回但不删除链表第一个元素,如果链表为空则返回null
public E peekLast()
检索返回但不删除链表最后一个元素,如果链表为空则返回null
public E element()
检索但不删除此列表的头
public E set(int index, E element)
将指定索引位置的元素修改
public boolean offer(E e)
在链表尾部添加元素
public boolean offerFirst(E e)
在链表头部添加元素
public boolean offerLast(E e)
在链表尾部添加元素
public E get(int index)
返回链表中指定索引的元素
public E getFirst()
返回链表第一个元素
public E getLast()
返回链表中最后一个元素
public E remove()
检索返回并删除链表第一个元素,如果链表为空抛出
NoSuchElementException
public E remove(int index)
删除指定索引位置的元素,并将该元素返回
public boolean remove(Object o)
删除在该链表中第一次出现该元素位置的元素
public E removeFirst()

删除并返回链表中的第一个元素

Throws:
NoSuchElementException – if this list is empty

public E removeLast()

删除并返回链表中最后一个元素

Throws:
NoSuchElementException – if this list is empty

public boolean removeFirstOccurrence(Object o)
删除链表中第一次出现的元素O
public boolean removeLastOccurrence(Object o)
删除链表中最后一次出现的元素O

手写LinkedList加深印象 

下面我们自己尝试手写一个简单的LinkedList。由于底层是链表,如果学过数据结构,我相信即使不看源码自己也能手写一个能够储存若干数据的链表,并完成增删改查等功能。按照ArrayList分析思路,最好的思路是看一下LinkedList底层源码,并自己手写一个简单的LinkedList,这里源码不在贴出,手写的LinkedList仅仅实现了小部分功能,仅供参考

//带头尾指针的双向链表
public class MyLinkedList<E> {
    public MyLinkedList() {
    }

    ;

    //定义数据结点
    private class Node<E> {
        Node previous;//指向前一个结点的指针
        Node next;//指向后一个结点的指针
        E element;//储存数据的变量空间

        public Node(Node previous, Node next, E element) {
            super();
            this.element = element;
            this.next = next;
            this.previous = previous;
        }

        public Node(E element) {
            super();
            this.element = element;
        }
    }

    private Node first;//链表的头指针
    private Node last;//链表的尾指针
    private int size;

    public void add(Object o) {
        Node node = new Node(o);
        //如果第一个节点为空。即第一次增加节点
        if (first == null) {
            node.previous = null;
            node.next = null;
            first = node;
            last = node;
        } else {//如果不是第一次增加节点
            node.previous = last;//将原来链表的尾指针指向的结点设置为新增结点的前驱
            node.next = null;//新结点的后继为空
            last.next = node;//将原来链表的尾指针指向的结点的后继设置为新增结点
            last = node;//更新链表尾指针指向新增结点
        }
        size++;
    }

    public Object get(int index) {
        //检查索引是否合法
        checkIndex(index);
        //获取指定索引位置的结点对象
        Node node=getNodeByIndex(index);
        return node!=null?node.element:null;
    }

    public int size() {
        return size;
    }

    public void remove(int index) {
        checkIndex(index);
        Node node=getNodeByIndex(index);
        if(node!=null){
            Node prior=node.previous;
            Node next=node.next;
            if (prior!=null){
                //将要删除的结点的前驱结点的后继指针指向要删结点的后继结点
                prior.next=next;
            }else {
                //如果要删除的结点前驱为空,证明要删除的头节点,此时移动头指针
                first=first.next;
            }
            if (next!=null){
                //将要删除的结点的后继结点的前驱指针指向要删结点的前驱结点
                next.previous=prior;
            }else {
                //如果要删除的结点后继为空,证明要删除的尾节点,此时移动尾指针
                last=last.previous;
            }
        }
        size--;
    }

    public void checkIndex(int index) {
        if (index < 0 || index > size - 1) {
            throw new RuntimeException("非法的索引:" + index);
        }
    }

    public Node getNodeByIndex(int index) {
        if(size==0){
            return null;
        }
        Node temp = first;
        for (int i = 0; i < index; i++) {
            temp = temp.next;
        }
        return temp;
    }
}

LinkedList集合中迭代器的fail-fast机制

有关迭代器的中的fail-fast机制及其原理,请查看本人写的另外一篇文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

城南皮卡丘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值