二叉树进阶 --- 中
目录
1. find 的递归实现
2. insert 的递归实现
3. erase 的递归实现
3.1. 被删除的节点右孩子为空
3.2. 被删除的节点左孩子为空
3.3. 被删除的节点左右孩子都不为空
4. 析构函数的实现
5. copy constructor的实现
6. 赋值运算符重载
7. 搜索二叉树的完整实现
1. find 的递归实现
find的递归实现较为简单,思路是:根据当前节点的 key 与传入的 key 作比较:
- 如果前者大于后者,那么当前节点往左子树走;
- 如果前者小于后者,那么当前节点往右子树走;
- 如果两者相等,返回true;
- 走到空,返回false。
代码实现:
// 对外提供的
bool find_recursion(const T& key)
{return _find_recursion(_root, key);
}
// 类中私有的
bool _find_recursion(Node* root, const T& key)
{if (root == nullptr)return false;else{if (root->_key < key)return _find_recursion(root->_right, key);else if (root->_key > key)return _find_recursion(root->_left, key);elsereturn true;}
}
2. insert 的递归实现
insert分为两个过程
- 第一个过程:找到合适位置;
- 第二个过程:构建新节点,并完成连接关系。
假如现在我们已经得到了合适的插入位置,那么如何连接呢?
例如,如下图所示:我们要插入13这个数据,现在的关键问题是,如何将15和13这两个节点连接起来呢? 具体如下:
第一种方法:调用函数时,将父亲节点即这里的15也传进来。找到合适位置,创建节点并连接。
但是我们在这里提出一个较好玩的玩法,利用引用传参,如下所示:
// 对外提供的
bool insert_recursion(const T& key)
{ return _insert_recursion(_root, key);
}
// 类中私有的
bool _insert_recursion(Node*& root, const T& key)
{if (root == nullptr){// 走到空, 说明找到了目标位置, 需要构建新节点, 并完成连接关系// 在这里, 我们用上图解释:// root就是15这个节点的左孩子的引用,即root就是15的左孩子// 给root new了一个node(key),等价于插入了这个节点,并连接了起来.root = new Node(key);return true;}else{if (root->_key < key)return _insert_recursion(root->_right, key);else if (root->_key > key)return _insert_recursion(root->_left, key);elsereturn false;}
}
3. erase 的递归实现
对于erase的递归实现,其实也可以分为两个过程:
- 第一个过程:找到这个要删除的特殊节点;
- 第二个过程:可以分为三种情况(左孩子为空、右孩子为空、左右孩子都不为空),根据不同情况进行删除。
假设我们现在已经得到了要删除节点的位置,该如何删除呢?
3.1. 被删除的节点右孩子为空
如图所示:我们要删除6号节点(其右孩子为空),该如何删除:
由于 root 是4的右孩子的引用,且 root 的右孩子为空,那么root = root->_left,就可以将4的右孩子由6变更为5,我们在删除6即可,因此我们需要提前保存6节点,当指向变更之后,delete 6。
3.2. 被删除的节点左孩子为空
如图所示:我们要删除15号节点(其左孩子为空),该如何删除:
由于 root 是8的右孩子的引用,且 root 没有左孩子,那么我们此时只需要更改 root 即可,让 root 到它的右孩子 (root = root->_right),等价于将8连接了19,当然我们也需要提前将 root 节点进行保存,更改指向后,在释放 root 节点即可。
3.3. 被删除的节点左右孩子都不为空
较为复杂的就是第三种情况了,由于此时被删除节点有两个孩子,因此无法像上面两种情况进行处理。此时我们还是要利用循环实现的思路:
- (1):从被删除的节点开始,先找到左子树的最大节点or右子树的最小节点(我在这里称之为"合适节点");
- (2):交换这个"合适结点"和被删除节点的key;
- (3):将删除原节点转化为删除我们后找的这个"合适节点"。
在这里我们用实例说明,如下图所示:如果我要删除下图中的4,该如何删除?
我在这里实现的"合适节点"是: 左子树的最大(右)节点
相信前两个过程是没有困难的,最后一步可能不好实现,但是当我们经过了前两个过程,我们发现被删除节点变成了我们找到的"合适节点",而且这个"合适节点"很有特征,如果它是左子树的最大值,那么它一定不会有右子树,反之,如果他是右子树的最小节点,那么它一定不会有左子树。因此我们可以在递归一次,如果"合适节点"是左子树的最大节点,那么我们递归树的左子树即可,反之如果是右子树的最小节点,那么我们递归树的右子树即可。
代码如下:
// 对外提供的
bool erase_recursion(const T& key)
{return _erase_recursion(_root, key);
}
// 类中私有的
bool _erase_recursion(Node*& root, const T& key)
{if (!root)return false;else{// 如果当前节点的key > 目标key,那么递归它的左子树即可if (root->_key > key)return _erase_recursion(root->_left, key);// 如果当前节点的key < 目标key,那么递归它的右子树即可else if (root->_key < key)return _erase_recursion(root->_right, key);// 如果找到了,进行删除else{// 此时的root就是要删除的节点Node* del = root;// a. 左子树为空if (root->_left == nullptr)root = root->_right;// b. 右子树为空else if (root->_right == nullptr)root = root->_left;// c. 左右子树都不为空else{// 左子树的最右节点Node* left_max = root->_left;while (left_max->_right)left_max = left_max->_right;// 交换"合适节点"和"被删除节点"的keystd::swap(left_max->_key, root->_key);// 在这里递归左子树即可return _erase_recursion(root->_left, key);}delete del;del = nullptr;return true;}}
}
4. 析构函数的实现
析构函数的实现我们依据的是后序的思想(LRN),先析构左子树、然后是右子树、最后才是根。这种实现的原因是是少了许多的记录信息,例如在这里我们就不用记录下一个节点。因为我们释放的就是当前的叶子节点。
具体实现如下:
~BinarySearchTree()
{_BSTDestroy(_root);
}
// 注意我们这里传递的是根的引用
void _BSTDestroy(Node*& root)
{if (root == nullptr)return;else{// 依据后序的思想_BSTDestroy(root->_left);_BSTDestroy(root->_right);delete root;root = nullptr;}
}
5. copy constructor的实现
老生常谈的问题,如果我们没有显示实现拷贝构造函数,那么编译器默认生成的拷贝构造会对内置类型按照字节序的方式进行拷贝,对自定义类型成员属性会去调用它的拷贝构造函数。而字节序的方式进行拷贝会带来两个问题:
- 其一,其中一个对象的修改会影响另一个对象;
- 其二,同一空间会被析构两次,进程crash。
因此,我们在这里必须要实现深拷贝,那如何实现呢?我们可以借助前序的思想(NLR)。从根节点开始进行构造节点,然后递归构造它的左子树和右子树。注意构造的时候需要它们的连接关系。
代码如下:
BinarySearchTree(const BinarySearchTree<T>& copy)
{_root = _creat_new_root(copy._root);
}
Node* _creat_new_root(Node* root)
{// 如果遇到空了,就不用构造了if (root == nullptr)return nullptr;else{// 根据前序的思想(NLR),依次构造它的根、左子树、右子树 // 同时将它们连接起来Node* new_root = new Node(root->_key);new_root->_left = _creat_new_root(root->_left);new_root->_right = _creat_new_root(root->_right);return new_root;}
}
6. 赋值运算符重载
赋值运算符重载就比较简单了,因为我们已经实现了copy constructor,在这里利用传值传参会进行拷贝构造的特性实现我们的赋值
代码如下:
// 传值传参会进行拷贝构造
BinarySearchTree<T>& operator=(BinarySearchTree<T> copy)
{std::swap(_root, copy._root);return *this;
}
7. 搜索二叉树的完整实现
代码如下:
#ifndef _BINARY_SEARCH_TREE_HPP_
#define _BINARY_SEARCH_TREE_HPP_#include <iostream>namespace Xq
{template<class T>struct BinarySearchTreeNode{BinarySearchTreeNode<T>* _left;BinarySearchTreeNode<T>* _right;T _key;BinarySearchTreeNode(const T& key) :_key(key), _left(nullptr), _right(nullptr) {}};template<class T>class BinarySearchTree{private:typedef BinarySearchTreeNode<T> Node;public:BinarySearchTree(Node* root = nullptr) :_root(root) {}bool insert(const T& key){// 1. 如果是空树,直接对_root赋值即可,插入成功并返回trueif (_root == nullptr){_root = new Node(key);return true;}else{// step 1: 先找目标位置Node* cur = _root;// 为了更好的连接新节点, 因此记录父节点Node* parent = nullptr;while (cur){// 如果当前节点的Key大于目标Key// 当前节点应该向左子树走if (cur->_key > key){parent = cur;cur = cur->_left;}// 如果当前节点的Key小于目标Key// 当前节点应该向右子树走else if (cur->_key < key){parent = cur;cur = cur->_right;}else{// 找到了相同的 key, 在这里不插入return false;}}// cur 走到了空, 即 cur 就是合适的位置cur = new Node(key);// 我们需要判断cur是parent的左节点还是右节点// 如果key小于parent的key,那么插入左节点if (key < parent->_key)parent->_left = cur;// 反之连接到右节点elseparent->_right = cur;return true;}}bool find(const T& key){// 1. 从根节点开始Node* cur = _root;while (cur){// 2. 如果当前关键字大于目标关键字,那么向左子树走if (cur->_key > key)cur = cur->_left;// 3. 如果小于目标关键字,那么向右子树走else if (cur->_key < key)cur = cur->_right;// 4. 相等,就返回trueelsereturn true;}// 5. 循环结束,说明没找到, 返回falsereturn false;}bool erase(const T& key){// 先找要删除的节点Node* del = _root;Node* del_parent = nullptr;while (del){if (del->_key < key){del_parent = del;del = del->_right;}else if (del->_key > key){del_parent = del;del = del->_left;}else{// 锁定了要删除的节点// 分三种情况:// case 1: 左子树为空if (del->_left == nullptr){// 如果要删除的节点是根if (del == _root){Node* newroot = del->_right;delete _root;_root = newroot;}else{// 托孤法删除if (del_parent->_left == del)del_parent->_left = del->_right;elsedel_parent->_right = del->_right;delete del;del = nullptr;}}// case 2: 右子树为空else if (del->_right == nullptr){if (_root == del){Node* newroot = del->_left;delete _root;_root = newroot;}else{if (del_parent->_left == del)del_parent->_left = del->_left;elsedel_parent->_right = del->_left;delete del;del = nullptr;}}// case 3: 左右子树都不为空else{// 从被删除节点开始, 找右子树的最小(左)节点 || 找左子树的最大(右)节点if (del->_right)_erase_right_min_node(del);else_erase_left_max_node(del);}return true;}}return false;}bool find_recursion(const T& key){return _find_recursion(_root, key);}bool insert_recursion(const T& key){return _insert_recursion(_root, key);}bool erase_recursion(const T& key){return _erase_recursion(_root, key);}~BinarySearchTree(){_BSTDestroy(_root);}BinarySearchTree(const BinarySearchTree<T>& copy){_root = _creat_new_root(copy._root);}// 传值传参会进行拷贝构造BinarySearchTree<T>& operator=(BinarySearchTree<T> copy){std::swap(_root, copy._root);return *this;}void InOrder(){_InOrder(_root);std::cout << std::endl;}private:void _InOrder(Node* root){if (root){_InOrder(root->_left);std::cout << root->_key << " ";_InOrder(root->_right);}}bool _find_recursion(Node* root, const T& key){if (root == nullptr)return false;else{if (root->_key < key)return _find_recursion(root->_right, key);else if (root->_key > key)_find_recursion(root->_left, key);elsereturn true;}}bool _insert_recursion(Node*& root, const T& key){if (root == nullptr){root = new Node(key);return true;}else{if (root->_key < key)return _insert_recursion(root->_right, key);else if (root->_key > key)return _insert_recursion(root->_left, key);elsereturn false;}}void _erase_right_min_node(Node* del){// 从被删除结点开始, 找右子树的最小(左)节点Node* right_min = del->_right;// 并记录这个节点的父亲节点, 让其从del开始Node* right_min_parent = del;while (right_min->_left){right_min_parent = right_min;right_min = right_min->_left;}// 交换这个节点和要删除节点的 keystd::swap(del->_key, right_min->_key);// 将删除 del 转化为删除 right_min (托孤法删除)if (right_min_parent->_left == right_min)right_min_parent->_left = right_min->_right;elseright_min_parent->_right = right_min->_right;delete right_min;right_min = nullptr;}void _erase_left_max_node(Node* del){// 从被删除节点开始, 找左子树的最大(右)节点Node* left_max = del->_left;// 并记录这个节点的父亲节点, 让其从del开始Node* left_max_parent = del;while (left_max->_right){left_max_parent = left_max;left_max = left_max->_right;}// 交换这个节点和要删除节点的 keystd::swap(del->_key, left_max->_key);// 将删除 del 转化为删除 left_max (托孤法删除)if (left_max_parent->_left == left_max)left_max_parent->_left = left_max->_left;elseleft_max_parent->_right = left_max->_left;delete left_max;left_max = nullptr;}bool _erase_recursion(Node*& root, const T& key){if (!root)return false;else{// 如果当前节点的key > 目标key,那么递归它的左子树即可if (root->_key > key)return _erase_recursion(root->_left, key);// 如果当前节点的key < 目标key,那么递归它的右子树即可else if (root->_key < key)return _erase_recursion(root->_right, key);// 如果找到了,进行删除else{// 此时的root就是要删除的节点Node* del = root;// a. 左子树为空if (root->_left == nullptr)root = root->_right;// b. 右子树为空else if (root->_right == nullptr)root = root->_left;// c. 左右子树都不为空else{// 左子树的最右节点Node* left_max = root->_left;while (left_max->_right)left_max = left_max->_right;// 交换"合适节点"和"被删除节点"的keystd::swap(left_max->_key, root->_key);// 在这里递归左子树即可return _erase_recursion(root->_left, key);}delete del;del = nullptr;return true;}}}Node* _creat_new_root(Node* root){// 如果遇到空了,就不用构造了if (root == nullptr)return nullptr;else{// 根据前序的思想(NLR),依次构造它的根、左子树、右子树 // 同时将它们连接起来Node* new_root = new Node(root->_key);new_root->_left = _creat_new_root(root->_left);new_root->_right = _creat_new_root(root->_right);return new_root;}}// 注意我们这里传递的是根的引用void _BSTDestroy(Node*& root){if (root == nullptr)return;else{// 依据后序的思想_BSTDestroy(root->_left);_BSTDestroy(root->_right);delete root;root = nullptr;}}private:Node* _root;};
}#endif
二叉树进阶 --- 中,结束。
相关文章:

二叉树进阶 --- 中
目录 1. find 的递归实现 2. insert 的递归实现 3. erase 的递归实现 3.1. 被删除的节点右孩子为空 3.2. 被删除的节点左孩子为空 3.3. 被删除的节点左右孩子都不为空 4. 析构函数的实现 5. copy constructor的实现 6. 赋值运算符重载 7. 搜索二叉树的完整实现 1. fi…...

ChatGPT DALL-E绘图,制作各种表情包,实现穿衣风格的自由切换
DALL-E绘图功能探索: 1、保持人物形象一致,适配更多的表情、动作 2、改变穿衣风格 3、小女孩的不同年龄段展示 4、不同社交平台的个性头像创作 如果不会写代码,可以问GPT。使用地址:我的GPT4 视频,B站会发&#…...

程序环境和预处理、编译链接过程、编译的几个阶段、运行环境、预定义符号等的介绍
文章目录 前言一、程序的翻译环境和执行环境二、编译链接过程三、编译的几个阶段四、运行环境五、预定义符号总结 前言 程序环境和预处理、编译链接过程、编译的几个阶段、运行环境、预定义符号的介绍。 一、程序的翻译环境和执行环境 在 ANSI C 的任何一种实现中,…...
MySQL导入导出详细教程
导出 语法 mysqldump [OPTIONS] database [tables] mysqldump [OPTIONS] --databases [OPTIONS] DB1 [DB2 DB3...] mysqldump [OPTIONS] --all-databases [OPTIONS]导出所有数据库 mysqldump -uroot -proot --all-databases >/tmp/all.sql导出db1、db2两个数据库的所有数…...
STM32F103学习笔记 | 8. 二,八,十,十六进制表示方式
文章目录 进制基本信息参考文献 进制基本信息 C语言中的表示,前缀加0表示八进制数,前缀加0x表示十六进制数 基数数码名称描述代码和书本中的表示举例20 和 1二进制逢二进一,几乎所有的电子计算机内部都使用二进位制,分别为“0”…...

ROS2 工作空间
文章目录 ROS2 工作空间创建工作空间自动安装依赖编译工作空间设置环境变量参考链接 ROS2 工作空间 工作空间可以简单理解为工程目录。 ROS 系统中一个典型的工作空间结构如图所示: dev_ws: 根目录,里面会有四个子目录(子空间&a…...

基于CCS5.5的双音多频(DTMF)信号检测仿真实验(①检测型音频文件②输入生成音频并检测)
DTMF的优点 我们知道,DTMF根本上仍然是频谱分析,基础还是DFT,但DFT通常需要对一整段数据做变换,而DTMF不同,每输入一个采样点就计算一次,更有利于硬件实现。 基于CCS的双音多频(DTMF)信号检测原理 公式详细推导 详细的公式推导在下面这篇博客中已经进行了详细的描述,…...

Postgresql中JIT函数能否inline的依据function_inlinable
相关 《Postgresql源码(128)深入分析JIT中的函数内联llvm_inline》 《LLVM的ThinLTO编译优化技术在Postgresql中的应用》 前置阅读:《Postgresql源码(128)深入分析JIT中的函数内联llvm_inline》 在JIT inline函数的过…...
存储过程作为初始化数据例子
查询出每个人员,为每个人员插入11条数据,作为初始化数据 oracle存储过程 CREATE OR REPLACE PROCEDURE initialize_order_warn_config ISv_id NUMBER; BEGINSELECT COALESCE(MAX(id), 0) INTO v_id FROM order_warn_config;FOR rec IN (SELECT DISTINCT…...

【数据分析】 JupyterNotebook安装及使用简介
各位大佬好 ,这里是阿川的博客 , 祝您变得更强 个人主页:在线OJ的阿川 大佬的支持和鼓励,将是我成长路上最大的动力 阿川水平有限,如有错误,欢迎大佬指正 在数据分析中,一般用Pycharm编辑代…...
nginx命令大全
以下是Nginx的一些常用命令,适用于Linux环境,部分命令在Windows系统下也适用,但命令形式可能有所不同: 查看Nginx版本号: nginx -v:简短显示版本号。nginx -V:详细显示版本号及编译配置信息。 启动Nginx…...

【数据结构】顺序表与链表的差异
顺序表和链表都是线性表,它们有着相似的部分,但是同时也有着很大的差异。 存储空间上的差异: 对于插入上的不同点,顺序表在空间不够时需要扩容,而如果在使用realloc函数去扩容,会有原地扩容和异地扩容两种情…...

小程序如何进行评分评价
小程序以其便捷、快速、无需安装的特点,成为了众多企业、品牌与消费者之间的重要连接桥梁。而评价评分机制,作为小程序中不可或缺的一环,对于提升用户体验、建立用户信任、促进商家与用户的互动等方面,都具有至关重要的意义。本文…...

【MATLAB源码-第206期】基于matlab的差分进化算法(DE)机器人栅格路径规划,输出做短路径图和适应度曲线。
操作环境: MATLAB 2022a 1、算法描述 差分进化算法(Differential Evolution, DE)是一种有效的实数编码的进化算法,主要用于解决实值函数的全局优化问题。本文将详细介绍差分进化算法的背景、原理、操作步骤、参数选择以及实际应…...
Python图形界面(GUI)Tkinter笔记(三):控件的定位(1)
Tkinter(GUI)设计图形界面时有三种控件的包装方法去定位各控件在窗口(父容器、根窗口)上的位置。 【1】pack()方法:用方位来定位位置,类似于Word文档中的文字对齐方式。 【2】grid()方法:用二维表格式的坐标值定位,类似于EXCEL单位元。 【3】place()方法:用窗口的像…...

数据结构--单链表 详解(附代码
目录: 1:链表的概念及结构 2:实现单链表 3:常见疑问 解答 (看到最后!!) 一:链表的概念及结构 1.1 概念: 链表是⼀种 物理存储结构上非连续、非顺序的 存储结…...
leetcode 1749.任意子数组和的绝对值的最大值
思路:dp 说到绝对值,大家肯定不陌生,但是用在dp上就会使问题变得稍微复杂一些了。 我们在最大子数组和的那道题中知道,在状态转移的时候,我们会舍弃掉为负数的连续部分,重新构建连续的子串。但是…...

Linux进程——进程地址空间
前言:在讲完环境变量后,相信大家对Linux有更进一步的认识,而Linux进程概念到这也快接近尾声了,现在我们了解Linux进程中的地址空间! 本篇主要内容: 了解程序地址空间 理解进程地址空间 探究页表和虚拟地址空…...

基于 LlaMA 3 + LangGraph 在windows本地部署大模型 (三)
基于 LlaMA 3 LangGraph 在windows本地部署大模型 (三) 大家继续看 https://lilianweng.github.io/posts/2023-06-23-agent/的文档内容 第二部分:内存 记忆的类型 记忆可以定义为用于获取、存储、保留以及随后检索信息的过程。人脑中有多…...

python3如何安装bs4
在python官网找到beautifulsoup模块的下载页面,点击"downloap"将该模块的安装包下载到本地。 将该安装包解压,然后在打开cmd,并通过cmd进入到该安装包解压后的文件夹目录下。 在该文件目录下输入"python install setup.py&quo…...

构建 MCP 服务器:第一部分 — 资源入门
什么是模型上下文协议? 模型上下文协议(MCP) 是Claude等大型语言模型 (LLM) 与外部数据和功能安全交互的标准化方式。您可以将其想象成一个平视显示器,或者 AI 的 USB 端口——它提供了一个通用接口,允许任何兼容 MCP 的 LLM 连接到您的数据和工具。 MCP 提供了一个集中式协…...

2025前端微服务 - 无界 的实战应用
遇饮酒时须饮酒,得高歌处且高歌 文章目录 什么是前端微服务主流框架概述无界 - 腾讯乾坤 - 阿里Micro-app Vue3项目引用⑴. 项目依赖安装⑵. main.ts 文件配置⑶. 路由配置⑷. 页面设置 隐藏子应用菜单及顶部信息栏子应用样式冲突问题虚拟路由⑴. 路由⑵. 页面 跨域…...
业务到解决方案构想
解决方案构想的核心理解 解决方案构想是连接业务需求与技术实现的关键桥梁,从您描述的内容和我的理解,这个阶段的核心点包括: 核心要点解读 转化视角:将业务视角的需求转变为解决方案视角 业务能力探索阶段识别了"做什么&q…...
Web 架构相关文章目录(持续更新中)
文章目录 目录结构总结 目录结构 序号标题链接1Web 架构之数据库开发规范Web 架构之数据库开发规范2Web 架构之状态码全解Web 架构之状态码全解3Web 架构之会话保持深度解析Web 架构之会话保持深度解析4Web 架构之负载均衡会话保持Web 架构之负载均衡会话保持5Web 架构之攻击应…...
如何屏蔽端口
通过Windows防火墙屏蔽端口 1. 按下“Windows”键“R”键,输入“control”并回车,打开控制面板。 2. 在控制面板中找到“系统和安全”,点击进入。 3. 选择“Windows Defender 防火墙”,然后在左侧点击“高级设置”。 4.…...

【Redis实战:缓存与消息队列的应用】
在现代互联网开发中,Redis 作为一款高性能的内存数据库,广泛应用于缓存和消息队列等场景。本文将深入探讨 Redis 在这两个领域的应用,并通过代码示例比较两个流行的框架(Redis 和 RabbitMQ)的特点与适用场景࿰…...

单北斗定位芯片AT9880B
AT9880B 是面向北斗卫星导航系统的单模接收机单芯片(SOC),内部集成射频前端、数字基带处理单元、北斗多频信号处理引擎及电源管理模块,支持北斗二号与三号系统的 B1I、B1C、B2I、B3I、B2a、B2b 频点信号接收。 主要特征 支持北斗二…...

视频自动化分割方案:支持按时间与段数拆分
在日常视频处理任务中,如何快速将一个较长的视频文件按照指定规则拆分为多个片段,是许多用户都会遇到的问题。尤其对于需要批量处理视频的开发者、自媒体运营者或内容创作者来说,手动剪辑不仅效率低下,还容易出错。这是一款绿色免…...

股指期货合约价值怎么算?
股指期货合约价值就是你买一手股指期货合约,理论上值多少钱。这个价值是根据期货的价格和合约乘数来计算的。就好比你买了一斤苹果,价格是5块钱一斤,那你买一斤就得付5块钱。股指期货也是一样,只不过它的计算稍微复杂一点点。 一…...
十八、【用户认证篇】安全第一步:基于 JWT 的前后端分离认证方案
【用户认证篇】安全第一步:基于 JWT 的前后端分离认证方案 前言什么是 JWT (JSON Web Token)?准备工作第一部分:后端 Django 配置 JWT 认证1. 安装 `djangorestframework-simplejwt`2. 在 `settings.py` 中配置 `djangorestframework-simplejwt`3. 在项目的 `urls.py` 中添加…...