【设计模式】03:单例模式

news2024/11/24 11:00:34

单例模式


OVERVIOW

  • 单例模式
      • 1.单例模式实现
      • 2.饿汉与懒汉
        • (1)饿汉模式
        • (2)懒汉模式
      • 3.懒汉线程安全1
        • (1)引入互斥锁
        • (2)引入双重检查锁定
        • (3)引入原子变量
      • 4.懒汉线程安全2
        • (1)设置局部静态对象
      • 5.简单案例运用
        • (1)任务队列简单实现
        • (2)用户登录

项目全局范围内,某个类的实例有且仅有一个,通过这个实例向其他模块提供数据的全局访问,这种模式就叫单例模式。

单例模式的典型应用就是任务队列。使用单例模式来替代全局变量(对全局变量进行管理),直接使用全局变量会破坏类的封装(全局变量随意读写),通过单例模式的类提供的成员函数进行访问。

单例模式优点:

  1. 提高性能:避免频繁的创建销毁对象,提高性能,
  2. 节省内存空间:在内存中只有一个对象,节省内存空间,
  3. 避免多重占用:避免对共享资源的多重占用,
  4. 全局访问:可全局访问,利用单例模式避免全局变量的出现

单例模式缺点:

  1. 扩展困难:单例模式中没有抽象层,因此扩展困难,

  2. 不适用于变化的对象:如果同类型的对象总是要在不同的用例场景发生变化,单例就会引起数据错误,不能保存状态。

  3. 职责过重:违背了单一职责原则

  4. 负面问题:

    为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多,而出现连接池溢出。

    如果实例化的单例对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

单利模式使用场景:

  1. 需要频繁实例化然后销毁的对象
  2. 创建对象耗时过多or消耗资源过多,但又经常使用到的对象,
  3. 有状态的工具类对象,
  4. 频繁访问数据库或文件的对象,
  5. 要求只有一个对象的场景

1.单例模式实现

如果使用单例模式,首先要保证这个类的实例有且仅有一个。因此就必须采取一些操作,涉及一个类多对象操作的函数有以下几个:

  • 构造函数:创建一个新的对象
  • 拷贝构造函数:根据已有对象拷贝出一个新的对象
  • 拷贝赋值操作符重载函数:两个对象之间的赋值

为了把一个类可以实例化多个对象的路堵死,需要对以上几个函数做如下处理:

  1. 构造函数私有化,在类内部只调用一次这是可控的。
    • 由于类外部不能使用构造函数,所以在类内部创建的唯一的对象必须是静态的,这样就可以通过类名来访问了,为了不破坏类的封装把这个静态对象设置为私有。
    • 在类中只有它的静态成员函数才能访问其静态成员变量,所以给这个单例类提供一个静态函数用于得到这个静态的单例对象。
  2. 拷贝构造函数私有化或者禁用(使用 = delete)
  3. 拷贝赋值操作符重载函数私有化或者禁用(从单例的语义上讲该函数已经毫无意义,所以在类中不再提供这样一个函数,故将它也一并处理)

单例模式就是给类创建一个唯一的实例对象,UML类图如下:

在这里插入图片描述

#include<iostream>
using namespace std;
/*
   	1.关于类创建后的默认提供的函数
  	 - 在创建一个新的类之后 会默认提供3个构造函数 1个析构函数
  	 - 2个操作符重载(移动赋值操作符重载、拷贝赋值操作符重载)移动构造函数 拷贝构造函数
  	2.关于单例模式下类的实例化
	 - 在通过将 无参构造函数、拷贝构造函数、拷贝赋值操作符重载函数禁用之后 TaskQueue类已经无法在外部创建任何的对象
	 - 要得到TaskQueue的实例无法通过new操作符得到 只能通过类名得到(需要将对象设置为静态对象)
	 - 通过类名访问类内部的属性和方法 其属性和方法一定是静态的(若不是静态需要通过对象来调用)
	 - 能够操作静态成员变量的函数 只有静态成员函数
*/

//单例模式任务队列
class TaskQueue {
public:
	//无参构造函数
	//TaskQueue() = delete;
	//拷贝构造函数
	TaskQueue(const TaskQueue &t) = delete;
	//赋值操作符重载函数
	TaskQueue& operator=(const TaskQueue &t) = delete;
	// = delete 代表函数禁用, 也可以将其访问权限设置为私有
	
	//静态成员公共函数用于获取实例
	static TaskQueue *getInstance() { return m_taskq; }

