Java-数据结构-链表(LinkedList)-单向链表

一、ArrayList 的优缺点

📚 在介绍"LinkedList"之前,我们先回顾一下上一篇文章中,我们所提到的内容
上一篇文章中,我们讲到了"List"以及它的一个实现类"ArrayList",并且我们知道了"List(线性表)"分为"顺序表"和"链表",而上篇文章中我们所讲到的"ArrayList"就是"顺序表"~

📚 由于"ArrayList"有很多的方法,并且也有它独特的优点,比如

📌 随机访问性能好:因为"ArrayList"的元素在内存中都是连续存储的,所以可以通过下标直接访问指定元素,因此随机访问的时间复杂度仅仅为O(1)

📌 存储效率高:又由于"ArrayList"的元素在内存中是连续存储,所以不用额外空间存储每个元素的地址,故存储效率也较高。

📚 而相应的,"ArrayList"也有很多缺点

📌 插入和删除操作效率低:恰恰也是因为"ArrayList"的元素在内存是连续存储,所以当需要移动或删除某元素时,则需要移动其他的元素来腾出空间或填补空间,因此"ArrayList"的插入和删除时间复杂度为O(n)。

📌 容易造成空间浪费:虽然"ArrayList"能够做到自动扩容,但是自动扩容是"按倍扩容"的,所以有时当"顺序表已满"时如果我们只想再加入一个元素,顺序表也可能给我们开辟出一个非常大的空间,但我们只要一个就够了,这就会导致空间的浪费。

所以当应对一些"需要频繁使用插入和删除操作"的需求时,"ArrayList"就派不上什么用场了~所以为此,"List"便有了第二个实现类 —> "LinkedList(链表)"

二、认识链表

① 链表的概念

我们知道,线性表是由n个具有相同数据类型的元素组成的有限序列并且线性表中的元素在逻辑上是相互关联的,表中的每个元素都有唯一的前驱和后继元素,之前我们介绍的"顺序表"就是"物理存储结构上连续"的存储结构,由图来表示是这样的:

那么对应的,链表的概念就是:

物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。

那么什么是"逻辑顺序通过引用链接次序实现"呢?我们来看看:

上面这张图就是一个链表,从上图中我们能看出一些链表的特征

📕 链式结构在逻辑上是连续的,但在物理上不一定连续。

📕 现实中的结点一般都是从堆上申请出来的。

📕 从堆上申请的空间按照一定策略分配,两次申请的空间是否连续都有可能。

📚 而链表并不像顺序表,链表是多种多样的,以下主要有3种因素,就能组合成8种链表结构

② 链表的分类

📕 单向或者双向

📕 带头或者不带头

📕 循环或者非循环

📚 其实这么多链表结构,我们重点掌握两种即可

📕 无头单项非循环链表:结构简单,一般不用来单独存储数据,但在之后的学习中更多用来作为其他数据结构的子结构。

📕 无头双向链表:在Java的集合框架库中LinkedList底层实现就是⽆头双向循环链表。

三、单向链表(方法及实现)

让我们先看一下"单向链表"都有什么方法:

方法名方法作用
void addFirst(int data)头插法
void addLast(int data)尾插法
void addIndex(int index,int data)任意位置插入 , 第一个数据节点为 0 号下标
boolean contains(int key)查找关键字 key 是否在单链表当中
void remove(int key)删除第一次出现关键字为 key 的节点
void removeAllKey(int key)删除所有值为 key 的节点
int size()得到单链表的长度
void clear()清空单链表
void display()打印单链表

接下来我们将一边讲解"单向链表"的方法,一边对其模拟实现,巩固大家对它的理解~

① 单向链表基本框架

首先,在介绍各种方法之前,我们先来思考一下:单向链表的基本框架应该都包含什么呢?

📚 再让我们观察一下单向链表的结构

由此我们能够梳理出,想要实现单向链表,先要在类中包含这么多的东西~

📕 我们需要定义一个"ListNode"内部类,其中需要有"该结点数据""下结点地址"两种元素

📕 我们还需要定义一个"头结点",用于记录该单向链表的头

📖 代码示例

public class SingleLinkedList {
    static class ListNode {
        private int val;
        private ListNode next;

