使用Java语言深度探索数据结构中的单向链表:完美结合详解与示例代码

news2025/1/11 23:36:14

版本说明

当前版本号[20231007]。

版本修改说明
20231007初版

目录

文章目录

2.2 链表

1) 概述

定义

​ 在计算机科学中,链表是数据元素的线性集合,其每个元素都指向下一个元素元素存储上并不连续

简单分类
  • 单向链表,每个元素只知道其下一个元素是谁

image-20221110083407176

  • 双向链表,每个元素知道其上一个元素和下一个元素

image-20221110083427372

  • 循环链表,通常的链表尾节点 tail 指向的都是 null,而循环链表的 tail 指向的是头节点 head

image-20221110083538273

​ 链表内还有一种特殊的节点称为哨兵(Sentinel)节点,也叫做哑元( Dummy)节点,它不存储数据,通常用作头尾,用来简化边界判断,如下图所示

image-20221110084611550

随机访问性能

根据 index 查找,时间复杂度 O ( n ) O(n) O(n)

插入或删除性能
  • 起始位置: O ( 1 ) O(1) O(1)
  • 结束位置:如果已知 tail 尾节点是 O ( 1 ) O(1) O(1),不知道 tail 尾节点是 O ( n ) O(n) O(n)
  • 中间位置:根据 index 查找时间 + O ( 1 ) O(1) O(1)

2) 单向链表

根据单向链表的定义,首先定义一个存储 value 和 next 指针的类 Node,和一个描述头部节点的引用

public class SinglyLinkedList {
    
    private Node head; // 头部节点
    
    private static class Node { // 节点类
        int value;
        Node next;

        public Node(int value, Node next) {
            this.value = value;
            this.next = next;
        }
    }
}
  • 这个类有一个私有成员变量head,它是一个指向Node类型的指针,用于存储链表的头节点
  • public Node 是一个私有静态内部类,表示链表中的节点。其中还具有一个构造函数,用于初始化节点的值和下一个节点的引用。包含两个成员变量:
    • value:一个整数类型的值,表示节点存储的数据
    • next:一个指向下一个节点的引用,用于在链表中连接多个节点
  • 定义为 static 内部类,是因为 Node 不需要与 SinglyLinkedList 实例相关,多个 SinglyLinkedList实例能共用 Node 类定义
头部添加

addFirst方法:用于在链表的头部添加一个新的节点

public class SinglyLinkedList {
    // ...
    public void addFirst(int value) {
		this.head = new Node(value, this.head);
    }
}
  • 创建一个新的Node对象,将其值设置为value,并将其next指针指向当前的头节点。最后,将头节点更新为新创建的节点
  • 如果 this.head == null,新增节点指向 null,并作为新的 this.head
  • 如果 this.head != null,新增节点指向原来的 this.head,并作为新的 this.head
    • 注意赋值操作执行顺序是从右到左

如何创建一个新的节点?

分为两种情况。一种是链表为空,另一种是链表非空。

1、在链表为空下,只要将头指针指向新节点。最后,新节点就是我们链表中第一个节点

this.head = new Node(value, null);

image-20231006220839393

2、在链表非空下,只要将新节点指向下一个节点,然后头指针指向新节点。最后,新节点就是我们链表中第一个节点,而原来的那个节点就是第二个节点了。

this.head = new Node(value, head);//括号里的head指的是原来旧的那个节点,新节点下一个就要指向这个节点

image-20231006221639219

循环遍历
while遍历

​ 在循环中,首先将头节点赋值给指针p。然后,使用条件判断p != null来检查指针是否为空。如果指针不为空,就执行循环体内的操作。在这个示例中,我们简单地打印了当前节点的值。

​ 接下来,通过p = p.next将指针p更新为下一个节点,以便在下一次循环中处理下一个节点。循环会一直执行,直到指针p为空,即到达链表的末尾。

