【数据结构】3000字剖析链表及双向链表

news2025/1/21 18:55:10

文章目录

  • 💐 链表的概念与结构
    • 💐链表的介绍
      • 💐链表的模拟实现
    • 💐双向链表
      • 💐双向链表的模拟实现
    • 💐链表常用的方法
    • 💐链表及顺序表的遍历
    • 💐ArrayList和LinkedList的差异

💐 链表的概念与结构

前面讲解了 ArrayList 实现的顺序表,下面讲解线性表的另一种存储结构 链表

在顺序表中,所有的元素都是在一段地址连续的存储单元上保存的,每个元素挨着每一个元素,通过下标也可以进行访问;但缺点是在非首尾的插入和删除时,需要移动大量的元素,时间复杂度较高;
在这里插入图片描述

所以,为了解决这个问题,就衍生出了另一种数据结构——链表

链表链表是用一段物理地址不连续的存储单元依次存储数据的线性结构;

有n个节点组成,每个节点里面除了存储数值以外,还保存了下一个节点的地址,这样就可以通过地址依次找到对应的下一个节点;
在这里插入图片描述
在这里插入图片描述

💐链表的介绍

在这里插入图片描述

2、有头或者无头链表
在这里插入图片描述
3、循环或非循环链表
在这里插入图片描述

根据以上三种链表结构又可以组合成一下八种结构:
在这里插入图片描述
而最常用的是以上无头单向非循环链表无头双向链表,下面就针对于两种链表进行详细的剖析!!!

💐链表的模拟实现

要想深刻的理解链表,就要亲手实现一下,这样才能深切的领会到链表他是怎样进行操作的,如何使用的!!!

首先,先思考,如果要实现一个链表的话,都要有哪些功能呢?下面我实现了一个链表功能,我将针对实现的链表每一部分进行一个剖析:

要实现一个链表,首先实现以下几个基础的功能:

1、节点的增加

2、节点的删除

3、数据的查找

4、节点的插入

//无头单向非循环链表实现
public class LinkedList {
    
    static class listNode {
        private int value;//结点中的值
        private listNode next;//下一个结点的地址
        public listNode(int value) {
            this.value = value;
        }
    }
        //创建一个链表
    public void createList() {
        ListNode listNode1 = new ListNode(20);
        ListNode listNode2 = new ListNode(30);
        ListNode listNode3 = new ListNode(40);
        ListNode listNode4 = new ListNode(50);
        ListNode listNode5 = new ListNode(60);

        listNode1.next = listNode2;
        listNode2.next = listNode3;
        listNode3.next = listNode4;
        listNode4.next = listNode5;

        this.head = listNode1.next;
    }
    
    private listNode head;//第一个结点的地址

