Java数据结构_链表

目录

一、理解链表

二、链表的实现:

[2.1 我们先来手动实现--创建一个链表:createList](#2.1 我们先来手动实现--创建一个链表:createList)

[2.2 那最重要的链表遍历呢???--display](#2.2 那最重要的链表遍历呢???--display)

[2.3 先来写一个 size 的方法](#2.3 先来写一个 size 的方法)

[2.4 如何判断链表中 是否有某个元素呢--contains方法](#2.4 如何判断链表中 是否有某个元素呢--contains方法)

[2.5 先来讲 头插法--addFirst](#2.5 先来讲 头插法--addFirst)

[2.6 理解了头插,那么再来看尾插法!--addLast](#2.6 理解了头插,那么再来看尾插法!--addLast)

[2.7 那来看 addIndex 在任意位置 插入元素!](#2.7 那来看 addIndex 在任意位置 插入元素!)

[2.8 删除一个元素--remove方法](#2.8 删除一个元素--remove方法)

[2.9 删除所有相同的元素--removeAllKey要求只遍历一次链表](#2.9 删除所有相同的元素--removeAllKey要求只遍历一次链表)


我是驿只码农,欢迎关注。

今天开始讲解Java数据结构相关的知识。

这节课将先讲解有关链表的知识--如何手动的实现一个链表:

一、理解链表

在这里我放了一个火车的照片。

要想掌握链表,先明白链表是由一个一个的--节点所组成的,节点可以看作车厢,链表就是由车厢构成的火车,也就是--部分与整体(组合)的关系。

那么:为什么有了顺序表,还要有链表呢?

我们知道,ArrayList 非常适合给定下标的查找,更新.... 但是不是非常适合插入和删除的操作。

而链表 相较于ArrayList来说 比较适合 用来插入和删除数据。

现在我们给定一组数据:

12 23 34 45

假如要用链表存储,那么一个数据就放到一节车厢当中去,而一个车厢可以看作为一个--节点/结点对象,既然是一个对象,那就是用类来描述:

class XXX{

XXX//属性

}

用图来理解一下:

也就是节点的属性:val代表存储的数据值,next负责连接车厢,也就是存储下一节🚋的地址。

比如上方给定的数据存储到链表当中,如上图所示。

每个车厢有自己的地址,也可以看作是车厢号码(唯一),而车厢中会存储乘客的数值和下一届车厢的号码,也就是节点地址。

好,这里再补充几个概念:

1.尾节点:

next域含有的值为null的节点

2.如何标记第一个节点?--head

然后我们也知道,链表有很多种类型:

但是实际上我们只需掌握两种概念!!!

单向/双向 不带头 非循环


二、链表的实现:

这里我们先明白:单向 不带头 非循环:如下图:

前期我讲了内部类,先来复习一下:

当一个事物的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,那么这个内部的完整结构最好使用内部类。在 Java 中,++可以将一个类定义在另一个类或者一个方法的内部++,前者称为内部类,后者称为外部类。内部类也是封装的一种体现。

java 复制代码
public class OutClass {
    class InnerClass{
        
    }
}

// OutClass是外部类
// InnerClass是内部类

先来创建一个类:

java 复制代码
public class MySingleList {

    static class ListNode {
        //数据
        public int val;
        //节点的引用
        public ListNode next;

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

    //存储链表的头节点的引用
    public ListNode head;

    //public int usedSize;
}

然后定义一个接口来辅助完成其中相关的方法:

java 复制代码
public interface ILinkedList {
    // 头插法
    void addFirst(int data);
    // 尾插法
    void addLast(int data);
    // 任意位置插入,第一个数据节点为0号下标
    void addIndex(int index, int data);
    // 查找是否包含关键字key是否在单链表当中
    boolean contains(int key);
    // 删除第一次出现关键字为key的节点
    void remove(int key);
    // 删除所有值为key的节点
    void removeAllKey(int key);
    // 得到单链表的长度
    int size();
    void clear();
    void display();
}

这个时候让MySingleList实现这个接口:

java 复制代码
public class MySingleList implements ILinkedList{
//定义静态内部类 来表示节点对象
    static class ListNode {
        //数据
        public int val;
        //节点的引用
        public ListNode next;

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

    //存储链表的头节点的引用
    public ListNode head;

    //public int usedSize;
}

同时需要重写其中相关的方法(接口的相关内容)

然后开始实现以下方法:以下方法均在 MySingleList 类当中

2.1 我们先来手动实现--创建一个链表:createList

下图为没有链接时候的链表:

那么我们来画一个链接好的链表图:

下面为实际代码:

java 复制代码
public void createList() {
    ListNode node1 = new ListNode(val: 12);
    ListNode node2 = new ListNode(val: 23);
    ListNode node3 = new ListNode(val: 34);
    ListNode node4 = new ListNode(val: 45);
    ListNode node5 = new ListNode(val: 56);

    node1.next = node2;
    node2.next = node3;
    node3.next = node4;
    node4.next = node5;

    head = node1;
}

需要注意:局部变量在栈上面,所以用完就会被系统回收!!


2.2 那最重要的链表遍历呢???--display

没错,关键就是让这个 head 引用来不断遍历节点!

也就是写成循环:head = head.next;

但是循环条件改写成啥样呢???

我们来分析一下:

正确写法为:

那么第一个简单方法出现:

display方法:

java 复制代码
@Override
public void display() {
    while (head != null) {
        System.out.print(head.val + " ");
        head = head.next;
    }
    System.out.println();
}

我们来测试一下:

新建一个 Test 类来测试:

java 复制代码
public class Test {
    public static void main(String[] args) {
        MySingleList mySingleList = new MySingleList();
        mySingleList.createList();

        mySingleList.display();
    }
}

输出结果为:

所以我们的代码是正确的!!!

那为什么:不选择循环条件为 head.next != null 呢?

通过画图我们明白,当使用上述条件时,会少打印一个数据!

可如下图分析:

总结一下:!!!!!

  1. head != null----> head == null 结束循环

把整个链表 一个不拉的 遍历完成了

  1. head.next != null----> head.next == null 结束循环

此时 head 指向的是 最后一个节点

  1. head 往后执行的语句

head = head.next;

但是,代码似乎有一些 不太妥的地方:

也就是 当 head 便利完成之后,head引用将 无法再次找到头节点了!!

要想解决也很简单,我再请一个 引用保镖 来帮我指代 节点 不就行了吗。。。

来看代码:

java 复制代码
@Override
public void display() {
    ListNode cur = this.head;
    while (cur != null) {
        System.out.print(cur.val + " ");
        cur = cur.next;
    }
    System.out.println();
}

也就是 大佬head 不用亲自做事,请个名字叫 cur 的保镖不就行了。


OK,当了解这些基础知识后,再来看其他的操作 就会变得无比简单了,哈哈哈。

2.3 先来写一个 size 的方法

java 复制代码
@Override
public int size() {
    int count = 0;
    ListNode cur = this.head;
    while (cur != null) {
        count++;
        cur = cur.next;
    }
    return count;
}

也就是在 遍历链表 的基础上,多加了一个 count 的变量,哈哈哈,开胃小菜了。


2.4 如何判断链表中 是否有某个元素呢--contains方法

java 复制代码
@Override
public boolean contains(int key) {
    ListNode cur = this.head;
    while (cur != null) {
        if (cur.val == key) {
            return true;
        }
        cur = cur.next;
    }
    return false;
}

是不是也是遍历为主!!!没错,那我们把上面三个方法 放到一起看看:

可以看到,这三个方法 都是依赖于 遍历来完成的,很简单对吧。


接下来讲一些别的操作:

2.5 先来讲 头插法--addFirst

什么是头插法呢,比如有个数据:val = 99;

那要插入,先把数据变成一个节点吧!

那种如何变呢?

在上面的 节点静态类ListNode 当中 ,我定义了一个构造方法:

java 复制代码
public ListNode(int val) {
    this.val = val;
}

头插法 也就是让 新节点变成第一个节点,也就是 让head引用 指向这个节点!

来画图理解:

如图,左侧为新节点,那就先让它的 next域 值为原头节点的 引用地址!

再让head 指向新的头节点,也就是:

java 复制代码
node.next = head;
head = node;

并且注意⚠️:代码顺序别给我调换哈!!!也就是:

先把新节点--绑定后面,再去移动 head...

写完代码后:图就变成了这样:

这样就成功实现了--头插法-插入元素。

有思路后,下面我们来具体实现代码:

java 复制代码
@Override
public void addFirst(int data) {
    ListNode node = new ListNode(data);
    
    node.next = head;
    head = node;
}

那有了这个,我们也可不通过createList方法,而是通过 头插法来 直接创建链表,

如下:

java 复制代码
public class Test {
    public static void main(String[] args) {
        MySingleList mySingleList = new MySingleList();
        
        //mySingleList.createList();
        
        mySingleList.addFirst(12);
        mySingleList.addFirst(23);
        mySingleList.addFirst(34);
        mySingleList.addFirst(45);
        mySingleList.addFirst(56);
        mySingleList.addFirst(67);
        
        mySingleList.display();
    }
}

那我想请问大家:会打印什么??

看结果:

对了没有呢?因为头插法是--每次新插入的 节点 都会放到第一个哈😄


2.6 理解了头插,那么再来看尾插法!--addLast

首先明白:插入元素,首先都是先有 一个节点 ,再会想着插入到哪里!

那尾插法,首先需要找到 链表的尾巴!

那怎么找呢?也就是让 节点保镖cur 满足循环条件,

但这个条件核心为:cur.next != null;

这样就可以找到最后的一个节点!

如上图,cur引用 要想找到最后一个节点,写出如下代码即可:

java 复制代码
@Override
public void addLast(int data) {
    ListNode node = new ListNode(data);
    
    //1.找到链表的尾巴
    ListNode cur = head;
    while (cur.next != null) {
        cur = cur.next;
    }
    //cur指向的节点 就是尾巴节点
    
}

接下来画图理解之后的 操作:

直接用 cur.next = node; 这样就可以让尾巴cur 成功接上一个新的节点。

代码如下:

java 复制代码
@Override
public void addLast(int data) {
    ListNode node = new ListNode(data);
    
    //1.找到链表的尾巴
    ListNode cur = head;
    while (cur.next != null) {
        cur = cur.next;
    }
    //cur指向的节点 就是尾巴节点
    cur.next = node;
}

是不是就觉得上面代码完美了???

我们来测试一下:

java 复制代码
public class Test {
    public static void main(String[] args) {
        MySingleList mySingleList = new MySingleList();
        
        //mySingleList.createList();
        
        mySingleList.addLast(12);
        mySingleList.addLast(23);
        mySingleList.addLast(34);
        mySingleList.addLast(45);
        mySingleList.addLast(56);
        mySingleList.addLast(67);
        
        mySingleList.display();
    }
}

居然报了一个 空指针异常!!

这是为啥??

假如如上图所示,我是从无到有创建一个 链表,那么cur = head,也就是cur = head = null;

那我问你,cur.next 不就变成了 null.next, 我问你,空有next吗???

所以肯定 会报一个 空指针异常。

如何解决???

加一个判断就好了:代码:

java 复制代码
@Override
public void addLast(int data) {
    ListNode node = new ListNode(data);
    
    //0.如果链表当中一个元素都没有 此时 插入的节点 就是第一个节点
    if(head == null) {
        head = node;
        return;
    }
    
    //1.找到链表的尾巴
    ListNode cur = head;
    while (cur.next != null) {
        cur = cur.next;
    }
    
    //cur指向的节点 就是尾巴节点
    cur.next = node;
}

加入判断语句后:可画图理解如下:

再来测试一下:

java 复制代码
public class Test {
    public static void main(String[] args) {
        MySingleList mySingleList = new MySingleList();
        
        //mySingleList.createList();
        
        mySingleList.addLast(12);
        mySingleList.addLast(23);
        mySingleList.addLast(34);
        mySingleList.addLast(45);
        mySingleList.addLast(56);
        mySingleList.addLast(67);
        mySingleList.addFirst(199);
        
        mySingleList.display();
    }
}

测试成功了!

此外,我们发现:

这些插入 都只是 修改指向,而并没有 移动元素,所以我才说

插入元素

用链表 会比 用顺序表 更好用。


2.7 那来看 addIndex 在任意位置 插入元素!

java 复制代码
@Override
public void addIndex(int index, int data) {
}

链表不是数组,但是 我们可以先 给链表 标注一个 位置号码--index,方便理解

假如 node 要插入到 2号位置(index = 2),画图理解:

先让 cur节点 到1号位置(cur-->index - 1)

来写代码吧:

任何一个方法写的时候,一定先看参数:

index的合法性 我们是不是 要检查一下?

先来一个 自定义异常:

java 复制代码
public class CheckPosException extends RuntimeException{
    public CheckPosException() {
    }

    public CheckPosException(String message) {
        super(message);
    }
}

里面的两个构造方法:Alt + Insert + Fn

再在MySingleList类中:

java 复制代码
public void addIndex(int index, int data) {
    checkPos(index);
}

private void checkPos(int index) {
    if(index < 0 || index > size()) {
        throw new CheckPosException("index位置不合法:"+index);
    }
}

先把 头插法 和 尾插法 两种方法的特殊情况给他考虑了:

java 复制代码
@Override
public void addIndex(int index, int data) {
    checkPos(index);
    if (index == 0) {
        addFirst(data);
        return;
    }
    if (index == size()) {
        addLast(data);
        return;
    }
}

那完善 插入到中间,所以要先找到 index-1 的节点,对不对?

写一个方法:

java 复制代码
private ListNode findIndex(int index) {
    ListNode cur = head;
    int count = 0;//count代表 我的cur 所走过的步数
//当 count = index-1 时,代表cur已经 走到了这个index-1 的位置
    while (count != index-1) {
        cur = cur.next;
        count++; 
    }
    return cur;
}

最后完善终极版本的 addIndex方法,如下:

java 复制代码
public void addIndex(int index, int data) {
    checkPos(index);
    if (index == 0) {
        addFirst(data);
        return;
    }
    if (index == size()) {
        addLast(data);
        return;
    }

//插入 到 中间位置
    ListNode cur = findIndex(index);

    ListNode node = new ListNode(data);

    node.next = cur.next;

    cur.next = node;
}

来测试一下:

java 复制代码
MySingleList mySingleList = new MySingleList();
//mySingleList.createList();

mySingleList.addLast(data: 12);
mySingleList.addLast(data: 23);
mySingleList.addLast(data: 34);
mySingleList.addLast(data: 45);
mySingleList.addLast(data: 56);
mySingleList.addLast(data: 67);
mySingleList.addFirst(data: 199);
mySingleList.addIndex(index: 3, data: 299);

mySingleList.display();

如图,3位置 成功地 插入了299


٩(•̤̀ᵕ•̤́๑)ᵒᵏᵏᵎᵎᵎᵎ来,一起来看删除操作吧!--remove和removeAllKey操作

2.8 删除一个元素--remove方法

来看,这个图中,假如我要删除这个 34 该怎么办?

如果是顺序表,那我们就是 后一个给前一个 盖住,但这是链表,该如何操作呢?

假如 del 是我们要删除的节点,那么只需要一个语句:

java 复制代码
cur.next = del.next;

图就会变成:

那么,第一个难点:确定 cur 和 del 的位置:

  1. 要找到34这个节点 的前一个节点 --> cur

  2. 要删除的节点 是不是就是del = cue.next

  1. 要删除的节点 为 头节点:
java 复制代码
@Override
public void remove(int key) {
    if(head == null) {
        System.out.println("链表为空,无法进行删除!");
        return;
    }

    if(head.val == key) {
        head = head.next;
        return;
    }
}

加上 2. 删除的节点是 其他节点

定义一个方法search,来找到 要删除的 节点的前驱,但是下面这段代码会有一个问题:

如上图链表,如果要找 69 这个节点,找不到,那么当cur 来到尾节点后,cur.next.val中 cur.next会为null,空指针了!!!所以我们的这个循环条件有问题,应该这样写:

java 复制代码
//找到 key的 前驱
private ListNode search(int key) {
    ListNode cur = head;
//while的循环条件 改一下
    while (cur.next != null) {
        if (cur.next.val == key) {
            return cur;
        }
        cur = cur.next;
    }
    return null;//没有找到的时候 返回一个null
}

完善代码之后,完整的代码 如下:

java 复制代码
@Override
public void remove(int key) {
    if (head == null) {
        System.out.println("链表为空,无法进行删除!");
        return;
    }
    //1. 要删除的节点是头节点
    if (head.val == key) {
        head = head.next;
        return;
    }

    //2. 删除的节点是其他节点
    ListNode cur = search(key);
    if (cur == null) {
        System.out.println("没有你要删除的数字:" + key);
        return;
    }

    ListNode del = cur.next;

    cur.next = del.next;
}

//找到 key的 前驱
private ListNode search(int key) {
    ListNode cur = head;
//while的循环条件 改一下
    while (cur.next != null) {
        if (cur.next.val == key) {
            return cur;
        }
        cur = cur.next;
    }
    return null;//没有找到的时候 返回一个null
}

2.9 删除所有相同的元素--removeAllKey要求只遍历一次链表

如图:

cur:是你要删除的节点

prev:是cur这个节点的前驱

假如我要删除:34

画图理解思路:如上图

那假如第一个元素是 34 呢?

简单!判断一下就好了!

加上这个代码:

java 复制代码
//一开始的时候 直接用循环判断 头节点是不是要删除的节点
while (head.val == key) {
    head = head.next;
}

所以图就会变成:

😂但其实还有一种绝妙的方法:

也就是先执行如下代码:

java 复制代码
while (cur != null) {
    if (cur.val == key) {
        prev.next = cur.next;
        cur = cur.next;
    } else {
        prev = cur;
        cur = cur.next;
    }
}

在程序的最后 再次执行一次判断即可:

两种方法如图所示:自行记忆:

所以总体的代码如下:

java 复制代码
@Override
public void removeAllKey(int key) {

    if (head == null) {
        return;
    }

    ListNode prev = head;
    ListNode cur = head.next;
    while (cur != null) {
        if (cur.val == key) {
            prev.next = cur.next;
            cur = cur.next;
        } else {
            prev = cur;
            cur = cur.next;
        }
    }
    //最后判断一次头节点
    if (head.val == key) {
        head = head.next;
    }
}

同时,这个也是力扣的一道题:

https://leetcode.cn/problems/remove-linked-list-elements/description/

大家可自行尝试:

参考代码我放下面了:

java 复制代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeElements(ListNode head, int val) {
            if(head == null) {
            return null;
        }

        ListNode prev = head;
        ListNode cur = head.next;
        while (cur != null) {
            if(cur.val == val) {
                prev.next = cur.next;
                cur = cur.next;
            } else {
                prev = cur;
                cur = cur.next;
            }
        }
        //最后判断一次头节点
        if(head.val == val) {
            head = head.next;
        }
        return head;
    }
}

2.10 clear方法--清空所有元素

假如要把这个链表给清空,那么:

next域 需要 设置为null,

val域 如果是 引用类型则 设置为空,否则如果是 基本数据类型则 设置为0

当然了,还有一种简单的办法就是直接 手动设置 this.head = null;

那么这个链表 就不会有人 再次引用它了,也就是clear成功了。

我们这里写这种操作:

val域 如果是 引用类型则 设置为空,否则如果是 基本数据类型则 设置为0

来,我用图来帮你理解一下:

代码如下:

java 复制代码
@Override
public void clear() {
    ListNode cur = head;
    while (cur != null) {
        //cur.val = null;
        ListNode curN = cur.next;
        cur.next = null;
        cur = curN;
    }
}

但是这样写 就万事大吉了吗???其实不然

因为当 cur 和 curN 都变成null了后,head还是引用了头节点,如下图:

所以最终代码:

java 复制代码
@Override
public void clear() {
    ListNode cur = head;
    while (cur != null) {
        //cur.val = null;
        ListNode curN = cur.next;
        cur.next = null;
        cur = curN;
    }
    head = null;
}

但实际上,我这样写:

java 复制代码
@Override
public void clear() {
    this.head = null;
}

也能够达到要求,也就是 没有引用 可以 引用任意一个节点了。而节点没人引用就会被自动回收的

那为什么要讲这个呢???

因为双向链表 我们😯最好这样去做!双向链表中 节点和节点 之间是会 互相引用的,就是一个一个的节点去遍历,然后手动置为空--null。


三、驿只码农:总结一下

我们发现:链表的所有操作 都是在--++修改指向!!!++

而所有的++修改指向都牵扯到了遍历!!!++

++如何学好数据结构:画图 + 思考 + 写代码 + 调试!!!++

相关推荐
小璐资源网2 小时前
C++中如何正确区分`=`和`==`的使用场景?
java·c++·算法
AMoon丶2 小时前
C++模版-函数模版,类模版基础
java·linux·c语言·开发语言·jvm·c++·算法
二十雨辰2 小时前
[Java]RuoYi框架原理分析
java
东离与糖宝2 小时前
Java 玩转 AI 智能体性能优化:OpenClaw 高并发调用与 Token 成本控制实战
java·人工智能
y = xⁿ3 小时前
【从零开始学习Redis|第七篇】Redis 进阶原理篇:消息队列、分布式锁、缓存击穿与事务实现
java·redis·学习·缓存
AMoon丶3 小时前
Golang--多种数据结构详解
linux·c语言·开发语言·数据结构·c++·后端·golang
深蓝轨迹3 小时前
SpringBoot YAML配置文件全解析:语法+读取+高级用法
java·spring boot·后端·学习
深蓝轨迹3 小时前
乐观锁 vs 悲观锁 含面试模板
java·spring boot·笔记·后端·学习·mysql·面试
东离与糖宝3 小时前
AI 智能体安全踩坑记:Java 为 OpenClaw 添加权限控制与审计日志实战
java·人工智能