数据结构之链表

目录

​编辑前言

一.链表

1.1链表的概念

1.2链表的8种结构

1.3单链表的实现

1.单链表的接口

1.4单链表的功能实现

1.定义链表节点

2.链表的头插

3.链表的尾插

4.在指定位置插入数据

5.获取单链表长度&清空单链表&单链表的打印

6.查找关键字key是否在单链表中出现

2.链表和顺序表的优缺点

3.完整代码


前言

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

一.链表

1.1链表的概念

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

注意:

1.链式结构在逻辑上是连续的,但是在物理上不一定连续。

2.节点(结点)一般都是从堆上申请出来的。

3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间有可能连续,但也有可能不连续。

1.2链表的8种结构

根据带头/不带头、单向/双向、循环/非循环来划分,链表有8种结构。

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

1.3单链表的实现

我们今天实现的是不带头单向非循环链表

1.单链表的接口

ILinkList.java

java 复制代码
package MLinkList;

public interface ILinkList {
    // 1、无头单向非循环链表实现
        //头插法
        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();
}

1.4单链表的功能实现

1.定义链表节点

我们需要先创建一个内部类,作为链表的节点。并创建个头指针、尾指针。

java 复制代码
/**
     * ListNode 类定义了一个链表节点。
     * 这个类包含一个整型数据和一个指向下一个节点的指针。
     */
    public class ListNode{
        // 节点存储的数据
        public int data;
        // 指向下一个节点的指针
        public ListNode next;
        
        /**
         * ListNode 类的构造函数,用于创建一个新的链表节点。
         * 
         * @param data 节点存储的数据。
         */
        public ListNode(int data)
        {
            this.data = data; // 初始化节点数据
        }
    }
 public ListNode head;//头指针
    public ListNode last;//尾指针
2.链表的头插

进行头插,需要考虑两种情况:

  1. 头指针为空,让新节点作为头指针
  2. 头指针不为空,可以进行头插,头插的时候,先让新节点的指针指向头结点,再让头指针移动到新节点位置。
java 复制代码
   public void addFirst(int data) {
        ListNode node = new ListNode(data); // 创建新节点,数据为传入的data
        // 判断链表是否为空,如果为空,说明当前没有节点,直接将新节点作为头节点
        if (head == null) {
            head = node;
            return;
        }
        // 链表不为空时,进行头插操作,新节点的next指向当前头节点,然后更新头节点为新节点
        node.next = head;
        head = node;
    }
3.链表的尾插

进行尾插时,同样需要考虑头结点是否为空

javascript 复制代码
 /**
     * 使用尾插法向链表中添加元素。
     * 该方法会将指定的元素添加到链表的末尾。
     * 
     * @param data 要添加到链表的数据。
     */
    public void addLast(int data) {
        ListNode node = new ListNode(data); // 创建新节点,数据为data
        
        if (head == null) {
            head = last = node; // 如果链表为空,将新节点同时设置为头节点和尾节点
            return;
        }
        last.next = node; // 将当前尾节点的next指向新节点
        last = last.next; // 更新尾节点为新添加的节点
        
    }
4.在指定位置插入数据

我们需要考虑3种情况:

  1. 要插入的位置是否合法
  2. index位置是在链表两端还是在链表内部,如果index==0,那么则直接调用头插法即可,如果index=size(),那么直接调用尾插法即可。
  3. 在链表内部,我们就需要找到index的前一个节点,再进行插入。(这里查找index的前一个节点,我们可以定义一个方法进行查找)
java 复制代码
 private void checkIndex(int index)throws IndexNoLegalException {
        if (index < 0 || index > size()) {
        throw new IndexNoLegalException("index位置不合法"+"index="+index);
        }
    }

    private ListNode findIndex(int index) {
        ListNode cur = head;
        while (index-- != 1) {
            cur = cur.next;
        }
        return cur;
    }
    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index, int data) {
        //判断index位置是合法
        try {
            checkIndex(index);
        } catch (IndexNotLegalException e) {
            e.printStackTrace();
        }
        if(index==0) {//头插
            addFirst(data);
        } 
        if(index==size()) {//尾插
            addLast(data);
        }
        //创建新节点,并查找index位置的前一个节点
        ListNode node = new ListNode(data);
        ListNode findlist=findIndex(index);
        node.next=findlist.next;
        findlist.next=node;
    }
5.获取单链表长度&清空单链表&单链表的打印

这里主要是单链表的清空,我们也可以直接让head=last=null,但是更好的是把单链表中的节点都清空。

