当前位置: 首页 > news >正文

Linux - POSIX信号量,基于环形队列的生产者消费者模型

信号量

在Linux下,POSIX信号量是一种线程同步机制,用于控制多个线程之间的访问顺序。POSIX信号量可以用于实现线程之间的互斥或者同步。


在之前的阻塞队列生产者消费者模型中,阻塞队列是一个共享资源,不管是生产者还是消费者,任何时刻只允许一个线程访问共享资源,所以对阻塞队列进行了加锁保护,使阻塞队列成为为临界资源。在这里,是将整个共享资源(阻塞队列)当作一个整体使用的。

信号量使用思路:一个共享资源,不当作整体使用,将其划分为若干个小的共享资源,使得不同的执行流访问不同的小共享资源,本质上,当不同执行流访问不同的小共享资源时,不需要进行加锁保护,可以并发执行。而如果两个执行流访问到了同一个小的共享资源时,再进行互斥保护共享资源。


POSIX信号量提供了两种类型的信号量:二进制信号量和计数信号量。二进制信号量只有两个取值:0和1,它用于表示资源是否被占用。计数信号量可以有多个取值,它表示可用资源的数量。

二进制信号量可以充当互斥锁的作用。因为信号量的初始值为1,p相当于加锁,v相当于解锁。

计数信号量的值通常被称为“资源计数器”,它记录了当前可用的共享资源数量。当一个线程需要使用共享资源时,它会尝试获取计数信号量,如果当前可用资源数量大于0,则线程可以获取资源并将计数信号量的值减1。如果当前可用资源数量为0,则表示共享资源已被占用,线程需要等待直到有资源可用。

POSIX信号量接口

       int sem_init(sem_t *sem, int pshared, unsigned int value); // 初始化int sem_destroy(sem_t *sem);    // 销毁int sem_wait(sem_t *sem);       // 申请信号量,P操作int sem_post(sem_t *sem);       // 释放信号量,V操作

因为信号量本质就是一个计数器,记录着当前的资源数目,其中初始化时的value值就表示初始时的资源数目。

在使用POSIX信号量时,线程可以通过调用sem_wait()函数来请求获取信号量,如果当前信号量的值大于0,则线程可以继续执行,同时信号量的值会减1;否则线程将被阻塞,等待信号量的值变为大于0。当线程释放资源时,可以通过调用sem_post()函数来释放信号量,使得其它线程可以获取资源。

基于环形队列的生产者消费者模型

1.缓冲区的改变

基于环形队列的生产者消费者模型,对比之前的基于阻塞队列的生产者消费者模型,只是缓冲区的具体数据结构变了。两个角色:生产者,消费者。三种关系:生产者与生产者:互斥。消费者与消费者:互斥。生产者与消费者:互斥与同步。

如上图,环节队列是逻辑抽象结构,底层的物理结构是数组。要想达到环形队列的效果,只需在访问数组的最后一个元素的下一个元素时将下标 %= size即可。再者,环形队列有一个判空和判满的问题,因为这里的容量是有上限的,从数据结构的角度来说,一般会有一个start和end下标,当start == end时为空,而当数据为满时,不能也将start == end标定为满,因为这样空和满的判断条件就相同了。解决方法:1. 空出一个位置,当(end + 1) % size == start时,判定为满。 2. 加一个计数器,当计数器 == size时为满,计数器 == 0时为空。

但是,因为信号量的作用,在环形队列生产消费模型中,我们不需要考虑判空判满的问题。见下文。

2. 结合生产者消费者

先考虑单生产者,单消费者的环形队列生产消费模型,有以下前提:

  1. 通过给生产者线程和消费者线程分别设定一个下标,从而标定它们当前生产和消费的下标位置。

  1. 我们不需要通过空出一个位置或者添加计数器的方式来避免队列为空和为满时生产消费线程的下标相同这样的判定条件重复。因为有信号量的存在。

  1. 因此,队列为空和为满时,生产者线程的下标一定等于消费者线程的下标。一种情况是当前缓冲区中没有数据,一种是当前缓冲区中数据已满。

目的期望:

  1. 当生产线程和消费线程指向了环形队列的同一个位置时(此时队列为空or为满),也就是访问了同一个共享资源,此时需要进行多线程互斥与同步。

  1. 当生产线程和消费线程不指向环形队列的同一个位置时,此时不需要进行互斥与同步,可以并发执行,因为没有访问同一个共享资源。

  1. 当环形队列为空时,必须让生产者先执行,消费者必须等待生产者生产完至少一个数据。这也就是第一点中的互斥与同步。

  1. 当环形队列为满时,必须让消费者先执行,生产者必须等待消费者消费完至少一个数据。这也就是第一点中的互斥与同步。

  1. 当环形队列不为空不为满时,生产线程与消费线程并发执行。