public void loop()
    {
        Node p = head;
        while(p != null)
        {
            System.out.println(p.value);
            p = p.next;
        }
    }

测试类:

 @Test
    public void test_loop(){
        单向链表 p = new 单向链表();
        p.addFirst(1);
        p.addFirst(2);
        p.addFirst(3);
        p.addFirst(4);
        p.loop();
    }

测试结果如下:

image-20231006223955555

或者以参数的形式传递:

你可以将要对每个节点执行的操作作为Consumer<Integer>类型的参数传递给loop方法。例如,你可以使用System.out.println打印节点的值,或者将节点的值添加到集合中等。

public void loop(Consumer<Integer> consumer) {
    Node p = head; // 将头节点赋值给指针p
    while (p != null) { // 当指针p不为空时执行循环
        consumer.accept(p.value); // 调用consumer的accept方法处理当前节点的值
        p = p.next; // 将指针p更新为下一个节点,继续遍历链表
    }
}

测试类

@Test
    public void test_loop(){
        单向链表 p = new 单向链表();
        p.addFirst(1);
        p.addFirst(2);
        p.addFirst(3);
        p.addFirst(4);

       p.loop(value->{
           System.out.println(value);
       });
    }

测试结果与上面的图相同。

for 遍历

​ 在循环中,我们使用了一个传统的for循环来遍历链表。首先,将头节点赋值给指针p。然后,使用条件判断p != null来检查指针是否为空。如果指针不为空,就执行循环体内的操作。在这个示例中,我们调用了consumeraccept方法,并将当前节点的值作为参数传递给它。

​ 接下来,通过p = p.next将指针p更新为下一个节点,以便在下一次循环中处理下一个节点。循环会一直执行,直到指针p为空,即到达链表的末尾。

 public void loop_for(Consumer<Integer> consumer) {
        for (Node p = head; p != null; p = p.next) {
            consumer.accept(p.value);
        }
    }
迭代器遍历

​ 这个类,实现了Iterable<Integer>接口。这个类用于表示一个单向链表,其中包含一个头指针head,以及一个内部类Node用于表示链表中的节点。

​ 在这个类中,实现了iterator()方法,该方法返回一个Iterator<Integer>对象,用于遍历链表中的元素。Iterator接口有两个方法:hasNext()next()

  • hasNext 用来判断是否还有必要调用 next

  • next 做两件事

    • 返回当前节点的 value
    • 指向下一个节点

    NodeIterator 要定义为非 static 内部类,是因为它与 SinglyLinkedList 实例相关,是对某个 SinglyLinkedList 实例的迭代

public class 单向链表 implements Iterable<Integer>//整体
{
    private Node head = null;//头指针

    //匿名内部类
    @Override //alt+enter 快捷键
    public Iterator<Integer> iterator() {
        return new Iterator<Integer>() {
            Node p = head;
            @Override
            public boolean hasNext() {//是否有下一个元素
                return p != null;
            }

            @Override
            public Integer next() {//返回当前值,并指向下一个元素
                int v = p.value;
                p = p.next;
                return v;
            }
        };
    }
}

测试类

@Test
    public void test_class(){
        单向链表 p = new 单向链表();
        p.addFirst(6);
        p.addFirst(9);
        p.addFirst(2);
        p.addFirst(5);

       for(Integer value : p)
       {
           System.out.println(value);
       }
    }

测试结果如下:

image-20231006231436247

匿名内部类转换为带名字的内部类

在 IDEA 中,可以使用以下快捷键将匿名内部类转换为带名称的内部类:

  1. 选中匿名内部类。
  2. 按下 右键,选Refactor(Windows/Linux)后,在选中 Convert Anonymous to Inner..即可将匿名内部类转换为带名称的内部类。
  3. IDEA 会自动为该匿名内部类生成一个具有描述性名称的新内部类,并将其放置在当前文件中。

image-20231007104049538