        public ListNode(int val) {
            this.val = val;
        }
    }
    public ListNode head;

需要注意的是,我们的构造方法只初始化了 val 而并没有初始化 next,这是因为:当我们创建该结点的时候,下一个结点还未存在,于是我们尚不可得知下个结点的地址是什么,所以只能够初始化 val~

随后我们再将刚刚列举出的方法也都一一写入单向链表中,于是我们的代码现在是这样的

② addFirst 方法

单向链表的addFirst(头插法)方法,顾名思义,也就是在单向链表的头部插入一个元素~

那么该如何实现呢?我们先思考一下,想要在单向链表的头部插入元素,那么就意味着,之前的头结点便不再是头结点了,而是我们创建的这个新元素取代头结点,于是我们就得到了第一个结论:

📕 需要将 head 代表的"旧头结点"改换成插入的"新头结点"

而单向链表又是通过 next 环环相扣的,如果不将"新头结点"与"旧头结点"连接起来,那么即便有 head 也找不到原先的链表了,所以就来了第二个需求:

📕 需要在"新头结点"的后面连接"旧头结点"

📖 代码示例

    public void addFirst(int data){
        ListNode node = new ListNode(data);
        node.next = head;
        head = node;
    }

想打印试试看看对不对?别着急嘛,吃一堑长一智,别忘了我们还没写打印单向链表的函数呢~

③ display 方法

📚 想要打印单向链表,我们首先要明确,链表与顺序表的差别

顺序表的元素只存储数字,所以需要用获取长度的方法来打印顺序表

链表的结点存储数字和下一结点的地址,我们从头结点遍历,直到某一刻结点的next为null即可

📖 代码示例

