首页 > 编程语言 >05容器篇(D2_集合 - D6_容器源码分析篇 - D2_LinkedList)

05容器篇(D2_集合 - D6_容器源码分析篇 - D2_LinkedList)

时间:2025-01-11 20:29:18浏览次数:3  
标签:Node 容器 LinkedList index ArrayList 链表 源码 D2 节点

目录

本章目标

一、基本介绍

二、原理分析

1. 数据结构

2. 默认容量&最大容量

3. 扩容机制

4. 为什么LinkedList查询慢,增删快?

1> 为什么增删快?

2> 为什么查询慢?

三、经典大厂面试题

1. ArrayList的JDK1.8之前与之后的实现区别?

2. List 和 Map 区别?

3. Array 和 ArrayList 有何区别?什么时候更适合用Array?

4. ArrayList 与 LinkedList 区别?

5. ArrayList 集合加入 10万条数据,应该怎么提高效率?

6. ArrayList 与 Vector 区别?


本章目标

  • 理解LinkedList的底层数据结构
  • 深入源码掌握LinkedList查询慢,新增快的原因

一、基本介绍

List 接口的链接列表实现。实现所有可选的列表操作,并且允许所有元素(包括 null )。

除了实现 List 接口外, LinkedList 类还为在列表的开头及结尾 get 、 remove 和 insert 元素提供了统一 的命名

方法。

这些操作允许将链接列表用作堆栈、队列或双端队列。

特点 :

  • 有序性 : 存入和取出的顺序是一致的
  • 元素可以重复 :
  • 含有带索引的方法
  • 独有特点 : 数据结构是链表,可以作为栈、队列或者双端队列!

LinkedList是一个双向的链表结构,双向链表的长相,如下图!

二、原理分析

1. 数据结构

LinkedList是一个双向链表!

底层数据结构的源码

public class LinkedList<E>{
    transient int size = 0;
    //双向链表的头结点
    transient Node<E> first;
     //双向链表的最后一个节点
    transient Node<E> last;
    //节点类【内部类】
    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是双向链表,在代码中是一个Node类。内部并没有数组的结构。双向链表肯定存在一个头节 点和一个尾部节点。

node节点类,是以内部类的形式存在于LinkedList中的。Node类都有两个成员变 量:

  • prev : 当前节点上一个节点,头节点的上一个节点是null
  • next : 当前节点下一个节点,尾结点的下一个节点是null

链表数据结构的特点 : 查询慢,增删快!

  • 链表数据结构基本构成,是一个node类
  • 每个node类中,有上一个节点【prev】和下一个节点【next】
  • 链表一定存在至少两个节点,first和last节点
  • 如果LinkedList没有数据,irst和last都是为null

2. 默认容量&最大容量

没有默认容量,也没有最大容量

3. 扩容机制

无需扩容机制,只要你的内存足够大,可以无限制扩容下去。前提是不考虑查询的效率。

4. 为什么LinkedList查询慢,增删快?

LinkedList的数据结构的特点,链表的数据结构就是这样的特点!

  • 链表是一种查询慢的结构【相对于数组来说】
  • 链表是一种增删快的结构【相对于数组来说】

上动画

1> 为什么增删快?

新增add

//想LinkedList添加一个元素
public boolean add(E e) {
    //连接到链表的末尾
    linkLast(e);
    return true;
}

//连接到最后一个节点上去
void linkLast(E e) {
    //将全局末尾节点赋值给l
    final Node<E> l = last;
    //创建一个新节点 : (上一个节点, 当前插入元素, null)
    final Node<E> newNode = new Node<>(l, e, null);
    //将当前节点作为末尾节点
    last = newNode;
    //判断l节点是否为null
    if (l == null)
        //既是尾结点也是头节点
        first = newNode;
    else
        //之前的末尾节点,下一个节点时末尾节点!
        l.next = newNode;
    size++;//当前集合的元素数量+1
    modCount++;//操作集合数+1。modCount属性是修改技术器
}

//------------------------------------------------------------------
//向链表中部添加
//参数1,添加的索引位置,添加元素
public void add(int index, E element) {
    //检查索引位是否符合要求
    checkPositionIndex(index);
     //判断当前所有是否是存储元素个数
    if (index == size)//true,最后一个元素
        linkLast(element);
    else
        //连接到指定节点的后面【链表中部插入】
        linkBefore(element, node(index));
}

//根据索引查询链表中节点!
Node<E> node(int index) {
    // 判断索引是否小于 已经存储元素个数的1/2
    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;
   }
}

//将当前元素添加到指定节点之前
void linkBefore(E e, Node<E> succ) {
    // 取出当前节点的前一个节点
    final Node<E> pred = succ.prev;
    //创建当前元素的节点 : 上一个节点,当前元素,下一个节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    //为指定节点上一个节点重新值
    succ.prev = newNode;
    //判断当前节点的上一个节点是否为null
    if (pred == null)
        first = newNode;//当前节点作为头部节点
    else
        pred.next = newNode;//将新插入节点作为上一个节点的下个节点
    size++;//新增元素+1
    modCount++;//操作次数+1
}

