几分钟带你解决数据结构问题-------单链表(超详细)

Posted 梦の澜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了几分钟带你解决数据结构问题-------单链表(超详细)相关的知识,希望对你有一定的参考价值。

🌈文章前提

链表在数据结构中占据很重要的地位,本篇博客将从单链表的创建,以及操作单链表实现几种功能展开,下面正式开始但链表的讲解 👇👇👇

📌文章目录:

1️⃣.链表的概念及其结构

2️⃣.结点的创建

3️⃣.单链表的创建

4️⃣.单链表功能的实现

5️⃣.单链表实现的全部代码

1.链表的概念及其结构

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

链表的几种结构:👇

  • 单向、带头、循环
  • 单向、带头、非循环
  • 单向、不带头、循环
  • 单向、不带头、非循环
  • 双向、带头、循环
  • 双向、带头、非循环
  • 双向、不带头、循环
  • 双向、不带头、非循环

☝️ 一共有 8 中数据结构

本博客主要讨论两种链表:👇

  1. 单向、不带头、非循环
  2. 双向、不带头、非循环(后期博客涉及)

❗️ 考试、做项目、笔试题基本都是以上两种链表 ☝️

如果需要存储一组数据:👇

12 23 34 45 56

❓ 用顺序表存储即是用一个数组去进行存储,再进行增删查改等操作

❓ 那如果是用链表进行存储呢 👇

👉 链表是由一个一个叫做结点的东西组成的

如图是单向链表一个结点的图解:👇

我们用实际的数据存储来画图解释一下单向链表 👇

❗️ 注:图中每个结点的地址是随意编写的


❓ 如上图当结点中存储完数据后,每个结点都是独立的,怎样把它们串起来生成一个链表呢

👉 用每一个结点的 next 结点域保存下一个结点的地址,当前结点就可以指向下一个结点,这样一步一步就可以把结点串联起来生成一个链表


👉 最后一个结点由于不知道下一个结点的地址是什么,所以在最后一个结点域中保存的是 null,上图就是一个单链表结构

🔥 我们发现每一个结点在没有串联起来之前都是独立的,所以可以把每一个结点都定义成一个

👉 head 结点(头结点):存放第一个结点的地址

❓ 那 head.next 存储的是什么地址呢

👉 即下一个结点的地址,并且由于 head 存放的是结点对象的地址,所以由 head.可以访问到结点的 val 域和 next 结点域

❓ 链表最后一个结点叫做什么呢

👉 链表最后一个结点称为尾结点,尾结点有一个特点—>它的尾结点 next 域是一个 null ,如果有一个结点的 next 域为 null 即该结点为尾结点


2.结点的创建

👉 抽象化一个类代表的是一个结点类型

class ListNode {
    public int val;//数值域  存放数值
    public ListNode next;//next结点域
}


👉 这里的 next 类型为 ListNode 类型,因为结点域存放的是下一个结点的地址,所以 next 类型当然是结点类型(ListNode)

👉构造方法

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

在其他方法中实例化一个结点对象,可以生成一个结点:👇

public static void main(String[] args) {
        ListNode listNode = new ListNode(1);
    }

👉 这样就生成了一个数值域为 1 的一个结点,由于没给它的 next 域进行赋值,所以其 next 域为 null


👉 由于不知道当前结点的下一个结点是哪个,所以在初始化时,next 域不需要进行赋值

当新开辟一个对象时内存示意图:👇

☝️ 以上就是一个结点的创建

❓ 那么头结点如何创建呢 👇

public class MyLinkList {
    public ListNode head;//链表的头引用
    public static void main(String[] args) {
        ListNode listNode = new ListNode(1);
    }
}

👉 我们可以看到头结点是在 MyLinkList 链表类中定义的,因为 head 是链表的头,并不是结点的头

❓ 有这样一个问题我们上面创建的是一个单向不带头非循环的链表,那么带头的又是怎样的呢 👇