java 复制代码
    /**
     * 获取单链表的长度。
     * @return 链表中节点的数目。
     */
    public int size() {
        int count = 0; // 初始化计数器
        ListNode cur = head; // 从头节点开始遍历
        while (cur != null) {
            count++;
            cur = cur.next; // 移动到下一个节点
        }
        return count; // 返回计数结果
    }
    
    /**
     * 清空单链表,使其变为空链表。
     */
    public void clear() {
        if (head == null) {
            return; // 如果链表为空,则直接返回
        }
        ListNode cur = head; // 保存头节点的下一个节点,以避免丢失整个链表
        while (cur != null) {
            ListNode curN=cur.next;
            cur=null;
            cur=curN;
        }
        head = last = null; // 最后将头节点和尾节点都设置为null
    }
    
    /**
     * 打印单链表中的所有节点数据。
     */
    public void display() {
        if (head == null) {
            return; // 如果链表为空,则直接返回
        }
        ListNode cur = head; // 从头节点开始遍历
        while (cur != null) {
            System.out.print(cur.data + " "); // 打印当前节点的数据
            cur = cur.next; // 移动到下一个节点
        }
        System.out.println(); // 换行
    }
6.查找关键字key是否在单链表中出现

我们直接遍历单链表一遍即可,找到返回true

java 复制代码
/**
     * 检查单链表中是否包含指定关键字。
     * @param key 需要在单链表中查找的关键字。
     * @return 如果链表中包含关键字,则返回true;否则返回false。
     */
    public boolean contains(int key){
        // 如果链表头节点为空,直接返回false,表示链表为空,不可能包含关键字
        if(head==null){
            return false;
        }
        // 遍历链表,查找是否存在与key相等的节点
        while(head!=null){
            if(head.data==key){
                // 找到相等的节点,返回true
                return true;
            }
            head=head.next; // 移动到下一个节点继续查找
        }
        // 遍历完整个链表都没有找到相等的节点,返回false
        return false;
    }

7.移除第一次出现key关键字的节点&移除所有key关键字的节点

移除出现一次关键字的节点:

  1. 判断链表是否为空
  2. 我们可以查找当前节点的下一个节点的值是否与key相同,如果相同,那就让当前节点的指针 指向下一个节点的指针

移除所有出现关键字的节点

  1. 我们需要用到两个指针,一个用来跟踪prev,一个(cur)用来查找与key相同的节点,如果找到,那就让prev.next指向cur.next,删除key节点。
  2. 当走完循环时,我们还需要判断头结点是否与key相同,如果相同,那就需要让head指向下一个节点。
java 复制代码
 /**
     * 删除链表中第一次出现的关键字为key的节点
     * @param key 需要删除的节点的值
     * 如果链表为空或者没有找到值为key的节点,则链表结构不变
     */
    public void remove(int key){
        // 链表为空,直接返回
        if(head==null){
            return;
        }
        ListNode cur=head;
        // 遍历链表,查找第一个值为key的节点
        while(cur!=null) {
            // 找到后,删除该节点
            if(cur.next.data==key){
                cur.next=cur.next.next;
                return;
            }
            cur=cur.next;
        }
    }
    /**
     * 删除链表中所有值为key的节点
     * @param key 需要删除的节点的值
     * 该方法不返回任何内容
     */
    public void removeAllKey(int key){
        // 如果链表为空,则直接返回
        if(head==null){
            return;
        }
        ListNode prev=head;// 记录当前节点的前一个节点
        ListNode cur=head.next;// 记录当前操作的节点
        while(cur!=null){
            if(cur.data==key){
                prev.next=cur.next; // 如果当前节点的值等于key,则删除该节点
            }else{
                prev=cur; // 否则,继续向前遍历
            }
            cur=cur.next;
        }
        // 如果头节点的值等于key,则更新头节点
        if(head.data==key){
            head=head.next;
        }
    }

这些是单链表的一些基本方法,我们测试一下,确实是我们所想的。

2.链表和顺序表的优缺点

链表:

优点:

1.插入和删除操作简单:由于链表的节点通过指针链接,插入和删除操作只需要修改指针的指向,不需要移动其他元素,因此时间复杂度为O(1)。

2.链表可以根据实际情况动态分配内存空间,只需要在插入新节点时分配新的内存,因此避免了固定大小的存储空间的限制。

缺点:

1.随机访问性能差,链表开辟的空间在内存中上随机开辟的,不一定连续,所以在访问的时候需要遍历查找。时间复杂度为O(N).

