【Java---数据结构】链表(单向不带头非循环链表)

🚆上一篇文章绍顺序表的基本使用,这篇文章将介绍单向链表的一些基础操作及实现。

目录

一、链表

🍓 链表的概念及结构

二、单向不头非循环链表

🍓打印链表

三、单向不带头非循环链表方法的实现

🍓判断关键字key是否在链表中

🍓获取链表长度

🍓头插法

🍓尾插法

🍓在任意位置插入结点,第一个节点的下标为0

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

🍓删除所有值为key的节点

🍓清空链表


一、链表

🍓 链表的概念及结构

  • 链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的,链表是由一个一个的结点(数据域、指针域)组成。

✨链表的结构非常多样,以下情况组合起来就有8种链表结构:

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

🌌虽然有这么多的链表的结构,但是重点掌握的有两种:

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

✨区分单向单头非循环链表与单向不带头非循环链表

二、单向不头非循环链表

  • 定义一个 MyLinkedList 类实现链表的基本操作
  • 因为一个结点是由数据域和地址域共同组成,所以定义一个ListNode类生成结点
  • 定义一个 Test 类调用 MyLinkedList 类中所实现的方法进行测试。
//ListNode 代表一个结点
class ListNode{
    public  int value;
    //next存放下一个结点的地址,所以定义它的类型为ListNode(结点类型)
    public ListNode next;

    //定义一个带一个参数的构造方法,对链表赋值
    public ListNode(int value){
        this.value = value;
    }
}
public class MyList {
    //head 属于链表的属性,它引用的是链表的头
    //指代MyList这个链表的头引用(指向第一个结点)所以定义在链表中
    public ListNode head; //链表的头引用

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

使用穷举法创建一个链表感受一下链表是怎样生成的(这个方法很low,只是为了演示),真正的创建链表是使用头插法与尾插法。

🌊在 MyLinkedList 类中写一个 createList 方法创建链表。

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

创建结点后,可以通过第一个结点的next保存第二个结点的地址,以此类推,到最后一个结点listNode5 不用进行任何操作(next域默认为null),这样就可以将每个结点串起来。再将head指向listNode1(第一个结点)。

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

  • 当执行完这个方法后,listNode1 ~ listNdoe5 就会被回收(局部变量),而head是成员变量不会被回收,这样就创建出了一个链表。

🍓打印链表

  • 定义一个 display 方法打印链表的数据。

因为head指向链表中的第一个结点,即可以通过head遍历链表,但是当遍历完最后一个结点时head指向的地址为null(head的值为null),如果head的值为null,那么就会找不到链表的头结点,找不到头结点就找不到整个链表,所以定义一个ListNode类型的变量cur指向head,使用成员变量 cur 遍历链表cur = cur.next。

    public void display(){
        ListNode cur = this.head; //定义一个变量cur指向head
        //结点中next域的值为null 代表是最后一个结点
        while (cur!=null){
            System.out.print(cur.value+" ");
            //将写一个结点的地址赋给cur,cur就会走到下一个结点
            cur = cur.next;
        }
        System.out.println();
    }

 🌊链表的创建与打印都实现了,接下来定义一个 Test 类调用这两个方法,测试一下

🛫了解了链表的基本结构,接下来就可以实现链表的基本方法。

三、单向不带头非循环链表方法的实现

🍓判断关键字key是否在链表中