生成的新内部类代码如下:

 @Override //alt+enter 快捷键
    public Iterator<Integer> iterator() {
        return new NodelIterator();
    }
    
    private class NodelIterator implements Iterator<Integer> {
        Node p = head;

        @Override
        public boolean hasNext() {//是否有下一个元素
            return p != null;
        }

        @Override
        public Integer next() {//返回当前值,并指向下一个元素
            int v = p.value;
            p = p.next;
            return v;
        }
    }
尾部添加
递归遍历

在尾部添加之前,需要先找到链表中最后一个元素,才可以指向下一个元素,完成尾部的添加。

  • 第一个findLast方法首先检查链表是否为空(head == null),如果为空,则返回 null
  • 接下来,它定义了一个指针 p,并将其初始化为链表的头节点(head)。
  • 然后,使用一个循环来遍历链表,直到找到最后一个节点为止。
  • image-20231007231750429
  • 在每次迭代中,它将指针 p 更新为下一个节点(p.next),直到 p.nextnull,即找到了最后一个节点。
  • image-20231007231949361
  • 最后,它返回最后一个节点的引用(p)。
private Node findLast(){
    if(head == null) {
        return null;
    }
    Node p;
    for(p = head; p.next != null; p = p.next) {
        // 遍历链表,直到找到最后一个节点
    }
    return p;
}
  • 第二个addLast方法首先调用 findLast() 方法来查找链表的最后一个节点,并将其赋值给变量 last
  • 然后,它检查链表是否为空(head == null),如果为空,说明链表为空,因此调用 addFirst(value) 方法将新节点作为第一个节点添加到链表中,并立即返回。
  • 如果链表不为空,它将创建一个新的节点,使用给定的值 value 作为节点的值,并将下一个节点的引用设置为 null
  • 然后,它将新节点的 next 指针指向最后一个节点的 next 指针,即将新节点添加到最后一个节点的后面。
public void addLast(int value) {
    Node last = findLast();
    if(head == null) {
        addFirst(value);
        return;
    }
    last.next = new Node(value, null);
    // 将新节点添加到最后一个节点的后面
}
  • 注意,找最后一个节点,终止条件是 curr.next == null
  • 分成两个方法是为了代码清晰,而且 findLast() 之后还能复用

写了一个相对应的测试类进行测试代码是否正确:

 @Test
    public void test_addLast(){
        单向链表 p = new 单向链表();
        p.addFirst(2);
        p.addFirst(4);
        p.addFirst(6);
        p.addFirst(8);
        p.addLast(7);

        for(Integer value : p)
        {
            System.out.println(value);
        }
    }

测试结果如下,p.addLast添加的 7 在整个链表的最尾部。

image-20231007111330785

根据索引获取

查找到具有指定索引的节点,再对应去找到这个节点所对应的值。

寻找节点对象
  • 这个方法接受一个整数参数 index,表示要查找的节点的索引。它首先初始化一个计数器变量 i 为 0。
  • 然后,使用一个循环来遍历链表,从头节点开始,依次访问每个节点。在每次迭代中,它将指针 p 更新为下一个节点(p.next),并将计数器 i 增加 1。
  • 在每次迭代中,它检查计数器 i 是否等于要查找的索引 index
  • 如果相等,说明找到了具有指定索引的节点,它返回该节点的引用(p)。
  • 如果循环结束后仍未找到具有指定索引的节点,说明链表中不存在具有该索引的节点,它返回 null
 private Node findNode(int index)//返回节点对象
    {
        int i = 0;
        for(Node p = head; p != null; p = p.next, i++)
        {
            if(i == index)
            {
                return p;
            }
        }
        return null;
    }
