LinkedList详解(源码分析)

链表这门内功大致分为三个层次:

第一层叫做"单向链表",仅有一个后指针,指向下一个数据。

第二层叫做"双向链表",有两个指针,后指针指向下一个数据,前指针指向上一个数据。

第三层叫做"二叉树",把后指针去掉,换成左右指针。

01、LinkedList的内心功法(基本结构)

复制代码
    //链表中的节点类
    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;
        }
    }

由三部分组成:

节点的元素

下一个节点

上一个节点

对于第一个节点来说,prev为null。

对于最后一个节点来说,next为null。

其余节点是prev指向前一个,next指向后一个。

对这个基本的,一定要牢记于心。

02、LinkedList 增删改查

使用之前初始化。

复制代码
LinkedList<String> list = new LinkedList();

LinkedList并不需要指定大小,因为我没有大小(hhh),内存多大我多大。

1)add()方法插入元素:

复制代码
list.add("董李阳1");
list.add("董李阳2");
list.add("董李阳3");

add()方法内部源码:(其实是调用LinkLast方法)

复制代码
    public boolean add(E e) {
        linkLast(e); //在列表尾部添加元素
        return true; //成功返回true
    }

linkLat,顾名思义,就是链表尾部添加:

复制代码
    /**
     * Links e as last element.
     * 在列表的尾部添加指定的元素
     */
    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++;
    }

添加第一个元素的时候,first和last都为null(因为这个时候,这个元素既是第一个元素,又是最后一个)。

然后新建一个节点newNode,它的prev和next也为null。

然后last和first都赋值为newNode。

添加第二个元素的时候,first和last都指向的是第一个节点。

然后新建一个节点newNode,它的prev指向的是第一个节点,next为null。

然后把第一个节点的next赋值为newNode。

添加第三个元素的时候,first指向的是第一个节点,last指向的是最后一个节点。

然后新建一个节点newNode,它的prev指向的是第二个节点,next为null。

然后把第二个节点的next赋值为newNode。

除了add()

addFrist()方法将元素添加到第一位;

addLast()方法将元素添加到末尾;

addFirst内部其实调用的是linkFirst:

复制代码
    public void addFirst(E e) {
        linkFirst(e);
    }

linkFirst负责把新的节点设置为first,并将新的first的next更新为之前的first。

复制代码
    /**
     * Links e as first element.
     */
    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++;
    }

addLast其实和addFirst差不多,内部调用的是linkLast方法,前面分析过了。

复制代码
    public void addLast(E e) {
        linkLast(e);
    }

2) 删除

remove():删除第一个节点

remove(int):删除指定位置的节点

remove (Object):删除指定元素的节点

removeFirst():删除第一个节点

removeLast():删除最后一个节点

remove()内部调用的是removeFirst(),所以这两个功效一样。

r

emove(int)内部其实调用的是unlink方法。

复制代码
    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

unlink方法其实很好理解,就是更新当前节点的next和prev,然后把当前节点的元素设为空。

复制代码
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;
    }

remove(Object)内部也调用了ublink方法,只不过在此之前要先找到元素所在的节点。

复制代码
    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;
    }

元素为null的时候,必须使用==来判断;元素非空的时候,要使用equals判断。

removeFirst内部调用的是unlinkFirst方法:

复制代码
    public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }

unlinkFirst负责的就是把第一个节点去掉,顺带把后一个节点的prev设为null。

复制代码
    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

3)改:

set()方法来更新元素:

复制代码
list.set(0, "董李阳");

看一下set()源码:

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

来看一下node方法:

复制代码
    Node<E> node(int index) {
        // assert isElementIndex(index);

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

size >>1:右移,相当于除以2,对于计算机来说,移位操作比除法操作更高效,因为数据在计算机内部都是以二进制存储的。

换句话说,node方法会对下标检查,靠近前半段,从0开始,靠近后半截,就从尾巴开始,这样最大可以提高一半的效率。

找到之后,就替换元素的值就可以了。其他都不用变。

4)查找:

可以分为两种:通过位置找元素,通过元素找位置。

indexOf():查找某个元素所在的位置。

get(int):查找某个位置的元素。

先来看看indexOf方法的源码。

复制代码
    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;
    }

get方法的内核其实还是node方法。前面已经讲过,这里跳过。

复制代码
    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

这里查询的函数,还可以演化为一些其他的,比如说:

getFirst()方法用于获取第一个元素。

getLast()方法用于获取最后一个元素。

poll()和pollFirst()方法用于删除并返回第一个元素(尽管名字不同,但方法体是完全相同的)。

pollLast()用于删除并返回最后一个元素;

peekFisrt()方法用于返回但不删除第一个元素。

03、LinkedList的应用:

LinkedList可以用于实现LRU(Least Recently Used)缓存淘汰算法。

LRU缓存淘汰算法是一种常用的缓存淘汰策略,它的基本思想是,当缓存空间不够时候,优先淘汰最近最少使用的缓存数据。在实现LRU缓存淘汰算法时候,可以使用LinkedList来存储缓存数据,每次访问缓存数据时候,将该数据从链表中删除并移动到链表的头部,这样链表的尾部就是最近最少使用的缓存数据,当缓存空间不够的时候,只需要将缓存链表尾部的缓存数据淘汰即可。

相关推荐
weixin_382395233 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__3 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.3 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~4 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华5 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子7 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅8 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni8 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学9 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端