数据结构初阶(3)(链表:链表的基本概念、链表的类型、单向不带头非循环链表的实现、链表的优缺点 )

news2024/9/22 3:46:11

 接上次博客:和数组处理有关的一些OJ题;ArrayList 实现简单的洗牌算法(JAVA)(ArrayList)_di-Dora的博客-CSDN博客

目录

链表的基本概念 

链表的类型

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

遍历链表并打印节点值:

在链表头部插入节点:

 在链表尾部插入节点:

得到单链表的长度 :

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

删除第一次出现关键字为key的节点 (两种实现方式):

 删除所有值为key的节点:

指定任意位置插入数据:

 清空链表:

链表的优缺点 


 

数组是一块连续的内存,逻辑上和物理内存上都是连续的;

链表是在逻辑上是连续的,但是在物理内存上是不连续的。

链表的基本概念 

链表是一种常见的数据结构,它由一系列节点组成,每个节点包含两部分:数据元素 (value) 和指向下一个节点的指针 ( next 域 )。通过这些节点的连接,可以形成一个链式结构。

链表的基本概念如下:

1、节点(Node):链表的基本单元,包含数据元素和指针。数据元素可以是任意类型的数据,指针指向下一个节点。每个节点都是一个对象。最后一个节点的 next 域是 null 。

2、头节点(Head):链表的第一个节点,用于标识链表的起始位置。通常使用一个指针变量来指向头节点。

3、尾节点(Tail):链表的最后一个节点,其指针指向空(NULL),表示链表的结束。

4、链表长度(Length):链表中节点的数量,可以通过遍历链表来计算。

5、空链表(Empty List):不包含任何节点的链表。

6、单向链表(Singly Linked List):每个节点只有一个指针,指向下一个节点。最后一个节点的指针指向空。

7、双向链表(Doubly Linked List):每个节点有两个指针,一个指向前一个节点,一个指向下一个节点。头节点的前一个指针和尾节点的后一个指针都指向空。

注意:
1.链式结构在逻辑上是连续的,但是在物理上不一定连续;
2.现实中的节点一般都是从堆上申请出来的;
3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。

链表的类型

链表的组合方式有多种,可以根据以下两个方面来区分和计算组合的种类:

1、单向链表和双向链表:

根据节点的指针数量,链表可以分为单向链表和双向链表。

单向链表每个节点只有一个指针,指向下一个节点;

而双向链表每个节点有两个指针,分别指向前一个节点和后一个节点。

2、是否带头节点:

带头节点的链表在第一个节点之前有一个额外的头节点,用于标识链表的起始位置。(head的value是无意义的,如果想从最开头插入数据时,head是不可变的,从head后面插入)

而不带头节点的链表则直接以第一个节点作为链表的起始位置。(head是有value的,如果想从最开头插入数据时,head是可变的,变成新插入的数据)

3、是否循环: 

循环链表是在链表的尾部节点和头部节点之间形成一个循环连接,使得链表的最后一个节点指向头部节点。

综合考虑上述两个方面,我们可以得到链表的组合方式共有8种:

单向、不带头节点、非循环链表(重点)
单向、不带头节点、循环链表

单向、带头节点、非循环链表
单向、带头节点、循环链表


双向、不带头节点、非循环链表(重点)
双向、不带头节点、循环链表

双向、带头节点、非循环链表
双向、带头节点、循环链表


每种组合方式都有自己的特点和应用场景,我们可以根据具体需求选择合适的链表类型。

 

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