寻找节点的值
  • 这个方法接受一个整数参数 index,表示要查找的节点的索引。
  • 它首先调用 findNode 方法来查找具有指定索引的节点,并将结果存储在变量 node 中。
  • 然后,它检查 node 是否为 null,即是否找到了具有指定索引的节点。
  • 如果 nodenull,说明链表中不存在具有该索引的节点,它抛出一个 IllegalArgumentException 异常,并使用格式化字符串提供有关错误的详细信息。
  • 如果 node 不为 null,则说明找到了具有指定索引的节点,它返回该节点的值(node.Value)。
public int getNode(int index) { //返回节点中的值
        Node node = findNode(index);
        if (node == null) {
            throw new IllegalArgumentException
                    (String.format("索引 [%d] 不合法,无法找到对应的节点", index));
        }
        return node.value;
    }

测试类

@Test
    public void test_getNode(){
        单向链表 p = new 单向链表();
        p.addFirst(2);
        p.addFirst(4);
        p.addFirst(6);
        p.addFirst(8);

        System.out.println("原链表为:");
        for(Integer value : p)
        {
            System.out.println(value);
        }
        System.out.println("---------------");
        int i = p.getNode(2);
        System.out.println("下标为所对应的值为:"+i);
    }

测试结果如下,输入下标 2 后就会返回其所对应的值为 4 .

image-20231007115616510

插入
  • 这个方法接受两个参数:index表示要插入的位置,value表示要插入的值。
  • 首先,它调用findNode方法来查找给定索引的前一个节点,并将结果存储在prev变量中。
  • 如果找不到前一个节点(即prevnull),则抛出一个IllegalArgumentException异常,指示索引不合法。
  • 如果找到了前一个节点,那么将创建一个新的节点,并将其插入到链表中。
  • image-20231007232259874
  • 新节点的值为value,它将作为前一个节点的下一个节点。
  • 通过将新节点的next指针指向前一个节点的下一个节点,实现了节点的插入操作。
  • image-20231007232405565
 public void insert(int index, int value)
    {
        Node prev = findNode(index - 1);
        if(prev == null)
        {
            throw new IllegalArgumentException(
                    String.format("索引 [%d] 不合法,无法找到对应的节点", index));
        }
        prev.next = new Node(value, prev.next);
    }

测试类

@Test
    public void test_insert(){
        单向链表 p = new 单向链表();
        p.addLast(2);
        p.addLast(4);
        p.addLast(6);
        p.addLast(8);

        p.insert(2,5);
        for(Integer value : p)
        {
            System.out.println(value);
        }
    }

测试结果如下:

image-20231007142535216

删除
删除链表中的第一个节点
  • 首先,它会检查链表的头节点是否为空(即head == null),如果为空,则抛出一个IllegalArgumentException异常,表示没有节点可以继续删除。
  • 如果链表不为空,那么它会将头节点指向下一个节点(即head = head.next)。
  • 这样就实现了删除第一个节点的操作。
  • 需要注意的是,在调用该方法之后,原先的第一个节点将不再存在,并且与该节点相关联的其他节点的引用也会发生变化。
public void removeFirst()
    {
        if(head == null)
        {
            throw new IllegalArgumentException(
                    String.format("无节点继续删除"));
        }
        head = head.next;
    }

测试类

@Test
    public void test_removeFirst(){
        单向链表 p = new 单向链表();
        p.addLast(2);
        p.addLast(4);
        p.addLast(6);
        p.addLast(8);

        p.removeFirst();
        for(Integer value : p)
        {
            System.out.println(value);
        }
    }

测试结果如下:

image-20231007144103414

删除某个索引位置中的节点

​ 这个方法接受一个整数参数index,表示要删除节点的位置。首先,它会检查index是否为0,如果是,则调用removeFirst()方法来删除第一个节点,并立即返回。

​ 如果index不为0,那么它会调用findNode(index - 1)方法来查找指定位置的前一个节点,并将结果存储在prev变量中。如果找不到前一个节点(即prevnull),则抛出一个IllegalArgumentException异常,指示无法找到上一个节点。

