【源码系列】Java中的数据结构——栈,队列,链表与LinkedList1

简介: 【源码系列】Java中的数据结构——栈,队列,链表与LinkedList

一、数据结构通讲

1.链表

①链表基本介绍

在上篇讲完了数组【源码系列】Java中的数据结构——数组与ArrayList之后,我们知道了数组因为连续存储的原因,所以用下标访问时时间复杂度为O(1)。但连续存储也带来一个问题——数组对于内存条件太苛刻了,系统不可能为它之后预留一大块连续空间,所以数组的大小在一开始便确认了。


在这种情况下,数组对于增删扩容的操作并不友好,每次删除增加都伴随着后续元素的前移和后移,如果容量不够,还得进行数组的复制来达到扩容的效果。


所以数组适合于一些增删操作不怎么频繁且不会经常需要扩容的情况。


但是面对增删操作比较频繁的情况该怎么办呢?

面对这种情况,有没有一种方式可以适应这种增删操作特别平凡的情况呢?


这时候就轮到我们的链表出场了。


我们知道数组这些优缺点的根本原因在于它的存储方式——连续存储。那么我们能不能不连续存储,采取零散存储的方式来避免这种情况呢。


答案是可以的,但此时又面临着一个问题——如何访问对应的元素?


数组因为连续存储的方式可以根据下标轻而易举地确定元素的地址,可是链表是如何做的呢?


在链表中,我们把每个元素叫做结点,结点一般包括该元素的值以及指向下一个元素的指针,有了下一个元素的指针我们便可以方便地访问下一个元素的地址,这样就可以访问对应的元素了。


换个形象点的说法,我们可以把每个元素想象成一个铁链上的一个铁环,元素的指针就是那个环扣,这样我们把每个铁环环环相扣,那么便组成了一条长长的链子,而这便是链表。



q1.png

(图片来自王争的数据结构与算法之美)


而链表的结构也五花八门,常见的有单链表,双向链表,循环链表。

q2.png

q3.png

q4.png




(图片来自王争的数据结构与算法之美)


看图应该很快就能明白,它们的区别就在于结点之间的连接方式,或者说结点里的指针。


对于单链表,

如果我们要删除链表中的结点时,只需将前一个结点指针指向这个结点即可。事实上,从内存上看,这个结点并未“删除”,只是链表中指向它的指针指向了另一个结点,这样这个结点就脱离了这个链表,逻辑上就是在这个链表上删除了这个元素。


增加也同理,只需把前一个结点的指针指向新加的结点,新加的结点指针只需指向下一个结点即可。



q5.png

这样链表增加删除元素实际只需做好指针重新赋值即可,时间复杂度也是O(1)。


②链表的优缺点

优点:

1.由于分散存储,对于内存的要求不高,可以有效利用现有内存空间

2.增删元素非常方便快速,只需把结点的指针重新赋值即可


缺点:

1.根据下标查询效率慢,只能一个一个遍历

2.因为每个结点还需要存储其他结点的指针,所以占用的内存空间会多一些


2.栈

关于“栈”,你可以把它想象成一摞叠在一起的盘子。我们平时放盘子的时候,都是从下往上一个一个放;取的时候,我们也是从上往下一个一个地依次取,不能从中间任意抽出。后进者先出,先进者后出,这就是典型的“栈”结构。


w1.png

(来自王争的数据结构与算法之美)


从栈的操作特性上来看,栈是一种“操作受限”的线性表,只允许在一端插入和删除数据。


3.队列

队列这个概念非常好理解。

你可以把它想象成排队买票,先来的先买,后来的人只能站末尾,不允许插队。先进者先出,这就是典型的“队列”。


我们知道,栈只支持两个基本操作:入栈 push()和出栈 pop()。队列跟栈非常相似,支持的操作也很有限,最基本的操作也是两个:入队 enqueue(),放一个数据到队列尾部;出队 dequeue(),从队列头部取一个元素。


w2.png


(来自王争的数据结构与算法之美)


所以,队列跟栈一样,也是一种操作受限的线性表数据结构。


二、LinkedList源码探究

1.LinkedList继承关系

w3.png


可以看到LinkedList既实现了List接口,也实现了Queue接口(队列),同时也实现了Queue的子类接口Deque接口(双向队列)。


我们前面讲队列和栈的时候讲到栈和队列都是操作受限的线性表,所以我们完全可以用链表来实现队列和栈。而这就是为什么LinkedList会实现Queue和Deque的原因,目的就是提供一个操作受限的接口供用户使用。


2.LinkedList核心原理

2.1内部类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;
        }
    }


这里看到结点里有两个指针,一个指向上一个结点,一个指向下一个结点,所以我们可以知道LinkedList实际上是一个双向链表。


2.2属性

//当前list中元素的数量
  transient int size = 0;
    /**
     * 指向第一个结点的指针
     */
    transient Node<E> first;
    /**
     * 指向最后一个结点的指针
     */
    transient Node<E> last;


LinkedList对象中的属性就只有三个,事实上在所有集合类中,为了尽可能减少内存消耗,都会尽可能少地去设计属性。


2.3 构造方法

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

