知识图库
Java知识库
JDK线程池实现原理
Java中的强、软、弱、虚引用
深入拆解Java虚拟机
01 开篇词 | 为什么我们要学习Java虚拟机?
02 Java代码是怎么运行的?
03 Java的基本类型
04 Java虚拟机是如何加载Java类的?
05 JVM是如何执行方法调用的?(上)
06 JVM是如何执行方法调用的?(下)
7 JVM是如何处理异常的?
Java面试常见问题整理
Java面试常见问题-Java 基础篇
Java面试常见问题-Jvm篇
Java面试常见问题-并发篇
Android知识库
Kotlin编程第一课
1 开篇词 | 入门Kotlin有多容易,精通Kotlin就有多难
2 Kotlin基础语法:正式开启学习之旅
3 面向对象:理解Kotlin设计者的良苦用心
4 Kotlin原理:编译器在幕后干了哪些“好事”?
5 实战:构建一个Kotlin版本的四则运算计算器
6 object关键字:你到底有多少种用法?
7 扩展:你的能力边界到底在哪里?
8 高阶函数:为什么说函数是Kotlin的“一等公民”?
9 实战:用Kotlin写一个英语词频统计程序
10 加餐一 | 初识Kotlin函数式编程
11 委托:你为何总是被低估?
12 泛型:逆变or协变,傻傻分不清?
13 注解与反射:进阶必备技能
14 实战:用Kotlin实现一个网络请求框架KtHttp
15 加餐二 | 什么是“表达式思维”?
16 加餐三 | 什么是“不变性思维”?
17 加餐四 | 什么是“空安全思维”?
18 春节刷题计划(一)| 当Kotlin遇上LeetCode
19 春节刷题计划(二)| 一题三解,搞定版本号判断
20 春节刷题计划(三)| 一题双解,搞定求解方程
21 春节刷题计划(四)| 一题三解,搞定分式加减法
22 什么是“协程思维模型”?
23 如何启动协程?
24 挂起函数:Kotlin协程的核心
25 Job:协程也有生命周期吗?
26 Context:万物皆为Context?
27 实战:让KtHttp支持挂起函数
28 期中考试 | 用Kotlin实现图片处理程序
29 题目解答 | 期中考试版本参考实现
30 Channel:为什么说Channel是“热”的?
31 Flow:为什么说Flow是“冷”的?
32 select:到底是在选择什么?
33 并发:协程不需要处理同步吗?
34 异常:try-catch居然会不起作用?坑!
35 实战:让KtHttp支持Flow
36 答疑(一)| Java和Kotlin到底谁好谁坏?
37 集合操作符:你也会“看完就忘”吗?
38 协程源码的地图:如何读源码才不会迷失?
39 图解挂起函数:原来你就是个状态机?
40 加餐五 | 深入理解协程基础元素
41 launch的背后到底发生了什么?
42 Dispatchers是如何工作的?
43 CoroutineScope是如何管理协程的?
44 图解Channel:如何理解它的CSP通信模型?
45 图解Flow:原来你是只纸老虎?
46 Java Android开发者还会有未来吗?
47 Kotlin与Jetpack简直是天生一对!
48 用Kotlin写一个GitHub Trending App
49 结课测试 | “Kotlin编程第一课”100分试卷等你来挑战!
50 结束语 | 不忘初心
Android Framework 教程—基础篇
01 Ubuntu 使用快速入门
02 Make 构建工具入门
03 理解 Unicode UTF-8 UTF-16 UTF-32
04 Linux Shell 脚本编程入门1——核心基础语法
05 SeAndroid 使用极速上手
06 理解 C++ 的 Memory Order
07 AOSP 极速上手
08 系统开发工具推荐
09 添加 Product
运动相关知识
爱上跑步
01 开篇词 | 跑步,不那么简单的事儿
02 跑两步就喘了,是不是我不适合跑步?
03 正确的跑步姿势是什么样的?
04 为什么跑步要先热身?
05 怎样制定你的第一个10公里跑步计划?
06 快跑和慢跑,哪个更燃脂?
07 普通跑步者应该如何选择跑鞋?
08 买跑步装备,不要踩这些坑儿
09 跑步前到底应不应该吃东西?
10 跑步到底伤不伤膝盖?
11 参加了20场马拉松,我是如何准备的?
12 除了马拉松,还能参加哪些跑步赛事?
13 热点问题答疑 :跑完第二天浑身疼,还要不要继续跑?
健身房计划
[DeepSeek]减脂塑形计划
【DeepSeek】训练周期安排
每日餐饮热量控制
减脂期间食物推荐避坑指南
HarmonyOS知识库
其他知识类目
心理学相关
如何学点心理学——关于非专业人士学心理学的一点建议
投射性认同
-
+
首页
20 春节刷题计划(三)| 一题双解,搞定求解方程
初二过年好! 在上节课里,我给你留了一个作业,那就是:用Kotlin来完成 LeetCode的640号题《求解方程》。那么这节课,我就来讲讲我的解题思路,我们互相学习。 这道题也非常容易理解,程序的输入是一个“一元一次方程”,我们需要根据输入的方程,计算出正确的结果。根据输入方程的不同,结果可能有三种情况: - 方程仅有一个解,这时,我们只需要按照格式返回结果即可,比如输入“2x=4”,那么输出就应该是“x=2”。 - 方程有无数个解,比如输入“x=x”,那么输出就应该是“Infinite solutions”。 - 方程无解,比如输入“x=x+5”,那么输出结果就应该是“No solution”。 另外,对于程序的输入格式,其实我们还有几个问题需要弄清楚。只有弄清楚了这些问题,我们才能开始写代码: - 方程当中的未知数只会用x表示,不会是y,也不会是大写的“X”。 - 方程当中不会出现空格,比如“2x=4”,不会出现“2x = 4 ”的情况。 - 方程当中只会有加减法,不会出现乘除法。 - 方程当中的数字,一定是整数,不会出现分数、小数。 - 输入的方程一定是一个正确的方程,不会出现“x=…”之类的脏数据。 好,问题的细节都弄清楚了,下面我们来分析一下解题的思路。 对于这种简单的一元一次方程的解法,其实我们在小学就学过了,概括起来,就是分为三个步骤。 - 第一步,移项。将含有x的式子全部移到等式的左边,将数字全部都移到等式的右边。另外,移项的时候符号要变。比如“3x-4=x+2”这个方程,移项以后,就会变成这样:“3x-x=2+4”。 - 第二步,合并同类项。这里其实就是将等式的左边与右边合并起来,对于“3x-x=2+4”这个式子,合并完以后,就会变成“2x=6”。 - 第三步,系数化为一。这时候,我们就需要拿右边的数字,除以左边的系数。比如上面的式子“2x=6”,系数化为一之后,就会变成“x=3”,这就是我们想要的方程解。当然,这只是方程只有一个解的情况,其实在系数化为一之前,还存在其他的情况,比如“x=x+5”最终会变成“0=5”,这时候左边是零,右边不是零,这时候就代表方程无解;对于“2x=2x”这样的方程,它最终会变成“0=0”,这种两边都等于零的情况,就代表了方程有无数个解。 好,如何求解方程的思路我们已经知道了,那么代码该如何写呢?这里,我们仍然有两种解法,这两种解法的思路是一致的,只是其中一种是偏命令式的,另一种是偏函数式的。 这里,我照样是制作了一张动图,给你展示下程序运行的整体思路:  ### 解法一:命令式 首先,我们按照前面分析的思路,把待实现的程序分为以下几个步骤: ```kotlin fun solveEquation(equation: String): String { // ① 分割等号 // ② 遍历左边的等式,移项,合并同类项 // ③ 遍历右边的等式,移项,合并同类项 // ④ 系数化为一,返回结果 } ``` 根据注释,我们很容易就能完成其中①、④两个步骤的代码: ```kotlin fun solveEquation(equation: String): String { // ① 分割等号 val list = equation.split("=") // ② 遍历左边的等式,移项,合并同类项 // ③ 遍历右边的等式,移项,合并同类项 // ④ 系数化为一,返回结果 return when { leftSum == 0 && rightSum == 0 -> "Infinite solutions" leftSum == 0 && rightSum != 0 -> "No solution" else -> "x=${rightSum / leftSum}" } } ``` 现在,关键还是在于②、③两个步骤的代码。这里,list[0]其实就代表了左边的式子,list[1]就代表了右边的式子。 按照之前的思路分析,我们其实用两个for循环,分别遍历它们,然后顺便完成移项与合并同类项就行了。具体的代码如下: ```kotlin var leftSum = 0 var rightSum = 0 val leftList = splitByOperator(list[0]) val rightList = splitByOperator(list[1]) // ② 遍历左边的等式,移项,合并同类项 leftList.forEach { if (it.contains("x")) { leftSum += xToInt(it) } else { rightSum -= it.toInt() } } // ③ 遍历右边的等式,移项,合并同类项 rightList.forEach{ if (it.contains("x")) { leftSum -= xToInt(it) } else { rightSum += it.toInt() } } ``` 这段代码的逻辑其实也比较清晰了,leftList、rightList是根据“+”、“-”分割出来的元素。在完成分割以后,我们再对它们进行了遍历,从而完成了移项与合并同类项。 并且,这里我们还用到了另外两个方法,分别是splitByOperator()、xToInt(),它们具体的代码如下: ```kotlin private fun splitByOperator(list: String): List { val result = mutableListOf() var temp = "" list.forEach { if (it == '+' || it == '-') { if (temp.isNotEmpty()) { result.add(temp) } temp = it.toString() } else { temp += it } } result.add(temp) return result } private fun xToInt(x: String) = when (x) { "x", "+x" -> 1 "-x" -> -1 else -> x.replace("x", "").toInt() } ``` 从以上代码中,我们可以看到splitByOperator()就是使用“+”、“-”作为分隔符,将字符串类型的式子,分割成一个个的元素。而xToInt()的作用则是为了提取x的系数,比如“2x”,提取系数以后,就是“2”;而“-2x”的系数就是“-2”。 最后,我们再来看看整体的代码: ```kotlin fun solveEquation(equation: String): String { // ① 分割等号 val list = equation.split("=") var leftSum = 0 var rightSum = 0 val leftList = splitByOperator(list[0]) val rightList = splitByOperator(list[1]) // ② 遍历左边的等式,移项,合并同类项 leftList.forEach { if (it.contains("x")) { leftSum += xToInt(it) } else { rightSum -= it.toInt() } } // ③ 遍历右边的等式,移项,合并同类项 rightList.forEach{ if (it.contains("x")) { leftSum -= xToInt(it) } else { rightSum += it.toInt() } } // ④ 系数化为一,返回结果 return when { leftSum == 0 && rightSum == 0 -> "Infinite solutions" leftSum == 0 && rightSum != 0 -> "No solution" else -> "x=${rightSum / leftSum}" } } // 根据“+”、“-”分割式子 private fun splitByOperator(list: String): List { val result = mutableListOf() var temp = "" list.forEach { if (it == '+' || it == '-') { if (temp.isNotEmpty()) { result.add(temp) } temp = it.toString() } else { temp += it } } result.add(temp) return result } // 提取x的系数:“-2x” ->“-2” private fun xToInt(x: String) = when (x) { "x", "+x" -> 1 "-x" -> -1 else -> x.replace("x", "").toInt() } ``` 至此,偏命令式的代码就完成了,接下来我们看看偏函数式的代码该怎么写。 ### 解法二:函数式 这里你要注意了,函数式的思路呢,和命令式的思路其实是一样的。解方程的步骤是不会变的,仍然是移项、合并同类项、系数化为一。只不过,对比前面的实现方式,我们这里会更多地借助Kotlin的标准库函数。 首先,我们来看看第一部分的代码怎么写: ```kotlin fun solveEquation(equation: String): String { val list = equation .replace("-", "+-") // 预处理逻辑 .split("=") // 用“+”分割字符串 val leftList = list[0].split("+") val rightList = list[1].split("+") // 省略 } ``` 这里,为了可以直接使用Kotlin的库函数split来实现算式的分割,我使用了一种数据预处理的办法。你可以看到,在上面代码的注释处,replace("-", "+-") 的作用是将算式当中的所有“-”替换成“+-”,这就是预处理。经过这个预处理后,我们就可以直接使用 split("+") 来分割算式了。 为了体现这个细节,我这里也做了一个动图,你可以看看:  这样一来,我们得到的leftList、rightList其实就是干净的、独立的数字和x式子了。以“x+5-3+x=6+x-2”为例,leftList=["x","5","-3","x"],而rightList=["6","x","-2"]。 既然它们两者都是普通的集合,那么我们接下来,就完全可以借助Kotlin强大的库函数来做剩下的事情了。我们只需要将所有x的式子挪到左边,所有数字挪到右边,然后合并,最后系数化为一即可。大致代码如下: ```kotlin leftList .filter { it.hasX() } .map { xToInt(it) } // ① .toMutableList() .apply { rightList .filter { it.hasX() } .map { xToInt(it).times(-1) } // ② .let { addAll(it) } }.sum() // ③ .let { leftSum = it } rightList .filter { it.isNumber() } .map { it.toInt() } // ④ .toMutableList() .apply { leftList .filter { it.isNumber() } .map { it.toInt().times(-1) } // ⑤ .let { addAll(it) } }.sum() // ⑥ .let { rightSum = it } // 返回结果 return when { leftSum == 0 && rightSum == 0 -> "Infinite solutions" leftSum == 0 && rightSum != 0 -> "No solution" else -> "x=${rightSum / leftSum}" // ⑦ } ``` 上面这段代码中,一共有6个注释,我们一个个看: - 注释①,我们提取出了左边式子里所有x的系数,这里不需要移项,因为它本来就在左边。 - 注释②,我们提取了右边式子里所有x的系数,由于这里涉及到移项,因此需要变号,这里我们通过乘以一个“-1”来实现的。 - 注释③,我们将所有x的系数合并到了一起,得到了左边x的系数之和。 - 注释④,我们收集了右边式子里所有的数字,这里也不需要移项,因为它本来就在右边。 - 注释⑤,我们收集了左边式子里所有的数字,这里要移项,所以要变号。 - 注释⑥,我们将所有数字求和了。 - 注释⑦,如果方程有解的话,我们通过“rightSum / leftSum”就可以计算出来了。 另外,以上代码其实还涉及到三个辅助的函数,需要我们自己实现,它们的逻辑都很简单: ```kotlin private fun String.isNumber(): Boolean = this != "" && !this.contains("x") private fun String.hasX(): Boolean = this != "" && this.contains("x") // 提取x的系数:“-2x” ->“-2” private fun xToInt(x: String) = when (x) { "x" -> 1 "-x" -> -1 else -> x.replace("x", "").toInt() } ``` xToInt()这个函数和之前的逻辑是相似的,isNumber()和hasX()这两个扩展函数,它们是用来判断式子是纯数字、还是含有x的,这是因为我们要把x放到等式左边,而数字要放到等式右边。 最后,我们再来看看整体的代码: ```kotlin fun solveEquation(equation: String): String { val leftSum: Int val rightSum: Int val list = equation .replace("-", "+-") // 预处理数据 .split("=") val leftList = list[0].split("+") val rightList = list[1].split("+") // 求出所有x的系数之和 leftList .filter { it.hasX() } .map { xToInt(it) } .toMutableList() .apply { rightList .filter { it.hasX() } .map { xToInt(it).times(-1) } .let { addAll(it) } }.sum() .let { leftSum = it } // 求出所有数字之和 rightList .filter { it.isNumber() } .map { it.toInt() } .toMutableList() .apply { leftList .filter { it.isNumber() } .map { it.toInt().times(-1) } .let { addAll(it) } }.sum() .let { rightSum = it } // 返回结果 return when { leftSum == 0 && rightSum == 0 -> "Infinite solutions" leftSum == 0 && rightSum != 0 -> "No solution" else -> "x=${rightSum / leftSum}" } } private fun String.isNumber(): Boolean = this != "" && !this.contains("x") private fun String.hasX(): Boolean = this != "" && this.contains("x") // 提取x的系数:“-2x” ->“-2” private fun xToInt(x: String) = when (x) { "x" -> 1 "-x" -> -1 else -> x.replace("x", "").toInt() } ``` ### 小结 这节课,我们用两种方式实现了[LeetCode的640号题《求解方程》](https://leetcode.cn/problems/solve-the-equation/description/ "LeetCode的640号题《求解方程》")。这两种解法的核心思路其实是一致的,不过前者是偏命令式的,后者是偏函数式的。而你要清楚,即使它们是用的一种思路,也仍然是各有优劣的。 - 解法一,命令式的代码,它的时间复杂度和空间复杂度要稍微好一些,但总体差距不大,所以不一定能体现出运行时的差异。这种方式的劣势在于,逻辑相对复杂,可读性稍差,且编码过程中容易出错。 - 解法二,偏函数式的代码,它的优势在于,代码逻辑相对清晰,并且,由于运用了大量Kotlin库函数,没那么容易出错。 ### 小作业 好,最后,我还是给你留一个小作业,请你用Kotlin来完成 [LeetCode的592号题《分数加减运算》](https://leetcode.cn/problems/fraction-addition-and-subtraction/description/ "LeetCode的592号题《分数加减运算》"),下节课我也会给出我的答案。
嘿手大叔
2024年10月28日 16:51
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码