我们可以先来实现一个最简易的链表,即手动创建一个单向链表:

 public class   MySingleList {
      static class ListNode {
          public int val; // 节点的值域
          public ListNode next; // 下一个节点的地址

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

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

      //我们先来写一个最笨的方法:手动创建链表节点
      public void createlist() {
          // 创建链表节点
          head = new MySingleList.ListNode(-1);
          MySingleList.ListNode node1 = new MySingleList.ListNode(12);
          MySingleList.ListNode node2 = new MySingleList.ListNode(23);
          MySingleList.ListNode node3 = new MySingleList.ListNode(34);
          MySingleList.ListNode node4 = new MySingleList.ListNode(45);
          MySingleList.ListNode node5 = new MySingleList.ListNode(56);

          // 构建链表关系

          node1.next = node2;
          node2.next = node3;
          node3.next = node4;
          node4.next = node5;

          this.head = node1;//head 是一个指向第一个节点的引用

      }

  }
public class Test {

    public static void main(String[] args) {
        MySingleList list = new MySingleList();
        list.createlist();
        System.out.println(list);
        System.out.println("12345");
    }
}

通过这个代码,我们可以直观地观察到链表的大致结构: 

好了,现在我们就正式开始实现一个完整的单向链表了:

首先我们还是先给出链表的基本代码:

我们先要有一个引用 head 指向第一个节点,它是“节点”类型,就如同 Person person = new Person; 一样。

链表的头节点,是链表的成员变量、链表的属性,而不是一个节点类的成员变量。

    public class MySingleList {
        static class ListNode {
            public int val; // 节点的值域
            public ListNode next; // 下一个节点的地址

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


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



        // 在链表头部插入节点
        public void insertAtHead(int val) 


        // 在链表尾部插入节点
        public void insertAtTail(int val) 


        //得到单链表的长度
        public int size()


        //查找是否包含关键字key是否在单链表当中
        public boolean contains(int key)


        //删除第一次出现关键字为key的节点
        public void deleteNode(int key) 
            
    
        // 删除所有值为key的节点
        public void removeAllKey(int key) 


        //任意位置插入,第一个数据节点为0号下标
        public void insertAtIndex(int index, int val) 


        // 遍历链表并打印节点值
        public void display() 



        // 清空链表
        public void clear() 


    }

 以上都是我们需要实现的方法。

先来实现第一个:

遍历链表并打印节点值:

        // 遍历链表并打印节点值
        public void display() {
            //不可以让head本身移动,否则将遗失head的位置
            ListNode curr = head;
            while (curr != null) {
                System.out.print(curr.val + " ");
                curr = curr.next; //引用向后移动一位
            }
            System.out.println();
        }

这里我们要注意:curr 是一个引用!!!

curr = null 代表的是已经遍历了整个链表。 

在链表头部插入节点:

 

        // 在链表头部插入节点
        //一般建议,再插入的时候,先绑定后面的节点信息
        //就算链表中一个代码都没有,也不影响我们插入节点
        //以头插法插入,数据是倒序的
        public void insertAtHead(int val) {
            ListNode newNode = new ListNode(val);
            newNode.next = head;
            head = newNode;
        }
    public static void main(String[] args) {
        MySingleList list = new MySingleList();
        //list.createlist();
        list.insertAtHead(12);
        list.insertAtHead(23);
        list.insertAtHead(34);
        list.insertAtHead(45);
        list.insertAtHead(56);
        list.display();
    }

  注意:以头插法插入,数据是倒序的:

 在链表尾部插入节点:

        // 在链表尾部插入节点
        public void insertAtTail(int val) {
            ListNode newNode = new ListNode(val);

            //cur = null 代表把链表的每一个节点都遍历完了
            //cur.next = null 代表cur现在是最后一个节的位置

            //一定要写,否则会报:空指针异常
            //如果head等于null,curr也就等于null,就不存在curr.next
            if (head == null) {
                head = newNode;
            } else {
                ListNode curr = head;
                while (curr.next != null) {
                    curr = curr.next;
                }
                curr.next = newNode;
            }
        }

 注意区分:

curr = null 表示当前节点 curr 引用已经指向了链表的末尾,即已经遍历完了链表的所有节点。在这种情况下,可以用来判断是否已经遍历到了链表的末尾。

curr.next = null 表示当前节点 curr 的下一个节点指针指向 null,即当前节点 curr 是链表中的最后一个节点。这通常用于在遍历链表时进行判断,以确定是否已经到达了链表的末尾节点。

得到单链表的长度 :

        //得到单链表的长度
        public int size() {
            int length = 0;
            ListNode curr = head;
            while (curr != null) {
                length++;
                curr = curr.next;
            }
            return length;
        }

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

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

删除第一次出现关键字为key的节点 (两种实现方式):

找到你要删除的节点的前驱,用 del = curr.next;进行删除:curr.next = del.next;

        //删除第一次出现关键字为key的节点
        //找到指定删除的节点的前一个节点,即找到key的前驱
        public void deleteNode(int key) {
            if (head == null) {
                System.out.println("当前链表无数据");
                return;
            }
            //单独删除头节点
            if (head.val == key) {
                head = head.next;
                return;
            }
            ListNode curr = head;
            //如果 curr.next = null ,表示已经没有下一个节点了
            while (curr.next != null) {
                if (curr.next.val == key) {
                    curr.next = curr.next.next;
                    return;
                }
                //curr 后移,继续往后寻找
                curr = curr.next;
            }
        }

        //删除第一次出现关键字为key的节点 -------第2种方法
        public void remove(int key){
            if(head == null) {
                System.out.println("当前链表无数据");
                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;
        }
        private ListNode searchPrev(int key) {
            ListNode cur = head;
            while (cur.next != null) {
                if(cur.next.val == key) {
                    return cur;
                }
                cur = cur.next;
            }
            return null;
        }

 删除所有值为key的节点:

        // 删除所有值为key的节点
        public void removeAllKey(int key) {
            ListNode dummy = new ListNode(0); // 创建一个虚拟头节点,方便处理头节点的情况
            dummy.next = head;
            ListNode prev = dummy;
            ListNode curr = head;

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

            head = dummy.next;
        }

指定任意位置插入数据:

定义一个引用 curr,让它走到即将插入位置的前一个位置,这样我们可以同时访问到插入位置前和插入位置后的节点。先把 curr.next 赋值给newNode.next ,即新插入节点的指向原来位于插入位置的节点,再把 curr.next 变成 newNode 的值。

往 0 位置插入,相当于头插法,往结尾插入,相当于尾插法。

        //任意位置插入,第一个数据节点为0号下标
        public void insertAtIndex(int index, int val) {
            if (index < 0 || index > size()) {
                throw new IndexOutOfBoundsException("Invalid index: " + index);
            }
            if (index == 0) {
                insertAtHead(val);
                return;
            }
            if(index==size()){
                insertAtTail(val);
                return;
            }
            ListNode newNode = new ListNode(val);
            ListNode curr = head;
            int count = 0;//定义一个计数器
            while (curr != null && count < index - 1) {
                curr = curr.next;
                count++;
            }
            if (curr == null) {
                throw new IndexOutOfBoundsException("Invalid index: " + index);
            }
            newNode.next = curr.next;
            curr.next = newNode;
        }

或者,你也可以单独封装出去一个方法: 

        private ListNode findIndexSubOne(int index){
            ListNode curr=head;
            while (index-1!=0){
                curr=curr.next;
                index--;
            }
            return curr;
        }

 清空链表:

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

最后可以测试了看看: 

public class Test {

    public static void main(String[] args) {
        MySingleList list = new MySingleList();
        //list.createlist();
        list.insertAtHead(12);
        list.insertAtHead(23);
        list.insertAtHead(34);
        list.insertAtHead(45);
        list.insertAtHead(56);
        list.display();
        list.insertAtTail(666);
        list.display();
        list.deleteNode(12);
        list.display();
        list.insertAtTail(23);
        list.insertAtTail(34);
        list.insertAtTail(45);
        list.insertAtTail(23);
        list.display();
        list.removeAllKey(23);
        list.display();
        list.insertAtIndex(2,99999);
        list.display();
        list.insertAtIndex(5,188);
        list.display();
        int lengh=list.size();
        System.out.println(lengh);
    }
}

链表的优缺点 

链表相比于数组具有以下特点和优势:

  • 动态性:链表的长度可以根据需要动态地增长或缩小,不需要预先定义大小。
  • 插入和删除操作效率高:由于链表的节点之间通过指针连接,插入和删除节点的操作只需要改变指针的指向,时间复杂度为O(1)。
  • 空间利用效率高:链表节点在内存中分散存储,不需要连续的内存空间,可以更灵活地利用内存。
  • 链表长度没有固定限制:链表的长度可以根据需要动态调整,不受固定大小的限制。

然而,链表也有一些缺点:

访问效率较低:链表中的节点不是连续存储的,访问特定位置的节点需要从头节点开始遍历,时间复杂度为O(n),其中n为链表长度。

额外的存储空间:链表中的每个节点都需要额外的指针来指向下一个节点(以及前一个节点,对于双向链表),因此需要额外的存储空间。

综上,链表适用于需要频繁插入和删除节点的场景,而不太关注访问效率。我们还是需要根据具体的应用场景和需求,选择合适的数据结构(如数组或链表),这是很重要的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/547525.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uni-app小程序uni.navigateBack返回上一个页面并传递参数.返回上个页面并刷新

返回上一个打开的页面并传递一个参数。有种办法就是使用 假如从B页面返回A页面&#xff1a; var pages getCurrentPages(); var prevPage pages[pages.length - 2]; //上一个页面 prevPage.setData({ mdata:1 })经过测试&#xff0c;在uni.app中使用B页面使用setData设置A页…

【Spring篇】AOP案例

&#x1f353;系列专栏:Spring系列专栏 &#x1f349;个人主页:个人主页 一、案例&#xff1a;业务层接口执行效率 1.需求分析 这个需求也比较简单&#xff0c;前面我们在介绍 AOP 的时候已经演示过 : 需求 : 任意业务层接口执行均可显示其执行效率&#xff08;执行时长&…

如何选对适合你的FPGA?快速掌握选型技巧!

FPGA厂家和芯片型号众多&#xff0c;在开发过程中&#xff0c;特别是新产品新项目时&#xff0c;都会面临FPGA选型的问题。 如何选择出适合的FPGA型号非常关键&#xff0c;需要评估需求、功能、成本、存储器、高速收发器等各种因素&#xff0c;选出性能与成本平衡的FPGA芯片。…

从零玩转设计模式之外观模式-waiguanmos

title: 从零玩转设计模式之外观模式 date: 2022-12-12 15:49:05.322 updated: 2022-12-23 15:34:40.394 url: https://www.yby6.com/archives/waiguanmos categories: - 设计模式 tags: - 设计模式 什么是外观模式 外观模式是一种软件设计模式&#xff0c;它提供了一种将多个…

进阶必看:高速PCB Layout设计的技术指南

当今电子行业中&#xff0c;高速PCB电路越来越广泛&#xff0c;已成为当代PCB工程师的重要技能&#xff0c;而在高速PCB电路中&#xff0c;高速PCB Layout设计是一项高难度高技术的工作&#xff0c;其设计质量直接关系到电路的性能。所以做好PCB Layout设计是非常非常重要的。 …

Boost开发指南-1.2progress_display

Progress_display progress_display可以在控制台上显示程序的执行进度&#xff0c;如果程序执行很耗费时间&#xff0c;那么它能够提供一个友好的用户界面&#xff0c;不至于让用户在等待中失去耐心。 progress_display位于名字空间boost&#xff0c;为了使用progress_displa…

内网自建代理ChatGPT

使用GPT比较频繁&#xff0c;一开始翻墙还能接受&#xff0c;但是用美国节点访问其他国外网站&#xff0c;确实比较麻烦。因此决定自己转发一个出来。 一、获取OpenAI授权密钥 首先&#xff0c;进入platform.openai.com-Personal-View API keys 不过OpenAI的key并不是免费的&…

VMware虚拟机三种网络模式详解之Bridged(桥接模式)

VMware虚拟机三种网络模式详解 Bridged&#xff08;桥接模式&#xff09; 由于Linux目前很热门&#xff0c;越来越多的人在学习Linux&#xff0c;但是买一台服务放家里来学习&#xff0c;实在是很浪费。那么如何解决这个问题&#xff1f;虚拟机软件是很好的选择&#xff0c;常…

登高作业安全带穿戴识别系统 yolov5

登高作业安全带穿戴识别系统通过yolov5python网络框架模型技术&#xff0c;登高作业安全带穿戴识别算法模型实现对登高作业人员是否穿戴安全带进行监测并及时发出警报。YOLO系列算法是一类典型的one-stage目标检测算法&#xff0c;其利用anchor box将分类与目标定位的回归问题结…

前端web入门-HTML-day02

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 列表 无序列表 有序列表 定义列表 表格 基本使用 合并单元格 跨列合并 跨行合并 表单 input …

网络安全里主要的岗位有哪些?小白如何快速入门学习黑客?

入门Web安全、安卓安全、二进制安全、工控安全还是智能硬件安全等等&#xff0c;每个不同的领域要掌握的技能也不同。 当然入门Web安全相对难度较低&#xff0c;也是很多人的首选。主要还是看自己的兴趣方向吧。 本文就以下几个问题来说明网络安全大致学习过程&#x1f447; 网…

软件设计师 数据库刷题项并包含知识点总结

**两级映像 有概念模式和内模式跟物理独立性相关&#xff0c;有外模式和概念模式跟逻辑独立性相关 ** 属性列就是RS共同拥有的ABC&#xff0c;一般去除后面的&#xff0c;所以就只有前面三个ABC&#xff0c;元组就是有没有自然连接成功的&#xff0c;就是R.AS.A R.BS.B… 选项里…

[人工智能原理]

软件工程 定义 采用工程概念、原理、技术、方法来开发、维护软件&#xff0c;把经过时间考验而证明正确的管理技术和当前能够得到的最好技术方法结合起来&#xff0c;经济开发出高质量软件并有效的维护 基本目标 目标 可用性 正确性 合算性 原则 采用适合的开发范型、开…

计算机操作系统(慕课版)第一章课后题答案

第一章 操作系统引论 一、简答题 1.在计算机系统上配置OS的目标是什么&#xff1f;作用表现在哪几个方面&#xff1f; 在计算机系统上配置OS&#xff0c;主要目标是实现&#xff1a;方便性、有效性、可扩充性和开放性&#xff1b; OS的作用主要表现在以下3个方面&#xff1a; 1…

matplotlib后端@backend@高清图输出格式控制@SVG格式输出

文章目录 notebookmatplotlib&#x1f388;matplotlib backendbackendfrontend后端类型AGG配置后端Note不区分大小写三种配置方式在matplotlibrc文件中使用rcParams["backend"]参数&#xff1a;使用MPLBACKEND环境变量&#xff1a;使用matplotlib.use()函数&#xff…

阿里三面过了,却无理由挂了,HR反问一句话:为什么不考虑阿里?

进入互联网大厂一般都是“过五关斩六将”&#xff0c;难度堪比西天取经&#xff0c;但当你真正面对这些大厂的面试时&#xff0c;有时候又会被其中的神操作弄的很是蒙圈。 近日&#xff0c;某位测试员发帖称&#xff0c;自己去阿里面试&#xff0c;三面都过了&#xff0c;却被…

SpringCloud微服务调用方式(RestTemplate)

服务调用方式 RPC和HTTP 无论是微服务还是SOA&#xff0c;都面临着服务间的远程调用。那么服务间的远程调用方式有哪些呢&#xff1f; 常见的远程调用方式有以下2种&#xff1a; RPC&#xff1a;Remote Produce Call远程过程调用&#xff0c;类似的还有 。自定义数据格式&am…

由浅入深Netty组件实战

目录 1 EventLoop1.1 演示 NioEventLoop 处理 io 事件1.2 演示 NioEventLoop 处理普通任务1.3 演示 NioEventLoop 处理定时任务 2 Channel2.1 ChannelFuture2.2 CloseFuture 3 Future & Promise3.1 例1&#xff1a;同步处理任务成功3.2 例2&#xff1a;异步处理任务成功3.3…

Navicat premium 15激活教程及安装教程+报错解决办法

Navicat premium 15激活教程及安装教程报错解决办法 1、安装包和注册工具下载2、安装Navicate Premium 15&#xff0c;直接下一步安装即可&#xff0c;安装位置可以按照到D盘3、激活Navicate Premium 15打开安装包里面的Navicat Keygen Patch v5.6.0 DFoX.exe工具当点击path选择…

看完这篇文章你就彻底懂啦{保姆级讲解}-----(I.MX6U驱动UART串口通信) 2023.5.20

目录 前言整体文件结构源码分析&#xff08;保姆级讲解&#xff09;串口驱动初始化部分UART1数据收发函数部分 编译结果验证结束语 前言 串口是我们在开发过程中最常用到的外设&#xff0c;所以我们必须掌握。 整体文件结构 源码分析&#xff08;保姆级讲解&#xff09; 串口…