【JAVA集合篇】LinkedList详解

news2024/11/15 18:06:27

文章目录

  • 简介
  • 继承体系
  • 源码分析
    • 主要属性
    • Node节点
    • 构造方法
    • 添加元素
    • 删除元素
  • 总结
  • 经典面试题


简介

上篇文章我们详细分析ArrayList的使用及源码:【JAVA集合篇】ArrayList源码详解 ,本章我们来聊聊LinkedList的使用及源码,LinkedListArrayList数据结构是完全不一样的,ArrayList 底层是数组的结构,而 LinkedList 的底层则是链表的结构, 它可以进行高效的插入和移除的操作,它基于的是一个双向链表的结构。

LinkedList的整体结构图

image-20220210193621308

从图中也能看出,LinkedList 有好多的Node,并且还有firstlast这两个变量保存头部和尾部节点的信息;还有就是它不是一个循环的双向链表,因为它前后都是null,这个也是我们需要注意的地方。

继承体系

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{...}

通过继承体系,我们可以看到 LinkedList 不仅实现了List接口,还实现了QueueDeque接口,所以它既能作为 List 使用,也能作为双端队列使用,当然也可以作为栈使用。

源码分析

主要属性

// 元素个数
transient int size = 0;
// 链表首节点
transient Node<E> first;
// 链表尾节点
transient Node<E> last;

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

构造方法

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
    this();
    //将集合C中的所有的元素都插入到链表中
    addAll(c);
}

添加元素

作为一个双端队列,添加元素主要有两种,一种是在队列尾部添加元素,一种是在队列首部添加元素,这两种形式在LinkedList中主要是通过下面两个方法来实现的。

// 从队列首添加元素
private void linkFirst(E e) {
    // 首节点
    final Node<E> f = first;
    // 创建新节点,新节点的next是首节点
    final Node<E> newNode = new Node<>(null, e, f);
    // 让新节点作为新的首节点
    first = newNode;
    // 判断是不是第一个添加的元素
    // 如果是就把last也置为新节点
    // 否则把原首节点的prev指针置为新节点
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    // 元素个数加1
    size++;
    // 修改次数加1,说明这是一个支持fail-fast的集合
    modCount++;
}

// 从队列尾添加元素
void linkLast(E e) {
    // 队列尾节点
    final Node<E> l = last;
    // 创建新节点,新节点的prev是尾节点
    final Node<E> newNode = new Node<>(l, e, null);
    // 让新节点成为新的尾节点
    last = newNode;
    // 判断是不是第一个添加的元素
    // 如果是就把first也置为新节点
    // 否则把原尾节点的next指针置为新节点
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    // 元素个数加1
    size++;
    // 修改次数加1
    modCount++;
}

public void addFirst(E e) {
    linkFirst(e);
}

public void addLast(E e) {
    linkLast(e);
}

// 作为无界队列,添加元素总是会成功的
public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}

public boolean offerLast(E e) {
    addLast(e);
    return true;
}

上面是作为双端队列来看,它的添加元素分为首尾添加元素,作为List,是要支持在中间添加元素的,主要是通过下面这个方法实现的。

// 在节点succ之前添加元素
void linkBefore(E e, Node<E> succ) {
    // succ是待添加节点的后继节点
    // 找到待添加节点的前置节点
    final Node<E> pred = succ.prev;
    // 在其前置节点和后继节点之间创建一个新节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 修改后继节点的前置指针指向新节点
    succ.prev = newNode;
    // 判断前置节点是否为空
    // 如果为空,说明是第一个添加的元素,修改first指针
    // 否则修改前置节点的next为新节点
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    // 修改元素个数
    size++;
    // 修改次数加1
    modCount++;
}

// 寻找index位置的节点
Node<E> node(int index) {
    // 因为是双链表
    // 所以根据index是在前半段还是后半段决定从前遍历还是从后遍历
    // 这样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;
    }
}

