java-网络编程socket-聊天室-03

完整版代码 

java -聊天室的代码: 用于存放聊天室的项目的代码和思路导图icon-default.png?t=N7T8https://gitee.com/to-uphold-justice-for-others/java---code-for-chat-rooms.git

多线程并发问题

多线程的并发问题主要出现在当一个程序涉及多个线程同时运行时,这些线程可能会同时访问共享资源(如数据、内存、文件等)。由于多个线程之间的竞争和冲突,这可能导致程序出现不稳定、不可预测的行为。

上次我们对服务端存储的客户端的数据集合进行操作,真实情况 服务端可能面临多频繁的客户传递数据,这次我们需要考虑多线程的问题

synchronized的引用

synchronized是一个关键字,用于控制多个线程对共享资源的访问,以避免并发问题。 

1.方法上的引入

修饰实例方法
当一个方法被synchronized修饰时,该方法就被称为同步方法。这意味着一次只能有一个线程可以进入该方法。当某个线程正在执行同步方法时,其他任何线程都无法访问该方法,直到当前线程执行完该方法并释放锁。

public synchronized void synchronizedMethod() { // 线程安全的代码 }

修饰静态方法
synchronized修饰一个静态方法时,它锁定的是该类的Class对象。这意味着所有访问这个静态同步方法的线程都需要获得这个类的Class对象的锁,这样在一个时间点只能有一个线程能够执行这个静态方法。

public static synchronized void staticSynchronizedMethod() {  
    // 线程安全的代码  
}

2.代码块的引入

修饰代码块
synchronized也可以用来修饰一个代码块,允许更细粒度的控制。这允许开发者指定哪些特定的代码段需要同步,而不是整个方法。在这种情况下,开发者需要提供一个对象作为锁。

this 是 调用改方法的对象类

public void someMethod() {  
    synchronized(this) {  
        // 线程安全的代码  
    }  
}

 总结 : synchronized修饰方法时候整个方法相当于带了一把锁,而修饰代码块的时候就整个代码快就上了一把锁,而且修饰代码快,可以根据具体义务灵活使用,且可以根据争抢的资源进行上锁避免了并发问题出现.

这边举个例子 多个线程调用同一个类的同一个方法

这边同步锁synchronized修饰的代码块锁的是这个boo对象类,这边可以在改为sychronized(this) 注意的是sychronized(boo.class)中的boo.class获取对象的类涉及到反射的概念

public class SyncDemo3 {public static void main(String[] args) {
//        Boo b1 = new Boo();
//        Boo b2 = new Boo();
//        Thread t1 = new Thread(()->b1.doSome());
//        Thread t2 = new Thread(()->b2.doSome());Thread t1 = new Thread(()->Boo.doSome());Thread t2 = new Thread(()->Boo.doSome());t1.start();t2.start();}
}class Boo{
//    public synchronized static void doSome(){public static void doSome(){//显示的获取类对象可以用:类名.class获取到synchronized (Boo.class) {//静态方法中指定同步监视器对象通常就用当前类的类对象try {Thread t = Thread.currentThread();System.out.println(t.getName() + ":正在执行doSome方法...");Thread.sleep(5000);System.out.println(t.getName() + ":执行doSome方法完毕!");} catch (InterruptedException e) {}}}
}

回归正题

服务端

因为我们是服务端收集客户端的数据集合是对集合进行删减,所以我们使用同步锁同步块

  //这边是锁添加集合元素synchronized (allOut) {allOut.add(pw);} 
//这边是锁删除集合元素synchronized (allOut) {allOut.remove(pw);}

同于遍历集合同时,我们服务端的集合可能在添加集合元素也可能在删除集合元素,我们需要加一把互斥锁

使用synchronized关键字实现互斥锁

synchronized是Java内置的语言特性,它提供了隐式的互斥锁。当线程进入synchronized方法或代码块时,它会自动获取对象的内置锁;当线程退出synchronized方法或代码块时,它会自动释放锁。

这边定义一个同步方法,我们知道当两个方法上锁的锁的是对象类,当我们新建一个实例化类的时候,

在调用一个方法时候其对象类会上锁,以保证另一个方法不会被调用 

public class Counter {  private int count = 0;  public synchronized void increment() {  count++;  }  public synchronized int getCount() {  return count;  }  
}

接下来我们解决如何私聊的问题,我们知道在进入聊天室之前会输入昵称

这边只有服务端根据昵称选择对应的客户端进行发送对应的信息即可,

这边先私有一个方法

这边我们使用一个正则表达式,根据 @符判断其属于私聊,

截取信息方面 @nickname: message 可以根据":"进行拆分,这边因为只有一个":"我们使用的substring进行对信息进行拆分  indexof 用于查询对应的元素的返回其所用索引值

思路 第一次if判断用户输出是否符合私聊的正则表达式,第二次if是判断用户是否存在

 private void sendMessageToSomeOne(String message){/*张三->李四 发送一个私聊message:@李四:在吗?@对方昵称:聊天消息@.+:.+tips:.在正则表达式里表示任意一个字符+是一个量词,表示前面的内容出现1次以上所以".+"表示1次以上的任意字符*///进行私聊格式验证if(message.matches("@.+:.+")){//根据聊天消息,截取出对方的昵称        @abc:你好String toNickname = message.substring(1, message.indexOf(":"));if(allOut.contains(toNickname)) {//对方昵称在allOut中是否存在//获取对方的输出流PrintWriter pw = allOut.get(toNickname);//张三悄悄对你说:在吗?String content = message.substring(message.indexOf(":") + 1);pw.println(nickname+"悄悄对你说:"+content);}else{PrintWriter pw = allOut.get(nickname);//用户[abc]不存在pw.println("用户["+toNickname+"]不存在");}}else{//格式不对,则同时该客户端格式不对PrintWriter pw = allOut.get(nickname);pw.println("私聊格式不对,应当是@对方昵称:聊天消息");}}

调用私聊的方法 因为我们服务端接受客户端发送的消息和私聊对应的用户名进行判断

          while ((message = br.readLine()) != null) {//聊天信息以"@"开始,应当是私聊if(message.startsWith("@")){sendMessageToSomeOne(message);}else {sendMessage(nickname + "[" + ip + "]说:" + message);}

 问题,私聊的信息有点乱,我们集合是根据用户的输入的数据存储,并没有将昵称和输入到服务端的值绑定

解决 使用MAP 二维集合利用键值对将其昵称和数据存储

这边利用map添加元素put(index,element)方法 ,

remove(key) 注意这边移除是键值对,c

ontainskey()判断包含元素,

values()获取集合中的值

服务端

// private Collection<PrintWriter> allOut = new ArrayList<>();

private  Map<String,PrintWriter> allOut = new HashMap<>(); 

修改点 集合 添加元素 

 synchronized (allOut) {
//                    allOut.add(pw);allOut.put(nickname,pw);}

 修改点 集合移除文件输出流变为移除对应的昵称

  synchronized (allOut) {
//                    allOut.remove(pw);allOut.remove(nickname);}

修改点 遍历集合 改为对应values遍历

   synchronized (allOut) {
//                for (PrintWriter o : allOut) {//发送给所有客户端for(PrintWriter o : allOut.values()){o.println(message);}}

修改点 私聊是否包含该用户时候变为containkey

    if(allOut.contains(toNickname)) -> if   if(allOut.containskey(toNickname))

 这边是全部代码

客户端

package socket;import java.io.*;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;/***  聊天室客户端*/
public class Client {/*java.net.Socket 套接字Socket封装了TCP协议的通讯细节,使用它可以和远端计算机建立网络链接,并基于两条流(一条输入,一条输出)的读写与对方进行数据交换。*/private Socket socket;/*** 构造器,用于初始化客户端*/public Client(){try {System.out.println("正在链接服务端...");/*Socket实例化时就是与服务端建立链接的过程,此时需要传入两个参数参数1:服务端的IP地址,用于找到服务端的计算机参数2:服务端口,用于找到服务端程序如何查看IP地址:windows:窗口键+R 打开控制台输入ipconfig查看以太网适配器-以太网,找到ipv4查看自己的IP地址mac:打开[终端]程序输入/sbin/ifconfig查看自己的IP地址*/
//            socket = new Socket("127.0.0.1",8088);//127.0.0.1和localhost都是表示本机socket = new Socket("localhost",8088);System.out.println("与服务端成功链接!");} catch (IOException e) {e.printStackTrace();}}/*** 用于客户端开始工作的方法*/public void start(){/*Socket提供的方法:OutputStream getOutputStream()通过Socket获取一个字节输出流,通过向该流写出字节,就可以发送给远端链接的计算机的Socket了*/try {OutputStream out = socket.getOutputStream();OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);BufferedWriter bw = new BufferedWriter(osw);PrintWriter pw = new PrintWriter(bw, true);/*write(int d)通过流向目标位置写出1个字节,写出的是int值2进制的"低八位"*/
//            out.write(1);//00000001Scanner scanner = new Scanner(System.in);//首先要求用户输入一个昵称String nickname = "";while(true) {System.out.println("请输入昵称:");nickname = scanner.nextLine();if(nickname.trim().length() > 0){pw.println(nickname);//将昵称发送给服务端break;}System.out.println("昵称不能为空");}//将接收服务端发送过来消息的线程启动ServerHandler handler = new ServerHandler();Thread t = new Thread(handler);t.setDaemon(true);t.start();System.out.println("开始聊天吧");while(true) {String line = scanner.nextLine();if("exit".equalsIgnoreCase(line)){break;}pw.println(line);}} catch (IOException e) {e.printStackTrace();} finally {try {//socket的close方法会进行四次挥手//并且也会关闭通过socket获取的输入流和输出流socket.close();} catch (IOException e) {e.printStackTrace();}}}public static void main(String[] args) {//实际开发中不会在main方法中写业务逻辑,main方法是静态方法会有很多不便Client client = new Client();//调用构造器初始化客户端client.start();//调用start方法使客户端开始工作}private class ServerHandler implements Runnable{@Overridepublic void run() {try {//通过socket获取输入流,用于读取服务端发送过来的消息InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);BufferedReader br = new BufferedReader(isr);String line;while((line = br.readLine())!=null){System.out.println(line);}} catch (IOException e) {}}}}

服务端

package socket;import java.io.*;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 聊天室服务端*/
public class Server {/*java.net.ServerSocket运行在服务端的ServerSocket相当于时客户中心的"总机",上面有若干的插座(Socket)客户端的插座就是与总机建立链接,然后总机这边分配一个插座与之建立链接,来保持双方通讯的。ServerSocket有两个主要工作1:创建时向系统申请服务端口,以便客户端可以通过端口找到2:监听该端口,一旦一个客户端链接,便创建一个Socket,通过它与客户端通讯*/private ServerSocket serverSocket;//存放所有客户端的输出流,用于广播消息
//    private List<PrintWriter> allOut = new ArrayList<>();/*存放所有客户端的输出流Mapkey:该客户端的昵称value:对应该客户端的输出流,用于给开客户端发送消息*/private Map<String,PrintWriter> allOut = new HashMap<>();public Server(){try {System.out.println("正在启动服务端");/*实例化ServerSocket时需要指定向系统申请的服务端口,如果该端口已经被系统的其他应用程序占据,则这里会抛出异常java.net.BindException: Address already in use: bind()*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕");} catch (IOException e) {throw new RuntimeException(e);}}public void start(){try {while(true) {System.out.println("等待客户端链接...");/*ServerSocket的重要方法:Socket accept()该方法是一个阻塞方法,调用该方法后程序会"卡住",直到一个客户端使用Socket与服务端建立链接为止,此时accept方法会立即返回一个Socket通过返回的Socket就可以与链接的客户端双向通讯了。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");//实例化线程任务ClientHandler handler = new ClientHandler(socket);//实例化线程Thread t = new Thread(handler);//启动线程t.start();}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}private class ClientHandler implements Runnable{private Socket socket;private String ip;//记录当前客户端的IP地址private String nickname;//记录当前客户端的昵称public ClientHandler(Socket socket){this.socket = socket;//通过socket获取远端计算机的IP地址ip = socket.getInetAddress().getHostAddress();}public void run(){PrintWriter pw = null;try {/*Socket的方法:InputStream getInputStream()通过socket获取一个字节输入流,读取该流就可以读取到远端计算机发送过来的数据*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);BufferedReader br = new BufferedReader(isr);//通过socket获取输出流并流链接为PrintWriter,为了给客户端发送消息OutputStream out =  socket.getOutputStream();OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);BufferedWriter bw = new BufferedWriter(osw);pw = new PrintWriter(bw, true);//首先单独读取一行字符串,客户端发送过来的第一行字符串应当是昵称nickname = br.readLine();//将该客户端的输出流存入共享集合中/*每个客户端链接后,主线程都会实例化一个ClientHandler(线程任务)然后实例化一个线程来执行这个任务.当线程获取时间片后开始执行任务的run方法因此,每个线程执行到下面要操作集合的这里,需要让多个线程不能同时操作这个集合那么同步监视器必须让多个线程看到的是同一个对象.这里不能使用this,因为线程在执行ClientHandler的run方法,因此run方法中的这个this是一个ClientHandler的实例,每个线程都在执行各自ClientHandler任务的run方法,因此他们看到的ClientHandler并非同一个对象*/
//                synchronized (this) {//由于他们都要操作allOut集合,因此将它作为同步监视器对象是合适的//实际开发中我们总是使用临界资源作为同步监视器对象,即:抢谁就锁谁synchronized (allOut) {
//                    allOut.add(pw);allOut.put(nickname,pw);}sendMessage(nickname+"上线了,当前在线人数:"+allOut.size());String message;while ((message = br.readLine()) != null) {//聊天信息以"@"开始,应当是私聊if(message.startsWith("@")){sendMessageToSomeOne(message);}else {sendMessage(nickname + "[" + ip + "]说:" + message);}}} catch (IOException e) {//可以添加处理客户端异常断开的操作} finally {//处理客户端断开链接后的操作//将该客户端的输出流从共享集合allOut中删除synchronized (allOut) {
//                    allOut.remove(pw);allOut.remove(nickname);}sendMessage(nickname+"下线了,当前在线人数:"+allOut.size());//将socket关闭,释放底层资源try {socket.close();} catch (IOException e) {throw new RuntimeException(e);}}}/*** 将消息发送给所有客户端*/public void sendMessage(String message){System.out.println(message);//先在服务端控制台上输出一下//遍历要和增删互斥,迭代器要求遍历过程中不可以通过集合方法增删元素synchronized (allOut) {
//                for (PrintWriter o : allOut) {//发送给所有客户端for(PrintWriter o : allOut.values()){o.println(message);}}}/*** 将消息发送给指定用户(私聊)* @param message   格式:@对方昵称:聊天消息*/private void sendMessageToSomeOne(String message){/*张三->李四 发送一个私聊message:@克晶:在吗?@对方昵称:聊天消息@.+:.+tips:.在正则表达式里表示任意一个字符+是一个量词,表示前面的内容出现1次以上所以".+"表示1次以上的任意字符*///进行私聊格式验证if(message.matches("@.+:.+")){//根据聊天消息,截取出对方的昵称        @abc:你好String toNickname = message.substring(1, message.indexOf(":"));if(allOut.containsKey(toNickname)) {//对方昵称在allOut中是否存在//获取对方的输出流PrintWriter pw = allOut.get(toNickname);//张三悄悄对你说:在吗?String content = message.substring(message.indexOf(":") + 1);pw.println(nickname+"悄悄对你说:"+content);}else{PrintWriter pw = allOut.get(nickname);//用户[abc]不存在pw.println("用户["+toNickname+"]不存在");}}else{//格式不对,则同时该客户端格式不对PrintWriter pw = allOut.get(nickname);pw.println("私聊格式不对,应当是@对方昵称:聊天消息");}}}}

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

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

相关文章

Java:接口应用(Clonable 接口和深拷贝)

目录 1.引例2.Object中clone方法的实现3.Cloneable接口讲解4.深拷贝和浅拷贝4.1浅拷贝4.2深拷贝 1.引例 Java 中内置了一些很有用的接口, Clonable 就是其中之一. Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 “拷贝”. 但是要想合法调用 clone 方法。必…

精密电阻阻值表和电容容值表

前面2张是电阻阻值表&#xff08;E-96/0603/1%&#xff09; 常见贴片电容的容值表

解决windows下Qt Creator显示界面过大的问题

&#x1f40c;博主主页&#xff1a;&#x1f40c;​倔强的大蜗牛&#x1f40c;​ &#x1f4da;专栏分类&#xff1a;QT❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 问题描述 解决方法 1、右击此电脑--->属性 2、点击高级系统设置--->点击环境变量 3、 找到系…

【美团笔试题汇总】2023-08-26-美团春秋招笔试题-三语言题解(CPP/Python/Java)

&#x1f36d; 大家好这里是KK爱Coding &#xff0c;一枚热爱算法的程序员 ✨ 本系列打算持续跟新小米近期的春秋招笔试题汇总&#xff5e; &#x1f4bb; ACM银牌&#x1f948;| 多次AK大厂笔试 &#xff5c; 编程一对一辅导 &#x1f44f; 感谢大家的订阅➕ 和 喜欢&#x1f…

想要安装ssh?

SSH&#xff08;Secure Shell&#xff09;是一种加密的网络协议&#xff0c;用于在不安全的网络上安全地进行远程登录和执行命令。它通过加密通信和身份验证机制&#xff0c;确保用户和系统之间的通信是安全的。 SSH协议的主要功能包括&#xff1a; 加密通信&#xff1a;SSH使…

虚良SEO-蜘蛛池的作用与工作原理

蜘蛛池是一种SEO优化工具&#xff0c;其主要作用是吸引搜索引擎蜘蛛到特定网站进行爬行和索引&#xff0c;从而提高网站的可见性和排名。下面分别介绍蜘蛛池的作用和工作原理。 蜘蛛池的作用&#xff1a; 提高网站收录&#xff1a; 当一个网站新发布时&#xff0c;或者长时间…

哈佛大学商业评论 --- 第三篇:真实世界中的增强现实

AR将全面融入公司发展战略&#xff01; AR将成为人类和机器之间的新接口&#xff01; AR将成为人类的关键技术之一&#xff01; 请将此文转发给您的老板&#xff01; --- 本文作者&#xff1a;Michael E.Porter和James E.Heppelmann 虽然物理世界是三维的&#xff0c;但大…

如何查询大数据信用报告?查询时需要注意的事项有哪些?

在数字化时代&#xff0c;大数据信用评分对于需要资金周转的个人或企业来说至关重要。许多机构在贷款审批过程中使用大数据信用评分作为风险控制的重要手段。因此&#xff0c;了解自己的大数据信用状况成为了常规操作。本文将详细介绍如何查询大数据信用报告以及查询时需要注意…

ES6: class类

类 class 面相对象class关键字创建类关于类的继承 面相对象 一切皆对象。 举例&#xff1a; 操作浏览器要使用window对象&#xff1b;操作网页要使用document对象&#xff1b;操作控制台要使用console对象&#xff1b; ES6中增加了类的概念&#xff0c;其实ES5中已经可以实现类…

震惊!子元素的padding/margin-top依据的是父元素的宽度!

子元素的margin-top和padding-top都是以父元素的宽度为基准 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" />…

7.java openCV4.x 入门-Mat之转换、重塑与计算

专栏简介 &#x1f492;个人主页 &#x1f4f0;专栏目录 点击上方查看更多内容 &#x1f4d6;心灵鸡汤&#x1f4d6;我们唯一拥有的就是今天&#xff0c;唯一能把握的也是今天建议把本文当作笔记来看&#xff0c;据说专栏目录里面有相应视频&#x1f92b; &#x1f9ed;文…

openEuler 22.03 SP3 安装图像桌面 UKUI

UKUI 是麒麟软件团队历经多年打造的一款 Linux 桌面&#xff0c;主要基于 GTK 和 QT 开发。与其他 UI 界面相比&#xff0c;UKUI 更加注重易用性和敏捷度&#xff0c;各元件相依性小&#xff0c;可以不依赖其他套件而独自运行&#xff0c;给用户带来亲切和高效的使用体验。 UK…

day2 | 数组 part-2 | 977 有序数组的平方、209 长度最小的子数组、59 螺旋矩阵 II

今日任务 977 有序数组的平方 (题目: . - 力扣&#xff08;LeetCode&#xff09;)209 长度最小的子数组 (题目: ​​​​​​​. - 力扣&#xff08;LeetCode&#xff09;)59 螺旋矩阵 II (题目:. - 力扣&#xff08;LeetCode&#xff09;) 有序数组的平方 (双指针) 给你一个…

如何使用 Viggle AI 生成模特动作视频

Viggle AI 是一款基于骨骼动画的 AI 工具&#xff0c;可以将图片转换为流畅且一致的角色动画。 这意味着您可以上传一张模特全身照&#xff0c;然后指定该模特要执行的动作&#xff0c;Viggle AI 会自动生成一段由该模特执行该动作的视频。 步骤 1&#xff1a;准备工作 首先&…

基于VUE实现的餐厅经营游戏项目源码

WebMOOC 餐厅游戏 项目介绍 实现了一个类游戏的餐厅经营模拟&#xff0c;涉及的前端知识有移动端 HTML 页面布局及样式实现。实现了厨师、顾客等角色的关键操作&#xff0c;完成从顾客等位、点菜、烹饪、用餐、支付的一系列状态变更的数据、信息、交互、展现的变化及处理。 …

【Redis系列】Spring Boot 集成 Redis 实现缓存功能

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

c# wpf template itemtemplate+dataGrid

1.概要 2.代码 <Window x:Class"WpfApp2.Window8"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d"http://schemas.microsoft.com/expression/blend…

SpringCloudAlibabaSeate处理分布式事务

SpringCloudAlibabaSeate处理分布式事务 1、部分面试题 微服务boot/cloud做的项目&#xff0c;你不可能只有一个数据库吧&#xff1f;那么多个数据库之间如何处理分布式事务的&#xff1f; 一个场景&#xff1a;在订单支付成功后&#xff0c;交易中心会调用订单中心的服务把订…

导入项目运行后,报错java: Cannot find JDK ‘XX‘ for module ‘XX‘

解决方案&#xff1a; 1、删除.idea和.iml文件 2、右击此module&#xff0c;点击 Open Module Settings 在 Module SDK 中选择所安装的java版本后&#xff0c;点击右下角 Apply 3、再运行试试吧&#xff0c;成功&#xff01;

蓝桥杯(4):python动态规划DF[1]

动态规划相当于正着想&#xff1f;dfs主要适用于位置的变化&#xff1f; 子问题&#xff01;状态&#xff0c;状态转移方程 1 一维DP 1.1 定义 重叠子问题&#xff01;转换成子问题 &#xff0c;与记忆化搜索很像 1.2 例子 1.2.1 上楼梯 子问题到最终的问题只能跨一步&…