remove删除指定索引元素

//删除指定索引位置元素
public E remove(int index) {
    //检查元素索引
    checkElementIndex(index);
    //删除元素节点,
    //node(index) 根据索引查到要删除的节点
    /unlink()删除节点
    return unlink(node(index));
}

//根据索引查询链表中节点!
Node<E> node(int index) {
    // 判断索引是否小于 已经存储元素个数的1/2
    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;
   }
}

//删除一个指定节点
E unlink(Node<E> x) {
    //获取当前节点中的元素
    final E element = x.item;
    //获取当前节点的上一个节点
    final Node<E> next = x.next;
    //获取当前节点的下一个节点
    final Node<E> prev = x.prev;
     //判断上一个节点是否为null
    if (prev == null) {
        //如果为null,说明当前节点为头部节点
        first = next;
   } else {
        //上一个节点,的下一个节点改为下下节点
        prev.next = next;
        //将当前节点的上一个节点置空
        x.prev = null;
   }
     //判断下一个节点是否为null
    if (next == null) {
        //如果为null,说明当前节点为尾部节点
        last = prev;
   } else {
        //下一个节点的上节点,改为上上节点
        next.prev = prev;
        //当前节点的上节点置空
        x.next = null;
   }
     //删除当前节点内的元素
    x.item = null;
    size--;//集合中的元素个数-1
    modCount++;//当前集合操作数+1。modCount计数器,记录当前集合操作次数
    return element;//返回删除的元素
}

2> 为什么查询慢?

查询快和慢是一个相对概念!相对于数组来说

//根据索引查询一个元素
public E get(int index) {
    //检查索引是否存在
    checkElementIndex(index);
    // node(index)获取索引对应节点,获取节点中的数据item
    return node(index).item;
}

//根据索引获取对应节点对象
Node<E> node(int 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;
   }
}

//查看ArrayList里的数组获取元素的方式
public E get(int index) {
    rangeCheck(index);//检查范围
    return elementData(index);//获取元素
}

E elementData(int index) {
    return (E) elementData[index];//一次性操作
}

三、经典大厂面试题

1. ArrayList的JDK1.8之前与之后的实现区别?

  • JDK1.6 : ArrayList像饿汉式,直接创建一个初始化容量为10的数组。缺点就是占用空间较大

  • JDK1.7 & JDK1.8 : ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创 建一个初始容量为10 的数组
private static final Object[] EMPTY_ELEMENTDATA = {};

public ArrayList() {
    super();
    this.elementData = EMPTY_ELEMENTDATA;
}

2. List 和 Map 区别?

Map集合

  • 双列集合 : 一次存一对
  • key是不允许重复的,value可以重复
  • 一个key只能对应一个值value
  • Map集合三兄弟 : HashMap【无序集合】、LinkedHashMap【有序集合】、TreeMap【有序集 合,自带排序能力】

List集合

  • 单列集合 : 一次存一个
  • 有序集合
  • 元素可以重复
  • 带索引
  • List集合主要有两个实现类 : ArrayList和LinkedList

3. Array 和 ArrayList 有何区别?什么时候更适合用Array?

区别:

  • Array可以容纳基本类型和对象,而 ArrayList 只能容纳对象【底层是一个对象数组】。
  • Array指定大小的固定不变,而ArrayList大小是动态的,可自动扩容。
  • Array没有ArrayList 方法多。

尽管 ArrayList 明显是更好的选择,但也有些时候 Array 比较好用,比如下面的三种情况。

  • 1、如果列表的大小已经指定,大部分情况下是存储和遍历它们
  • 2、基本数据类型使用Array更合适。

4. ArrayList 与 LinkedList 区别?

ArrayList

  • **优点:**ArrayList 是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操 作效率会比较高(在内存里是连着放的)。查询快,增删相对慢
  • **缺点:**因为地址连续,ArrayList 要移动数据,所以插入和删除操作效率比较低。

LinkedList

  • **优点:**LinkedList 基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个 连续的地址。对于新增和删除操作 add 和 remove ,LinedList 比较占优势。LinkedList 适用于要 头尾操作或插入指定位置的场景。
  • **缺点:**因为 LinkedList 要移动指针,所以查询操作性能比较低。查询慢,增删快

适用场景分析:

  • 当需要对数据进行对随机访问的情况下,选用 ArrayList 。
  • 当需要对数据进行多次增加删除修改时,采用 LinkedList 。
  • 当然,绝大数业务的场景下,使用 ArrayList 就够了。主要是,注意 : 最好避免 ArrayList 扩容,以 及非顺序的插入。

ArrayList 是如何扩容的?

参考 ArrayList 源码分析中的扩容原理讲解

  • 如果通过无参构造的话,初始数组容量为 0 ,当真正对数组进行添加时,才真正分配容量。每次按 照 1.5 倍(位运算)的比率通过 copeOf 的方式扩容。

重点是 1.5 倍扩容,这是和 HashMap 2 倍扩容不同的地方。

