数据结构初阶(16)排序算法——归并排序

2.4 归并排序

归并排序(Merge Sort)是基于分治思想的经典排序算法。

核心逻辑: 分而治之——把复杂排序问题拆分成简单子问题解决,再合并子问题的结果

联系

链表的合并:两个有序链表l1、l2

  • 创建新链表l3(带头结点),遍历l1、l2,小的尾插到l3。

数组的合并:两个有序数组a1(sz = s1+s2)、a2(sz=s2)

  • 从大下标开始遍历两个数组,大的放到a1的末尾。

时间复杂度:O(N)。


差异

  • 链表可以把结点取下来,可以达到空间复杂度O(1)。
  • 数组a1(sz=s1)、a2(sz=s2),则只能把数据拷贝到新的空间,空间复杂度O(N)。

前提:左区间有序、右区间有序。

怎么让左区间有序、右区间有序呢?

这就需要一种类似于二叉树后序的思想——分治:

  1. 先让左区间有序;
  2. 再让右区间有序
  3. 最后让整体有序;

基本思想

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

归并排序的基本操作是将已有序的子序列合并,得到完全有序的序列——即先使每个子序列有序,再使子序列段间有序。

若将两个有序表合并成一个有序表,称为二路归并

归并排序核心步骤:

  1. 将无序数据从中间分成两个子序列。
  2. 将子序列不断的划分,直到只剩一个数据——该子序列有序了。
    拆分的终点:能够进行下一步归并——即子区间有序,只有剩1个数据的时候,才能保证子区间有序;
    拆分的终点:只剩一个数据。
    归并的起点:只有一个数据。
  3. 子序列有序了,就可以开始进行两两合并,直到所有的子序列合并完,排序就完成了。

时间复杂度能够达到O(N*logN),还算不错。

递归的时候,不是每次递归都去开辟一个子数组(4个数据空间、2个数据空间、……),归并过去再拷贝回来。

而是一次性开辟一个和arr等大的tmp数组,每次归并都在tmp执行,执行完拷贝回arr数组。

如下图所示。

动图演示

下图只展示了从最初一个有序子序列只含有一个数据开始的归并的过程。

没有展示分解的过程。

算法步骤

分解阶段:

  1. 将当前待排序数组从中间位置分为两部分。
  2. 计算中点: mid = left + (right - left) / 2。
  3. 递归地对左右两部分继续分解,直到子数组长度为1。

  • 当前子数组长度为1时,说明该子数组是有序的。
  • 开始从最底层向上回朔。

