DFS算法篇:理解递归,熟悉递归,成为递归
1.DFS原理
那么dfs就是大家熟知的一个深度优先搜索,那么听起来很高大尚的一个名字,但是实际上dfs的本质就是一个递归,而且是一个带路径的递归,那么递归大家一定很熟悉了,大学c语言课程里面就介绍过递归,我们知道就是定义一个函数,然后自己调用自己
那么我们要理解DFS,那么其实就是要理解递归,那么要理解好掌握好递归,那么我们就得明白递归的一个过程,那么递归的一个过程无非就两个字:
”递“和”归“
那么所谓的“递”,那么我们知道要实现递归,就是定义一个函数,然后在该函数内部再调用自己,那么我们知道当执行这个函数体的内的语句的时候,一旦执行到调用该函数本身的语句时,那么我们调用该函数就会为该函数在栈上开辟一份空间,那么开辟的这个空间我们有一个专有名词也就是栈帧来称呼,那么我们会在栈帧中存放该函数体内部所定义的各种局部变量,那么每次调用一个函数我们就会为函数创建一份栈帧,那么这里我们在函数体内部调用了该函数本身,那么这种套娃的行为,程序会为我们调用的该函数又开辟一份空间,只不过这个函数就是本身,然后我们就进入到该函数内部,又重新从函数体开头执行一行一行语句,然后又执行到调用自己的函数语句,又重复我们上述的行为,那么这整个过程,从第一个函数到之后调用自己的函数的一个个过程中,这里就是一个“递”过程,那么我们可以理解第一个函数一直到调用的最后一个函数的关系我们如果用一个层级关系来描述想象的话,那么我们从第一个函数到最后一个函数就是不断往下探索深入的过程,这就是为什么我们要取名为“深度”优先搜索,这里的deep是不是就很形象的描述了递归的一个”递“过程

而所谓的“归”,那么我们知道我们函数在自己调用自己的时候,我们一定会设置一个终止条件,如果不设置终止条件,那函数会无限的调用自己,反复套娃,而我们刚才说了调用函数会在栈上开辟空间,如果我们反复调用,那么最终会对空间的损耗极其大,所以一旦设置好终止条件之后,也就是我们在函数体内定义的一个return语句,那么我们就会回溯,那么我们上文说了,我们从第一个函数到调用自己创建的最后一个函数的过程,我们可以用层状关系来理解,那么我们结束该调用的函数,那么也就会返回调用我们该函数的上一级函数当中去,那么同理,我们上一级函数调用结束返回也是到调用它的上一级函数当中去,所以我们就这样从底层一直会回溯到顶层的第一个函数,那么第一个函数结束就是回到调用它的main主函数当中去了,那么这就是我们的归过程.
那么在我们知道了我们递归的一个过程,那么我们在理解递归的时候,如果想不通的话就可以通过画图,然后梳理函数之间的调用关系,得到一个层级的图
那么在我们上文中,我描述“递”与“归”过程中举了一个函数自己调用自己的例子来讲解,那么该函数之间的关系就是一个层级关系,但是一旦我们将我们这个例子给修改,也就是我们在一个函数体内部调用我们函数本身的语句不只有一个,而是有多个,那么这种场景就是我们的dfs的情况了
那么此时我们要理解这种场景的话,我们就可以不能用层状关系来描述了,而得是通过树状关系来描述,没错,这个树就是我们学的数据结构的那个树,那么该函数体内有着多个函数调用语句,那么我们就以该函数作为节点,那么其下有多个分支,多个子节点,那么这个子节点就是调用的函数,那么我们执行该递归函数的过程,那么我们可以将其形象的转换为遍历这棵二叉树,假设我们在函数体内部调用该函数本身两次,那么我们执行该递归函数的过程就是从顶部到底部的一个二叉树的前序遍历,也就是先遍历左子树再遍历右子树,而如果我们在该函数体内定义了多个调用函数语句,那么该递归的函数的过程就是遍历一棵多叉树,也是先遍历左子树然后再遍历右侧的子树
那么接下来我有一个问题,读者可以自行思考一下:
那么为什么要我们该递归函数的执行流程转换成多叉树的遍历是前序遍历也就是先遍历左子树再遍历右子树而不是中序或者后序遍历呢?
那是因为我们的递归函数调用,一旦我们执行到该函数调用语句之后,我们会该调用的函数创建一份新的空间,然后就直接进入到该调用函数体的内部,而不会往下执行该调用函数之后的语句,那么也就是在二叉树中最左侧的孩子就是先被调用被创建执行的函数,而右边的函数还没有被调用,也就还没创建出它的空间,所以我们递归的执行只能是一个前序遍历的一个过程