2.需要额外的存储空间,由于节点与节点之间是通过指针相连的,所以有额外的空间来存储下一个节点的位置。

顺序表:

优点:

1.支持随机访问:由于顺序表在内存中是连续的,可以通过下标直接访问顺序表中的元素。时间复杂度是O(1).

2.存储效率高,不需要开辟额外的空间存储指针。

缺点:

1.在插入和删除数据的时候,需要搬动其他元素,时间复杂度为O(N)。

2.存储空间固定:如果要扩容的话,可能会造成空间浪费。

3.完整代码

接口在上

单链表功能实现:MyLinkltstto.java

java 复制代码
package MLinkList;

import SlowList.IndexNoLegalException;

public class MyLinklistto implements ILinkList{
    // 1、无头单向非循环链表实现
    /**
     * ListNode 类定义了一个链表节点。
     * 这个类包含一个整型数据和一个指向下一个节点的指针。
     */
    public class ListNode{
        // 节点存储的数据
        public int data;
        // 指向下一个节点的指针
        public ListNode next;

        /**
         * ListNode 类的构造函数,用于创建一个新的链表节点。
         *
         * @param data 节点存储的数据。
         */
        public ListNode(int data)
        {
            this.data = data; // 初始化节点数据
        }
    }
    public ListNode head;//头指针
    public ListNode last;//尾指针

    /**
     * 在链表的头部进行插入操作。
     * 该方法会将新节点插入到链表的头部,如果链表为空,则新节点成为头节点;
     * 如果链表不为空,则新节点将成为新的头节点,原头节点成为新节点的下一个节点。
     *
     * @param data 要插入节点的数据。
     */
    public void addFirst(int data) {
        ListNode node = new ListNode(data); // 创建新节点,数据为传入的data
        // 判断链表是否为空,如果为空,说明当前没有节点,直接将新节点作为头节点
        if (head == null) {
            head =last= node;
            return;
        }
        // 链表不为空时,进行头插操作,新节点的next指向当前头节点,然后更新头节点为新节点
        node.next = head;
        head = node;
    }
    /**
     * 使用尾插法向链表中添加元素。
     * 该方法会将指定的元素添加到链表的末尾。
     *
     * @param data 要添加到链表的数据。
     */
    public void addLast(int data) {
        ListNode node = new ListNode(data); // 创建新节点,数据为data

        if (head == null) {
            head = last = node; // 如果链表为空,将新节点同时设置为头节点和尾节点
            return;
        }
        last.next = node; // 将当前尾节点的next指向新节点
        last = last.next; // 更新尾节点为新添加的节点

    }

    /**
     * 检查索引是否合法。
     * @param index 要检查的索引值。
     * @throws IndexNoLegalException 如果索引不合法,则抛出异常。
     */
    private void checkIndex(int index)throws IndexNoLegalException {
        if (index < 0 || index > size()) { // 索引超出范围不合法
            throw new IndexNoLegalException("index位置不合法"+"index="+index);
        }
    }

    /**
     * 查找指定索引位置的节点。
     * @param index 目标索引位置。
     * @return 返回指定索引位置的节点。
     */
    private ListNode findIndex(int index) {
        ListNode cur = head; // 从头节点开始遍历
        while (index-- != 1) { // 递减索引,找到目标位置的前一个节点
            cur = cur.next;
        }
        return cur;
    }

    /**
     * 在任意位置插入一个节点。
     * @param index 插入位置的索引。
     * @param data 要插入的数据。
     */
    public void addIndex(int index, int data) {
        // 判断插入位置是否合法
        try {
            checkIndex(index);
        } catch (IndexNotLegalException e) {
            e.printStackTrace();
        }
        if(index==0) { // 如果是插入到头部
            addFirst(data);
        }
        if(index==size()) { // 如果是插入到尾部
            addLast(data);
        }
        // 创建新节点,并插入到指定位置
        ListNode node = new ListNode(data);
        ListNode findlist=findIndex(index); // 找到插入位置的前一个节点
        node.next=findlist.next; // 新节点接在前一个节点之后
        findlist.next=node; // 前一个节点指向新节点
    }