5. ArrayList 集合加入 10万条数据,应该怎么提高效率?

ArrayList 的默认初始容量为 10 ,要插入大量数据的时候需要不断扩容,而扩容是非常影响性能的。

因 此,现在明确了 10 万条数据了,我们可以直接在初始化的时候就设置 ArrayList 的容量!

这样就可以提高效率了~

6. ArrayList 与 Vector 区别?

ArrayList 和 Vector 都是用数组实现的,主要有这么三个区别:

  • 1、Vector 是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果,而 ArrayList 不是。这个可以从源码中看出,Vector 类中的方法很多有 synchronized 进行修饰,这 样就导致了 Vector 在效率上无法与 ArrayList 相比。

Vector 是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。

  • 2、两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同。
  • 3、Vector 可以设置增长因子,而 ArrayList 不可以,ArrayList集合没有增长因子。

适用场景分析:

  • 1、Vector 是线程同步的,所以它也是线程安全的,而 ArrayList 是线程无需同步的,是不安全 的。如果不考虑到线程的安全因素,一般用 ArrayList 效率比较高。

实际场景下,如果需要多线程访问安全的数组,使用 CopyOnWriteArrayList 。

标签:Node,容器,LinkedList,index,ArrayList,链表,源码,D2,节点
From: https://blog.csdn.net/qq_51226710/article/details/144996186

相关文章

  • 05容器篇(D2_集合 - D6_容器源码分析篇 - D3_HashMap)
    目录本章目标一、基本介绍1.什么是HashMap?2.HashMap类的继承关系二、原理分析1.哈希表2.存储数据过程1>存储过程中相关属性2>存储过程图解3>存储过程源码分析3.底层数据1>什么是数据结构?2>HashMap的数据结构3>数据结构的源码三、源码分析1.HashMa......
  • java香烟销售管理系统论文+源码 2025毕设
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容一、研究背景随着社会的发展,烟草行业在经济中占据着重要的地位。然而,传统的香烟销售管理方式面临着诸多挑战。在市场方面,消费者需求日益多样化,对香烟的种类、......
  • java智能排课系统的设计与实现论文+源码 2025毕设
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容一、研究背景随着教育规模的不断扩大和教育资源的日益丰富,传统的人工排课方式已经难以满足现代教育管理的需求。在学校中,课程种类繁多、教师数量众多、教学资......
  • Dreamweaver修改织梦网站源码全攻略:从基础操作到高级定制
    Dreamweaver是一款强大的可视化网页编辑工具,非常适合用来修改基于织梦CMS构建的网站源码。以下是几个实用技巧,帮助开发者更高效地完成这项任务:项目结构理解:熟悉织梦网站的整体目录结构,了解各个文件夹和文件的作用。特别是data、include、templets等关键路径下的内容,对于后续开发......
  • flask框架体育科技运动综合信息平台毕设源码+论文
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、选题背景关于体育科技运动综合信息平台的研究,现有研究多侧重于体育管理的某一单独方面,如运动员管理或者赛事组织等部分内容。专门针对整合运动......
  • flask框架社团管理系统毕设源码+论文
    校园二手货物交易平台m1a2o本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、选题背景关于社团管理系统的研究,现有研究主要集中在通用的管理系统功能实现方面,专门针对高校社团管理系统特点及需......
  • 【关注可白嫖源码】电影票购票选座系统,怎么设计这个系统呢,不会的看过来吧
    设计一个电影票购票选座系统的目的是为用户提供在线选择电影、选座、支付购票的完整体验,同时确保电影排片、座位情况的实时同步。以下是详细的设计方案:1.系统架构设计前端技术:使用React或Vue.js开发用户界面,支持响应式设计,适配PC和移动设备。前端主要负责电影列表展示、......
  • 可白嫖源码-Springboot+vue毕业论文管理系统(案例分析)
    摘 要随着Internet的发展,以网络为支撑的论文管理系统不但可以让学生随时随地提交论文,老师也可以通过电脑或者移动终端随时随地下载论文,对论文进行审核,有关单位部门的工作人员和导师也能随时获取相关信息进行毕业生论文的管理工作,相较于传统手工方式管理毕业生的论文,这种方式......
  • 毕业设计-SSM考勤管理系统(案例分析)-附源码
    摘 要信息化社会内需要与之针对性的信息获取途径,但是途径的扩展基本上为人们所努力的方向,由于站在的角度存在偏差,人们经常能够获得不同类型信息,这也是技术最为难以攻克的课题。针对考勤管理系统等问题,对考勤管理系统进行研究分析,然后开发设计出考勤管理系统以解决问题。考......
  • 可白嫖源码-springboot校园送餐系统(案例分析)
    摘 要随着互联网的飞速发展,网上消费逐渐演变为一种趋势,成为现代商业越来越受欢迎的消费方式。为了提高校园餐饮行业的整体效率和服务水平,给同学们提供更方便快捷的餐饮服务,校园送餐系统随之产生。通过对同学们的用餐方式和用餐时间的全面考察分析,结合软件行业先进的开发技......