// 在指定index位置处添加元素
public void add(int index, E element) {
    // 判断是否越界
    checkPositionIndex(index);
    // 如果index是在队列尾节点之后的一个位置
    // 把新节点直接添加到尾节点之后
    // 否则调用linkBefore()方法在中间添加节点
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

在中间添加元素的方法也很简单,典型的双链表在中间添加元素的方法。

添加元素的三种方式大致如下图所示:

qrcode

在队列首尾添加元素很高效,时间复杂度为O(1)。

在中间添加元素比较低效,首先要先找到插入位置的节点,再修改前后节点的指针,时间复杂度为O(n)。

删除元素

作为双端队列,删除元素也有两种方式,一种是队列首删除元素,一种是队列尾删除元素。

作为List,又要支持中间删除元素,所以删除元素一个有三个方法,分别如下。

// 删除首节点
private E unlinkFirst(Node<E> f) {
    // 首节点的元素值
    final E element = f.item;
    // 首节点的next指针
    final Node<E> next = f.next;
    // 添加首节点的内容,协助GC
    f.item = null;
    f.next = null; // help GC
    // 把首节点的next作为新的首节点
    first = next;
    // 如果只有一个元素,删除了,把last也置为空
    // 否则把next的前置指针置为空
    if (next == null)
        last = null;
    else
        next.prev = null;
    // 元素个数减1
    size--;
    // 修改次数加1
    modCount++;
    // 返回删除的元素
    return element;
}
// 删除尾节点
private E unlinkLast(Node<E> l) {
    // 尾节点的元素值
    final E element = l.item;
    // 尾节点的前置指针
    final Node<E> prev = l.prev;
    // 清空尾节点的内容,协助GC
    l.item = null;
    l.prev = null; // help GC
    // 让前置节点成为新的尾节点
    last = prev;
    // 如果只有一个元素,删除了把first置为空
    // 否则把前置节点的next置为空
    if (prev == null)
        first = null;
    else
        prev.next = null;
    // 元素个数减1
    size--;
    // 修改次数加1
    modCount++;
    // 返回删除的元素
    return element;
}
// 删除指定节点x
E unlink(Node<E> x) {
    // x的元素值
    final E element = x.item;
    // x的前置节点
    final Node<E> next = x.next;
    // x的后置节点
    final Node<E> prev = x.prev;

    // 如果前置节点为空
    // 说明是首节点,让first指向x的后置节点
    // 否则修改前置节点的next为x的后置节点
    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    // 如果后置节点为空
    // 说明是尾节点,让last指向x的前置节点
    // 否则修改后置节点的prev为x的前置节点
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    // 清空x的元素值,协助GC
    x.item = null;
    // 元素个数减1
    size--;
    // 修改次数加1
    modCount++;
    // 返回删除的元素
    return element;
}
// remove的时候如果没有元素抛出异常
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
// remove的时候如果没有元素抛出异常
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}
// poll的时候如果没有元素返回null
public E pollFirst() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}
// poll的时候如果没有元素返回null
public E pollLast() {
    final Node<E> l = last;
    return (l == null) ? null : unlinkLast(l);
}
// 删除中间节点
public E remove(int index) {
    // 检查是否越界
    checkElementIndex(index);
    // 删除指定index位置的节点
    return unlink(node(index));
}

删除元素的三种方法都是典型的双链表删除元素的方法,大致流程如下图所示。