    /**
     * 检查单链表中是否包含指定关键字。
     * @param key 需要在单链表中查找的关键字。
     * @return 如果链表中包含关键字,则返回true;否则返回false。
     */
    public boolean contains(int key){
        // 如果链表头节点为空,直接返回false,表示链表为空,不可能包含关键字
        if(head==null){
            return false;
        }
        // 遍历链表,查找是否存在与key相等的节点
        while(head!=null){
            if(head.data==key){
                // 找到相等的节点,返回true
                return true;
            }
            head=head.next; // 移动到下一个节点继续查找
        }
        // 遍历完整个链表都没有找到相等的节点,返回false
        return false;
    }

    /**
     * 删除链表中第一次出现的关键字为key的节点
     * @param key 需要删除的节点的值
     * 如果链表为空或者没有找到值为key的节点,则链表结构不变
     */
    public void remove(int key){
        // 链表为空,直接返回
        if(head==null){
            return;
        }
        ListNode cur=head;
        // 遍历链表,查找第一个值为key的节点
        while(cur!=null) {
            // 找到后,删除该节点
            if(cur.next.data==key){
                cur.next=cur.next.next;
                return;
            }
            cur=cur.next;
        }
    }
    /**
     * 删除链表中所有值为key的节点
     * @param key 需要删除的节点的值
     * 该方法不返回任何内容
     */
    public void removeAllKey(int key){
        // 如果链表为空,则直接返回
        if(head==null){
            return;
        }
        ListNode prev=head;// 记录当前节点的前一个节点
        ListNode cur=head.next;// 记录当前操作的节点
        while(cur!=null){
            if(cur.data==key){
                prev.next=cur.next; // 如果当前节点的值等于key,则删除该节点
            }else{
                prev=cur; // 否则,继续向前遍历
            }
            cur=cur.next;
        }
        // 如果头节点的值等于key,则更新头节点
        if(head.data==key){
            head=head.next;
        }
    }

    /**
     * 获取单链表的长度。
     * @return 链表中节点的数目。
     */
    public int size() {
        int count = 0; // 初始化计数器
        ListNode cur = head; // 从头节点开始遍历
        while (cur != null) {
            count++;
            cur = cur.next; // 移动到下一个节点
        }
        return count; // 返回计数结果
    }

    /**
     * 清空单链表,使其变为空链表。
     */
    public void clear() {
        if (head == null) {
            return; // 如果链表为空,则直接返回
        }
        ListNode cur = head; // 保存头节点的下一个节点,以避免丢失整个链表
        while (cur != null) {
            ListNode curN=cur.next;
            cur=null;
            cur=curN;
        }
        head = last = null; // 最后将头节点和尾节点都设置为null
    }

    /**
     * 打印单链表中的所有节点数据。
     */
    public void display() {
        if (head == null) {
            return; // 如果链表为空,则直接返回
        }
        ListNode cur = head; // 从头节点开始遍历
        while (cur != null) {
            System.out.print(cur.data + " "); // 打印当前节点的数据
            cur = cur.next; // 移动到下一个节点
        }
        System.out.println(); // 换行
    }
}

测试:test2.java

java 复制代码
package MLinkList;

public class test2 {
    public static void main(String[] args) {
        MyLinklistto list = new MyLinklistto();
        list.addFirst(2);
        list.addFirst(3);
        list.addFirst(4);
        list.addFirst(4);
        list.addFirst(4);
        list.addLast(5);
        list.addIndex(2, 6);
        list.display();
        System.out.println("=========");
        list.removeAllKey(4);
        list.display();
    }
}
相关推荐
巫师不要去魔法部乱说24 分钟前
PyCharm专项练习3 图的存储:邻接矩阵+邻接链表
链表·pycharm
Dong雨1 小时前
六大排序算法:插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序
数据结构·算法·排序算法
茶猫_1 小时前
力扣面试题 39 - 三步问题 C语言解法
c语言·数据结构·算法·leetcode·职场和发展
初学者丶一起加油1 小时前
C语言基础:指针(数组指针与指针数组)
linux·c语言·开发语言·数据结构·c++·算法·visual studio
半盏茶香3 小时前
C语言勘破之路-最终篇 —— 预处理(上)
c语言·开发语言·数据结构·c++·算法
2401_858286113 小时前
118.【C语言】数据结构之排序(堆排序和冒泡排序)
c语言·数据结构·算法
没事就去码3 小时前
RBTree(红黑树)
数据结构·c++
就爱学编程4 小时前
重生之我在异世界学编程之数据结构与算法:单链表篇
数据结构·算法·链表
CSCN新手听安9 小时前
list的常用操作
数据结构·list
梅茜Mercy11 小时前
数据结构:链表(经典算法例题)详解
数据结构·链表