单链表(图解以及分析)

目录

今日良言:理性和激情是生活中不可或缺的调味品

一、介绍单链表

1.单链表的相关定义

2.单链表与顺序表相比的好处

二、实现单链表

1.思路分析

2.单链表中的相关操作

3.单链表的相关题型

三、完整代码


今日良言:理性和激情是生活中不可或缺的调味品

一、介绍单链表

1.单链表的相关定义

链表是一种物理存储结构上不一定连续的存储结构,但在逻辑上来说是连续的,所谓单链表就是:单向链表,我们将链表中的每个元素称之为结点/节点单链表中的每个节点是单向的,也就是说,我们无法直接获得某一结点的前面的第一个结点,即:前驱结点。

下图是列举的一个单链表:

 上图所示为不带头节点的单链表,由图可以知道一个节点有两个属性存放数据的数据域data,以及指向下一个结点的指针域next 

2.单链表与顺序表相比的好处

顺序表的本质其实可以当做是一个数组,而数组的存储空间是连续的,经过之前的学习,我们了解到,数组对于添加、删除某个数据的操作相较于查找某个数据是比较慢的,顺序表也是如此,而单链表对于添加、删除某个数据的操作是非常快的,单链表的缺点根据其结构可以看出,因为我们无法知道某个结点的前一个结点,所以只能通过遍历单链表来查找,这种效率也是比较低的。

二、实现单链表

1.思路分析

首先,我们已经分析出单链表的两种属性,那么可以创建一个类ListNode,该类就是单链表的节点类,里面有如下两个属性:

public int val;// 数据域
public ListNode next;// 指针域

 同时,提供一个构造方法,用来初始化。

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

 我们还需要一个属性head作为该单链表的头结点(注意:在这里的head是有意义的,而并非无意义的头结点),默认初始值为null

2.单链表中的相关操作

接下来,就是对于单链表中的相关操作的分析:

2.1.打印单链表

我们需要对单链表进行遍历打印,那么,试着思考一下,如果只是一个循环,那么循环结束的条件是什么呢? 

其实,仔细想想,如果已经遍历完了单链表,那么就结束打印,而遍历完整个单链表,其实也就是打印完最后一个结点,因而,我们可以创建一个辅助结点 cur ,先判断链表是否为空,不为空进行打印,让cur的初始值为head,然后通过cur进行打印,直到cur为空,结束打印

    public void display() {
        if (this.head == null) {
            System.out.println("链表为空,无法打印");
            return;
        }
        ListNode cur = this.head;
        while (cur != null) {
            System.out.print(cur.val+" ");
            cur = cur.next;
        }
        System.out.println();
    }

2.2.头插法

 所谓头插法,就是将数据插入到单链表的头结点处,我们通过图解做出解释:

 如图所示,我们采用头插法将上述45这个节点插入单链表,只需要让45这个结点的next指向head,然后将head修改为当前45这个结点即可

代码如下:

   public void addFirst(int val) {
        ListNode listNode = new ListNode(val);
        // 最好每次先连接后面的结点
        listNode.next = this.head;
        this.head = listNode;
    }

 试想一下,如果单链表为空的话,进行上述操作是否正确?显而易见,即使单链表为空,上述代码也是正确的,那么尾插法是否也是如此?

2.3尾插法

顾名思义,也就是将节点插入到单链表的尾部,但是我们没有记录单链表的最后一个结点,所以说,我们需要遍历单链表,找到最后一个结点,那么,这个循环遍历结束的条件是什么?还是辅助结点不为空吗?很明显,如果以cur != null 为循环结束的条件,当循环结束时,cur指向为空,此时显然无法满足插入操作,所以说,循环结束的条件是cur.next != null,此时cur指向的就是最后一个结点,然后插入链表即可

    public void addLast(int val) {
        ListNode node = new ListNode(val);
        // 首先判断链表是否为空
        if (this.head == null) {
            this.head = node;
            return;
        }
        // 正常插入
        ListNode cur = this.head;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = node;
    }

 这里,我们需要注意,如果是尾插法插入数据,当第一次插入数据是,此时head 为空,单链表为空,我们需要单独处理,将这个新结点当做head即可,否则直接进行尾插操作会出现空指针异常

代码如下:

    public void addLast(int val) {
        ListNode node = new ListNode(val);
        // 首先判断链表是否为空
        if (this.head == null) {
            this.head = node;
            return;
        }
        // 正常插入
        ListNode cur = this.head;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = node;
    }

 2.4得到单链表的长度

我们创建一个计数器count,用来统计节点个数,也就是单链表的长度,创建一个辅助结点cur,初始值为head,因为最后一个结点也需要统计,所以循换结束的条件是 cur != null

代码如下:

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

2.5查找是否包含关键字key的节点在链表中

 该操作也是对单链表进行遍历,最后一个结点也需要进行遍历,每拿到一个结点,我们就对该结点的数据域的值和key值进行比较,如果存在返回true,不存在就返回false

代码如下:

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

2.6在任意位置插入数据,第一个节点为0下标