	void printTest() { cout << "i am a public method of a singleton class" << endl; }

private:
	//无参构造函数
	TaskQueue() = default;
	//拷贝构造函数
	//TaskQueue(const TaskQueue &t) = default;
	//赋值操作符重载函数
	//TaskQueue& operator=(const TaskQueue &t) = default;
	//通过类名访问静态属性或方法来创建类实例(需要在类外部做初始化处理)
	static TaskQueue *m_taskq;
};

//静态成员初始化放到类外部处理
TaskQueue* TaskQueue::m_taskq = new TaskQueue;

int main() {
	//获取TaskQueue的单例对象 由m_taskq指针指向
	TaskQueue* m_taskq = TaskQueue::getInstance();
	//由m_taskq指针调用单例类内部的成员方法
	m_taskq->printTest();
	return 0;
}

以上为单例模式中的饿汉模式,在定义单例类的时候就将类对应的单例对象一并创建出来了。

2.饿汉与懒汉

在实现一个单例模式的类的时候,有两种处理模式:

  • 饿汉模式:在将单例类定义出来后实例就已经存在了,饿汉模式没有线程安全问题
  • 懒汉模式:在使用单例对象的时候才会去创建单例对象的实例(节省内存空间),懒汉模式存在线程安全问题(多个线程同时访问单例的实例)
(1)饿汉模式
  1. 多个线程在访问单例对象时,没有线程安全问题,单例对象已经存在,不会出现多个线程创建出多个单例对象的情况。
  2. 多线程拿到单例对象后,在访问单例对象内部的数据时,有线程安全问题(多线程共享资源),
//饿汉模式
class TaskQueue {
public:
	TaskQueue(const TaskQueue &t) = delete;
	TaskQueue& operator=(const TaskQueue &t) = delete;
	static TaskQueue *getInstance() { return m_taskq; }
	void printTest() { cout << "i am a public method of a singleton class" << endl; }

private:
	TaskQueue() = default;
	static TaskQueue *m_taskq;
};

TaskQueue* TaskQueue::m_taskq = new TaskQueue;
//饿汉模式
class TaskQueue {
public:
	TaskQueue(const TaskQueue &t) = delete;
	TaskQueue& operator=(const TaskQueue &t) = delete;
	static TaskQueue *getInstance() { return &m_taskq; }
	void printTest() { cout << "i am a public method of a singleton class" << endl; }

private:
	TaskQueue() = default;
	static TaskQueue m_taskq;//已经创建对象
};

TaskQueue* TaskQueue::m_taskq;//改为对象声明
(2)懒汉模式
//懒汉模式
class TaskQueue {
public:
	TaskQueue(const TaskQueue &t) = delete;
	TaskQueue& operator=(const TaskQueue &t) = delete;
	static TaskQueue *getInstance() {
		if (m_taskq == nullptr) m_taskq = new TaskQueue;
		return m_taskq;
	}
	void printTest() { cout << "i am a public method of a singleton class" << endl; }

private:
	TaskQueue() = default;
	static TaskQueue *m_taskq;
};

TaskQueue* TaskQueue::m_taskq = nullptr;

3.懒汉线程安全1

在单例模式中饿汉模式下,针对在多线程中可能存在的线程安全问题(创建多个实例),进行问题修改:

(1)引入互斥锁

在多线程环境下,有可能的情况是:多个线程同时进入到getInstance()方法中的if语句判断中,这时对象就可能被同时创建多个,

//懒汉模式 引入互斥锁
class TaskQueue {
public:
	TaskQueue(const TaskQueue &t) = delete;
	TaskQueue& operator=(const TaskQueue &t) = delete;
	static TaskQueue *getInstance() {
		m_mutex.lock();
		if (m_taskq == nullptr) m_taskq = new TaskQueue;
		m_mutex.unlock();
		return m_taskq;
	}
	void printTest() { cout << "i am a public method of a singleton class" << endl; }

private:
	TaskQueue() = default;
	static TaskQueue *m_taskq;
	static mutex m_mutex;
};

mutex TaskQueue::m_mutex;
TaskQueue* TaskQueue::m_taskq = nullptr;

使用互斥锁对new操作创建实例时进行加锁操作,防止同时创建多个实例,但是程序执行的效率太低(多线程访问单例对象时都是顺序访问)

(2)引入双重检查锁定