👉 区别:

  • 不带头的链表头结点位置不确定,位置一直在改变
  • 带头的链表头结点位置一直是固定的,非常简单,其头结点又被称为傀儡结点,其数值没有任何参考意义
    二者没有好坏之分,只是简单与复杂的区别

❓ 科普一下------> 循环的链表又是如何定义的呢


3.单链表的创建

❗️ 我们先不使用头插和尾插这样的方式实现单链表的创建

👉 我们先用穷举法实现单链表的创建

public class MyLinkList {
    public ListNode head;//链表的头引用
    public void createList(){
        ListNode listNode = new ListNode(12);
        ListNode listNode1 = new ListNode(23);
        ListNode listNode2 = new ListNode(34);
        ListNode listNode3 = new ListNode(45);
        ListNode listNode4 = new ListNode(56);
    }
}

☝️ 在链表类中先定义方法,实例化五个结点对象,这五个结点目前的情况是:👇

❗️ 由于没有给 next 结点域赋值,由于其是引用类型,默认为:null

紧接着上面的代码把结点都连接起来,上一个结点的 next 域存储下一个结点的地址 ,头结点存储第一个结点的地址👇

        listNode.next = listNode1;
        listNode1.next = listNode2;
        listNode2.next = listNode3;
        listNode3.next = listNode4;
        this.head = listNode;

现在的各个结点就链接起来了,生成了一个单链表👇

❓ 那么链表已经有了怎样去遍历链表,拿到这些数据呢。如果是数组可以以0,1,2,3,4……下标对其进行遍历,链表又是什么方法遍历

👉 利用头结点的移动进行单链表的遍历,直到头结点为

head = head.next

👉 把头结点的下一个结点地址赋值给头结点,再用新的头结点去访问其 val 数值

❓ 思考上述循环遍历的代码是不是有问题☝️

❗️ 代码最后循环的终止条件是 head!=null ,当循环终止时,这时候的 head 指向的是空对象,使得我们不知道 head 导致指向哪里了,这样就导致 head 用过一次就不能再次重复使用了

👉 代码改进:head 不能动,让 head 的替身动即可

ListNode cur = this.head;

☝️ 把 head 保存到的地址赋值给 cur ,即 cur 也可以指向头结点

遍历单链表拿到结点数值:👇

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

在测试类中调用并打印:👇

public class TestDemo {
    public static void main(String[] args) {
        MyLinkList myLinkList = new MyLinkList();
        myLinkList.createList();
        myLinkList.display();
    }
}

12 23 34 45 56

☝️ 这里成功拿到了链表的所有数值


4.单链表功能的实现

功能一:查找是否包括关键字 key(查询链表中的数据) 👇


👉 移动 cur 对每个结点的 val 值进行访问,对比和 key(需要进行查找的数值)是否相同,相同返回 true 当循环结束仍没有返回,就证明没有此数值即返回false ,循环结束的标志是 cur 访问到最后一个结点

代码实现:📌

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

测试类中调用此方法,传入需要进行查询的数值作为方法的实参,并打印 👇

System.out.println(myLinkList.contains(13));
System.out.println(myLinkList.contains(56));

功能二、求得单链表的长度 👇

👉 利用 cur 去进行遍历,定义一个计数器,循环去计算,循环终止条件是,cur指向 null,即 cur 指向尾结点时结束循环

代码实现:📌

public int size() {
        int count = 0;//定义一个计数器
        ListNode cur = this.head;
        while (cur != null) {
            count++;
            cur = cur.next;
        }
        return count;
    }

在测试类中调用该方法并打印:👇

System.out.println(myLinkList.size());

功能三、头插法 👇

比如:需要在单链表的头结点处插入如图数据 👇

我们可以看到插入的数据是 1 ,然而插入链表的需要是结点,所以需要拿 1 来构造一个结点 👇

ListNode node = new ListNode(1);


头插法需要对两个地方进行修改:

一、插入元素的 next 结点存储第一个结点的地址
二、头结点存储当前结点的地址


❗️ 在实现插入代码时候,容易出现下图赋值顺序的错误 👇