​ 接下来,它会根据index找到要删除的节点,并将其存储在removed变量中。如果找不到要删除的节点(即removednull),则抛出一个IllegalArgumentException异常,指示无法找到需要删除的节点。

image-20231007232732358

​ 最后,它将前一个节点的next指针指向要删除节点的下一个节点,从而完成了节点的删除操作。需要注意的是,在调用该方法之后,原先指定位置的节点将被移除,并且与该节点相关联的其他节点的引用也会发生变化。

image-20231007232945700

public void remove(int index)
    {
        if(index == 0)
        {
            removeFirst();
            return;
        }

        Node prev = findNode(index - 1);
        if(prev == null)
        {
            throw new IllegalArgumentException(
                    String.format("上一个节点无法找到"));
        }

        Node removed = prev.next;
        if(removed == null)
        {
            throw new IllegalArgumentException(
                    String.format("需要删除的节点无法找到"));
        }
        prev.next = removed.next;
    }

测试类

 @Test
    public void test_remove(){
        单向链表 p = new 单向链表();
        p.addLast(1);
        p.addLast(3);
        p.addLast(5);
        p.addLast(7);

        p.remove(2);
        for(Integer value : p)
        {
            System.out.println(value);
        }
    }

测试结果如下:

image-20231007150331720

3) 单向链表(带哨兵)

观察之前单向链表的实现,发现每个方法内几乎都有判断是不是 head 这样的代码,能不能简化呢?

​ 用一个不参与数据存储的特殊 Node 作为哨兵,它一般被称为哨兵或哑元拥有哨兵节点的链表称为带头链表。

image-20231007233059893

//private Node head = null;//头指针
    //加上哨兵节点后,哨兵的值不那么重要,其下一个指向为空
    private Node head = new Node(666,null);

加入哨兵后,相关代码就可以适当进行修改了。

如:

​ 既然已经有哨兵节点了,就证明其头节点不可能为空。

​ 修改后的代码将直接在找到的最后一个节点后插入新的节点,而不需要先调用findLast()方法来获取最后一个节点。这样做可以减少一次不必要的查找操作,提高代码的效率。

//原代码 
public void addLast(int value) {
        Node last = findLast();
        if (head == null) {
            addFirst(value);
            return;
        }
        last.next = new Node(value, null);
    }

//修改后的代码
public void addLast(int value) {
        Node last = findLast();
        last.next = new Node(value, null);
    }

使用原先未进修改的测试类进行测试:

@Test
    public void test_addLast(){
        单向链表 p = new 单向链表();
        p.addFirst(2);
        p.addFirst(4);
        p.addFirst(6);
        p.addFirst(8);

        for(Integer value : p)
        {
            System.out.println(value);
        }
    }

测试的结果如下。发现它会把哨兵节点的值也一并输出:

image-20231007205506520

然后我们对输出的接口方法修改一下,要求只需要输出不包括哨兵节点的节点值即可。

一旦涉及要遍历,都需要将代码修改成 Node p = head.next;.

private class NodelIterator implements Iterator<Integer> {
        Node p = head.next;// 将其改成 head.next 即可直接遍历哨兵节点后面的节点值了

        @Override
        public boolean hasNext() {//是否有下一个元素
            return p != null;
        }

        @Override
        public Integer next() {//返回当前值,并指向下一个元素
            int v = p.value;
            p = p.next;
            return v;
        }
    }

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

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

相关文章

HTML 笔记:初识 HTML(HTML文本标签、文本列表、嵌入图片、背景色、网页链接)

1 何为HTML 用来描述网页的一种语言超文本标记语言(Hyper Text Markup Language)不是一种编程语言&#xff0c;而是一种标记语言 (markup language) 2 HTML标签 HTML 标签是由尖括号包围的关键词&#xff0c;比如 <html> 作用是为了“标记”页面中的内容&#xff0c;使…

群晖搭建docker系统和办公服务2

