【数据结构】单链表

链表(一)

01 引入

接上文顺序表,我们可以知道ArrayList的缺陷。当在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低,因此ArrayList不适合做任意位置插入和删除比较多的场景。因此:java集合中又引入了LinkedList,即链表结构。

那么下面,让我们来了解了解链表。

02 概念及结构

链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。其实就是节点组成,一个节点类似于火车一个车厢那样:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nxFKWdPq-1691313293619)(C:\Users\86159\AppData\Roaming\Typora\typora-user-images\image-20230802182622237.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NmTz2djR-1691313293621)(E:\Typora_note\数据结构笔记\Java\链表.assets\image-20230805165401086.png)]

下面是一个单向带头非循环链表:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cIeoB8dK-1691313293621)(C:\Users\86159\AppData\Roaming\Typora\typora-user-images\image-20230803142840530.png)]

下面是链表的种类:

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kxeLdsML-1691313293622)(https://gitee.com/liuhb-clanguage/picture/raw/master/png/image-20230803144159308.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yb7FYu2f-1691313293622)(https://gitee.com/liuhb-clanguage/picture/raw/master/png/image-20230803144412289.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PCab9kv4-1691313293623)(C:\Users\86159\AppData\Roaming\Typora\typora-user-images\image-20230803144625610.png)]

通过彼此间的排列组合,一共可以分为:8种,如下:

  1. 单向 不带头 非循环
  2. 单向 不带头 循环
  3. 单向 带头 非循环
  4. 单向 带头 循环
  5. 双向 不带头 非循环
  6. 双向 不带头 循环
  7. 双向 带头 非循环
  8. 双向 带头 循环

这里着重介绍单向 不带头 非循环 和 双向 不带头 非循环。

  • 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
  • 无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表。

03 单向不带头不循环链表实现

3.1 创建节点类型

链表是由一个一个节点组成的,每一个节点对应每一个对象,如果我们去抽象他,他其实有两个域值,所以我们可以把节点定义成内部类。

创建一个ListNode类来作为节点类型,包含两个成员变量:val域用来储存数据(数据域),next用来存储下一个节点的地址(指针域)。
再创建一个带参的构造方法来实例化对象,同时给val赋值,next的默认值是null。接下来我们用代码来实现一下:

//静态内部类
    static class ListNode{
        public int val; //节点的值域
        public ListNode next; //下一个节点的地址

        //实例化节点对象
        public ListNode(int val){
            this.val = val;
        }
    }

3.2 简易创建一个链表

public ListNode head;//表示当前链表的头节点

    //以穷举的方式创建一个链表
    public void createList(){
        ListNode node1 = new ListNode(12);
        ListNode node2 = new ListNode(23);
        ListNode node3 = new ListNode(34);
        ListNode node4 = new ListNode(45);
        ListNode node5 = new ListNode(56);
    }

此时创建的链表如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a22rDdR2-1691313293623)(E:\Typora_note\数据结构笔记\Java\链表.assets\image-20230803161957003.png)]

目前各节点间不存在关系。

那么接下来的操作就是要让node1->node2->node3->node4->node5

//以穷举的方式创建一个链表
    public void createList(){
        ListNode node1 = new ListNode(12);
        ListNode node2 = new ListNode(23);
        ListNode node3 = new ListNode(34);
        ListNode node4 = new ListNode(45);
        ListNode node5 = new ListNode(56);
        
        node1.next = node2;
        node2.next = node3;
        node3.next = node4;
        node4.next = node5;
        
        this.head = node1;
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KSoJaQPS-1691313293623)(E:\Typora_note\数据结构笔记\Java\链表.assets\image-20230803163008316.png)]

这里我们不妨在编译器里debug来看看:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0gyacBuy-1691313293624)(E:\Typora_note\数据结构笔记\Java\链表.assets\image-20230803221846684.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r9st8GkK-1691313293624)(E:\Typora_note\数据结构笔记\Java\链表.assets\image-20230803221913786.png)]

3.3 遍历链表每个节点

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