❗️ 第一种不可以,如果先把 head 指向插入的新元素的地址,这时候 head 存储的地址是 0x444,再 head 赋值给 node.next ,node.next 存储的地址是 0x444,这就相当于把自己指向自己了,代码显然是错误的

❗️ 一定记住绑定位置的时候一定先绑定后面,以防把后面的东西弄丢

❓ 如果插入的时候链表没有元素,链表为时,怎样插入呢


☝️ 这里空链表用上述的代码实现头插也是完全可以的,效果一样

代码实现:📌

public void addFirst(int data){
        ListNode node = new ListNode(data);//创建一个新的结点
        node.next = this.head;
        this.head = node;
    }

在类方法中调用:👇

myLinkList.addFirst(12);
myLinkList.addFirst(23);
myLinkList.addFirst(34);
myLinkList.addFirst(45);
myLinkList.addFirst(56);

56 45 34 23 12

☝️ 这里数值是倒序打印出来的,因为先存储存储第一个数据(12),再把后面的数据依次插在第一个插入元素的前面,所以得到的结果是与输入顺序相反的

功能四、尾插法 👇

❓ 尾插法表示已经有一个链表了,把一个元素放在最后面,如何实现

👉 把链表最后一个结点(尾结点)的结点域保存尾插元素的地址即可,首要任务是寻找链表的最后一个结点

👉 即 cur 所指向的结点是尾结点

❓ 上图所示的代码,是单链表中存在其他结点,那么当链表为空时,怎么实现尾插法呢

❗️ 由于在插入途中会对 cur.next 进行访问,如果是空列表这样就会造成空指针异常,所以尾插法第一次插入必须判断,如果为空,用上图的方法把 node 赋值给 head,然后再进行正常的尾插法操作

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

在测试类中调用并打印插入结点后的链表

myLinkList.addLast(1);
myLinkList.addLast(2);
myLinkList.display();

56 45 34 23 12 1 2

功能五、在任意位置插入新元素 👇

👉 先人为的给链表加上一个标号,几号位置

假设我需要在 2 号位置进行插入 👇

需要往 2 号位置插入,就需要找到插入位置的前一个元素,然后进行两步操作

一、node.next = cur.next

二、cur.next = node

☝️ 通过上述两步把插入的结点插入到链表中

代码实现:📌

public ListNode findList(int index){
        ListNode cur = this.head;
        while(index - 1 != 0){
            cur = cur.next;
            index--;
        }
        return cur;
    }
    public void addIndex(int index, int data){
        ListNode node = new ListNode(data);
        if (index < 0 || index > size()){
            System.out.println("位置不合法");
            return;
        }
        if (index == 0){
            addFirst(data);
            return;
        }
        if (index == size()){
            addLast(data);
            return;
        }
        ListNode pos = findList(index);
        node.next = pos.next;
        pos.next = node;
    }

👉 先进行判断插入结点的位置合不合法,下标是从 0 开始的没有负数,如果大于元素个数,没有前一个结点没办法插入元素,

📌 如果在下标为 0 的位置进行结点插入,即是头插法,调用头插法的实现方法即可

📌 如果在尾结点进行插入,即是尾插法,调用尾插法的方法即可

📌 在中间进行插入时,调用 findList 方法找到插入位置的前一个结点位置,返回值是一个结点类型,接

收其地址后,进行元素的插入即可完成指定位置的插入

在测试类中调用此方法,并进行打印即可: 👇

 myLinkList.addIndex(3,1);

功能六、删除第一次出现关键字为key的节点 👇

❓ 如下图,想要删除链表中的数值为 23 的结点,怎样进行操作

实现的大致思想:👇


👉 循环的条件是 cur 的结点域不为空,当循环终止时也没有找到 key 结点元素,则证明在链表中不存在该元素,无法进行删除

操作

❗️ 由于判断条件是 cur.next.val == key,头结点的元素会被忽略过去,所以头结点需要单独判断

删除步骤:👇

一、判断头结点(head向后移即可)