那么在此模型下,也就是多叉树的遍历模型,我们在加入一个元素那就是我们的DFS了,也就是记录路径信息,那么什么是记录路径信息呢?
我们直到我们DFS的模型就是遍历一棵多叉树,那么该多叉树的第一个节点也就是根节点就是我们的第一个函数,而底部的叶子节点就是我们调用的最后一个函数,那么我们从顶部到达底部的这个路径中,我们沿途通过一个数组或者表等结构来记录我们沿途遍历的各个节点的内容,那么这就是我们的DFS,也就是我开篇所提到的带路径的递归
而不带路径的递归则是我们不记录沿途的各个节点的内容,只是接受节点往上回溯的返回值,例如斐波那契数列,那么这个不带路径的递归,就是我们的动态规划,那么动态规划也可谓是我们算法的一个重量级人物,那么我在后文会更新该内容。
那么彻底知道了我们DFS的一个原理以及模型,那么我们DFS的应用题目就是需要暴力枚举的题目,而大家经常听到的蓝桥杯的暴力方法,也就是本文的主角DFS,
那么我们接下来就看怎么用我们这DFS,那么我在下文准备了几道题目
2.应用
题目一:字符串的全部子序列 难度:EZ
题目:现在我们有一个字符串a,我们要得到该字符串的全部子序列。
例:abc的全部子序列有:"",“a”,“b”,“c”,“ab”,“ac”,“bc”,“abc”
题目思路:那么这里我们要得到该字符串的全部子序列,我们怎么得到我们一个字符串的全部子序列呢,我们就是可以通过枚举每一个位置的两种可能性,也就是该位置的字符在我们的子序列中存在还是不存在,那么我们的一个子序列的确定就是枚举每种位置的这两种可能性然后组合就可以得到我们的子序列,那么我们的枚举过程可以用一个决策树来描述,那么我们每一个位置就是该决策树的一个节点,那么该节点就有两个分支,分别对应我们该位置是否存在在子序列当中还是不存在在子序列当中,那么在每一个分支下有同理也有着两个分支,那么我们这棵决策树就是一棵完全展开的二叉树,那么我们从二叉树的顶部也就是根节点到底部的叶子节点的每一个路径就是我们一个子序列,那么我们就从根节点往下遍历,那么在遍历的过程中同时用一个变量来记录我们遍历的节点的内容,那么一旦到达底部,我们从根节点沿途记录的内容就是我们的其中一个答案
所以只要理解了DFS的一个多叉树遍历模型,那么这题的理解以及DFS的递归的实现就很轻松
代码实现:
class solution
{public:void dfs(string& a,vector<string>& ans,string temp,int i){if(i==a.size()){ans.push_back(temp);return;}dfs(a,ans,temp+a[i],i+1)//当前字符存在在子序列中dfs(a,ans,temp,i+1);//当前字符不存在在子序列当中return;}void allsubstring(string& a,vector<string>& ans){string temp;dfs(a,ans,temp,0);return;}
}
题目二:字符串的全部不重复的子序列 难度:EZ
题目:现在我们有一个字符串a,我们要求该字符串的全部且不重复的子序列
例:abcc的全部不重复子序列:"",“a”,“b”,“c”,“ab”,“ac”,“bc”,"“cc”,“abc”,“abcc”
这里的c以及abc会有重复
题目思路:那么这个题我们就得在上一个题的思路下进行一个优化,那么我们知道我们上一个题就是暴力枚举出所有的全部子序列,那么我们这个枚举的过程就是一棵二叉树,我们从顶部到底部进行遍历,而这里我们只不过在添加一个哈希表的结构,然后我们会将从顶部到底部的沿途的所有节点的记录加入到哈希表中去重即可.
代码实现:
class solution
{public:void dfs(string& a,vector<string>& ans,string temp,int i,unordered_map<string,int>& value){if(i==a.size()){if(value.find(temp)==value.end()){ans.push_back(temp);value[temp]++;}return ;}dfs(a,ans,temp+a[i],i+1)dfs(a,ans,temp,i+1);return;}void allsubstring(string& a,vector<string>& ans){string temp;unordered_map<string,int> value;dfs(a,ans,temp,0,value);return;}
}
题目三:子集 难度:MID
题目:现在有一个数组,那么数组中每个元素可以组成一个集合,请返回所有且不重复的集合。
例:[1,2,3,4]的子集有:[1],[2],[3],[4],[1,2],[1,3],[1,4],[2,3],[2,4],[3,4],[1,2,3],[1,2,4],
[1,3,4],[2,3,4],[1,2,3,4]
注意:[1,2,3]和[3,2,1]以及[2,1,3]是一个集合
题目思路:那么这里我们解决该题的一个思路还是一个枚举,那么我们按照每个位置是否在这个集合中存在来作为枚举,那么每一个位置都有两种可能,也就是存在在这个集合当中或者不存在在这个集合当中,那么这里我们就枚举每一个位置的可能性,然后每个位置的可能性所组合就是我们的子集了,那么我们枚举的过程就是一棵决策树,那么在当前每个位置的当前所做的决策就是二叉树的一个节点,那么我们现在有两种决策,要么存在在集合中要么不存在在集合中,那么在二叉树的该节点中就对应就会有两个分支,那么同理,这两个分支下有对应着两个分支,那么我们从顶部到达底部的路径的沿途的各个节点组合就是我们的子集,那么我们就遍历我们这棵二叉树,然后我们定义一个变量来记录沿途的路径上的节点内容即可。
class solution
{public:void dfs(vector<int>& a,vector<vector<int>>& ans,int index,vector<int>& current,unordered_map<string,int>& value){if(index==a.size()){for(int i=0;i<current.size();i++){a+=to_string(current[i]);}if(value.find(a)==value.end()){ans.push_back(current);value[a]++;}return;}current.push_back(a[index]);dfs(a,ans,index+1,current);current.pop_back();dfs(a,ans,index+1,current);}void all(vector<int>& a,vector<vector<int>>& ans){vector<int> current;unordered_map<string,int> value;dfs(a,ans,0,current,value);return;}
}
但是我们可以优化一下,因为我们知道这题存在重复的子集,那么所谓的重复的子集就是集合的各个值的数量一样,但是顺序不一样,那么就是重复的子集,那么我们就可以按照集合的特定的值的数量来进行一个枚举,那么首先我们可以先对数组进行一个排序,这样相同大小的数会在相邻位置,然后我们按照这样的方式来枚举,那么我们枚举集合中值为k的数在集合的数量为0的子集,然后再枚举为值为k数量为1的子集有几个,那么同理对于其他值也是这样,那么我们将每个值在当前集合的数量的可能给组合就是我们的子集,那么按照这样枚举的话我们就能够优化很多重复子集的枚举,实现剪枝
而所谓的剪枝就跟园丁修建树木的花花草草一样,我们这里的剪枝则是要修剪掉重复计算过的分支,这样减少树的遍历从而优化时间复杂度,并且也优化了空间复杂度因为减少了递归函数的调用从而不用为函数创建栈帧,而在上一个题中,我们用哈希表的方式来去重注意那个不是剪枝,我们还是会从树的顶部走到底部,然后将记录的结果加进哈希表中去重,树的分支其实并没有减少
代码实现:
class Solution {
public:void dfs(const vector<int>& nums, vector<vector<int>>& subsets, vector<int>& current, int index) {if(index==nums.size()){ans.push_back(current);return;}int i=index+1;while(i<nums.size()&&nums[i]==nums[i-1]){i++;}for(int j=0;j<i-index;j++){current.push_back(nums[index]);dfs(nums,subsets.current,i);}}vector<vector<int>> subsets(vector<int>& nums) {vector<vector<int>> result;vector<int> current;sort(nums.begin(), nums.end()); // 对数组进行排序dfs(nums, result, current, 0);return result;}
};
题目四:全排列 难度:MID
题目:那么现在有一个数组,我们要得到该数组的所有全排列
例:[1,2,3]的全排列有[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,2,1],[3,1,2]
题目思路:那么这里我们要枚举我们的所有全排列,那么这里我们的枚举思路就是我们假设我们的数组长度为n,那么意味着有n个数,那么我们分别来枚举每一个位置的数的情况,比如第一个位置有n种情况,那么第二个位置有n-1种情况,以此类推,那么我们每个位置的情况组合就得到了我们的全排列,那么我们这个枚举过程就是每一个数的决策就是该多叉树的一个节点,那么我们其中从顶部到底部的各个路径就是我们其中一个全排列,那么我们定义一个变量来记录我们从顶部到达底部的路径沿途的各个节点内容,然后该记录就是我们的一个答案
代码实现:
class solution
{public:void dfs(vector<int>& a,vector<vector<int>>& ans,vetcor<int>& temp,vector<int>& check){if(temp.size()==a.size()){ans.push_back(temp);return;}for(int i=0;i<a.size();i++){if(check[i]==0){check[i]=1;temp.push_back(a[i]);dfs(a,ans,temp,check);check[i]=0;temp.pop_back();}}}void all(vector<int>& a,vector<vector<int>>& ans){vector<int> temp;vector<int> check(a.size(),0);dfs(a,ans,temp,check);return;}
}
3.结语
那么这就是我们DFS的全部内容,那么我们DFS的应用场景就是那些需要我们去暴力枚举尝试的题目,那么每一次枚举也就是每一次所做的要做的决策就是我们树当中的节点,而其中枚举的可能性就是这个节点所对应的分支,那么我们就沿途遍历这些所有的从顶部到达底部的所有分支,并将沿途的节点内容给记录即可,那么这就是我们的DFS,本质上就是带路径的递归,那么要掌握DFS只要熟悉我们的递归,其实DFS的算法原理与思想其实很简单,挺无脑的,并且知道了DFS的算法模型就是一棵多叉树的话,那么通过递归来实现的难度就减小很多
那么最后感谢你能够耐心的看完本文,那么希望本文能够让你有所收获,我会持续更新,希望你能够多多关注,那么如果我的文章有帮组到你,那还请你多多三连支持哦,你的支持就是我创作的最大的动力!

相关文章:
DFS算法篇:理解递归,熟悉递归,成为递归
1.DFS原理 那么dfs就是大家熟知的一个深度优先搜索,那么听起来很高大尚的一个名字,但是实际上dfs的本质就是一个递归,而且是一个带路径的递归,那么递归大家一定很熟悉了,大学c语言课程里面就介绍过递归,我…...
2025java常见面试题第一弹
1. Java中的HashMap和Hashtable有什么区别? 答案: 线程安全性: HashMap是线程不安全的,适合单线程环境。如果在多线程环境下使用,可能会出现数据不一致的问题。 Hashtable是线程安全的,内部方法通过synch…...
JMeter工具介绍、元件和组件的介绍
Jmeter功能概要 JDK常用文件目录介绍 Bin目录:存放可执行文件和配置文件 Docs目录:是Jmeter的API文档,用于开发扩展组件 printable_docs目录:用户帮助手册 lib目录:存放JMeter依赖的jar包和用户扩展所依赖的Jar包…...
机舱卫生和空气质量改善
公共卫生挑战:在密闭空间内控制病原体 由于公共交通等密闭空间内的人员密度很高,因此保持良好的空气质量至关重要。有效的通风系统在循环新鲜空气和降低空气中病原体和污染物的浓度方面起着关键作用。使用高效微粒空气 (HEPA) 过滤…...
springBoot之环境变量
springboot 在new SpringBootApplication()时, 会扫描所有的spring.factory; 它会给每个接口当做group,所有实现类为List当做value,形成map; group -> List 系统属性 java的相关属性 系统环境属性,指的是操作系统相关的配置 每个配置对应一个contro…...
萨班斯-奥克斯利法案(Sarbanes-Oxley Act, SOX):公司财务透明度的守护者(中英双语)
萨班斯-奥克斯利法案(Sarbanes-Oxley Act):公司财务透明度的守护者 在2001年安然(Enron)和世通(WorldCom)等公司财务造假丑闻爆发后,美国政府迅速出台了《萨班斯-奥克斯利法案》&am…...
iOS 中使用 FFmpeg 的高级功能 - 滤镜(Filters)
FFmpeg 提供了强大的滤镜功能,可以对音视频进行各种处理,例如裁剪、缩放、添加水印、调整颜色、添加特效等。 1. FFmpeg 滤镜基础知识 1.1 什么是滤镜(Filters)? 滤镜是 FFmpeg 提供的一种功能,用于对音视频流进行处理。滤镜链(Filter Chain)是多个滤镜的组合,按顺序…...
tomcat html乱码
web tomcat html中文乱码 将html文件改成jsp <% page language"java" contentType"text/html; charsetUTF-8" pageEncoding"UTF-8"%>添加 <meta charset"UTF-8">...
kubectl exec 实现的原理
kubectl exec 是 Kubernetes 提供的一个命令,它允许你在指定的 Pod 中执行命令,类似于在容器中打开一个终端会话。这个功能对于调试、监控和管理容器化应用非常有用。kubectl exec 的实现涉及到多个 Kubernetes 组件和机制,包括 API Server、…...
Unity中可靠的UDP实现
可靠 UDP(Reliable UDP)是一种在用户数据报协议(UDP)基础上,通过添加额外机制来实现可靠数据传输的技术。与传统 UDP 相比,它克服了 UDP 本身不保证数据可靠性、顺序性以及可能丢失数据的缺点,同…...
CentOS 7操作系统部署KVM软件和创建虚拟机
CentOS 7.9操作系统部署KVM软件和配置指南,包括如何创建一个虚拟机。 步骤 1: 检查硬件支持 首先,确认您的CPU支持虚拟化技术,并且已在BIOS中启用: egrep -c (vmx|svm) /proc/cpuinfo 如果输出大于0,则表示支持虚拟…...
Golang GORM系列:GORM分页和排序
高效的数据检索和表示是应用程序开发的关键方面。GORM是健壮的Go对象关系映射库,它为开发人员提供了强大的工具来实现这一点。无论你是在构建动态web应用程序还是数据密集型服务,掌握GORM中的分页和排序使您能够提供无缝且高效的用户体验。本文我们将深入…...
WPF的MVVMLight框架
在NuGet中引入该库: MVVMLight框架中的命令模式的使用: <StackPanel><TextBox Text"{Binding Name}"/><TextBox Text"{Binding Title}"/><Button Content"点我" Command"{Binding ShowCommand…...
微服务SpringCloudAlibaba组件sentinel教程【详解sentinel的使用以及流量控制、熔断降级、热点参数限流等,附有示例+代码】
文章目录 四.Sentinel限流熔断4.1 sentinel介绍4.2 Sentinel 的历史4.3 Sentinel 基本概念资源规则 4.4 Sentinel 功能和设计理念4.4.1 流量控制4.4.2熔断降级什么是熔断降级熔断降级设计理念系统负载保护 4.5 Sentinel 是如何工作的4.6 Sentinel使用4.7 Sentinel 控制台4.8 Sp…...
ScoreFlow:通过基于分数的偏好优化掌握 LLM 智体工作流程
25年2月来自 U of Chicago、Princeton U 和 U of Oxford 的论文“ScoreFlow: Mastering LLM Agent Workflows via Score-based Preference Optimization”。 最近的研究利用大语言模型多智体系统来解决复杂问题,同时试图减少构建它们所需的手动工作量,从…...
数字水印嵌入及提取系统——基于小波变换GUI
数字水印嵌入及提取系统——基于小波变换GUI 基于小波变换的数字水印系统(Matlab代码GUI操作) 【有简洁程序报告】【可作開题完整文档达辩PPT】 本系统主要的内容包括: (1)使用小波变换技术实现二值水印图像的加密、…...
基于海思soc的智能产品开发(图像处理的几种需求)
【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing 163.com】 对于一个嵌入式设备来说,如果上面有一个camera,那么就可以有很多的用途。简单的用途就是拍照,比拍照更多一点的…...
【R语言】聚类分析
聚类分析是一种常用的无监督学习方法,是将所观测的事物或者指标进行分类的一种统计分析方法,其目的是通过辨认在某些特征上相似的事物,并将它们分成各种类别。R语言提供了多种聚类分析的方法和包。 方法优点缺点适用场景K-means计算效率高需…...
Spring 项目接入 DeepSeek,分享两种超简单的方式!
⭐自荐一个非常不错的开源 Java 面试指南:JavaGuide (Github 收获148k Star)。这是我在大三开始准备秋招面试的时候创建的,目前已经持续维护 6 年多了,累计提交了 5600 commit ,共有 550 多位贡献者共同参与…...
docker 进阶命令(基于Ubuntu)
数据卷 Volume: 目录映射, 目录挂载 匿名绑定: 匿名绑定的 volume 在容器删除的时候, 数据卷也会被删除, 匿名绑定是不能做到持久化的, 地址一般是 /var/lib/docker/volumes/xxxxx/_data 绑定卷时修改宿主机的目录或文件, 容器内的数据也会同步修改, 反之亦然 # 查看所有 vo…...
微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...
【Go】3、Go语言进阶与依赖管理
前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课,做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程,它的核心机制是 Goroutine 协程、Channel 通道,并基于CSP(Communicating Sequential Processes࿰…...
VTK如何让部分单位不可见
最近遇到一个需求,需要让一个vtkDataSet中的部分单元不可见,查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行,是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示,主要是最后一个参数,透明度…...
【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验
系列回顾: 在上一篇中,我们成功地为应用集成了数据库,并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了!但是,如果你仔细审视那些 API,会发现它们还很“粗糙”:有…...
【HTML-16】深入理解HTML中的块元素与行内元素
HTML元素根据其显示特性可以分为两大类:块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
分布式增量爬虫实现方案
之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面,避免重复抓取,以节省资源和时间。 在分布式环境下,增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路:将增量判…...
计算机基础知识解析:从应用到架构的全面拆解
目录 前言 1、 计算机的应用领域:无处不在的数字助手 2、 计算机的进化史:从算盘到量子计算 3、计算机的分类:不止 “台式机和笔记本” 4、计算机的组件:硬件与软件的协同 4.1 硬件:五大核心部件 4.2 软件&#…...
BLEU评分:机器翻译质量评估的黄金标准
BLEU评分:机器翻译质量评估的黄金标准 1. 引言 在自然语言处理(NLP)领域,衡量一个机器翻译模型的性能至关重要。BLEU (Bilingual Evaluation Understudy) 作为一种自动化评估指标,自2002年由IBM的Kishore Papineni等人提出以来,…...
Web后端基础(基础知识)
BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。 优点:维护方便缺点:体验一般 CS架构:Client/Server,客户端/服务器架构模式。需要单独…...
