LinkedList双向链表的详细介绍

一、LinkedList链表的存储图解

1.LinkedList底层存储数据由三部分组成,分别为:上一个节点的地址值(prev),下一个节点的地址值(next),存储的数据(data)。如下图所示:

二、LinkedList在Java中的底层实现

(一)LinkedList的常用的父接口及其祖宗接口

     在Java源代码中,可以看出其常用的父接口有List<E>接口,而List<E>接口又继承于Collection<E>接口,由此可以推断出:LinkedList类父接口为List<E>,祖宗接口为Collection<E>。由于实现接口就要重写接口中的全部方法,可以推断出LiskedList<E>中具有List<E>与Collection<E>接口中的所有方法。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
public interface List<E> extends Collection<E>
(二)LinkedList存储方式

        由于LinkedList存储数据的特殊性,Java中没有满足该条件的基础数据类型,所以Java底层实现LinkedList时,使用内部类(Node<E>)的方式对数据进行封装。(注:内部类是供外部类使用的)底层代码如下:该内部类中提供有参构造,为该节点中的变量赋值。

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成员变量

有三个成员变量,size用来记录集合的大小(存放了多少个元素),first记录第一个节点,last记录最后一个节点。

    //该集合的大小
    transient int size = 0;

    //集合第一个元素
    transient Node<E> first;

    //集合最后一个元素
    transient Node<E> last;
(四)LinkedList构造方法

有两个一个无参构造、一个有参构造,重点介绍一下有参构造。参数是Collection或其子类,而参数的泛型只能是E(创建LinkedList<E>时你所规定E的类型)或其子类型。

    public LinkedList() {
    }

    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

在使用有参构造时会调用addAll(c)方法,而后在该方法中又调用重载的addAll(int index, Collection<? extends E> c)方法进行操作。

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

以上可以拆分为6个详细的步骤:

1.检验索引合法性

该方法又套用以下三个方法,主要作用确保index在有效范围内。

checkPositionIndex(index);
    //判断index是否在0到size之间
    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            //如果下标越界,则程序中断抛出异常并输出outofBoundsMsg(int index)方法的返回值
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    //用来判断索引是否合法
    private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }
    //当索引异常时,返回该索引值与集合大小
    private String outOfBoundsMsg(int index) {
        return "Index: "+index+", Size: "+size;
    }

2.将集合转化为数组

将集合c转化为对象数组,并判断数组是否为空,为空则返回false添加失败。

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

3.定位插入点(即index)前后的元素

根据插入位置的不同,定位插入点的前一个节点pred和后一个节点succ

        Node<E> pred, succ;
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            succ = node(index);
            pred = succ.prev;
        }

      (1)如果index==size则说明插入的元素实在最后,则将last地址值赋给pred,succ赋为null。

      (2)否则先调用node(index)方法找到该位置上面的节点,并将地址值赋给succ,并将succ节点中的prev(前一个节点的地址值)赋值给pred。

    Node<E> node(int index) {
        //size>>1 向右偏移一位,相当于缩小2倍
        //判断index在集合中间的左边还是右边
        if (index < (size >> 1)) {
            //在集合中间的左边,从第一个开始查该节点的next查到index值的前一个即可
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            //在集合中间的右边,从最后一个开始查该节点的prev查到index值的后一个即可
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

4.插入新节点

遍历2步骤中的对象数组,通过@SuppressWarnings("unchecked")注解消除强转警告(由于泛型类型擦除,编译器无法完全确认此转换的类型安全性,因此会生成未经检查的警告)。

将遍历后元素创建一个新的节点newNode对象。

先对3步骤中的pred节点进行判断:

  • pred为空,说明是集合中没有元素,该newNode节点为第一个,将该newNode节点的地址值赋给first节点,并将该newNode节点的地址值赋给pred节点。(以上可以看出第一次循环的时候pred的地址值可能为空,当第二次时pred地址值一定不为空)
  • pred不为空,则将newNode的地址值赋给pred的next(相当于将该链表中前一个节点与后一个节点相关联)
    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;
        }

5.更新最后一个节点的指针

插入完成后,处理链表尾部:

  • 如果succ为null,说明插入的是链表的末尾,更新last指向最后一个新节点。
  • 否则,连接新节点和原来的后续节点,将pred的next指针指向succ,并将succ的prev指针指向pred。
    if (succ == null) {
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

6.更新链表大小和修改计数器

更新链表的size(增加新插入元素的数量),并增加修改计数器modCount,然后返回true表示插入成功。

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

上面解释可能很抽象,以下是图解:

(五)常用方法

1.在开头添加元素public void addFirst(E e),将添加新的newNode节点的next指向first,并将原来的first节点的prev指向newNode即可,增加集合大小,增加修改计数器。

    public void addFirst(E e) {
        linkFirst(e);
    }
    private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
    }

2.在最后添加元素public void addLast(E e),原理与在开头添加元素差不多。

    public void addLast(E e) {
        linkLast(e);
    }
    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++;
    }

3.获取指定下标的元素public E get(int index),其中用到的checkElementIndex(index)方法与node(index)方法在(四)LinkedList构造方法

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

4.判断指定元素是否在集合中public int indexOf(Object o),从第一个节点找到最后一个节点,并对每个值与输入的值进行判断,如果存在让返回该位置索引,不存在则返回-1。(注意:由于该集合可以存null值,要对输入的值进行判断)

    public int indexOf(Object o) {
        int index = 0;
        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;
    }

5.根据指定索引删除元素public E remove(int index),首先对index索引进行校验,校验通过后调用unlink(Node<E> x)方法,判断要删除的元素所处的位置,进行不同的操作,最后把要删除的元素清空,减小集合大小,增加修改计数器。

    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }
    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;
    }

删除的位置共有三种如图所示:

三、优缺点

优点
1.动态大小:
        LinkedList是动态数据结构,不需要指定初始大小,可以根据需要动态地增加或减少元素。
2.高效的插入和删除操作:
        在链表中进行插入和删除操作时,只需调整相应节点的引用,不需要像数组那样移动元素。因此,在链表头部和中间位置插入或删除元素时,操作时间复杂度为O(1)。
3.双向遍历:
        由于是双向链表,LinkedList可以从头部和尾部两个方向进行遍历,增加了操作的灵活性。

缺点

1.高内存开销:
        每个元素都存储在一个节点对象中,节点对象不仅包含元素本身,还包含指向前后节点的引用。因此,LinkedList的内存开销比ArrayList高。

2.较慢的随机访问:
        LinkedList不支持高效的随机访问,查找特定索引位置的元素需要从头部或尾部开始遍历,时间复杂度为O(n)。这使得它在频繁随机访问的场景中性能较差。
3.额外的引用操作:
        在插入或删除节点时,虽然不需要移动元素,但需要调整节点的前后引用,对于较长的链表,这些操作可能带来一定的额外开销。

4.两端访问的不安全性:
        由于是双向链表,可以从头部和尾部进行操作,但在多线程环境下,双向访问可能导致并发修改问题。如果没有适当的同步机制,可能会出现数据不一致或意外行为。

总结
        LinkedList在需要频繁插入和删除操作的场景中表现优异,尤其适合于不知道数据大小或者需要动态调整大小的应用场景。然而,其内存开销较高且随机访问性能较差,因此在需要频繁随机访问的情况下,ArrayList可能是更好的选择。同时,由于双向链表的结构,在多线程环境中需要额外注意同步问题,确保操作的线程安全。理解这些优缺点有助于在具体应用中选择合适的数据结构,从而优化程序性能和资源利用。

  • 36
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值