一个是空构造函数,里面啥也没干;一个是创建一个LinkedList对象,然后将集合c中的元素都添加进对象中。


PS:这里并没有像ArrayList那样有规定初始大小的构造函数,因为链表容量本来就是可以增加的,所以不需要有这样的构造函数。


点开addAll()方法


public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }
public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);
  //调用集合c的toArray方法将集合类对象转化为对象数组
        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;
        //修改操作计数+1
        modCount++;
        return true;
    }



大致来讲它调用了集合的toArray()方法将集合c化为数组,然后循环创建结点然后加到对应的位置,同时修改计数+1


相关文章
|
9天前
|
存储 Java 索引
【数据结构】链表从实现到应用,保姆级攻略
本文详细介绍了链表这一重要数据结构。链表与数组不同,其元素在内存中非连续分布,通过指针连接。Java中链表常用于需动态添加或删除元素的场景。文章首先解释了单向链表的基本概念,包括节点定义及各种操作如插入、删除等的实现方法。随后介绍了双向链表,说明了其拥有前后两个指针的特点,并展示了相关操作的代码实现。最后,对比了ArrayList与LinkedList的不同之处,包括它们底层实现、时间复杂度以及适用场景等方面。
30 10
【数据结构】链表从实现到应用,保姆级攻略
|
3天前
|
存储 人工智能 C语言
数据结构基础详解(C语言): 栈的括号匹配(实战)与栈的表达式求值&&特殊矩阵的压缩存储
本文首先介绍了栈的应用之一——括号匹配,利用栈的特性实现左右括号的匹配检测。接着详细描述了南京理工大学的一道编程题,要求判断输入字符串中的括号是否正确匹配,并给出了完整的代码示例。此外,还探讨了栈在表达式求值中的应用,包括中缀、后缀和前缀表达式的转换与计算方法。最后,文章介绍了矩阵的压缩存储技术,涵盖对称矩阵、三角矩阵及稀疏矩阵的不同压缩存储策略,提高存储效率。
|
5天前
|
存储 C语言
数据结构基础详解(C语言): 栈与队列的详解附完整代码
栈是一种仅允许在一端进行插入和删除操作的线性表,常用于解决括号匹配、函数调用等问题。栈分为顺序栈和链栈,顺序栈使用数组存储,链栈基于单链表实现。栈的主要操作包括初始化、销毁、入栈、出栈等。栈的应用广泛,如表达式求值、递归等场景。栈的顺序存储结构由数组和栈顶指针构成,链栈则基于单链表的头插法实现。
|
6天前
|
Java
【数据结构】栈和队列的深度探索,从实现到应用详解
本文介绍了栈和队列这两种数据结构。栈是一种后进先出(LIFO)的数据结构,元素只能从栈顶进行插入和删除。栈的基本操作包括压栈、出栈、获取栈顶元素、判断是否为空及获取栈的大小。栈可以通过数组或链表实现,并可用于将递归转化为循环。队列则是一种先进先出(FIFO)的数据结构,元素只能从队尾插入,从队首移除。队列的基本操作包括入队、出队、获取队首元素、判断是否为空及获取队列大小。队列可通过双向链表或数组实现。此外,双端队列(Deque)支持两端插入和删除元素,提供了更丰富的操作。
11 0
【数据结构】栈和队列的深度探索,从实现到应用详解
|
10天前
|
Linux C++ Windows
栈对象返回的问题 RVO / NRVO
具名返回值优化((Name)Return Value Optimization,(N)RVO)是一种优化机制,在函数返回对象时,通过减少临时对象的构造、复制构造及析构调用次数来降低开销。在C++中,通过直接在返回位置构造对象并利用隐藏参数传递地址,可避免不必要的复制操作。然而,Windows和Linux上的RVO与NRVO实现有所不同,且接收栈对象的方式也会影响优化效果。
|
25天前
|
存储 安全 编译器
缓冲区溢出之栈溢出(Stack Overflow
【8月更文挑战第18天】
46 3
|
12天前
crash —— 获取内核地址布局、页大小、以及栈布局
crash —— 获取内核地址布局、页大小、以及栈布局
|
13天前
|
存储 程序员 C语言
堆和栈之间有什么区别
【9月更文挑战第1天】堆和栈之间有什么区别
84 0
|
21天前
|
存储 Java 开发者
揭秘!HashMap底层结构大起底:从数组到链表,再到红黑树,Java性能优化的秘密武器!
【8月更文挑战第24天】HashMap是Java集合框架中的核心组件,以其高效的键值对存储和快速访问能力广受开发者欢迎。在JDK 1.8及以后版本中,HashMap采用了数组+链表+红黑树的混合结构,实现了高性能的同时解决了哈希冲突问题。数组作为基石确保了快速定位;链表则用于处理哈希冲突;而当链表长度达到一定阈值时,通过转换为红黑树进一步提升性能。此外,HashMap还具备动态扩容机制,当负载因子超过预设值时自动扩大容量并重新哈希,确保整体性能。通过对HashMap底层结构的深入了解,我们可以更好地利用其优势解决实际开发中的问题。
44 0
|
22天前
|
机器学习/深度学习 消息中间件 缓存
栈与队列的实现
栈与队列的实现
36 0