合并阶段:

  1. 将两个已排序的子数组合并为一个有序数组。     
  2. 使用双指针法比较两个子数组的元素。
  3. 较小的元素放入临时数组。
  4. 归并完将数据从临时数组拷贝回原数组。
  5. 执行下一次归并,直到数组全部有序。

    代码实现(递归)

    算法思想是利用二叉树后序的思想,实际代码控制的是数组。

    逻辑结构和物理结构是分离的。

    若是完全按照逻辑结构来实现物理结构——链式二叉树,反而控制起来更加地复杂。

    逻辑结构那样去画,是为了更好地帮助我们理解,但是我们没有使用跟逻辑结构一样的物理结构去实现,因为这样反而是更加麻烦的。

    (理想很丰满、现实很骨感)

    思路(递归实现):

    • 前提:
      申请一个临时数组tmp,因为我们不能在原数组归并,不然会造成覆盖,需要在临时归并,在拷回原数组里。
      由于创建了临时数组,我们就需要在创建一个子函数用来专门递归,不然每次调用都会申请空间。
    • 子函数思路:
    1. 将序列从中间分成两个区间[left,mid] [mid + 1,right],调用递归,直到只剩一个数据,一个数据就表示有序了。
    2. 在把递归的两个区间进行合并,begin1和end2表示左序列,begin1和end2表示右序列,比较两个序列的值插入到临时数组里,其中肯定会有一个先结束,当其中一个序列拷贝完了就停止拷贝。
    3. 把剩下未合并的数据,全部放到tmp数组里。
    4. 最后在tmp数组的数据拷到原数组里。
    //子函数:用来递归
    void _MergeSort(int* a, int begin, int end, int* tmp)
    {//最小递归子问题——只剩1个数据(有序)if (begin == end)return;int mid = (begin + end) / 2;//把传进来的区间从中间分割开,分成两个子区间// [begin, mid-1] [mid, end]// [begin, mid] [mid+1, end] ——这种划分,在偶数个数据的时候才能平分区间//如果这两段区间有序——>那么就可以进行归并——>如何判断有序?//——>不判断,不管有没有序,直接分到只剩一个,再一次归并回来,每次归并回来的都有序//经典后序_MergeSort(a, begin, mid, tmp);            //1.先让左区间有序_MergeSort(a, mid + 1, end, tmp);          //2.再让右区间有序//两段子区间已经有序,归并,使整个区间有序    //3.最后让整体区间有序int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;         //操作:创建变量保存两个区间的下标//意义1:不直接操作begin、mid、end//意义2:增强代码可读性//i最好不给0,而是给数组的区间始端,使归并数据的在tmp中的位置与原a中的位置一一对应int i = begin;//(1)归并到tmp数组// 依次比较,取小的尾插到tmp数组——>有一个区间结束,循环就结束——结束条件“或”//循环:想的是结束条件,写的是继续条件:用“且”while (begin1 <= end1 && begin2 <= end2)   {//取小的尾插if (a[begin1] <= a[begin2]){tmp[i++] = a[begin1++];    //后置++:先拷贝,再递增}else{tmp[i++] = a[begin2++];}}//有一个结束了,谁没结束,就继续拷贝while (begin1 <= end1){tmp[i++] = a[begin1++];}while (begin2 <= end2){tmp[i++] = a[begin2++];}//(2)把归并好的数组区间拷贝回原区间//因为最后tmp数组空间是要释放的,要保留的是原数组memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));//闭区间元素个数= 区间差 + 1//不能图方便直接拷贝整个数组,拷贝区间之外的数据拷贝回原数组会覆盖掉原数组的数据
    }void MergeSort(int* a, int n)
    {//首先开一个临时数组int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}//开完数组,就导致当前这个结构无法帮助我们递归——不能每次递归都去开一个数组//就需要一个新的负责归并递归的函数,参数:a、tmp、区间_MergeSort(a, 0, n - 1, tmp);//释放临时数组free(tmp);tmp = NULL;
    }

    注意一些细节上的处理。

    [begin, mid-1] [mid, end]
    [begin, mid] [mid+1, end] ——这种划分,在偶数个数据的时候才能平分区间。

    例如8个数据,mid = (0 + 7) / 2 =3,第一种就是[0,2]、[3,7],第二种就是[0,3]、[4,7]。

    例如9个数据,mid = (0 + 8) / 2 =4,第一种就是[0,3]、[4,8],第二种就是[0,4]、[5,8]。


    注意保证归并排序稳定的细节:

            //……//取小的尾插if (a[begin1] <= a[begin2]){tmp[i++] = a[begin1++];    //后置++:先拷贝,再递增}else{tmp[i++] = a[begin2++];}//……
    //这里如果写成——if (a[begin1] < a[begin2]),就不稳定了

    细节注意:

    • 归并的时位置都是相对位置,如[2,2][3,3]合并,合并完存放在tmp数组里也要是[2,3],所以两个序列都是两个区间的相对位置,所以:
      左序列
      begin1 = left,end1 = mid
      右序列
      begin2 = mid+1,end2 = right
    • i = begin,不是i = 0,是为了将原数组的相对位置的数据合并到tmp数组的相对位置。
      如a[2]就要放到tmp[2]。
    • 拷贝数据函数memmove使用,格式:memmove(宿dst、源src、字节数num)。
    • 同样拷贝数据时,也要注意相对位置,如memmove(a, tmp, (right-left+1) * sizeof(int)),这样每次就会从tmp下标0开始拷贝,并不是相对位置。
    • 子函数使用的闭区间,传的是拷贝数据个数,需要+1,如:[0,9],一个10个数。

    递归展开图。

    代码实现(非递归)

    递归实现的经典问题——栈溢出。所以需要将递归改为非递归。

    快速排序的递归改递归,可以直接借助栈来实现——前序的思想,借助栈能比较容易地改成非递归。

    归并排序是后序的思想,就不太好简单地借助栈来改成非递归。

    分析:[0,7]拆分后,[4,7]入栈,[0,3]入栈,然后[0,3]出栈,[2,3]、[0,1]入栈,[0,1]出栈,[0,0]入栈,[1,1]入栈,这两个区间取出来就有序了,下一步该取哪个区间出来进行归并?——[0,1]已经出栈找不到了。

    另一种改递归的方法就是直接借助循环——类似于斐波那契数列。

    • 斐波那契数列的递归实现:用N-1项、N-2项算第N项。
    • 斐波那契数列的循环实现:知道第1项、第2项,以此来依次算出第3项、第4项、……、第N项。(顺着走就可以)

    归并排序的非递归改造也一样,因为知道最小条件是什么,并且是固定的——单个数默认有序。

    那就可以:

    1. 把arr数列看作单个数自身有序
    2. 两两归并;

    相当于直接走回溯归并的过程,把分割的过程跳过了。

    gap表示每组数据的个数,一次两组数据归并

    • 两两归并首先要确定本次两两归并的区间下标。

    (1)先来尝试写出第1组归并的,两个区间。

    注意

    • 闭区间:右下标 = 左下标 + 元素个数 - 1

    (2)然后执行第一次归并

    (3)然后循环起来,执行完gap=1的所有归并

    确定每次归并的两个区间,然后执行归并。

    注意

    • 循环迭代式 j += 2*gap:j每次跳过两个gap,来到下一次“两个区间归并”。

    那么2*gap就是一次归并的个数,通过每组个数(gap)推导出每组数据的区间,如[0,0]和[1,1]归并,gap为1,j = 0。

    得出[j,j + gap - 1][j+ gap,j + 2*gap - 1]

    排下一次就需要j += 2 * gap,就可以访问下一次归并的起始位置了,如果先是[0,0]和[1,1]归并,再就是[2,2]和[3,3]归并。

    归并好的数据需要放入到临时数组大家可以通过上图代入值。

    这是只是单趟排好多组的样子,这里模拟递归最后一层的情况,和递归相反,我们是从直接最后一层开始归并,递归还需要慢慢的分解递归下来,然后才能开始慢慢归并。

    (4)最后循环起来,执行完 gap=2 * gap 的所有归并(3层循环)

    想要整体排好序,gap每次就乘以2,直到gap为n/2,就是说把未排序的数据分成两组数据,两组数据归并完就排好了序。

    每次单趟归并完就把临时数组的数据拷回原数组里,再用原数据归并,直到排好序,如下图:

    实现思路

    1. 同样也需要创建一个临时数组tmp,不能在原数据归并会覆盖。
    2. gap为每组数据个数,先从gap为1,一次两组数据归并,模拟递归最后一层的归并,如:[0,0]和[1,1]归并
    3. 通过每组个数(gap)推导出每组数据的区间,得出两组数据区间是[j,j + gap - 1],[j + gap,j + 2*gap - 1]。
    4. j需要加上2*gap,原来的区间已归并好了,所以需要指向下一次归并的起始位置,直到 j >= n结束,循环条件就要设为j < n,表示排好差距为gap的多组数据。
    5. 排好差距为gap多组数据后,gap*2,下一趟需下一趟归并的一组数据个数翻倍,直到gap为gap >= n结束——只有一个组数据,不能归并。循环条件就是gap < n,就是说把未排序的数据分成两组数据,两组数据归并完就排好了序。
    6. 把临时数组的数据拷到原数组。

    代码测试

    正常运行。

    程序运行直接崩溃。所以只能调试运行,检查上述代码的bug。

    测试gap == 1的两两归并没有问题,再走一轮,按F5。

    gap == 2的递归出现问题,出现随机数——数组越界访问。

    打印观察。


    因为begin1 = j,而 j 是小于 n 的,所以begin1不可能越界,剩下的3个都有可能会越界。

    于是在代码中,需要增加对其余3个区间下标的处理,避免越界。

    越界处理

    • 归并排序的非递归改造的重点:解决越界问题

    上面的图解、第一次的测试代码,正好拿2的指数——8个数据演示的。

    而上面的测试代码,不是2的指数,2^3、2^4……等,而是10个数据——就会越界。

    分组无法平分,肯定会导致一方越界,一共有3种越界。

    越界情况:

    1. 右区间不存在                ——begin2 >= n
    2. 左区间缺少值                ——end1 >= n
    3. 右区间超过数据长度     ——end2 >= n

    越界情况1和2可以当作一种情况来处理:右区间不存在。

    • 此时只有左区间,左区间同时也是在原数组中是有序的,所以可以直接跳出循环,不用处理最后这一个区间。
    • 只需要处理——有双区间的、需要归并的数据。

    越界情况3的处理:纠正右区间的结束标识,让结束标识到最后一个数据即可。

    然后正常执行归并。

    处理

    1. 右区间不存在,直接跳出循环break,不进行归并。
    2. 右区间超过数据长度,进行修正,右区间结束标识n-1,就是最后一个数据的下标。

    由于右区间不存在直接跳出的情况,tmp数组就会有随机值情况,那就不能使用把当前间距为gap的数据,全部归并好之后,再一次性全部拷贝到原数据中去的方法。

    就只能使用:归并了多少,就拷背多少,归并完一次gap(而不是一整组gap),直接就拷贝。

    即把数据拷贝放入到for循环中。

    变化的代码。

    memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));

    • 注意1:拷贝源和目的地的起始位置
    • 注意2:拷贝的数据个数
    1. 首先不是2*gap——应该使用修正后的end2减去起始位置;
    2. 其次左闭右闭,end2 - begin1后还要 + 1才是数据个数;
    3. 由于这里begin1++了,故用 j 替代,j 是这两组数据归并的开始位置;


    完整代码

    void MergeSortNonR(int* a, int n)
    {int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}//定义每组数据归并的个数int gap = 1;while (gap < n){//printf("gap:%d->", gap);for (int j = 0; j < n; j += 2 * gap)    //两组gap进行一次归并——>j每次跳过两组gap{int begin1 = j, end1 = begin1 + gap - 1;int begin2 = begin1 + gap, end2 = begin2 + gap - 1;if (end1 >= n || begin2 >= n)break;if (end2 >= n)end2 = n - 1;//i是归并到tmp中的对应起始位置int i = j;// 依次比较,取小的尾插tmp数组while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2]){tmp[i++] = a[begin1++];}else{tmp[i++] = a[begin2++];}}while (begin1 <= end1){tmp[i++] = a[begin1++];}while (begin2 <= end2){tmp[i++] = a[begin2++];}//归并一个组,拷贝一个组memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));gap *= 2;}free(tmp);tmp = NULL;
    }

    性能分析

    时间复杂度

    分解:在每一层递归中,都需要将数组分成两个子数组,因此递归树的深度为logn。
    合并:在每一层递归中,需要将两个有序子数组合并成一个有序数组,这个操作的时间复杂度为O(n)。

    因此总的时间复杂度为O(nlogn)

    空间复杂度

    需要申请一个临时数组tmp,长度和原数组一样大。

    所以空间复杂度为O(N) 

    时间复杂度

    • 不管数组初始是否有序,时间复杂度都是O(N*logN)
      拆分过程是对数级(每次规模减半,拆分次数为log2n);
      合并过程是线性级(每次合并遍历n个元素);

    空间复杂度

    • 因合并需要额外临时数组存数据,空间复杂度O(n), n为元素个数。

    稳定性:

    • 稳定排序

    归并排序非递归的越界处理是难点,需要多加画图和调试分析情况,才能很好控制越界问题。

    特性总结

    归并排序的特性总结:

    1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

    2. 时间复杂度:O(N*logN)

    3. 空间复杂度:O(N)

    4. 稳定性:稳定。

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

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

    相关文章

    MATLAB实现匈牙利算法求解二分图最大匹配

    MATLAB实现匈牙利算法求解二分图最大匹配 匈牙利算法&#xff08;也称为Kuhn-Munkres算法&#xff09;是解决二分图最大匹配问题的经典算法。 代码 function [matching, max_match] hungarian_algorithm(adjMatrix)% HUNGARIAN_ALGORITHM 实现匈牙利算法求解二分图最大匹配% 输…

    自定义table

    更好<!DOCTYPE html> <html lang"zh-CN"><head><meta charset"utf-8"><title>数据表格</title><style>* {margin: 0;padding: 0;box-sizing: border-box;font-size: 14px;}html,body {width: 100%;height: 100%…

    面向R语言用户的Highcharts

    如果您喜欢使用 R 进行数据科学创建交互式数据可视化&#xff0c;那么请你收藏。今天&#xff0c;我们将使用折线图、柱状图和散点图来可视化资产回报。对于我们的数据&#xff0c;我们将使用以下 5 只 ETF 的 5 年月回报率。 SPY (S&P500 fund)EFA (a non-US equities fun…

    【测试工具】OnDo SIP Server--轻松搭建一个语音通话服务器

    前言 Ondo SIP Server 是一款基于 SIP(Session Initiation Protocol)协议的服务器软件&#xff0c;主要用于实现 VoIP(Voice over IP)通信&#xff0c;支持语音通话、视频会议等多媒体会话管理&#xff0c;非常适合学习和测试VoIP的基本功能。本文介绍Ondo SIP Server的安装、…

    疯狂星期四文案网第42天运营日记

    网站运营第42天&#xff0c;点击观站&#xff1a; 疯狂星期四 crazy-thursday.com 全网最全的疯狂星期四文案网站 运营报告 今日访问量 今日搜索引擎收录情况 网站优化点 优化一些发现的seo错误 增加颜文字栏目 增加了一些tag

    使用空模型实例调用辅助函数,确定在量化过程中哪些层会被跳过(43)

    在Facebook的OPT-350M中,模型的头部(lm_head)与解码器的嵌入标记层(decoder.embed_tokens)共享其权重。 print(model.model.decoder.embed_tokens) print(model.lm_head)输出结果 Embedding(50272, 512

    从0-1使用Fastmcp开发一个MCP服务,并部署到阿里云百炼 -持续更新中

    目的&#xff1a; 在本地使用fastmcp开发一个mcp,然后注册到阿里云的百炼里面。实现在百炼里面创建智能体的时候直接引用自己开发的MCP 已完成&#xff1a;本地环境安装 待完成&#xff1a; 1.根据需求实现一个MCP中可以调用某应用的多个API即 mcp.tool()、mcp.prompt()、接入大…

    设计模式之汇总

    设计模式 零、设计原则 0.1 单一职责 0.2 接口隔离 0.3 开闭原则 0.4 依赖倒置0.5 迪米特法则&#xff0c;最小知道原则用户关机 只和朋友通信 朋友条件&#xff1a; 1&#xff09;当前对象本身&#xff08;this&#xff09; 2&#xff09;以参量形式传入到当前对象方法中的对象…

    第6章 Decoder与Encoder核心组件

    前言 Netty从底层Java通道读取ByteBuf二进制数据&#xff0c;传入Netty通道的流水线&#xff0c;随后开始入站处理。在入站处理过程中&#xff0c;需要将ByteBuf二进制类型解码成Java POJO对象。这个解码过程可以通过Netty的Decoder&#xff08;解码器&#xff09;去完成。 在…

    [已解决]当启动 Spring Boot 应用时出现 Using generated security password xxx提示

    当启动 Spring Boot 应用时出现 Using generated security password xxx提示当启动 Spring Boot 应用时出现 Using generated security password xxx提示&#xff0c;这是 Spring Security 自动配置的默认行为&#xff0c;通常发生在你​​未自定义安全配置​​但引入了 Spring…

    自动分析需求,PRD 生成只需 SOLO 一步!

    资料来源&#xff1a;火山引擎-开发者社区 写不清需求&#xff1f;PRD 难产&#xff1f;开发总跑偏&#xff1f;这些痛点&#xff0c;SOLO 来解决。 TRAE SOLO 是行业首个 Context Engineer。它不止协助编码&#xff0c;更能基于精准上下文理解和工具调用&#xff0c;从构思、…

    物联网软件开发过程中,数据流图(DFD),用例图,类图,活动图,序列图,状态图,实体关系图(ERD),BPMN(业务流程建模)详解分析

    概述软件开发过程中&#xff0c;特别是在物联网&#xff08;IoT&#xff09;场景中&#xff0c;数据流图&#xff08;DFD&#xff09;、UML图&#xff08;包括用例图、类图、活动图、序列图、状态图&#xff09;、实体关系图&#xff08;ERD&#xff09;和业务流程建模&#xf…

    Mac(一)常用的快捷键整理

    目录1、系统操作与窗口管理2、应用与窗口切换3、常规编辑操作4、文本导航与光标控制✏️5、文本格式与文档功能&#xff08;支持应用中&#xff09;6、截图快捷键7、Safari 浏览器快捷键8、Finder 快捷键&#xff08;文件管理&#xff09;9、Fn / Globe 功能键&#xff08;部分…

    HAProxy使用方法以及和LVS区别

    HAProxy简介HAProxy是法国开发者 威利塔罗(Willy Tarreau) 在2000年使用C语言开发的一个开源软件 是一款具备高并发(万级以上)、高性能的TCP和HTTP负载均衡器 支持基于cookie的持久性&#xff0c;自动故障切换&#xff0c;支持正则表达式及web状态统计LVS 与 HAProxy 的核心区别…

    超越“小作文”:大模型指令设计的进阶之路——优化知识信噪比

    文章摘要&#xff1a;你是否认为&#xff0c;给大模型的指令&#xff08;Prompt&#xff09;写得越详细越好&#xff1f;真的是信息越多&#xff0c;模型就越懂你吗&#xff1f;本文将深入探讨一个反直覺的觀點&#xff1a;初級的指令設計專注於資訊的堆砌&#xff0c;而高階的…

    elasticsearch-集成prometheus监控(k8s)

    一. 简介&#xff1a; 关于elasticsearch的简介和部署&#xff0c;可以参考单独的文章elasticsearch基础概念与集群部署-CSDN博客&#xff0c;这里就不细说了。这里只讲讲如何在k8s中部署export并基于prometheus做es的指标采集。 二. 实现方式&#xff1a; 首先我们需要先部署…

    贪心算法(Greedy Algorithm)详解

    一、什么是贪心算法&#xff1f; 贪心算法是一种算法设计范式&#xff0c;指在解决问题时&#xff0c;依赖于每次选择最优的局部解&#xff0c;以期最终得到全局最优解。贪心算法的关键特点是&#xff1a; 局部最优选择&#xff1a;每个阶段选择当前看起来最好的选择&#xff0…

    电梯的构造|保养|维修视频全集_电梯安全与故障救援(课程下载)

    课程下载&#xff1a;https://download.csdn.net/download/m0_66047725/91699586 电梯原理与维修视频教程 相关简介: 电梯现在运用的非常广泛,比如大型商场,建筑工地,特别是现在建造的很多高楼、商品房,基本都是安装了电梯。电梯维保不力是导致电梯运行中安全事故频发的主要原…

    Traefik网关DNS解析超时问题优化

    1、背景 在生产环境使用 Traefik 网关时出现了偶发的 DNS 解析超时导致网关与后端服务建立连接异常的情况。通过调用链埋点数据观察发现&#xff0c;该部署环境中 Traefik 的 DNS 解析性能较差&#xff0c;耗时通常在 4ms 以上&#xff08;正常应该是 1ms 以内&#xff09; 初…

    从0到1掌握 Spring Security(第三篇):三种认证方式,按配置一键切换

    > 本文是Spring Security系列第三篇,将带你实现内存、JDBC和自定义三种认证方式的无缝切换,只需修改配置文件即可完成认证策略变更! ## 一、为什么需要多种认证方式? 在软件开发的不同阶段,我们需要不同的认证策略: - **开发阶段**:使用内存认证,快速配置测试账号…