二、找删除结点的 del 和 cur 位置,找到进行删除操作,找不到即打印该链表不存在此数据

代码实现:👇

public void remove(int key){
        if(this.head == null){
            System.out.println("单链表为空,不能进行删除操作");
            return;
        }
        if(this.head.val == key){
            this.head = this.head.next;
            return;
        }
        ListNode cur = findPos(key);
        if (cur == null){
            System.out.println("删除的元素在链表中不存在,无法完成删除操作");
            return;
        }
        ListNode del = cur.next;
        cur.next = del.next;
    }

在测试类中调用此方法并进行打印:👇

myLinkList.remove(56);
myLinkList.remove(23);

功能七、删除所有值为key的节点 👇

❗️ 要求只遍历一遍链表删除所有 key 数值的结点

删除的几种情况:

一、当需要删除的元素结点在链表的中间


cur 代表需要删除的结点,如果 cur 结点的数值刚好等于需要删除的数值,即

prev.next = cur.next;

☝️ 这时候第一个结点和第三个结点就连接起来了,然后 cur 继续向后走,寻找其他数值为 key 的结点

cur = cur.next;

👉 再进行判断目前 cur 的数值是不是 key,如上图的第三个结点数值是 34 ,不是删除的数值,就把目前 cur 的结点地址赋值给 prev

prev = cur;
cur = cur.next;


❗️ 循环的终止条件就是当 cur 遍历整个链表后,当 cur = null 的时候结束循环

二、当删除的元素结点在头和中间时,上方代码依旧适用,只不过头结点没有进行判断,少删除一个结点,等全部删除完毕,再来判断一下头结点,单独删除即可

代码实现: 👇

public ListNode removeAllKey(int key){
        ListNode prev = this.head;
        ListNode cur = this.head.next;
        //防止空指针异常
        if(this.head == null){
            return null;
        }
        while(cur != null){
            //是需要删除的元素
            if (cur.val == key){
                prev.next = cur.next;
                cur = cur.next;
            }
            //不是需要删除的元素
            else{
                prev = cur;
                cur = cur.next;
            }
        }
        //最后处理头
        if (this.head.val == key){
            this.head = this.head.next;
        }
        //返回删除后链表的头
        return this.head;
    }

在测试类调用并打印即可:

myLinkList.removeAllKey(23);

功能八、情况链表 👇

👉 把链表的每一个结点都清空,防止内存泄漏

粗暴算法:把 head 置空,之后的每一个结点都没人引用了 👇

head = null;

温柔算法:一个一个结点释放 👇

public void clear(){
        while(this.head != null){
            ListNode curNext = head.next;
            this.head.next = null;
            this.head = curNext;
        }
    }

👆 curNext 向后走,head 不断的置空,再把 curNext 赋给 head ,循环置空即可


五、全部代码👇

单链表的创建及功能实现:👇

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: Lenovo
 * Date: 2021-11-05
 * Time: 9:17
 */
class ListNode{
    public int val;
    public ListNode next;
    public ListNode(int val){
        this.val = val;
    }
}

public class MyLinkList {
    public ListNode head;
    public void createList(){
        ListNode listNode = new ListNode(12);
        ListNode listNode1 = new ListNode(23);
        ListNode listNode2 = new ListNode(34);
        ListNode listNode3 = new ListNode(45);
        ListNode listNode4 = new ListNode(56);
        listNode.next = listNode1;
        listNode1.next = listNode2;
        listNode2.next = listNode3;
        listNode3.next = listNode4;
        this.head = listNode;
    }
    public void display(){
        ListNode以上是关于几分钟带你解决数据结构问题-------单链表(超详细)的主要内容,如果未能解决你的问题,请参考以下文章

单链表创建之--头插法创建带头结点的单链表,超详细

数据结构--单链表的c语言实现(超详细注释/实验报告)

分分钟带你解决数据结构问题---- List接口中的ArrayList

数据结构与算法线性表的链式表示和实现,超详细C语言版

单链表java简易实现

超专业解析!10分钟带你搞懂Linux中直接I/O原理