Java数据结构(四)——链表与LinkedList

链表

概念及结构

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

如下图,每一个方框代表一个结点,每一个结点分为数据域引用域,每个结点的数据域存储数据,引用域则存放下一个结点的引用,各个结点通过引用连接起来,通过下图也可以观察到:每个结点的地址一般是跳跃式的,不一定是连续的,这也是链表与顺序表的区别之一。

在这里插入图片描述

链表的种类多种多样,分为带头或不带头单向或双向循环或不循环,组合起来共有8种:

  • 带头链表,即会创建一个额外的结点,这个结点的引用域引用链表实际的第一个结点,而数据域无意义,通过头结点就可以访问到整个链表
  • 双向链表,即每个结点会存在两个引用域,一个引用下一个结点,一个引用上一个结点,对于第一个结点,它没有上一个结点(不带头)所以为null
  • 循环链表,最后一个结点的引用域不为null,而是引用第一个结点,如此达到循环的效果

虽然链表的结构有8种,但是我们重点掌握两种结构:

  • 不带头单向不循环链表: 简称单链表,结构简单,一般不会单独存放数据,一般作为其他数据结构的子结构,如哈希桶、图的邻接表等。但实现操作的代码较困难复杂,许多的题目都是围绕这种结构
  • 不带头双向链表: Java集合框架的LinkedList的底层就是一个双向链表

单链表的实现

实现一个单链表,并实现基本的操作。首先,我们得有结点,即结点对象,实现一个结点类。

如下代码,外部类为MySingleList,将结点类ListNode作为静态内部类,其包含两个成员变量,分别是数据域val和引用域next(它将引用下一个结点对象),并给出构造方法。其次,为了方便操作,我们再定义一个成员变量head,存放链表的第一个结点的地址。

public class MySingleList implements IList {
   
   
    //结点内部类
    static class ListNode {
   
   
        public int val;
        public ListNode next;

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

    //链表的第一个结点
    public ListNode head;

   //重写的方法
}

实现了我们自定义的IList接口,所以必须重写方法,数据结构一定要多画图!建议结合下面的代码好好画图!

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

public void display()

先从最简单的打印入手,假设我们已经有一个链表,并且已经有第一个结点的引用head

思路:利用结点的引用域不断向后遍历链表并打印数据

    //打印链表元素
    @Override
    public void display() {
   
   
        //临时变量,避免修改成员变量head
        ListNode cur = this.head;
        //为null停下
        while(cur != null) {
   
   
            System.out.print(cur.val + " ");
            cur = cur.next;//向后寻找
        }
        System.out.println();//手动换行
    }

public int size()

思路:定义一个计数器,遍历链表,不断计数

    //链表结点数
    @Override
    public int size() {
   
   
        int count = 0;
        ListNode cur = this.head;
        while(cur != null) {
   
   
            count++;
            cur = cur.next;
        }
        return count;
    }

public void clear()

清空有"暴力"清空和非暴力清空,"暴力清空"只需要head == null;即可,这里我们给出非暴力清空,即将每个结点的next均置为null并将head置为null

    //清空
    @Override
    public void clear() {
   
   
        ListNode cur = head;
        while (cur != null) {
   
   
            ListNode curN = cur.next;
            cur.next = null;
            cur = curN;
        }
        head = null;
    }

public void addFirst(int data)

头插即将新的结点插入到链表的头部,使其成为链表新的头部

  • 必须申请一个新的结点对象
  • 考虑链表为空的情况,即head == null
    //头插
    @Override
    public void addFirst(int data) {
   
   
        //实例化一个结点对象
        ListNode newNode = new ListNode(data);
        //链表为空
        if(this.head == null) {
   
   
            this.head = newNode;
            return;
        }
        //链表不为空
        newNode.next = head;
        head = newNode;
    }

public void addLast(int data)

尾插即将新结点插入到链表结尾,使其成为新的尾结点

  • 考虑链表为空的情况
  • 想要尾插,必须找到最后一个结点,让它的next指向新的结点,按照之前的循环条件cur != nullcur会走到null,所以我们改变循环条件为cur.next != null,这样就能保证cur最后指向尾结点

如此我们有:

  • 如果想遍历链表的每个结点,循环条件为cur != null
  • 如果想找到尾结点,循环条件为cur.next != null
    //尾插
    @Override
    public void addLast(int data) {
   
   
        //实例化一个结点对象
        ListNode newNode = new ListNode(data);
        //链表为空
        if(this.head == null) {
   
   
            this.head = newNode;
            return;
        }
        //链表不为空
        ListNode cur = this.head;
        while(cur.next != null) {
   
   
            cur = cur.next;
        }
        cur.next = newNode;
    }

public void addIndex(int index,int data)

任意位置插入,前提:假设第一个结点的位置为0,以此类推,在指定位置插入新结点。

思路:必须找到原链表指定位置的前一个结点,让它的next指向新结点,并将新结点的next指向原结点。

  • 这里我们自定义了一个异常,“下标非法异常”

    public class IllegalIndexException extends RuntimeException {
         
         
        public IllegalIndexException(String message) {
         
         
            super(message);
        }
    }
    
    • 我们采用了让cur循环结束时停留在指定位置结点的前一个结点的位置,这样我们就可以拿到所需的所有结点,注意这里的语句顺序,必须先让新结点的next指向原来的指定位置结点,然后再让原指定位置结点的前一个结点指向新结点。
    //指定位置插入,假设第一个结点标记为0位置
    @Override
    public void addIndex(int index, int data) {
   
   
        //非法下标,抛出自定义异常
        if(index < 0 || index > size()) {
   
   
            throw new IllegalIndexException("Illegal Index !:下标非法");
        }
        //插入位置为0,相当于头插
        if(index == 0) {
   
   
            addFirst(data);
            return;
        }
        //寻找指定下标的前一个结点,方便更改链表的指向
        ListNode newNode = new ListNode(data);
        ListNode cur = this.head;
        for(int i = 0; i < index - 1; i++) {
   
   
            cur = cur.next;
        }
        //注意语句顺序
        newNode.next = cur.next;
        cur.next = newNode;
    }

public boolean contains(int key)

  • 链表为空,肯定不包含任何结点
  • 不为空,遍历寻找即可
    //检查是否包含某个元素
    @Override
    public boolean contains(int key) {
   
   
        if(this.head == null) {
   
   
            return false;
        }
        ListNode cur = this.head;
        while(cur != null) {
   
   
            if(cur.val == key) {
   
   
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

public void remove(int key)

删除结点,要让它的前一个结点的next指向它的后一个结点

  • 自定义了一个异常:“链表为空的异常”,空链表无法删除(看个人喜好)
  • 情况一:第一个结点就为待删除结点
  • 情况二:情况一之外,代码会直接从第二个结点开始判断,所以我们事先判断是否为情况一
    //删除第一个指定的数据
    @Override
    public void remove(int key) {
   
   
        if(this.head == null) {
   
   
            throw new ListIsEmptyException("The SingleList is Empty!: 链表为空!");
        }
        //第一个结点满足
        if(head.val == key) {
   
   
            head = head.next;
            return;
        }
        //遍历寻找并删除
        ListNode cur = this.head;
        while(cur.next != null) {
   
   
            if(cur.next.val == key) {
   
   
                cur.next = cur.next.next;
                break;
            }
            cur = cur.next;
        }
    }

public void removeAllKey(int key)

这个方法考虑的事情很多,代码一开始判断是否为空链表,然后跳过第一个结点向后寻找待删除的结点,方法是:定义两个引用,一个是寻找引用cur,它负责向后寻找待删除结点,另一个prev是为了执行删除操作,改变它的指向。

  • cur指向的结点不满足条件,则两个引用均向后一位(注意语句顺序)

  • cur指向的结点满足条件,则prev不动,让prev指向cur的下一个结点,删除成功,然后cur向后一位。

    为什么prev不动呢? 为了解决多个待删除的结点连续的情况

最后,判断第一个结点是否为待删除结点,执行操作。

    //删除全部的指定数据
    @Override
    public void removeAllKey(int key) {
   
   
        if(this.head == null) {
   
   
            throw new ListIsEmptyException("The SingleList is Empty!: 链表为空!");
        }
        //先删除所有第一个结点后面的指定结点
        ListNode prev = this.head;
        ListNode cur = prev.next;
        while(cur != null) {
   
   
            if(cur.val == key) {
   
   
                prev.next = cur.next;
            }else {
   
   
                prev = cur;
            }
            cur = cur.next;
        }
        //最后检查第一个结点是否满足删除条件
        if(this.head.val == key) {
   
   
            head = head.next;
        }
    }

完整实现如下(接口和异常类不额外再给出了):

public class MySingleList implements IList {
   
   
    //结点内部类
    static class ListNode {
   
   
        public int val;
        public ListNode next;

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

    //
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值