  • 遍历整个链表,如果有数据与key相等就表示找到了,返回true,否则返回false。
    //查找是否包含关键字key是否在单链表当中
    public boolean contains(int key){
        ListNode cur = this.head;
        while (cur!=null){
            if(cur.value==key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

🍓获取链表长度

  • 定义一个计数器 count,用 cur 遍历整个链表,当cur !=null,count就自增,遍历完整个链表,返回count 的值。
    //得到单链表的长度
    public int size(){
        ListNode cur = this.head;
        int count = 0;
        while (cur!=null){
            count++;
            cur = cur.next;
        }
        return count;
    }

🍓头插法

💦前面使用穷举法创建链表只是为了接收链表的结构,链表正确的创建是使用头插法和尾插法。

🍉顺序表出入数据的时候要判断是否满了,链表插入数据不用判断,直接插入就可以了。

  • 链表是随用随取,要插入一个数据,就创建一个结点。
  • 头插法是在第一个结点前面插入结点,创建一个结点将数据传参(ListNode类中定义了一个有参的构造方法)。
  • 插入有两种情况,一种是在有结点的链表中进行插入,另一种是在没有结点的链表(空链表)中插入。

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

        //分开的写法
       /* if(this.head==null){
            this.head = node;
        }else {
            node.next = this.head;
            this.head = node;
        }*/
    }

 

🍓尾插法

  • 尾插法是在最后一个结点的后面插入结点。
  • 遍历整个链表,如果某一个结点的next是null,就表示它是最后一个结点,然后在该结点的后面插入结点。
  • 插入有两种情况,一种是在有结点的链表中进行插入,另一种是在没有结点的链表(空链表)中插入。

    //尾插法
    public void addLast(int data){
        ListNode node = new ListNode(data);
       if(this.head==null){
           this.head = node;
       }else {
           ListNode cur = this.head;
          while (cur.next!=null){
              cur = cur.next;
          }
          //循环结束后 cur.next = null
          cur.next = node;
       }
    }

 

🍓在任意位置插入结点,第一个节点的下标为0

  • 在链表的中间位置插入结点,首先要找到待插入位置的前一个结点,假设要插入的位置为index,就要找到index-1位置的结点。
  • 写一个 findIndex 方法查找 index-1位置(因为返回的是地址,所以方法的返回类型是ListNode)。
  • 写一个方法插入节点,判断传入的参数,如果index<0 或者大于链表的长度(可以直接调用前面所写的返回链表长度的方法)位置不合法。
  • 如果插入的位置为0,就相当于头插,直接调用头插法。
  • 如果插入的位置是最后一个结点的后面(因为是从0位置开始,最后一个结点后面的位置就是等于链表的长度),就相当于尾插,就直接调用尾插法。

    //找到index-1的位置
    public ListNode findIndex(int index){
        ListNode cur = this.head;
        while (index-1!=0){
            cur = cur.next;
            index--;
        }
        return cur;
    }
    //任意位置插入,第一个数据节点为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 = findIndex(index);
        node.next = cur.next;
        cur.next = node;
    }

 

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

  • 先判断链表是否为空,如果为空就直接返回。
  • 判断链表中结点的值是否与待删除结点的值相同。
  • 如果删除的是头结点,让head指向下一个结点。head = head.next;
  • 写一个 findPreNode 方法判断待删结点的前驱,如果找到了就返回该结点的引用,找不到就表示链表中没有要删除的结点,返回null。
  • 调用 findPreNode 方法,该方法返回待删除的前驱结点,通过前驱结点定义待删除的结点的引用为del。删除结点:cur.next = del.next;(因为del是一个局部变量,出来该方法del指向的空间就会被回收,所以删除结点后不用将其置为null)
  • 如果 findPreNode 方法返回null,就表示链表中没有要删除的结点。

   //找待删除结点的前驱
    public ListNode findPreNode(int key){
        ListNode cur = this.head;
        while (cur.next!=null){
            if(cur.next.value==key){
                return cur;
            }
            cur =cur.next;
        }
        return null;
    }

    //删除第一次出现关键字为key的节点
    public void remove(int key){
        if(this.head==null){
            System.out.println("链表为空");
            return;
        }
        if(this.head.value == key){
            this.head = this.head.next;
            return;
        }
        ListNode cur = findPreNode(key);
        if(cur==null){
            System.out.println("没有要删除的结点");
            return;
        }
        ListNode del = cur.next;
        cur.next = del.next;
    }

 

🍓删除所有值为key的节点

  • 先判断链表是否为空,如果为空就直接返回。
  • 要删除所有值为key的结点有两种情况key值是不连续的与连续的。
  • 头结点最后判断是否要删除,如果一开始就判断是否要删除头结点,那么prev与cur就不好赋值。

🍉key值是不连续:

key值是连续:

  //删除所有值为key的节点
    public void removeAllKey(int key){
        if(this.head==null){
            System.out.println("链表为空");
            return;
        }
        ListNode prev = this.head;
        ListNode cur = this.head.next;
        while (cur!=null){
            if(cur.value==key){
                prev.next = cur.next;
                cur = cur.next;
            }else {
                prev = cur;
                cur = cur.next;
            }
            //最后处理头结点
            if(this.head.value==key){
                this.head = this.head.next;
            }
        }
    }

🍓清空链表

  • 第一种暴力的方法,直接将头结点置为空,head = null。
  • 第二种温柔的方法,使用head遍历每一个结点,将每一个结点置为空。
    public void clear(){   
        //this.head = null; //暴力的方法
        //温柔的方法
        while (this.head!=null){
            ListNode curNext = this.head.next;
            this.head = null;
            this.head = curNext;
        }
    }
  • 13
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

潇湘夜雨.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值