这里我们可以在测试类中debug一下看看:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mvZIpdf9-1691313293624)(E:\Typora_note\数据结构笔记\Java\链表.assets\image-20230803225415004.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zQCGDYFR-1691313293625)(E:\Typora_note\数据结构笔记\Java\链表.assets\image-20230803225425896.png)]

虽然这里的确是将链表遍历完全,但是,这里的头节点head = null
在这里插入图片描述

那么这样就会造成一个后果,鬼都不知道头节点head死哪里去了,那咋办?

这个后果虽然很严重,但是解决办法其实也很容易,既然head它不能乱来,那它可以叫个分身cur代替它去呀。

public void display() {
        ListNode cur = head;
        while (cur != null){
            //如果cur == null,说明把链表遍历完成
            System.out.println(cur.val+" ");
            cur = cur.next;
            //cur每次向后走一步
        }
        System.out.println();
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4tyC9HTA-1691313293625)(E:\Typora_note\数据结构笔记\Java\链表.assets\image-20230803232650482.png)]

3.4 获取链表长度

这个其实也就是基于3.3 遍历链表得来的。

//得到单链表的长度
    public int size(){
        int count = 0;
        ListNode cur = head;
        if (cur!=null){
        while(cur != null){
            cur = cur.next;
            count++;
          }
        return count;
        }
        else{
            return -1;
        }
    }

3.5 查找是否包含关键字key是否在单链表当中

这个其实也一样是基于3.3 遍历链表得来的。

//查找是否包含关键字key是否在单链表当中
    public boolean contains(int key){
        ListNode cur = head;
            while(cur != null){
                if (key == cur.val){
                    return true;
                }
                  cur = cur.next;
            }
        return false;
    }

3.6 头插法

头插法指的是在链表的头节点位置插入一个新的节点,定义一个node表示插入的新节点,然后将node.next = head,head = node,即可。

形象一点来看,如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wq1UPcRp-1691313293625)(E:\Typora_note\数据结构笔记\Java\链表.assets\image-20230804140014328.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uiECf3X7-1691313293626)(E:\Typora_note\数据结构笔记\Java\链表.assets\image-20230804140805784.png)]

//头插法
    public void addFirst(int data){
        ListNode node = new ListNode(data);

        node.next = head;
        head = node;
    }

3.7 尾插法

尾插法指的是:在链表的尾巴节点位置插入一个新节点,定义一个node表示新节点,如同头插法那样,对原尾巴节点的next进行赋值。下面是尾插链表出现的情况:

  • 当链表不为空的时候,定义一个cur来代替head(这里其实和遍历节点的道理一致),直到cur.next == null 的时候就跳出遍历,cur.next == node,这样即可完成尾插。

  • 当链表为空的时候,不论我们定义什么去代替head,都是竹篮打水一场空,都无法进入遍历,同时也会报空指针异常,解决方法其实也很简单,直接让head = node即可。

具体分析见以下:

如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cQZ0sLBB-1691313293626)(https://gitee.com/liuhb-clanguage/picture/raw/master/png/image-20230804142311057.png)]

其实这其中也有遍历链表那味了,其思想就是遍历到链表最后一个节点,然后再进行尾插就行了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UcGHqGsq-1691313293626)(https://gitee.com/liuhb-clanguage/picture/raw/master/png/image-20230804143200359.png)]

//尾插法
    public void addLast(int data){
        ListNode node = new ListNode(data);

        ListNode cur = head;
        while (cur.next != null){
            cur = cur.next;
        }
        cur.next = node;
    }

但是这个代码是具有一定的问题的,情景如下图:

在这里插入图片描述

这个问题其实就是,此时head是空的,同时我们定义一个cur= head,那么cur也是空,那么就无法进入到while循环中。修正如下:

//尾插法
    public void addLast(int data){
        ListNode node = new ListNode(data);

        ListNode cur = head;
        if (cur == null){
            head = node;
            return;
        }
        //找到链表的尾巴,注意是cur.next 不是cur
        while (cur.next != null){
            cur = cur.next;
        }
        cur.next = node;
    }

3.8 任意位置插入

思路:

  1. 定义curindex-1步,找到要插入位置的前一个节点
  2. 进行插入

给一个情景,定义cur = index - 1,在1号、2号间插入node.

关键就在于我们要将0x66 -> 0x777,null -> 0x32,即node.next = cur.next,cur.next = node.

需要将head先移至2号位置(注意:用cur代替head,防止head丢失),然后

node.next = cur.next使该节点的next域改为下一节点的地址,再cur.next = node.next使前一节点的next域改为该节点的地址。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ugPmgfIm-1691313293627)(https://gitee.com/liuhb-clanguage/picture/raw/master/png/image-20230804161544727.png)]

//任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
        if (index < 0 || index > size()){
            System.out.println("index不合法");
            return;
        }
        if (index == 0){
            addFirst(data);
            return;
        }
        if (index == size()){
            addLast(data);
            return;
        }

        /*ListNode node = new ListNode(data);
        ListNode cur = head;

        int tmp = index - 1;
        while (tmp != 0){
            cur = cur.next;
            tmp--;
        }
        node.next = cur.next;
        cur.next = node;
         */
        //将上面这一坨封装
        ListNode cur = findIndexSubOne(index);
        ListNode node = new ListNode(data);
        node.next = cur.next;
        cur.next = node;
    }

    /**
     * 找到要删除节点位置的前一个节点
     * @param index
     * @return
     */
    private ListNode findIndexSubOne(int index){
        ListNode cur = head;
        int tmp = index - 1;
        while (tmp != 0){
            cur = cur.next;
            tmp--;
        }
        return cur;
    }

3.9 删除第一次出现关键字为key的节点

效果图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XNjLHKsz-1691313293628)(E:\Typora_note\数据结构笔记\Java\链表.assets\image-20230804163610766.png)]

  • 对于删除第一次出现的key值的节点,若不是头节点,我们只需将key值对应的节点的前一节点的next的域改为key值对应的节点的next域即可。

  • 对于头节点,直接head = head.next即可。

  1. 找到要删除节点的前一个节点
  2. 此时要删除的节点del = cur.next;
  3. 进行删除cur.next = del.next

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Z4twZk9-1691313293628)(https://gitee.com/liuhb-clanguage/picture/raw/master/png/image-20230804165759876.png)]

//删除第一次出现关键字为key的节点
    public void remove(int key){
        if (head == null){
            return;
        }
        //单独删除头节点
        if (head.val == key){
            head = head.next;
            return;
        }
        
        ListNode cur = searchPrev(key);
        if (cur == null){
            System.out.println("没有你要删除的数字!");
            return;
        }
        ListNode del = cur.next;
        cur.next = del.next;
        
    }

    /**
     * 找到关键字key的前驱
     * @param key
     * @return
     */
    private ListNode searchPrev(int key){
        ListNode cur = head;
        while (cur.next != null){
            if (cur.next.val == key){
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }

3.10 删除所有值为key的节点

情景如下:

将值为23的删除

有种暴力的方法,我们只需要多次调用3.9种的remove函数即可,但是这并不是我们真正想要的,要求:遍历一遍就要删除完。

如图分析:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XnRqeEfk-1691313293628)(https://gitee.com/liuhb-clanguage/picture/raw/master/png/image-20230806160715303.png)]

//删除所有值为key的节点
    public void removeAllKey(int key){
        ListNode prev = head;
        ListNode cur = head.next;

        if (head == null){
            return;
        }

        while (cur != null){
            if(cur.val == key){
                prev = cur.next;
                cur = cur.next;
            }else {
                prev = cur;
                cur = cur.next;
            }
        }
        if (head.val == key){
            head = head.next;
        }
    }

3.11 清空

简单粗暴:

public void clear() {
        this.head = null;
    }

温柔:

    public void clear(){
        while(this.head != null){
            ListNode curNext = this.head.next;
            this.head.next = null;
            this.head = cur.next;
        }
    }
  • 15
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值