[qrcode

在队列首尾删除元素很高效,时间复杂度为O(1)。

在中间删除元素比较低效,首先要找到删除位置的节点,再修改前后指针,时间复杂度为O(n)。

前面我们说了,LinkedList是双端队列,还记得双端队列可以作为栈使用吗?

/**
 * 利用LinkedList来模拟栈
 * 栈的特点:先进后出
 */
public class Test {

    private LinkedList<String> linkList = new LinkedList<String>();

    // 压栈
    public void push(String str){
        linkList.addFirst(str);
    }

    // 出栈
    public String pop(){
        return linkList.removeFirst();
    }

    // 查看
    public String peek(){
        return linkList.peek();
    }

    // 判断是否为空
    public boolean isEmpty(){
        return linkList.isEmpty();
    }
}

class Test1 {
    public static void main(String[] args) {
        // 测试栈
        Test test = new Test();
        test.push("我是第1个进去的");
        test.push("我是第2个进去的");
        test.push("我是第3个进去的");
        test.push("我是第4个进去的");
        test.push("我是第5个进去的");
        // 取出
        while (!test.isEmpty()){
            String pop = test.pop();
            System.out.println(pop);
        }
        // 打印结果
        /*我是第5个进去的
        我是第4个进去的
        我是第3个进去的
        我是第2个进去的
        我是第1个进去的*/
    }
}

栈的特性是LIFO(Last In First Out),所以作为栈使用也很简单,添加删除元素都只操作队列首节点即可。

总结

(1)LinkedList是一个以双链表实现的List,因此不存在容量不足的问题,所以没有扩容的方法。

(2)LinkedList还是一个双端队列,具有队列、双端队列、栈的特性。

(3)LinkedList在队列首尾添加、删除元素非常高效,时间复杂度为O(1)。

(4)LinkedList在中间添加、删除元素比较低效,时间复杂度为O(n)。

(5)LinkedList不支持随机访问,所以访问非队列首尾的元素比较低效。

(6)LinkedList在功能上等于ArrayList + ArrayDeque。

(7)LinkedList是非线程安全的。

(8)LinkedList能存储null值。

经典面试题

谈谈ArrayList和LinkedList的区别。

可以分两部分答:一个是数组与链表底层实现的不同,另一个是答ArrayList和LinkedList的实现细节。

  • ArrayList的底层是数组,LinkedList的底层是双向链表。

  • 数组拥有O(1)的查询效率,可以通过下标直接定位元素;链表在查询元素的时候只能通过遍历的方式查询,效率比数组低。

  • 数组增删元素的效率比较低,通常要伴随拷贝数组的操作;链表增删元素的效率很高,只需要调整对应位置的指针即可。

以上是数组和链表的通俗对比,在日常的使用中,两者都能很好地在自己的适用场景发挥作用。

我们常常用ArrayList代替数组,因为封装了许多易用的api,而且它内部实现了自动扩容机制,由于它内部维护了一个当前容量的指针size,直接往ArrayList中添加元素的时间复杂度是O(1)的,使用非常方便。而LinkedList常常被用作Queue队列的实现类,由于底层是双向链表,能够轻松地提供先入先出的操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/655911.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ceph:关于Ceph 集群如何访问的一些笔记

写在前面 准备考试&#xff0c;整理 Ceph 相关笔记博文内容涉及,Ceph 集群四种访问方式介绍及 Demo&#xff0c;Ceph 客户端支持的操作介绍理解不足小伙伴帮忙指正 对每个人而言&#xff0c;真正的职责只有一个&#xff1a;找到自我。然后在心中坚守其一生&#xff0c;全心全意…

科大讯飞星火大模型评测:从职场到日常生活,样样精通

文 | 大力财经 今年最热门的话题当属OpenAI发布ChatGPT 3.5版本&#xff0c;全球用户对此纷纷追捧。在国内领域&#xff0c;百度、阿里巴巴、字节跳动、360、腾讯和科大讯飞等厂商纷纷推出大型模型&#xff0c;加入“百模大战”的竞赛。 大模型在自然语言处理领域可以显著提高…

推荐几款适用的项目管理工具,助力您的项目更高效运作

随着企业规模的扩大和业务范围的增加&#xff0c;中小型企业近年来越来越重视项目管理。Zoho Projects 是一款适用于中小型企业的项目管理工具&#xff0c;可以帮助中小型企业更好地进行项目管理&#xff0c;提高工作效率和协作效果。 一、强大的项目管理能力 Zoho Projects提供…

华为OD机试之AI面板识别

AI面板识别 题目描述 AI识别到面板上有N&#xff08;1 ≤ N ≤ 100&#xff09;个指示灯&#xff0c;灯大小一样&#xff0c;任意两个之间无重叠。 由于AI识别误差&#xff0c;每次别到的指示灯位置可能有差异&#xff0c;以4个坐标值描述AI识别的指示灯的大小和位置(左上角…

启智平台使用教程

1.创建项目 填写完需要的信息后&#xff0c;点击“创建项目”即可。 进入到下面这个界面 2.创建调试任务 点击“新建调试任务” 算力集群推荐选择“智算网络集群” 新建项目的时候必须要选择镜像&#xff0c;可以根据自己的需要选择合适的镜像。可以把原镜像的安装包删除&…

基于html+css的图展示132

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

关于ASA搜索竞价排名广告

通过调研发现&#xff0c;60%的应用是通过搜索引擎发现的&#xff0c;这一定程度上也反映出用户需要先通过搜索引擎和社交媒体搜索寻找APP的占比较大&#xff0c;我们可以借助应用商店内的排名推荐广告&#xff0c;来改变应用被发现的渠道。 通过广告投放和ASO优化&#xff0c…

软件开发项目的工作量估算方法 —— 代码统计分析工具

目录 软件开发项目的阶段 工作量估算方法 参考比例 最佳实践 移植项目工作量评估工具 —— 代码统计分析工具 代码统计分析工具 —— 分析用参数调整面板 结论 在软件开发项目中&#xff0c;工作量估算是项目成功的关键之一。正确的工作量估算可以帮助开发团队做好时间管…

Elasticsearch 如何把评分限定在0到1之间?

本文题目来自微信群讨论。 在 Elasticsearch 中&#xff0c;评分&#xff08;或打分&#xff09;通常在查询过程中进行&#xff0c;以判断文档的相关性。 默认的打分机制使用的是 BM25&#xff0c;但你也可以通过自定义的打分查询&#xff08;function_score&#xff09;来自定…

Python进阶语法之lambda函数

Python进阶语法之lambda函数 在Python中&#xff0c;lambda函数也被称为匿名函数&#xff0c;它是定义简单函数的一种快捷方式。lambda函数与普通函数一样&#xff0c;可以接收任意数量的参数&#xff0c;但是只能有一个表达式。在本文中&#xff0c;我们将通过丰富的例子和解…

Python与深度学习:Keras、PyTorch和Caffe的使用和模型设计

第一章&#xff1a;介绍 深度学习已经成为当今计算机科学领域的热门技术&#xff0c;而Python则是深度学习领域最受欢迎的编程语言之一。在Python中&#xff0c;有多个深度学习框架可供选择&#xff0c;其中最受欢迎的包括Keras、PyTorch和Caffe。本文将介绍这三个框架的使用和…

Advanced-C.02.程序设计

C语言概述 C程序的结构 C语句分类 表达式语句 xyz; 函数调用语句 printf("Hello World!"); 控制语句 if语句、switch语句、do while语句、while语句、for语句、break语句、goto语句、continue语句、return语句 复合语句 {xy;yz;zx;} 空语句 while(getchar()!\n){;}…

数据结构-各种树(二叉树、二叉查找树、平衡二叉树、红黑树、B树、B+树)

文章目录 二叉树二叉查找树平衡二叉树红黑树B树B树 二叉树 概念&#xff1a;二叉树&#xff08;binary tree&#xff09;是指树中节点的度不大于2的有序树&#xff0c;它是一种最简单且最重要的树。二叉树的递归定义为&#xff1a;二叉树是一棵空树&#xff0c;或者是一棵由一…

【FPGA入门】第三篇、modelsim软件的仿真与应用

目录 第一部分、写在前面 第二部分、关于modelsim软件的相关使用 1、modesim软件新建工程 2、文件报错的解决办法 3、仿真的办法 4、仿真的软件上面的功能按键丢失 5、不同波形颜色的仿真代表的问题 5.1、红色的波形&#xff1a;不定态信号&#xff08;X态&#xff09…

【前端 - CSS】第 18 课 - 背景属性

欢迎来到博主 Apeiron 的博客&#xff0c;祝您旅程愉快 &#xff01; 时止则止&#xff0c;时行则行。动静不失其时&#xff0c;其道光明。 ​​​​​​​ 目录 1、缘起 2、背景属性 2.1、背景图 2.2、背景图平铺方式 2.3、背景图位置 2.4、背景图缩放 2.5、背景图…

软件开发人员必须阅读的20本书

本文翻译自国外论坛 medium&#xff0c;原文地址&#xff1a;https://irina-seng.medium.com/top-20-books-a-software-developer-must-read-updated-b24bcc9ee3d 持续学习的心态是软件开发人员想要保持专业相关性并增长自身价值的关键品质。 在这篇博文中&#xff0c;我将推荐…

dp算法篇Day6

"垂死坚持啊" 26、环绕字符串中唯一的子字符串 (1) 题目解析 (2) 算法原理 class Solution { public:int findSubstringInWraproundString(string s) {int n s.size();vector<int> dp(n,1);for(int i1; i<n; i){if(s[i-1]1 s[i] || (s[i-1]z && …

JS 刷新保持iframe页面并支持浏览器前进后退

参考资料 html5新特性&#xff1a;利用history的pushState等方法来解决使用ajax导致页面后退和前进的问题击按钮切换iframe的src&#xff0c;这个路径如何不会被记录到history中&#xff1f;iframe 后退 浏览器history 问题ajax与HTML5 history pushState/replaceState实例 目…

Matlab论文插图绘制模板第104期—带缺口的分组箱线图

在之前的文章中&#xff0c;分享了Matlab分组箱线图的绘制模板&#xff1a; 进一步&#xff0c;再来分享一下带缺口的分组箱线图的绘制模板。 先来看一下成品效果&#xff1a; 特别提示&#xff1a;本期内容『数据代码』已上传资源群中&#xff0c;加群的朋友请自行下载。有需…

PyQT5介绍及基本使用

文章目录 PyQt51. PyQt5的下载与安装1.1 PyQt5介绍1.2 windows PyQt5的安装 2.PyQt5基本UI2.1 第一个PyQt程序2.2 argv2.3 模块介绍 3.基本UI3.1 按钮QPushButton3.2 文本QLabel3.3 输入框3.4 调整窗口大小3.5 调整窗口显示屏幕中间3.6 设置窗口icon 4.布局4.1 QBoxLayout4.1.1…