双重检查锁定,只有第一次访问时是顺序执行的,在TaskQueue被实例化出来之后,其他线程再去访问单例对象就是并行的了(不会进入if内)。

//懒汉模式 引入双重检查锁定
class TaskQueue {
public:
	TaskQueue(const TaskQueue &t) = delete;
	TaskQueue& operator=(const TaskQueue &t) = delete;
	static TaskQueue *getInstance() {
		//双重检查锁定
		if (m_taskq == nullptr) {
			m_mutex.lock();
			if (m_taskq == nullptr) m_taskq = new TaskQueue;
			m_mutex.unlock();	
		}
		return m_taskq;
	}
	void printTest() { cout << "i am a public method of a singleton class" << endl; }

private:
	TaskQueue() = default;
	static TaskQueue *m_taskq;
	static mutex m_mutex;
};

mutex TaskQueue::m_mutex;
TaskQueue* TaskQueue::m_taskq = nullptr;
(3)引入原子变量

通过引入双重检查锁定的方式,解决了在懒汉模式下多线程访问单例对象时,出现的线程安全问题,

从表面上观察引入双重检查锁定的方式是十分完美的,但是从底层上依旧存在漏洞:

  1. 对于 m_taskq = new TaskQueue; 操作,其对应的机器指令并不是一条,而有三条(对于计算机来说代码都是二进制指令/机器指令),

    step1:创建一块内存(没有数据)
    step2:创建 TaskQueue 类型的对象,并将数据写入到对象中
    step3:为 m_taskq 对象指针初始化,将有效的内存地址传递给 m_taskq 对象指针
    
  2. 在实际的执行过程中,m_taskq = new TaskQueue; 对应的机器指令可能会被重新排序,成为

    step1:创建一块内存(没有数据)
    step3:为 m_taskq 对象指针初始化,将有效的内存地址传递给 m_taskq 对象指针
    step2:创建 TaskQueue 类型的对象,并将数据写入到对象中
    
  3. 如果线程A执行完成前两步之后失去CPU时间片被挂起,此时线程B在进行指针判断时,发现指针 m_taskq 不为空(但该指针指向内存没有被初始化),导致线程B使用了一个没有被初始化的队列对象,就会出现问题(出现问题是概率性的)

  4. 在C++11中引入原子变量 atomic,在底层控制机器指令的执行顺序,可以实现一种更加安全的懒汉模式,代码如下:

    使用原子变量 atomicstore() 方法来存储单例对象,使用 load() 方法来加载单例对象,

    在原子变量中这两个函数在处理指令的时候,默认的原子顺序是 memory_order_seq_cst 顺序原子操作,

    使用顺序约束原子操作库,整个函数的执行都将保证顺序执行,并且不会出现数据竞态 data races,

    缺点:使用这种方法实现的懒汉模式的单例执行效率更低一些,

    对代码进行以下修改:

    • 通过原子变量将类的实例对象保存起来(m_taskq 指针指向的内存)

    • 类外初始化 指针指向为nullptr

    • 对 getInstance 方法进行相关的修改操作

      多线层在调用 getInstance 方法时 需要从原子变量中加载任务队列的实例

      抢到互斥锁的线程将继续向下执行 创建实例对象

//懒汉模式 引入原子变量
class TaskQueue {
public:
	TaskQueue(const TaskQueue &t) = delete;
	TaskQueue& operator=(const TaskQueue &t) = delete;
	static TaskQueue *getInstance() {
		TaskQueue* taskq = m_taskq.load();
		if (taskq == nullptr) {
			m_mutex.lock();
			taskq = m_taskq.load();
			if (taskq == nullptr) {
				taskq = new TaskQueue;
				m_taskq.store(taskq);
			}
			m_mutex.unlock();
		}
		return m_taskq.load();
	}
	void printTest() { cout << "i am a public method of a singleton class" << endl; }

private:
	TaskQueue() = default;
	// static TaskQueue *m_taskq;
	static atomic<TaskQueue*> m_taskq;
	static mutex m_mutex;
	
};

mutex TaskQueue::m_mutex;
atomic<TaskQueue*> TaskQueue::m_taskq;
// TaskQueue* TaskQueue::m_taskq = nullptr;

4.懒汉线程安全2

在懒汉模式线程安全问题中,除了可以通过引入双重检查锁定来解决线程安全问题,还可以使用局部静态对象处理线程安全问题,

(1)设置局部静态对象

