【源码系列】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


相关文章
|
3月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
53 1
|
1月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
142 77
|
2天前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
1月前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
43 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
1月前
|
存储 C语言 C++
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
46 9
|
1月前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
38 7
|
2月前
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
60 5
|
3月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
99 5
|
3月前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
71 0
|
安全 Java
Java并发编程笔记之CopyOnWriteArrayList源码分析
并发包中并发List只有CopyOnWriteArrayList这一个,CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行修改操作和元素迭代操作都是在底层创建一个拷贝数组(快照)上进行的,也就是写时拷贝策略。
19569 0