    //头插法
    public void addHead(int val) {
        ListNode listNode = new ListNode(val);
        listNode.next = head;
        head = listNode;
        return;      
    }
    //尾插法
    public void addLast() {
          ListNode listNode = new ListNode(val);
        ListNode cur = head;
        while(cur.next != null) {
            cur = cur.next;
        }
        cur.next = listNode;
    }
    //任意位置插入
    public void addIndex() {
         ListNode listNode = new ListNode(value);
        if(index == 0) {
            addHead(value);
            return;
        }
        if(index >figureSize() || index<0) {
           throw new IndexException("输入下标下标有误");
        }

        ListNode cur = head;
        ListNode pos = checkIndex(cur, index);//定义一个查找前一个节点的方法
        listNode.next = pos.next;
        pos.next = listNode;
    }
    //查找前一个节点方法
      public  static  ListNode checkIndex(ListNode cur, int index) {
        for(int i =0; i<index-1; i++) {
            cur = cur.next;
        }
        return cur;
    }
    //查找链表中是否包含查找的值
    public void contains() {
         ListNode cur = head;
        for(int i =0; i<figureSize(); i++) {
            if(cur.value == key) {
                System.out.println("链表中包含查找的值");
                return;
            }
            cur = cur.next;
        }
        System.out.println("链表中不包含查找的值");
    }
    //删除第一次出现的指定的值的结点
    public void delFirst(int key) {
        if(head == null) {
            return;
        }
        if(key == head.value) {
            head = head.next;
            return;
        }
        ListNode cur =  searchVal(key);
        if(cur == null) {
            System.out.println("没有您要删除的值");
        }

        assert cur != null;
        ListNode del = cur.next;
        cur.next = del.next;
    }
    //负责查找前一个节点
    public ListNode searchVal(int key) {
        ListNode cur = head;
        for(int i =0; i<figureSize(); i++) {
            if(key == cur.next.value) {
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }
    //删除所有值为key的结点
    public void allPoint(int key) {
         if(head == null) {
            return;
        }
        ListNode prev = head;
        ListNode cur = head.next;
        while(cur != null){
            if(cur.value == key) {
                prev.next = cur.next;
                cur = cur.next;
            }else {
                prev = cur;
                cur = cur.next;
            }
            if(head.value == key) {
                head = head.next;
            }

        }
    }
       //打印链表
    public void disPlay() {
        ListNode cur = head;
        while(cur != null) {
            System.out.println(cur.value +" ");
            cur = cur.next;
        }
    }
    //计算单链表的长度
    public void figureSize() {
        int count = 0;
        ListNode cur = head;
        while(cur!=null) {
            count++;
            cur = cur.next;
        }
        return count;
    }
    //清空节点
    public void clear() {
        head = null;
    }
    
}

下面对这些功能进行一一的实现:

1、如何创建一个结点

在实现这些功能前,需要先知道如何去写这个结点的代码,下图讲解:
在这里插入图片描述
在这里插入图片描述
2、创建一个链表

在已经创建好一个结点的基础上,如何创建链表呢,下面来实现一下:
在这里插入图片描述

1、头插法
在这里插入图片描述

2、尾插法
在这里插入图片描述
3、任意位置插入
在这里插入图片描述
在这里插入图片描述

4、查找链表中是否包含查找的值
在这里插入图片描述

5、删除第一次出现的指定的值的结点

在这里插入图片描述
在这里插入图片描述

6、删除所有值为key的节点

在这里插入图片描述
在这里插入图片描述

6、清除节点

直接将头节点置为空,因为只要头节点中的地址为空了,那么后面的结点就没有被引用指向了,所以就会被内存回收
在这里插入图片描述

💐双向链表

单向链表在使用上有以下几个缺点:

1、在删除和尾插时,时间复杂度较高

2、只能从前向后访问,无法从后向前访问,限制较大

而下面,介绍的双向链表是非常牛掰的,包括以后在实现栈、队列、双向队列等方面,使用双向链表会方便很多,下面先简单介绍一下双向链表:
在这里插入图片描述

💐双向链表的模拟实现

如果想要彻底的理解链表的话,我的建议是,自己多模拟实现几遍这个链表,这样可以更加有利于你灵活的去使用,而不是只记住它的方法的功能。

//无头双向链表的实现
public class LinkedList {

    static class ListNode{
        private int val;
        private ListNode next;
        private ListNode prev;

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

    private ListNode head;
    private ListNode last;
    //头插法
    public void addFirst(int val) {
        ListNode node = new ListNode(val);
        //情况一:链表中无节点
        if(head == null) {
            head = node;
            last = node;
            return;
        }
        //情况二:链表中无节点
        node.next = head;
        head.prev = node;
        head = node;
    }
    //尾插法
    public void addLast(int val) {
        ListNode node = new ListNode(val);
        //情况一:链表中无节点
        if(head == null && last == null) {
            head = node;
            last = node;
        }
        //情况二:链表中有节点
        last.next = node;
        node.prev = last;
        last = last.next;
    }
    //任意位置插入
    public void addIndex(int pos,int val) {
        ListNode node = new ListNode(val);
        if(pos == 0) {
            addFirst(val);
            return;
        }
        if(pos == size()) {
            addLast(val);
            return;
        }
        //寻找要插入节点的位置
        ListNode cur = searchIndex(pos);
        cur.prev.next = node;
        node.next = cur;
        node.prev = cur.prev;
        cur.prev = node;
    }
    public ListNode searchIndex(int pos) {
        ListNode cur = head;
        for(int i =0; i<pos; i++) {
            cur = cur.next;
        }
        return cur;
    }
    //查找关键字key是否包含在链表中
    public void contains(int key) {
        if(searchKey(key)) {
            System.out.println("找到了!!!");
        }else{
            System.out.println("链表中不包含此数据!!!");
        }
    }
    public boolean searchKey(int key) {
        ListNode cur = head;
        while(cur != null && cur.val != key) {
            cur = cur.next;
        }
        if(cur != null) return true;
        return false;
    }
    //删除第一次出现关键字为key的节点
    public void remove(int key) {
        ListNode cur = searchFirst(key);
        if(cur == null) {
            System.out.println("链表中不包含此数据!!!");
        }
        //情况一:头节点为key时
        if(cur == head) {
            //只有一个节点时
            if(head == last) {
                head = null;
                last = null;
            }else {
                //多个节点时
                head = head.next;
                head.prev = null;
            }
        }else if(cur == last) {
            //情况二:尾结点为key时
            last = last.prev;
            last.next = null;
        }else {
            cur.prev.next = cur.next;
            cur.next.prev = cur.prev;
        }

    }
    public ListNode searchFirst(int key) {
        ListNode cur = head;
        while(cur != null && cur.val != key) {
            cur = cur.next;
        }
        if(cur != null) {
            return cur;
        }
        return null;
    }
    //删除所有值为key的节点
    public void removeAll(int key) {

        ListNode cur = head;
        while(cur != null) {
            cur = searchFirst(key);
            if(cur == null) {
                return;
            }
            //为头节点时
            //情况一:头节点为key时
            if(cur == head) {
                //只有一个节点时
                if(head == last) {
                    head = null;
                    last = null;
                }else {
                    //多个节点时
                    head = head.next;
                    head.prev = null;
                }
            }else if(cur == last) {
                //情况二:尾结点为key时
                cur.prev.next = null;
                last = null;
            }else {
                cur.prev.next = cur.next;
                cur.next.prev = cur.prev;
            }
        }


    }
    //链表的长度
    public int size() {
        ListNode cur = head;
        int size = 0;
        while(cur != null) {
            size++;
            cur = cur.next;
        }
        return size;
    }
    //打印单链表
    public void display() {
        ListNode cur = head;
        while(cur != null) {
            System.out.print(cur.val+" ");
            cur = cur.next;
        }
    }
    //清空链表
    public void clear() {
        head = null;
        last = null;
    }
}

这里双链表与单链表的模拟非常相似,只要多多注意几种情况即可;

💐链表常用的方法

1、LinkedList的构造方法

方法解释
LinkedList()无参构造
public LinkedList(Collection< ? extends E > c)使用其他集合容器中元素构造List
    public static void main(String[] args) {
        //使用无参构造方法
        List<Integer> list = new LinkedList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println(list);

        //使用list构造LinkedList
        List<Integer> list2 = new LinkedList<>(list);
        list2.add(4);
        System.out.println(list2);
    }

在这里插入图片描述

2、LinkedList的其他方法

方法解释
boolean add(E e)尾插 e
void add(int index, E element)将 e 插入到 index位置
boolean addAll(Collection<? extends E> c)尾插 c 中的元素
E remove(int index)删除 index 位置的元素
boolean remove(Object o)删除遇见的第一个o
E get(int index)返回 index 位置的元素
E set(int index, E element)将下标index 位置元素设置为 element
void clear()清空链表
boolean contains(Object o)判断 o 是否在链表中
int indexOf(Object o)返回第一个 o 所在位置的下标
int lastIndexOf(Object o)返回最后一个 o 位置的下标

以上方法与ArrayList中的使用都是一样的,这里就不再一一列举!

💐链表及顺序表的遍历

1、for循环的方式遍历

2、foreach的方式遍历

3、Iterator迭代器遍历

 public static void main(String[] args) {
       LinkedList<Integer> list = new LinkedList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        //foreach方式遍历
        for(int x: list) {
            System.out.print(x+" ");
        }
        System.out.println();
        //Iterator迭代器遍历--正向遍历
        Iterator<Integer> it = list.iterator();
        while(it.hasNext()) {
            System.out.print(it.next()+" ");
        }
        System.out.println();
        //Iterator迭代器反向遍历
        //因为反向遍历是从后向前遍历,所以需要知道元素个数
        ListIterator<Integer> it2 = list.listIterator(list.size());
        while(it2.hasPrevious()) {
            System.out.print(it2.previous()+" ");
        }
        System.out.println();
    }

在这里插入图片描述

💐ArrayList和LinkedList的差异

1、数据结构实现:

​ ArrayList是动态数组的数据结构实现,而LinkedList是双向链表的数据结构实现;

2、随机访问效率:

​ ArrayList比LinkedList在随机访问的时候效率要高,因为LinkedList是线性的数据存储方式,所以需要移 动指针指针从前向后依次查找;

3、增加和删除效率:

​ 在非首尾的增加和删除操作,LinkedList要比ArrayList效率要高,因为ArrayList增删操作时,会导致其他下标的元素进行移动,时间复杂度高;

4、内存空间占用:

​ LinkedList比ArrayList更占用空间,因为LinkedList的节点处了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素;

5、线程安全:

​ ArrayList 和 LinkedList都是不同步的,也就是不保证线程安全;

6、综合来说:

​ 在需要频繁读取聚合中的元素时,更推荐使用ArrayList,而在插入和删除操作较多时,推荐使用LinkedList;

**7、LinkedList的双向链表:**它的每一个节点中都有两个指针,分别指向前驱和后继,所以,从双向链表的任意一个节点开始,都可以很方便的访问它的前驱节点和后继节点;

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

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

相关文章

Python常用库(五):图像处理【Pillow】

1. Pillow 1.1 介绍 Pillow 是第三方开源的 Python 图像处理库&#xff0c;它支持多种图片格式&#xff0c;包括 BMP、GIF、JPEG、PNG、TIFF 等。Pillow 库包含了大量的图片处理函数和方法&#xff0c;可以进行图片的读取、显示、旋转、缩放、裁剪、转换等操作。在后续的深度学…

python开发基础篇2——登陆机制

文章目录 一、管理平台页面布局二、登录页面2.1 token登录2.2. kubeconfig登录2.3 添加装饰器 一、管理平台页面布局 应用名称&#xff1a; dashboard&#xff1a;存放公共 k8s&#xff1a; Node&#xff1a;K8s集群计算节点。Namespaces&#xff1a;命名空间&#xff0c;用于…

ABAP WS_DELIVERY_UPDATE 报错 BS013

在使用 WS_DELIVERY_UPDATE 创建内向交货单时 报错&#xff1a;System status ESTO is active (EQU ***) 可以用事务代码IE03--->历史去看下 你可以手动将它的状态去改回EDEL 或者 SLOR IE02--> Special serial no. functions --> Manual transaction.

C#,《小白学程序》第十七课:随机数(Random)第四,移动平均值(Moving Average)的计算方法与代码

1 文本格式 /// <summary> /// 《小白学程序》第十七课&#xff1a;随机数&#xff08;Random&#xff09;第四&#xff0c;移动平均值的计算方法与代码 /// 继续学习数据统计&#xff0c;移动平均值的计算方法 /// 移动平均值就是一定步长内数值的平均值&#xff0c;用…

Llama 2 论文《Llama 2: Open Foundation and Fine-Tuned Chat Models》阅读笔记

文章目录 Llama 2: Open Foundation and Fine-Tuned Chat Models1.简介2.预训练2.1 预训练数据2.2 训练详情2.3 LLAMA 2 预训练模型评估 3. 微调3.1 supervised Fine-Tuning(SFT)3.2 Reinforcement Learning with Human Feedback (RLHF)3.2.1 人类偏好数据收集3.2.2 奖励模型训…

Matlab 如何选择窗函数和 FFT 的长度

Matlab 如何选择窗函数和 FFT 的长度 1、常用的四种窗函数 对于实际信号序列&#xff0c;如何选取窗函数呢&#xff1f;一般来说&#xff0c;选择第一旁瓣衰减大&#xff0c;旁瓣峰值衰减快的窗函数有利于緩解截断过程中产生的頻泄漏问题。但具有这两个特性的窗函数&#xff0…

[BFS] 广度优先搜索

1. 数字操作 常见的模板 // 使用一个数组判断元素是否入过队 int inqueue[N] {0}; // 层数或者可以称为深度 int step 0; // 判断是否可以入队的条件 int isvalid(){ } BFS(int x){ // 将初始的元素压入队列 // 注意每次压队的时候都要将inque[x] 1,表明入队过…

python实现adb辅助点击屏幕工具

#!/usr/bin/env python # -*- coding: utf-8 -*-import re import os import time import subprocess import tkinter as tk from tkinter import messagebox from PIL import Image, ImageTk# 设置ADB路径&#xff08;根据你的系统和安装路径进行调整&#xff09; ADB_PATH C…

Ubuntu快速搭建内网NTP Server

文章目录 安装NTP服务配置NTP配置NTP 同步源配置NTP 允许客户端访问重启NTP服务使得配置生效 推荐阅读 NTP(Network Time Protocol)------网络时间协议-----应用层协议&#xff0c;用来在分布式时间服务器和客户端之间进行时间同步。 是对网络内所有具有时钟的设备进行时钟同步…

图解 LeetCode 算法汇总——链表

本文首发公众号&#xff1a;小码A梦 一般数据主要存储的形式主要有两种&#xff0c;一种是数组&#xff0c;一种是链表。数组是用来存储固定大小的同类型元素&#xff0c;存储在内存中是一片连续的空间。而链表就不同于数组。链表中的元素不是存储在内存中可以是不连续的空间。…

酒店类型的软文怎么写?

马上就放长假了&#xff0c;有不少酒店行业来找盒子做推广&#xff0c;其实酒店行业想要写好软文只要掌握三种类型就好了&#xff0c;今天就让盒子告诉大家酒店类型的软文怎么写才能吸引用户。 一、以故事打动用户 故事型软文大多用于民宿酒店&#xff0c;民宿酒店文案除了展现…

Docker认识即安装

Docker及相关概念 Docker和虚拟机方式的区别&#xff1a;虚拟机技术是虚拟出一套硬件后&#xff0c;在其上运行一个完整的操作系统&#xff0c;在该系统上在运行所需应用进程&#xff1b;而容器内的应用进程是直接运行于宿主的内核&#xff0c;容器内没有自己的内核&#xff0…

2023区块链应用操作员认证(4级)报名来弘博创新

区块链应用操作员&#xff0c;是指运用区块链技术及工具&#xff0c;从事政务、金融、医疗、教育、养老等场景系统应用操作的人员。 腾讯作为广东省第一批公布的社会培训评价组织&#xff0c;可开展职业技能等级认定职业(工种)区块链应用操作员(4-3-2-1级)。 证书含金量 证书是…

小节4:input()函数的一些讲究

千万注意&#xff1a;不管用户输入的是什么&#xff0c;input()返回的一律都是字符串&#xff0c;代码如下&#xff1a; user_input input("Please input something: ") print(type(user_input)) 所以&#xff0c;如果直接拿用户输入的内容去做数学运算&#xff0c…

JavaScript 之 常用迭代方法forEach、filter()、map()、reduce()

JavaScript 之 常用迭代方法forEach、filter、map、reduce 1. for、forEach1.1 for 遍历的3种写法1.2 forEach 回调函数 遍历1.3 forEach 箭头函数 遍历 2. filter()2.1 介绍2.2 例子1——简单过滤2.3 例子2——在修改数组时 filter() 方法的行为2.4 例子3——在数组中搜索 3…

生成式 AI 中的风险认知

推荐&#xff1a;使用 NSDT场景编辑器 快速搭建3D应用场景 直到几年前&#xff0c;你能想象一台机器可以创造艺术、制作故事&#xff0c;甚至理解复杂的数据&#xff0c;如医疗和法律文件吗&#xff1f; 应该不会。对于我们大多数人来说&#xff0c;它仅限于一部看似牵强附会的…

博客系统项目

文章目录 数据库的增删改查草稿箱草稿箱自动保存分页查询后端前端 评论区后端前端 md5加盐加密 md5加盐对用户密码进行加密; 全服用户博客列表页,实现分页查询; 用户博客列表页; 写博客,发博客,改博客; 博客草稿箱,自动保存,定时发布; 博客访问量,博客评论区,博客点赞; 数据库…

MojoTween:使用「Burst、Jobs、Collections、Mathematics」优化实现的Unity顶级「Tween动画引擎」

MojoTween是一个令人惊叹的Tween动画引擎&#xff0c;针对C#和Unity进行了高度优化&#xff0c;使用了Burst、Jobs、Collections、Mathematics等新技术编码。 MojoTween提供了一套完整的解决方案&#xff0c;将Tween动画应用于Unity Objects的各个方面&#xff0c;并可以通过E…

恒运资本:人民币汇率何时走出低谷?

9月7日&#xff0c;国家外汇管理局发布统计数据显现&#xff0c;到2023年8月末&#xff0c;我国外汇储藏规划为31601亿美元&#xff0c;较7月末下降442亿美元&#xff0c;降幅为1.38%。 国家外汇管理局相关负责人表明&#xff0c;2023年8月&#xff0c;受首要经济体微观经济数…

《机器人学一(Robotics(1))》_台大林沛群 第 7 周 【轨迹规划_综合】Quiz 7

题 4-5 存疑&#xff0c;仅供参考&#xff0c;欢迎交流 文章目录 题4-9&#xff1a;题4-5求解代码&#xff1a; Python题6-7求解代码&#xff1a; Python求解 θ4-θ6 时&#xff0c; 记得 将 R 改成相应的&#xff01;&#xff01;&#xff01;&#xff01; 题8-9求解代码&…