假设我们要插入的下标为index,首先判断单链表是否为空,其次,我们需要对index进行合法性检查,我们可以创建一个异常类,如果index不合法,抛出该异常即可,那么index什么时候不合法呢?最容易想到的就是index < 0  然后就是index > 单链表的长度 ,此时index不合法。 当index合法后,我们需要进行判断,如果index == 0的话,说明是要进行头插操作,直接采用头插法即可,如果index == size() 也就是在单链表最后插入,采用尾插法即可

最后就是在中间位置插入这个新结点,那么该如何插入呢? 

首先想到的是找到要插入下标的前一个结点,那么创建一个辅助节点cur,初始值为head,那么让cur走多少步才算正确,这里分析一下,如果要插入到 2 下标处,cur 的初始下标为  0,只要让cur 走一步,就找到了 2 下标的前一个结点, 所以说,让cur移动的的步骤为 index -1 步

代码如下:

// 在任意位置插入,第一个节点为0下标
    public void addIndex(int index, int val) throws IndexWrongException{
        if (this.head == null) {
            System.out.println("单链表为空,插入失败");
            return;
        }
        // 判断index位置的合法性
        if (index < 0 || index > size()) {
            System.out.println("index位置不合法");
            throw new IndexWrongException("index位置不合法");
        }
        // 头插
        if (index == 0) {
            addFirst(val);
            return;
        }
        // 尾插
        if (index == size()) {
            addLast(val);
            return;
        }
        // 正常插入,先找要插入下标的前一个节点
        ListNode prev = search(index);
        ListNode node = new ListNode(val);
        node.next = prev.next;
        prev.next = node;
    }
    // 查找要插入下标的前一个节点
    public ListNode search(int index) {
        ListNode cur = this.head;
        while (index - 1 > 0) {
            cur = cur.next;
            index--;
        }
        return cur;
    }

 2.7 删除第一次出现的key值节点

首先还是对单链表进行判空操作,其次,我们先对头节点进行判断,如果头节点就是要删除的节点,我们直接将当前头节点的下一个节点当做新的头节点即可。

如果头结点不是的话,我们创建一个辅助结点cur,遍历找到要删除节点的前一个节点点,我们将查找这个待删除节点点的前一个节点封装成一个方法,循环条件为cur.next != null , 当cur.next.val == key 时,返回cur,如果循环结束还没有查找到就返回null。

返回值如果为null,说明没有要删除的节点,如果不为空,我们直接让pre(待删除节点的前一个节点)  指向待删除节点的后一个节点即可。

代码如下:

    // 删除第一次出现key值的节点
    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 pre = removePrev(key);
        if (pre == null) {
            System.out.println("没有你要删除的节点");
            return;
        }
        pre.next = pre.next.next;
    }
    // 先查找要删除结点的前一个节点
    private ListNode removePrev(int key) {
        ListNode cur = this.head;
        while (cur.next != null) {
            if (cur.next.val == key) {
                return cur;
            }
            cur = cur.next;
        }
        // 没找到
        return null;
    }

2.8 删除所有key值结点

先对链表进行判空必不可少。不为空就进行操作

这里使用前后指针法进行删除操作,首先创建一个辅助节点prev,初始值为head,然后再创建一个辅助节点cur,初始值为prev.next, 我们让cur进行循环,直到遍历完最后一个结点,循环条件为cur != null , 如果cur.val == key 说明当前cur这个结点需要删除, 那么我们让prev.next 指向 当前cur 的下一个结点,修改cur的值为下一个结点,即可删除该结点,如果cur.val !=  key 的话,说明cur 这个节点不用删除,先将cur 的值给prev, 然后让cur 移动到下一个节点即可。

如下图解,删除所有4:

 

后续结点也通过上述步骤进行删除,最后循环结束后,单链表如下:

 由图可知,我们在上述操作中,没有对头节点进行判断,所以说最后还需要对头节点进行操作,让 head = head.next 即可

代码如下:

   // 删除所有值为key的节点  前后指针法
    public void removeAllKey(int key) {
        if (this.head == null) {
            System.out.println("单链表为空");
            return;
        }
        ListNode prev= this.head;
        ListNode cur = prev.next;
        while (cur != null) {
            if (cur.val == key) {
                // 直接跳过cur这个节点
                prev.next = cur.next;
                // cur变化,进行下次操作
                cur = cur.next;
            } else {
                // 如果不是就让prev到cur的位置
                prev = cur;
                cur  = cur.next;
            }
        }
        // 最后处理头结点
        if (this.head.val == key) {
            this.head = this.head.next;
        }
    }

 2.9 清空单链表

直接置空head即可

代码:

// 清空单链表
public void clear() {
    this.head = null;
}

3.单链表的相关题型

对于力扣中单链表的一些题型,博主之前写过非常详细的一篇,有需要的可以点击下面的链接:

单链表典型OJ题(详细图解+核心思路讲解+题目链接)_qq_54469537的博客-CSDN博客

三、完整代码

完整代码放在了github中:java-/LinkedList at master · mhy2656810734/java- (github.com)

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值