使用静态的局部对象解决线程安全问题,要求编译器必修支持C++11标准,

  1. getInstance() 局部函数中定义一个静态局部对象 static TaskQueue taskq; (调用无参构造初始化)
  2. 在C++11标准中规定,如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化,

注:使用静态的局部对象没有线程安全问题,已经由C++11标准中的编译器解决,未被初始化的变量,必须等待其完成初始化才能并发执行,

step1:创建一块内存(没有数据)
step2:创建 TaskQueue 类型的对象,并将数据写入到对象中(完成初始化操作)
step3:为 m_taskq 对象指针初始化,将有效的内存地址传递给 m_taskq 对象指针
// 懒汉模式 静态局部对象
class TaskQueue {
public:
	TaskQueue(const TaskQueue &t) = delete;
	TaskQueue& operator=(const TaskQueue &t) = delete;
	static TaskQueue* getInstance() {
		static TaskQueue taskq;
		return &taskq;
	}
	void printTest() { cout << "i am a public method of a singleton class" << endl; }
private:
	TaskQueue() = default;
};

5.简单案例运用

(1)任务队列简单实现
  1. 多线程拿到单例对象后,在访问单例对象内部的数据时,有线程安全问题(多线程共享资源),使用互斥锁保护多线程中共享的资源,

  2. C++11中给互斥锁加/解锁有两种方式,

    方法1:调用mutex对象的 unlock(); lock(); 方法

    方法2:使用lock_gurd自动管理加/解锁 lock_guard<mutex> locker(m_mutex);

    使用 lock_gurd 可以有效的避免死锁的问题,自动加/解锁

// 饿汉模式
class TaskQueue {
public:
	TaskQueue(const TaskQueue &t) = delete;
	TaskQueue& operator=(const TaskQueue &t) = delete;
	static TaskQueue *getInstance() { return m_taskq; }
    void printTest() { cout << "i am a public method of a singleton class" << endl; }
    // 判断任务队列是否为空
    bool isEmpty() {
        lock_guard<mutex> locker(m_mutex);
        return m_data.empty();
    }
    // 添加任务
    void addTask(int node) { 
        lock_guard<mutex> locker(m_mutex);
        m_data.push(node);
    }
    // 删除任务
    bool removeTask() {
        lock_guard<mutex> locker(m_mutex);
        if (m_data.empty()) return false;
        m_data.pop();
        return true;
    }
    // 获取队头任务
    int takeTask() {
        lock_guard<mutex> locker(m_mutex);
        if (m_data.empty()) return -1;
        return m_data.front();
    }
private:
    TaskQueue() = default;
	static TaskQueue *m_taskq;
    // 任务队列
    queue<int> m_data;
    mutex m_mutex;
};

TaskQueue* TaskQueue::m_taskq = new TaskQueue;
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
using namespace std;

int main() {
    // 获取单例对象
    TaskQueue *taskq = TaskQueue::getInstance();
    taskq->printTest();
    // 生产者线程
    // 使用匿名函数指定线程的处理动作
    thread t1([=](){
        for (int i = 0; i < 25; ++i) {
            taskq->addTask(i + 100);
            cout << "++push data:" << i + 100 << ", threadId = " << this_thread::get_id() << endl;
            this_thread::sleep_for(chrono::milliseconds(500));//休眠500ms
        }
    });
    // 消费者线程
    thread t2([=](){
        this_thread::sleep_for(chrono::milliseconds(100));
        while(!taskq->isEmpty()) {
            // 开始消费
            cout << "--take data:" << taskq->takeTask() << ", threadId = " << this_thread::get_id() << endl;
            taskq->removeTask();
            this_thread::sleep_for(chrono::milliseconds(1000));//休眠500ms
        }
    });
    // 主线程阻塞 只有当t1、t2线程都结束后 主线程解除阻塞
    t1.join();
    t2.join();
    return 0;
}

在这里插入图片描述

(2)用户登录

当用户成功登录之后,用户名和密码就会被存储到内存中,可以创建一个单例类,将用户数据保存到单例对象中,

class Test {
public:
	static Test* getInstance() { return &m_test; }
    // m_user
    void setUserName(QString name) {
        // 多线程下需要加锁解锁(涉及写操作)
        // lock();
        m_user = name;
        // unlock();
    }
    QString getUserName(){ return m_user; }
    // m_passwd
    // ....
    // ....
    // ....
private:
	Test();
	Test(const Test& t);
	static Test* m_test;
    // static Test m_test;
    // 定义变量 -> 属于唯一的单例对象
    QString m_user;
    QString m_passwd;
    QString m_ip;
    QString m_port;
    QString m_token;
}
Test* Test::m_test = new Test();	// 初始化
// Test Test::m_test;