上方5点,其实345和12是一个原则。34对应1,5对应2。

3.结合信号量

信号量表征的是共享资源的数量,在环形队列生产消费模型中,设定两个信号量,分别表征数据资源数目和空间资源数目。生产者关注空间资源信号量,初始为N。消费者关注数据资源信号量,初始为0。

按照上面生产者和消费者的执行逻辑来看,每次在生产或者消费之前必须先申请信号量,申请信号量的本质是一种对资源的预定机制。若信号量大于0,则--信号量,继续执行。若为0,则阻塞等待信号量被其他线程释放。

这样一来,起始时,或当其他环形队列为空时,dataSem为0,消费者没有数据资源可以申请,故必须阻塞等待。这也就是我们期望的,当生产和消费的下标相等时,且队列为空时,必须让生产者先执行,消费者阻塞等待。

当环形队列为满时,spaceSem为0,dataSem为N,生产者没有空间资源可以申请,故必须阻塞等待。这也就是我们期望的,当生产和消费下标相等时,且队列为满时,必须让消费者先执行,生产线程必须阻塞等待。

(注意,上方两种互斥情况下,只有当另一方生产/消费完,执行完V操作之后,阻塞方才能P成功,这也就实现了互斥和同步

当环形队列不为空时,生产和消费的下标不同,此时并没有访问同一个资源,故可以并发执行。

4.代码...

ringQueue.hpp

#ifndef _RINGQUEUE_HPP_
#define _RINGQUEUE_HPP_#include <vector>
#include "sem.hpp"
#include "mutex.hpp"const int g_default_size = 5;template <class T>
class RingQueue
{
public:RingQueue(int size = g_default_size): ring_queue_(size), size_(size), space_sem_(size), data_sem_(0), p_index_(0), c_index_(0), p_mutex_(), c_mutex_(){}~RingQueue() = default;void push(const T &in){// 生产者: 环形队列的push的操作space_sem_.p(); // 空间资源的p操作p_mutex_.lock();ring_queue_[p_index_++] = in;p_index_ %= size_;p_mutex_.unlock();data_sem_.v(); // 数据资源v操作}void pop(T *out){data_sem_.p(); // 数据资源的p操作,消费者进行。若没有数据资源,则阻塞挂起消费者线程c_mutex_.lock();*out = ring_queue_[c_index_++];c_index_ %= size_;c_mutex_.unlock();space_sem_.v(); // 空间资源v操作}private:std::vector<T> ring_queue_; // 使用vector模拟环形队列int size_;                  // 环形队列的大小int p_index_;   // 生产者下标int c_index_;   // 消费者下标Sem space_sem_; // 空间资源信号量,生产者使用Sem data_sem_;  // 数据资源信号量,消费者使用Mutex p_mutex_;Mutex c_mutex_;
};// 自己写的:环形队列利用互斥锁+条件变量进行。
// template <class T>
// class RingQueue
// {
// public:
//     RingQueue(int size = g_default_size)
//     : ring_queue_(size)
//     , size_(size)
//     , c_index_(0), p_index_(0)
//     , c_mutex_(), p_mutex_()
//     {
//         pthread_cond_init(&not_empty_, nullptr);
//         pthread_cond_init(&not_full_, nullptr);
//     }
//     ~RingQueue()
//     {
//         pthread_cond_destroy(&not_empty_);
//         pthread_cond_destroy(&not_full_);
//     }
//     void push(const T &in)
//     {
//         // 生产者
//         p_mutex_.lock();  // 加锁//         while((p_index_ + 1) % size_ == c_index_)
//         {
//             // 满的
//             pthread_cond_wait(&not_full_, &p_mutex_.mtx_);
//         }
//         // 不满了
//         ring_queue_[p_index_++] = in;
//         p_index_ %= size_;
//         pthread_cond_signal(&not_empty_);//         p_mutex_.unlock(); 
//     }
//     void pop(T *out)
//     {
//         // 消费者
//         c_mutex_.lock();//         while(c_index_ == p_index_)  // 为空,没有数据资源,等待条件变量。
//         {
//             pthread_cond_wait(&not_empty_, &c_mutex_.mtx_);
//         }
//         *out = ring_queue_[c_index_++];
//         c_index_ %= size_;
//         pthread_cond_signal(&not_full_);//         c_mutex_.unlock();
//     }
// private:
//     std::vector<T> ring_queue_;  // 环形队列
//     int size_;
//     int c_index_;
//     int p_index_;
//     Mutex c_mutex_;
//     Mutex p_mutex_;
//     pthread_cond_t not_empty_;
//     pthread_cond_t not_full_;
// };#endif

testMain.cc

#include "ringQueue.hpp"
#include <pthread.h>
#include <iostream>
#include <unistd.h>
using namespace std;// 消费者
void *consumer(void *args)
{RingQueue<int> *rq = (RingQueue<int> *)args; // 生产消费的缓冲区:环形队列while (true){sleep(1);int x;rq->pop(&x);cout << "消费:" << x << " [" << pthread_self() << "]" << endl;}pthread_exit(nullptr);
}// 生产者
void *producer(void *args)
{RingQueue<int> *rq = (RingQueue<int> *)args;while (true){int x = rand() % 100 + 1;rq->push(x);cout << "生产:" << x << " [" << pthread_self() << "]" << endl;}pthread_exit(nullptr);
}// int main()
// {
//     srand((unsigned int)time(nullptr));//     // RingQueue<int>* ring_queue = new RingQueue<int>();
//     RingQueue<int> ringQueue;
//     pthread_t c[2], p[3];     // 多生产多消费,3生产2消费//     for(int i = 0; i < 2; ++i)
//         pthread_create(c + i, nullptr, consumer, &ringQueue);
//     for(int i = 0; i < 3; ++i)
//         pthread_create(p + i, nullptr, producer, &ringQueue);//     for(int i = 0; i < 2; ++i)
//         pthread_join(c[i], nullptr);
//     for(int i = 0; i < 3; ++i)
//         pthread_join(p[i], nullptr);
//     return 0;
// }
int main()
{srand((unsigned int)time(nullptr));RingQueue<int> ringQueue;pthread_t c, p;pthread_create(&c, nullptr, consumer, &ringQueue);pthread_create(&p, nullptr, producer, &ringQueue);pthread_join(c, nullptr);pthread_join(p, nullptr);return 0;
}// 二元信号量充当互斥锁// int tickets = 10000;
// pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;// void *getTickets(void *args)
// {
//     Sem *sem = (Sem *)args;
//     while (true)
//     {
//         sem->p(); // 申请信号量,原子的,只有一个线程会申请成功
//         // pthread_mutex_lock(&mtx);
//         if (tickets > 0)
//         {
//             usleep(1000); // 1ms,模拟抢票过程
//             cout << "tickets : " << tickets << " [" << pthread_self() << "]" << endl;
//             tickets--;
//             sem->v();
//             // pthread_mutex_unlock(&mtx);
//         }
//         else
//         {
//             sem->v();
//             // pthread_mutex_unlock(&mtx);
//             break;
//         }
//     }
//     pthread_exit(nullptr);
// }// int main()
// {
//     pthread_t tids[4];
//     Sem sem(1);
//     for (int i = 0; i < 4; ++i)
//     {
//         pthread_create(tids + i, nullptr, getTickets, &sem);
//     }//     for (int i = 0; i < 4; ++i)
//     {
//         pthread_join(tids[i], nullptr);
//     }
//     return 0;
// }

sem.hpp

#ifndef _SEM_HPP_
#define _SEM_HPP_#include <semaphore.h>// 信号量的封装
class Sem
{
public:Sem(int value){sem_init(&sem_, 0, value);}~Sem(){sem_destroy(&sem_); }void p(){sem_wait(&sem_);}void v(){sem_post(&sem_);}
private:sem_t sem_;
};#endif

5.多生产多消费

多生产多消费时,任何一个生产者和一个消费者之间都已经通过信号量达到了应有的互斥与同步,现在需要考虑的是生产生产之间和消费消费之间的互斥关系。

生产生产之间,消费消费之间,需要保护的共享资源为下标:c_index_或者p_index_,环形队列:ring_queue_(STL:vector,不是线程安全的),故,需要在push和pop内部,进行加锁,构建临界区,保护临界资源。

因为这里生产和消费之间的互斥,已经在当下标相等时,为空或为满时,由信号量维护了,所以这里用两把锁,分别维护生产生产之间和消费消费之间。(见上方代码实现)

6.基于环形队列的生产消费模型为什么效率高,优势?

我们可以看到,多个生产之间,在对环形队列push时,内部是互斥的,也就是串型执行的。多个消费之间,在对环形队列pop时,内部是互斥的,也是串行执行的。对比阻塞队列的生产消费模型来说,改进就是,当缓冲区,环形队列不为空且不为满时,生产和消费的push和pop可以并发执行。

还是那句话,生产消费的过程并非只是pushpop的过程(放数据和拿数据的过程),还有生产者放数据之前生产数据和消费者拿数据之后处理数据的过程(这里才是最耗时间的)。多生产多消费的意义是,可以让生产数据和处理数据时多个线程并发执行,从而提高效率。 (和基于阻塞队列的一样)

7.信号量的理解

信号量的本质就是一个计数器,表示当前资源数目。计数器的意义是什么呢?

在之前使用互斥锁,条件变量的使用场景下,常规流程是:申请锁->判断临界资源是否就绪->就绪则访问,不就绪则在条件变量下等待->访问临界资源->释放锁。

而信号量这个计数器的意义就是:可以不进入临界区就得知资源情况,减少了临界区内的资源是否就绪的判断。本来,信号量就是一种资源预定机制,当P操作成功时,就代表着此时有一个资源可以供你获取/访问(比如环形队列中的空间资源/数据资源)。

相关文章:

Linux - POSIX信号量,基于环形队列的生产者消费者模型

信号量在Linux下&#xff0c;POSIX信号量是一种线程同步机制&#xff0c;用于控制多个线程之间的访问顺序。POSIX信号量可以用于实现线程之间的互斥或者同步。在之前的阻塞队列生产者消费者模型中&#xff0c;阻塞队列是一个共享资源&#xff0c;不管是生产者还是消费者&#x…...

学习Flask之七、大型应用架构

学习Flask之七、大型应用架构 尽管存放在单一脚本的小型网络应用很方便&#xff0c;但是这种应用不能很好的放大。随着应用变得复杂&#xff0c;维护一个大的源文件会出现问题。不像别的网络应用&#xff0c;Flask没有强制的大型项目组织结构。构建应用的方法完全留给开发者。…...

CentOS9下编译FFMPEG源码

克隆...

炼石:八年饮冰难凉热血,初心如磐百炼成钢

炼石成立八周年 八载笃行&#xff0c;踔厉奋发。创立于2015年的炼石&#xff0c;今天迎来了八岁生日&#xff0c;全体员工共同举行了温暖又充满仪式感的周年庆典。过去的2022&#xff0c;是三年疫情的艰难“收官之年”&#xff0c;新的2023&#xff0c;将是数据安全行业成为独…...

Python基本数据类型

Python有六种基本数据类型Number&#xff08;数字&#xff09;String&#xff08;字符串&#xff09; List&#xff08;列表&#xff09; Tuple&#xff08;元组&#xff09; Set&#xff08;集合&#xff09;Dictionary&#xff08;字典&#xff09;String&#xff08;字符串&…...

【MySQL进阶】 锁

&#x1f60a;&#x1f60a;作者简介&#x1f60a;&#x1f60a; &#xff1a; 大家好&#xff0c;我是南瓜籽&#xff0c;一个在校大二学生&#xff0c;我将会持续分享Java相关知识。 &#x1f389;&#x1f389;个人主页&#x1f389;&#x1f389; &#xff1a; 南瓜籽的主页…...

javascript高级程序设计第四版读书笔记-第五章 基本引用类型

19.如何创建一个指定的本地时间&#xff1f; Dete只能接收时间戳&#xff0c;有两种方法可以将字符串参数变为时间戳,他们是Date隐式调用的&#xff0c; 分别是Date.parse() 创建的是GTM时间&#xff0c;Date.UTC()创建的是本地时间 Date.UTC()方法也返回日期的毫秒表示&#x…...

《爆肝整理》保姆级系列教程python接口自动化(二十一)--unittest简介(详解)

简介 前边的随笔主要介绍的requests模块的有关知识个内容&#xff0c;接下来看一下python的单元测试框架unittest。熟悉 或者了解java 的小伙伴应该都清楚常见的单元测试框架 Junit 和 TestNG&#xff0c;这个招聘的需求上也是经常见到的。python 里面也有单元 测试框架-unitt…...

【C++的OpenCV】第四课-OpenCV图像常用操作(一):Mat对象深化学习、灰度、ROI

我们开始图像处理的基本操作的了解一、图像对象本身的加深学习1.1 Mat对象和ROI1.1.1 创建一个明确的Mat对象1.1.2 感兴趣的区域ROI二、图像的灰度处理2.1 概念2.2 cvtColor()函数2.3 示例一、图像对象本身的加深学习 1.1 Mat对象和ROI 这是一个技术经验的浅尝&#xff0c;所以…...

Propargyl-PEG1-SS-PEG1-PFP ester,1817735-30-0,炔基应用于生物标记

【中文名称】丙炔-单乙二醇-二硫键-单乙二醇-五氟苯酚酯【英文名称】 Propargyl-PEG1-SS-PEG1-PFP ester【结 构 式】【CAS号】1817735-30-0【分子式】C16H15F5O4S2【分子量】430.4【基团部分】炔基基团【纯度标准】95%【包装规格】1g&#xff0c;5g&#xff0c;10g&#xff0c…...

产品运营︱用户活跃度低的解决方法

app用户活跃度低&#xff0c;产品拉新变现效率慢&#xff0c;这是运营app时难免会遇到的情况。要想解决这类问题&#xff0c;就要从可能的原因下手&#xff0c;进行产品的优化改进&#xff0c;记录下改变后的关键数据变化&#xff0c;定期做好复盘工作进行调整。 一、app用户量…...

【华为OD机试模拟题】用 C++ 实现 - 求最大数字

最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…...

吉卜力风格水彩画怎么画?

著名的水彩艺术家陈坚曾说&#xff1a;“水彩是用水润调和形成的饱和度极高的艺术画面&#xff0c;在纸上晕染的画面面积、强度等具有许多随意性&#xff0c;天空的颜色乌云密布&#xff0c;都是很随意的&#xff0c;难以模仿。” 是的&#xff0c;水彩画的妙处就在于不确定的…...

Python的类变量和对象变量声明解析

Python的类变量和对象变量声明解析 原文链接&#xff1a;https://www.cnblogs.com/bwangel23/p/4330268.html Python的类和C一样&#xff0c;也都是存在两种类型的变量&#xff0c;类变量和对象变量&#xff01;前者由类拥有&#xff0c;被所有对象共享&#xff0c;后者由每个…...

#笨鸟先飞 猴博士电路笔记 第一篇 电路基础

第零课 基础知识串联与并联电源电势与电位差第一课 电阻电路的等效变换电压源串联电流源并联电压源和电流源串联电压源和电流源并联电压源转化为电流源电流源转化为电压源Δ-Y等效变换第二课 基尔霍夫定律基尔霍夫电流定律任一结点上流出电流之和等于流入电流之和。受控电流源&…...

快捷式~node.js环境搭建

1、安装包官网下载&#xff1a;Node.js (nodejs.org) 2、安装完成后修改环境变量 在上面已经完成了 node.js 的安装&#xff0c;即使不进行此步骤的环境变量配置也不影响node.js的使用 但是&#xff0c;若不进行环境变量配置&#xff0c;那么在使用命令安装 node.js全局模块 …...

ZooKeeper实现分布式队列、分布式锁和选举详解

提示&#xff1a;本文章非原创&#xff0c;记录一下优秀的干货。 [原创参考]&#xff1a;https://blog.csdn.net/qq_40378034/article/details/117014648 前言 ZooKeeper源码的zookeeper-recipes目录下提供了分布式队列、分布式锁和选举的实现GitHub地址。 本文主要对这几种实…...

【swift】swift quick start

一、常量和变量 常量let&#xff0c;变量var 也可以用于确定数组和字典的不可变和可变 二、数据类型&#xff1a; Int&#xff1a;整数类型&#xff0c;可表示有符号整数或无符号整数&#xff0c;分别使用Int和UInt表示。 Float&#xff1a;单精度浮点数类型&#xff0c;用于…...

浅谈volatile关键字

文章目录1.保证内存可见性2.可见性验证3.原子性验证4.原子性问题解决5.禁止指令重排序6.JMM谈谈你的理解6.1.基本概念6.2.JMM同步规定6.2.1.可见性6.2.2.原子性6.2.3.有序性6.3.Volatile针对指令重排做了啥7.你在哪些地方用过Volatile&#xff1f;volatile是Java提供的轻量级的…...

10 种 Spring事务失效场景

10 种 Spring事务失效场景 1.概述 Spring针对Java Transaction API (JTA)、JDBC、Hibernate和Java Persistence API(JPA)等事务 API&#xff0c;实现了一致的编程模型&#xff0c;而Spring的声明式事务功能更是提供了极其方便的事务配置方式&#xff0c;配合Spring Boot的自动…...

重读《DOOM启世录》

许多游戏开发者都是网瘾少年&#xff0c;抱着对游戏的热爱进入游戏行业&#xff0c;在经历996的加班加点&#xff0c;买房的压力&#xff0c;浮躁同样跟随着我们&#xff0c;我们是否还热爱着自己的事业&#xff0c;我们不是天才&#xff0c;也成不了卡马克&#xff0c;但是我们…...

巧用性格上的差异来组建团队

你好&#xff0c;我是得物 App 交易平台及中间件平台的 Team Leader Alan。 组建团队过程中&#xff0c;你有没有遇到过类似的场景&#xff1a;团队中某些人之间总是互相不对付、气场不合&#xff0c;不管是日常沟通中还是方案对齐&#xff0c;总是会出现面红耳赤的场面。 从…...

Leetcode11. 盛最多水的容器

一、题目描述&#xff1a; 给定一个长度为 nnn 的整数数组 heightheightheight 。有 nnn 条垂线&#xff0c;第 iii 条线的两个端点是 (i,0)(i, 0)(i,0) 和 (i,height[i])(i, height[i])(i,height[i]) 。 找出其中的两条线&#xff0c;使得它们与 xxx 轴共同构成的容器可以容…...

Java笔记026-集合/数组、Collection接口、ArrayList、Vector、LinkedList

集合集合的理解和好处保存多个数据使用的是数组&#xff0c;分析数组的弊端数组1、长度开始必须指定&#xff0c;而且一旦指定&#xff0c;不能更改2、保存的必须为同一类型的元素3、使用数组进行增加/删除元素的示意代码-比较麻烦Person数组扩容示意代码Person[] pers new Pe…...

Hive学习——分桶抽样、侧视图与炸裂函数搭配、hive实现WordCount

目录 一、分桶抽样 1.抽取表中10%的数据 2.抽取表中30%的数据 3.取第一行 4.取第10行 5.数据块抽样 6.tablesample详解 二、UDTF——表生成函数 1.explode()——炸裂函数 2.posexpolde()——只能对array进行炸裂 3.inline()——炸裂结构体数组 三、UDTF与侧视图的搭…...

大数据算法

1. TOP K 算法 有10个⽂件&#xff0c;每个⽂件1G&#xff0c;每个⽂件的每⼀⾏存放的都是⽤户的 query&#xff0c;每个⽂件的 query 都可能重复。要求你按照 query 的频度排序。 方法1&#xff1a; 顺序读取10个⽂件&#xff0c;按照 hash(query)%10 的结果将 query 写⼊到…...

非暴力沟通读书笔记

浅读《非暴力沟通》&#xff0c;本书对于沟通的方式总结成了一个方法论&#xff0c;从13个章节去概述非暴力沟通的方法和重点。其中最重要的是非暴力沟通四要素&#xff0c;观察、感受、需要、请求。同时在沟通中注意观察&#xff0c;投入爱&#xff0c;重视倾听的力量&#xf…...

代码随想录【Day21】| 530. 二叉搜索树的最小绝对差、501. 二叉搜索树中的众数、236. 二叉树的最近公共祖先

530. 二叉搜索树的最小绝对差 题目链接 题目描述&#xff1a; 给你一棵所有节点为非负值的二叉搜索树&#xff0c;请你计算树中任意两节点的差的绝对值的最小值。 示例&#xff1a; 提示&#xff1a;树中至少有 2 个节点。 难点&#xff1a; 解答错误&#xff01;仅考虑了…...

注意啦,面试通过后,别忘了教师资格证认定

所有要「教师资格证认定」教程的宝子们看过来面试合格的小伙伴都可以进行认定工作 . 认定时间 查询各省份认定公告&#xff0c;确定认定时间范围。以下是公告汇总网址&#xff08;https://www.jszg.edu.cn/portal/qualification_cert/dynamics?id21691&#xff09; 认定次数 每…...

【LeetCode】No.154. 寻找旋转排序数组中的最小值 II -- Java Version

题目链接&#xff1a;https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array-ii/ 1. 题目介绍&#xff08;154. 寻找旋转排序数组中的最小值 II&#xff09; 已知一个长度为 n 的数组&#xff0c;预先按照升序排列&#xff0c;经由 1 到 n 次 旋转 后&#xff0…...