你好,我是朱涛。今天是除夕夜,先祝你虎年春节快乐!
在上节刷题课中,我给你留了一个作业,那就是:用Kotlin来完成 LeetCode的165号题《版本号判断》。那么今天这节课,我就来讲讲我的解题思路,希望能给你带来一些启发。
这道题目其实跟我们平时的工作息息相关。给你两个字符串代表的版本号,需要你判断哪个版本号是新的,哪个版本号是旧的。比如,2.0与1.0对比的话,2.0肯定是新版本,1.0肯定是旧版本。对吧?
不过,这里面还有一些问题需要留意,这些都是我们在正式写代码之前要弄清楚的。
好了,理解了题意以后,我们就可以开始写代码了,LeetCode上面给了我们一个待实现的方法,大致如下:
fun compareVersion(version1: String, version2: String): Int {// 待完善}
分析完题目以后,也许你已经发现了,这道题目其实并不需要什么特殊的数据结构和算法基础,这是一道单纯的“模拟题”。我们脑子里是如何对比两个版本号的,我们的代码就可以怎么写。
下面我做了一个动图,展示了版本号对比的整体流程。
我们可以看到,这个对比的流程,大致可以分为以下几个步骤。
所以,按照上面的思路,我们可以把compareVersion()这个函数分为以下几个部分:
fun compareVersion(version1: String, version2: String): Int {// ① 使用“.”,分割 version1 和 version2,得到list1、list2// ② 同时遍历list1、list2,取出的元素v1、v2,并将其转换成整数,这里注意补零操作// ③ 对比v1、v2的大小,如果它们两者不一样,我们就可以终止流程,直接返回结果。// ④ 当遍历完list1、list2后仍然没有判断出大小话,说明两个版本号其实是相等的,这时候应该返回0}
那么接下来,其实就很简单了。我们只需要将注释里面的自然语言,用代码写出来就行了。具体代码如下:
fun compareVersion(version1: String, version2: String): Int {// ① 分割val list1 = version1.split(".")val list2 = version2.split(".")var i = 0while (i < list1.size || i < list2.size) {// ② 遍历元素val v1 = list1.getOrNull(i)?.toInt()?:0val v2 = list2.getOrNull(i)?.toInt()?:0// ③ 对比if (v1 != v2) {return v1.compareTo(v2)}i++}// ④ 相等return 0}
在上面的代码中,有两个地方需要格外注意。
一个是while循环的条件。由于list1、list2的长度可能是不一样的,所以,我们的循环条件是:list1、list2当中只要有一个没有遍历完的话,我们就要继续遍历。
还有一个需要注意的地方,getOrNull(i),这是Kotlin独有的库函数。使用这个方法,我们不必担心越界问题,当index越界以后,这个方法会返回null,在这里我们把它跟 Elvis表达式结合起来,就实现了自动补零操作。这也体现出了Kotlin表达式语法的优势。
好,到这里,我们就用第一种思路实现了版本号对比的算法。下面我们再来看看第二种思路。
前面的思路,我们是使用的Kotlin的库函数split()进行分割,然后对列表进行遍历来判断的版本号。其实,这种思路还可以进一步优化,那就是我们自己遍历字符串,来模拟split的过程,然后在遍历过程中,我们顺便就把比对的工作一起做完了。
思路二的整体过程比较绕,我同样是制作了一个动图来描述这个算法的整体流程:
以上的整体算法过程,是典型的“双指针”思想。运用这样的思想,我们大致可以写出下面这样的代码:
fun compareVersion(version1: String, version2: String): Int {val length1 = version1.lengthval length2 = version2.length// ①var i = 0var j = 0// ②while (i < length1 || j < length2) {// ③var x = 0while (i < length1 && version1[i] != '.') {x = x * 10 + version1[i].toInt() - '0'.toInt()i++}i++// ④var y = 0while (j < length2 && version2[j] != '.') {y = y * 10 + version2[j].toInt() - '0'.toInt()j++}j++// ⑤if (x != y) {return x.compareTo(y)}}// ⑥return 0}
这段代码一共有6个注释,我们来一个个解释。
现在,我们就已经用Kotlin写出了两个题解,使用的思路都是命令式的编程方式。也许你会好奇,这个问题能用函数式的思路来实现吗?
答案当然是可以的!
我们在前面就提到过,Kotlin是支持多范式的,我们可以根据实际场景来灵活选择编程范式。那么在这里,我们可以借鉴一下前面第一种解法的思路。
其实,想要解决这个问题,我们只要能把version1、version2转换成两个整数的列表,就可以很好地进行对比了。我制作了一个动图,方便你理解:
根据这个流程,我们可以大致写出下面这样的代码:
fun compareVersion(version1: String, version2: String): Int =version1.split(".").zipLongest(version2.split("."), "0") // ①.onEach { // ②with(it) {if (first != second) {return first.compareTo(second)}}}.run { return 0 }
这段代码看起来很简洁,核心的逻辑在两个方法当中,我分别用注释标注了。
现在,你可能会感慨,这代码看起来真香啊!这个嘛……别高兴得太早。虽然Kotlin支持基础的zip语法,但它目前还不支持zipLongest()这么高级的操作符。
那么这该怎么办呢?我们只能自己来实现zipLongest()了!为了让前面的代码通过编译,我们必须要自己动手实现下面三个扩展函数。
private fun Iterable<String>.zipLongest(other: Iterable<String>,default: String): List<Pair<Int, Int>> {val first = iterator()val second = other.iterator()val list = ArrayList<Pair<Int, Int>>(minOf(collectionSizeOrDefault(10), other.collectionSizeOrDefault(10)))while (first.hasNext() || second.hasNext()) {val v1 = (first.nextOrNull() ?: default).toInt()val v2 = (second.nextOrNull() ?: default).toInt()list.add(Pair(v1, v2))}return list}private fun <T> Iterable<T>.collectionSizeOrDefault(default: Int): Int =if (this is Collection<*>) this.size else defaultprivate fun <T> Iterator<T>.nextOrNull(): T? = if (hasNext()) next() else null// Pair 是Kotlin标准库提供的一个数据类// 专门用于存储两个成员的数据// 提交代码的时候,Pair不需要拷贝进去public data class Pair<out A, out B>(public val first: A,public val second: B) : Serializable {public override fun toString(): String = "($first, $second)"}
这三个扩展函数实现起来还是比较简单的,zipLongest()其实就是合并了两个字符串列表,然后将它们按照index合并成Pair,另外那两个扩展函数都只是起了辅助作用。
这样,我们把前面的代码一起粘贴到LeetCode当中,其实代码是可以通过的。不过呢,我们的代码当中其实还有一个比较深的嵌套,看起来不是很顺眼:
fun compareVersion(version1: String, version2: String): Int =version1.split(".").zipLongest(version2.split("."), "0").onEach {// 这里的嵌套比较深with(it) {if (first != second) {return first.compareTo(second)}}}.run { return 0 }
你可以注意到,在onEach当中,有一个代码块,它有两层嵌套,这看起来有点丑陋。那么,我们能不能对它进一步优化呢?
当然是可以的。
这里,我们只需要想办法让onEach当中的Lambda,变成带接收者的函数类型即可。具体做法就是,我们自己实现一个新的onEachWithReceiver()的高阶函数。
// 注意这里// ↓inline fun <T, C : Iterable<T>> C.onEachWithReceiver(action: T.() -> Unit): C {return apply { for (element in this) action(element) }}// 注意这里// Kotlin库函数当中的onEach ↓public inline fun <T, C : Iterable<T>> C.onEach(action: (T) -> Unit): C {return apply { for (element in this) action(element) }}
上面的代码展示了onEach()和onEachWithReceiver()之间的差别,可以看到,它们两个的函数体其实没有任何变化,区别只是action的函数类型而已。
所以在这里,借助onEachWithReceiver(),就可以进一步简化我们的代码:
fun compareVersion(version1: String, version2: String): Int =version1.split(".").zipLongest(version2.split("."), "0").onEachWithReceiver {// 减少了一层嵌套if (first != second) {return first.compareTo(second)}}.run { return 0 }
在这段代码中,我们把onEach()改成了onEachWithReceiver(),因为它里面的Lambda是带有接收者,原本的Pair对象变成了this对象,这样,我们就可以直接使用first、second来访问Pair当中的成员了。
现在,就让我们来看看整体的代码吧:
fun compareVersion(version1: String, version2: String): Int =version1.split(".").zipLongest(version2.split("."), "0").onEachWithReceiver {if (first != second) {return first.compareTo(second)}}.run { return 0 }private inline fun <T, C : Iterable<T>> C.onEachWithReceiver(action: T.() -> Unit): C {return apply { for (element in this) action(element) }}private fun <T> Iterable<T>.collectionSizeOrDefault(default: Int): Int =if (this is Collection<*>) this.size else defaultprivate fun <T> Iterator<T>.nextOrNull(): T? = if (hasNext()) next() else nullprivate fun Iterable<String>.zipLongest(other: Iterable<String>,default: String): List<Pair<Int, Int>> {val first = iterator()val second = other.iterator()val list = ArrayList<Pair<Int, Int>>(minOf(collectionSizeOrDefault(10), other.collectionSizeOrDefault(10)))while (first.hasNext() || second.hasNext()) {val v1 = (first.nextOrNull() ?: default).toInt()val v2 = (second.nextOrNull() ?: default).toInt()list.add(Pair(v1, v2))}return list}
好了,这就是我们的第三种思路。看完这三种思路以后,你会更倾向于哪种思路呢?
这节课,我们使用了三种思路,实现了LeetCode的165号题《版本号判断》。其中,前两种思路,是命令式的编程方式,第三种是偏函数式的方式。在我看来呢,这三种方式各有优劣。
第三个思路其实还有一个额外的优势,那就是,我们自己实现的扩展函数,可以用于以后解决其他问题。这就相当于沉淀出了有用的工具。
好,最后,我还是给你留一个小作业,请你用Kotlin来完成LeetCode的640号题《求解方程》。这道题目我同样会在下节课给出答案解析。
欢迎继续给我留言,我们下节课再见。