    public void display() {
        ListNode cur = head;
        while(cur != null){
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }

看来是没有问题的~

④ addLast 方法

顾名思义,该方法就是尾插法,作用是在单向链表后插入一个结点,这个实现起来比较简单,我们只需找到最后一个结点,将新结点链接在其后就好了。

    public void addLast(int data){
        ListNode node = new ListNode(data);
        ListNode cur = head;
        if(cur == null){
            head = node;
            return;
        }
        while(cur.next != null){
            cur = cur.next;
        }
        cur.next = node;
    }

(需要注意的是,如果此时为空链表,则找不到尾结点链接 node 所以空链表情况需要单独处理)

⑤ size 方法

该方法作用是获取单向链表的结点个数,思路很简单,遍历链表,定义计数器随时自增就好了。

    public int size(){
        ListNode cur = head;
        int len = 0;
        while(cur != null){
            cur = cur.next;
            len++;
        }
        return len;
    }

⑥ addIndex 方法

该方法的作用是,在单向链表任意位置插入新结点,这个方法相较于头插和尾插要复杂不少,我们一点点分析

首先,传入的下标 Index 必须是合法的,不能小于0,也不能大于单向链表本身的长度,所以我们实现该方法有以下注意点:

📕 需要对 Index 是否合法进行判断,并且给出相应的异常信息

📕 我们还可以根据查看 index 是否等于 0 或 size 来适当使用 addFirst 和 addLast

接下来我们看一下,如何将结点插入对应位置:

(不让新结点先与前一位相连,是因为如果 cur.next = 新结点,那么便不能通过 cur 找到之前的链表,也就丢失了之前链表 cur 以后的全部结点。)

📖 代码示例

    private static class IndexException extends RuntimeException{
        public IndexException(){

        }
        public IndexException(String message){
            super(message);
        }
    }
    private void checkIndexOfAdd(int index){
        if(index < 0 || index > size()){
            throw new IndexException("插入的位置index不合法!");
        }
    }
    public void addIndex(int index,int data) {
        checkIndexOfAdd(index);
        if (index == 0) {
            addFirst(data);
            return;
        }
        if (index == size()) {
            addLast(data);
            return;
        }
        ListNode cur = head;
        ListNode node = new ListNode(data);
        int num = 0;
        while (num != index - 1) {
            cur = cur.next;
            num++;
        }
        node.next = cur.next;
        cur.next = node;
    }

头插,尾插,正常的插入,以及 index 不合法的情况,都是没有问题的~

⑦ contains 方法

该方法的作用是查找是否包含关键字 key 是否在单链表当中,想要实现该方法并不难,我们只需要遍历链表,当遇到 key 的时候返回 true 就好了~

📖 代码示例

    public boolean contains(int key){
        ListNode cur = head;
        while(cur != null){
            if(cur.val == key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

⑧ remove 方法

remove方法的作用是删除第一次出现关键字为 key 的节点,其实思路并不难,就和上一个方法 contains 一样,遍历找到 key 对其进行删除就好了,而重点在于应该如何删除~

其实,核心思路就是:我们不需要删除该节点,我们只需要单向链表链接时,将它跳过即可~也就是将之前与它链接的前一结点,直接 next 等于它的下一个结点即可~

但想要同时得到 key 结点的前驱与后继,只用一个遍历结点是不够的,所以还需要一个辅助结点跟在 cur 结点身后,用来在遇到 key 时,标记前驱结点~

(由于删除结点要求链表至少有一个结点,故可以对链表是否为空进行判断,如果为空则抛出异常)

这样看似是没有问题的,但是我们仔细观察一下,如果 head.val == key 的话,其实是解决不了的,因为我们是用 cur0 去查找 key,而 cur0 一上来就是 head.next ~(不必纠结,单向链表的删除就是要用两个结点,如果我们使用 cur 去查找 key 的位置,那么也会导致无法正常删除最后一项,所以这是正常的~)所以对于 head.val == key 我们需要单独处理一下~

📖 代码示例

    private static class listEmptyException extends RuntimeException{
        public listEmptyException(){

        }
        public listEmptyException(String message){
            super(message);
        }
    }
    private void isEmpty(){
        if(head == null){
            throw new listEmptyException("链表为空!");
        }
    }
    public void remove(int key){
        isEmpty();
        if(head.val == key){
            head = head.next;
            return;
        }
        ListNode cur0 = head;
        ListNode cur = head.next;
        while(cur != null){
            if(cur.val == key){
                cur0.next = cur.next;
                return;
            }
            cur0 = cur;
            cur = cur.next;
        }
    }

删除头结点,删除尾结点,删除中间结点,以及抛出异常都是正确的~

⑨ removeAllKey 方法

该方法的作用是删除单向链表中出现的所有 key,只要完成了 remove 方法,这个方法也就都到擒来了~我们只需要在"能找到 key "的前提下"循环 remove 方法"就可以了~

📖 代码示例

    public void removeAllKey(int key){
        while(this.contains(key)){
            remove(key);
        }
    }

⑩ clear 方法

清空链表~随手的事儿啦,直接 head = null 就好了。

📖 代码示例

    public void clear() {
        head = null;
    }

四、单向链表实现代码

📖 SingleLinkedList.java

public class SingleLinkedList {
    static class ListNode {
        private int val;
        private ListNode next;

        public ListNode(int val) {
            this.val = val;
        }
    }
    public ListNode head;
    //头插法
    public void addFirst(int data){
        ListNode node = new ListNode(data);
        node.next = head;
        head = node;
    }
    //尾插法
    public void addLast(int data){
        ListNode node = new ListNode(data);
        ListNode cur = head;
        if(cur == null){
            head = node;
            return;
        }
        while(cur.next != null){
            cur = cur.next;
        }
        cur.next = node;
    }
    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data) {
        checkIndexOfAdd(index);
        if (index == 0) {
            addFirst(data);
            return;
        }
        if (index == size()) {
            addLast(data);
            return;
        }
        ListNode cur = head;
        ListNode node = new ListNode(data);
        int num = 0;
        while (num != index - 1) {
            cur = cur.next;
            num++;
        }
        node.next = cur.next;
        cur.next = node;
    }
    //查找是否包含关键字key是否在单链表当中
    public boolean contains(int key){
        isEmpty();
        ListNode cur = head;
        while(cur != null){
            if(cur.val == key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }
    //删除第一次出现关键字为key的节点
    public void remove(int key){
        isEmpty();
        if(head.val == key){
            head = head.next;
            return;
        }
        ListNode cur0 = head;
        ListNode cur = head.next;
        while(cur != null){
            if(cur.val == key){
                cur0.next = cur.next;
                return;
            }
            cur0 = cur;
            cur = cur.next;
        }
    }
    //删除所有值为key的节点
    public void removeAllKey(int key){
        while(this.contains(key)){
            remove(key);
        }
    }
    //得到单链表的⻓度
    public int size(){
        ListNode cur = head;
        int len = 0;
        while(cur != null){
            cur = cur.next;
            len++;
        }
        return len;
    }
    public void clear() {
        head = null;
    }
    public void display() {
        ListNode cur = head;
        while(cur != null){
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }
    private static class IndexException extends RuntimeException{
        public IndexException(){

        }
        public IndexException(String message){
            super(message);
        }
    }
    private void checkIndexOfAdd(int index){
        if(index < 0 || index > size()){
            throw new IndexException("插入的位置index不合法!");
        }
    }
    private static class listEmptyException extends RuntimeException{
        public listEmptyException(){

        }
        public listEmptyException(String message){
            super(message);
        }
    }
    private void isEmpty(){
        if(head == null){
            throw new listEmptyException("链表为空!");
        }
    }
}

那么关于链表中的单向链表的知识就为大家分享到这里啦~作者能力有限,作者能力有限,如果有讲得不清晰或者不正确的地方,还请大家在评论区多多指出,我也会虚心学习的!那我们下次再见哦~

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值