首先先确认下我们的Office是否为VOL版&#xff0c;方法如下&#xff08;请您根据自身情况更改以下命令&#xff09;&#xff1a; 管理员身份运行命令提示符&#xff0c;输入 cd C:\Program Files\Microsoft Office\Office16 切换目录 &#xff08;这里请根据您自己的Office版本…

golang gin——中间件编程以及jwt认证和跨域配置中间件案例

中间件编程jwt认证 在不改变原有方法的基础上&#xff0c;添加自己的业务逻辑。相当于grpc中的拦截器一样&#xff0c;在不改变grpc请求的同时&#xff0c;插入自己的业务。 简单例子 func Sum(a, b int) int {return a b }func LoggerMiddleware(in func(a, b int) int) f…

【轻松玩转MacOS】外部设备篇

引言 在开始之前&#xff0c;我们先来了解一下为什么要连接外部设备。想象一下&#xff0c;你正在享受MacOS带来的便捷和高效&#xff0c;突然需要打印一份文件&#xff0c;但你发现打印机无法连接&#xff1b;或者你需要将手机投屏到电脑上&#xff0c;却不知道该如何操作。这…

在模拟器上安装magisk实现Charles抓https包(二)

在上一篇在模拟器上安装magisk实现Charles抓https包&#xff08;一&#xff09;_小小爬虾的博客-CSDN博客&#xff0c;好不容易在模拟器上安装好了Magisk&#xff0c;本篇记录安装movecert模块和AlwaysTrustUserCerts模块。 这两个模块的功能都是将Charles等证书从用户目录转移…

【ARM CoreLink 系列 1 -- SoC 片上互联介绍】

文章目录 概述1.1 片上互连架构的发展1.1.1 BUS 共享总线结构1.1.2 Crossbar 结构1.1.3 Ring 结构1.1.4 Mesh 网格结构 1.2 ARM 总线互联特点小结1.2.1 NOC 总线互联的特点 下篇文章&#xff1a;【ARM CoreLink 系列 1.1 – CoreLink 系列 产品介绍】 概述 在摩尔定律的推动下…

二叉树实现快速查找数字所在区间

背景 通过IP地址查询ip地址所属省市&#xff0c;存在这样的数据 3748137472,3748137727,223.104.10.0,223.104.10.255,中国,江西,,,移动,330000,,城域网 3748137728,3748137983,223.104.11.0,223.104.11.255,中国,陕西,,,移动,710000,,移动网络 3748137984,3748138239,223.104…

1.7 计算机网络体系结构

思维导图&#xff1a; 1.7.1 计算机网络的体系结构的形成 **1.7 计算机网络体系结构** 计算机网络体系结构中&#xff0c;分层的思想为核心。该方法使得复杂的网络设计变得更为简单和可管理。 **1.7.1 计算机网络体系结构的形成** - **计算机网络的复杂性**: 即使是简单的文…

2.4 信道复用技术

前言&#xff1a; 2.4.1 频分复用、时分复用和统计时分复用 **2.4 信道复用技术笔记** --- **1. 定义&#xff1a;** - **复用 (Multiplexing)**&#xff1a;允许多个用户共享一个传输介质&#xff0c;从而提高资源使用效率。 --- **2. 基本复用示意&#xff1a;** - 使用…

SQL进阶 - SQL的编程规范

性能优化是一个很有趣的探索方向&#xff0c;将耗时耗资源的查询优化下来也是一件很有成就感的事情&#xff0c;但既然编程是一种沟通手段&#xff0c;那每一个数据开发者就都有义务保证写出的代码逻辑清晰&#xff0c;具有很好的可读性。 目录 引子 小试牛刀 答案 引言 …

互联网图片安全风控实战训练营开营!