tips:部分内容参考课程、书籍与网络等,题解、图示及代码内容根据老师课程、二次整理以及自己对知识的理解,进行整理和补充,仅供学习参考使用,不可商业化。

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

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

相关文章

RWA+AI 叙事下的 ProsperEx,对 Web3 时代交易的重新定义

RWA&#xff08;Real World Assets&#xff09;即现实资产代币&#xff0c;其本质在于将现实世界中具有货币价值的东西转化为数字代币&#xff0c;使其可以在区块链上表现价值并进行交易。RWA 资产既可以包括有形资产&#xff0c;例如房产、珠宝、黄金等&#xff0c;也可以包无…

第一节HarmonyOS DevEcoStudio工具下载以及环境搭建

一、下载与安装DevEco Studio 在HarmonyOS应用开发学习之前&#xff0c;需要进行一些准备工作&#xff0c;首先需要完成开发工具DevEco Studio的下载与安装以及环境配置。 进入DevEco Studio 工具下载官网&#xff1a;https://developer.harmonyos.com/cn/develop/deveco-stu…

OpenCvSharp从入门到实践-(04)色彩空间

目录 1、GRAY色彩空间 2、从BGR色彩空间转换到GRAY色彩空间 2.1色彩空间转换码 2.2实例 BGR色彩空间转换到GRAY色彩空间 3、HSV色彩空间 4、从BGR色彩空间转换到HSV色彩空间 4.1色彩空间转换码 4.2实例 BGR色彩空间转换到HSV色彩空间 1、GRAY色彩空间 GRAY色彩空间通常…

上游任务和下游任务

起源多任务学习中的定义理解结合定义分析例子示例 1&#xff1a;计算机视觉示例 2&#xff1a;自然语言处理示例 3&#xff1a;语音处理示例 4&#xff1a;强化学习总结 起源 "上游任务"和"下游任务"这两个术语在深度学习领域中通常用来描述一种 多任务学…

适用于iOS 的顶级苹果数据恢复软件

数据丢失可能随时发生在任何人身上&#xff0c;这可能是一种令人沮丧的经历。丢失 iOS 设备上的重要数据可能会造成特别严重的损失&#xff0c;因为其中可能包括有价值的照片、联系人、消息和其他重要文件。幸运的是&#xff0c;有多种数据恢复工具可以帮助用户恢复丢失的数据。…

Java数据结构与算法----字符串匹配(KMP算法)

KMP算法简介 是一种线性时间复杂度的字符串匹配、查找算法。 暴力实现字符串匹配 对于字符串的匹配&#xff0c;可以使用暴力进行匹配&#xff1a; 如图进行演示:&#xff08;以a串 ABABABCAA 被b串 ABABC 匹配为例&#xff09;&#xff1a; 第一轮匹配&#xff1a;&#x…

Linux静态库,共享库,计算机基础知识

1.库文件: 1).库文件库是一组预先编译好的方法的集合;Linux系统存储库的位置一般在/lib 和 /usr/lib (64位系统/usr/lib64)库的头文件放在/usr/include 2).库的分类 静态库:libxxx.a(命名规则) 共享库:libxxx.so(命名规则) 3).准备文件: //add.c int add(int x,int y) { retu…

前端学习系列之html

目录 初识html 发展史 优势 W3C 标准 地址 格式 网页基本标签 标题标签 段落标签 换行标签 水平线标签 字体样式 注释和特殊符号 特殊符号 图像、超链接 图像 常见图像格式 格式 超链接 格式 重要属性 href&#xff1a;规定链接指向的页面的 URL target…

线上问题整理-ConcurrentModificationException异常

项目场景&#xff1a; 商品改价&#xff1a;商品改价中通过多线程批量处理经过 Lists.partition拆分的集合对象 问题描述 商品改价中通过多线程批量处理经过 Lists.partition拆分的集合对象&#xff0c;发现偶尔会报 java.util.ConcurrentModificationException: nullat jav…

Vue3 + Scss 实现主题切换效果