内容安全风控&#xff0c;即针对互联网产生的海量内容的外部、内部风险做宏观到微观的引导和审核&#xff0c;从内容安全领域帮助企业化解监管风险和社会舆论风险&#xff0c;其核心是识别文本、图片、视频、音频中的有害内容。 由于互联网内容类型繁杂、多如牛毛&#xff0c;加…

mysql面试题22:SQL优化的一般步骤是什么,怎么看执行计划(explain),如何理解其中各个字段的含义

该文章专注于面试,面试只要回答关键点即可,不需要对框架有非常深入的回答,如果你想应付面试,是足够了,抓住关键点 面试官:SQL优化的一般步骤是什么,怎么看执行计划(explain),如何理解其中各个字段的含义 SQL优化的一般步骤如下: 分析和理解问题:首先,要确保对问…

Mysql 8手动终止某个事务并释放其持有的锁

示范数据表 age具有index普通索引 在mysql数据库里的information_schema.INNODB_TRX表中存储有innodb的所有事务&#xff0c;我们可以查看该表来查看正在进行的事务 现在我开启一个事务&#xff0c;执行第1、2行SQL&#xff0c;启动事务并持有id3的行锁 刷新事务表可以看到…

JavaScript 笔记 初识JavaScript(变量)

1 打开js文件方法 1.1 方法1&#xff1a;用html打开 第⼀步&#xff1a;把JS程序存储为⼀个扩展名为js的⽂本⽂件 第⼆步&#xff1a;把js⽂件关联到⼀个HTML⽂件上 <!DOCTYPE html> <html><body><script src1.js></script><body> </…

【微服务】七. http客户端Feign

7.1 基于Feign远程调用 RestTimeplate方式调用存在的问题 先来看以前利用RestTemplate发起远程调用的代码&#xff1a; String url "http://userservice/user"order.getUserId(); User user restTemplate.getForObject(url,User.class);存在下面的问题&#xf…

数据产品读书笔记——认识数据产品经理

&#x1f33b;大家可能听说的更多是产品经理这个角色&#xff0c;对数据产品经理可能或多或少了解一些&#xff0c;但又不能准确的描述数据产品经理的主要职能和与其他产品的不同&#xff0c;因此通过读一些书来对数据产品经理有一个准确且全面的认知。 目录 1. 数据的产品分类…

STC89C51基础及项目第10天:LCD显示字符(非标协议外设)

1. 初识LCD1602&#xff08;233.79&#xff09; 非标协议外设 LCD1602显示 LCD1602&#xff08;Liquid Crystal Display&#xff09;是一种工业字符型液晶&#xff0c;能够同时显示 1602 即 32 字符(16列两行) 引脚说明 第 1 脚&#xff1a; VSS 为电源地第 2 脚&#xff1…

vue3前端开发-flex布局篇

文章目录 1.传统布局与flex布局优缺点传统布局flex布局建议 2. flex布局原理2.1 布局原理 3. flex常见属性3.1 父项常见属性3.2 子项常见属性 4.案例实战(携程网首页) 1.传统布局与flex布局优缺点 传统布局 兼容性好布局繁琐局限性&#xff0c;不能再移动端很好的布局 flex布…

【C++入门到精通】C++入门 —— 红黑树(自平衡二叉搜索树)

阅读导航 前言一、红黑树的概念二、红黑树的性质三、红黑树节点的定义四、红黑树结构&#xff08;带有头结点的红黑树&#xff09;五、红黑树的插入操作1. 按照二叉搜索的树规则插入新节点2. 新节点插入后&#xff0c;红黑树的变色以及旋转情况1&#xff1a; cur为红&#xff0…

nginx-proxy反向代理缓存

介绍&#xff1a; 反向代理缓存&#xff0c;类似于动静分离&#xff0c;即通过nginx代理服务器根据客户端发送的url请求&#xff0c;去后台服务器获取数据&#xff0c;将静态数据缓存到nginx代理服务器上&#xff0c;并配置有过期时间&#xff0c;当客户端下次以相同的url请求…