Vue3 Scss 实现主题切换效果 先给大家看一下主题切换的效果&#xff1a; 像这样的效果实现起来并不难&#xff0c;只是比较麻烦&#xff0c;目前我知道的有两种方式可以实现&#xff0c;分别是 CSS 变量、样式文件切换&#xff0c;下面是该效果的核心实现方法 CSS变量 给…

使用Docker compose方式安装Spug,并结合内网穿透实现远程访问

文章目录 前言1. Docker安装Spug2 . 本地访问测试3. Linux 安装cpolar4. 配置Spug公网访问地址5. 公网远程访问Spug管理界面6. 固定Spug公网地址 前言 Spug 面向中小型企业设计的轻量级无 Agent 的自动化运维平台&#xff0c;整合了主机管理、主机批量执行、主机在线终端、文件…

【数据结构初阶】单链表

各位读者老爷好&#xff0c;鼠鼠我又来了哈。鼠鼠我呀现在来基于C语言实现以下单链表&#xff0c;希望对你有所帮助&#xff01; 目录 1.链表的概念及结构 2.链表的分类 3.无头单向非循环链表的实现 3.1.单链表打印 3.2.单链表尾插 3.3.单链表头插 3.4.单链表尾删 3.5…

Idea空白目录自动折叠的问题

IDEA创建空白项目和文件夹会自动折叠的问题。 有时文件项目会自动折叠&#xff0c;折叠后&#xff0c;不仅不好找项目和文件&#xff0c;还容易造成特别低端的错误。 如图&#xff1a; 当我们要在example目录下创建文件时&#xff0c;很容易就在springgaopdemo下创建了。 因为…

正则表达式 通配符 awk文本处理工具

目录 什么是正则表达式 概念 正则表达式的结构 正则表达式的组成 元字符 元字符点&#xff08;.&#xff09; 代表字符. 点值表示点需要转义 \ r..t 代表r到t之间任意两个字符 过滤出小写 过滤出非小写 space空格 [[:space:]] 表示次数 位置锚定 例&#xff1a…

笔记十九*、选中高亮和嵌套路由使用

19.1 选中高亮 NavLink App.jsx import React from "react"; import {NavLink, useRoutes} from "react-router-dom"; import routes from "./routes/index.jsx"; import "./app.css"const App () > {const element useRoutes(…

“文件批量改名专家:轻松自定义重命名并智能导出文件信息“

在日常工作中&#xff0c;处理大量文件时&#xff0c;往往需要一款得力的文件批量改名工具来协助我们高效、有序地进行文件管理。今天&#xff0c;我要向大家介绍一款强大的文件批量改名工具&#xff0c;它不仅支持统一自定义重命名&#xff0c;还能将相关信息导出到表格中&…

一、Oceanbase基础

一、集群相关概念 集群&#xff1a;整个分布式数据库。Region&#xff1a;表示区域&#xff0c;是地域的逻辑概念&#xff0c;如1个城市&#xff0c;1个集群可以有多个Region&#xff0c;用于跨城市远 距离容灾。Zone&#xff1a;表示分区&#xff0c;是机房或机架的逻辑概念…

[PyTorch][chapter 1][李宏毅深度学习-AI 简介]

前言&#xff1a; 李宏毅深度学习从2017-2023的系列课程总结 内容 章节 强化学习 11 李宏毅机器学习 【2017】 40 李宏毅机器学习深度学习(完整版)国语 【2020】 119 李宏毅大佬的深度学习与机器学【2022】 90 李宏毅机器学习完整课程【2023】 43 总结 303 目录…

lenovo联想笔记本YogaPro 14s IRP8D 2023款(83BU)原装出厂Windows11预装OEM系统

链接&#xff1a;https://pan.baidu.com/s/1s7PcN-y8RyHSV7uJQzC5OQ?pwddy9y 提取码&#xff1a;dy9y 联想电脑原厂W11系统&#xff0c;自带所有驱动、出厂主题壁纸、系统属性专属LOGO标志、Office办公软件、联想电脑管家等预装程序 所需要工具&#xff1a;16GB或以上的U盘…

百度手机浏览器关键词排名优化——提升关键词排名 开源百度小程序源码系统 附带完整的搭建教程

百度作为国内领先的搜索引擎&#xff0c;一直致力于为用户提供最优质的信息服务。在移动互联网时代&#xff0c;手机浏览器成为了用户获取信息的主要渠道。而小程序作为轻量级的应用程序&#xff0c;具有即用即走、无需下载等优势&#xff0c;越来越受到用户的青睐。然而&#…