Kotlin函数式编程经典案例

望宸阁

Kotlin的一个很好的特性是支持函数式编程,本文分为集合操作幂集排序三部分来分析下Kotlin函数式编程。

image.png

集合操作

假设我们有一个学生的模型类:

data class Student(
        val name: String,
        val surname: String,
        val passing: Boolean,
        val averageGrade: Double // > 4
)

包含姓、名、是否及格、平均成绩四个属性。我们从所有学生中筛选出成绩最好的10个人:

var seleteTen = students.filter { it.passing && it.averageGrade > 4 }
            .sortedBy { it.averageGrade }
            .take(10)
            .sortedWith(compareBy({it.surname}, {it.name}))
  1. 首先必须及格并且平均成绩 > 4
  2. 根据平均成绩排序(默认从小到大)
  3. 从排好序的学生中选择前10个
  4. 将这10个学生根据姓排序,姓一样的话再根据名排序

如果我们不根据姓名排序,而是保持原来的顺序不变,我们可以添加索引保持顺序不变:

var obtainOrder = students.filter { it.passing && it.averageGrade > 4.0 }
            .withIndex() // 1
            .sortedBy { (_, s) -> s.averageGrade } // 2
            .take(10)
            .sortedBy { (i, _) -> i } // 3
            .map { (_, s) -> s } // 4
  1. 为每个学生添加当前的索引
  2. 使用之前我们需要析构值和索引
  3. 根据索引排序
  4. 删除索引并仅保持值

完整代码:

fun main(vararg args: String) {
    var zzh = Student("zzh", "zzh", true, 100.0)
    var zyw = Student("zyw", "zyw", true, 200.0)
    var lj = Student("lj", "lj", true, 120.0)
    var lcg = Student("lcg", "lcg", true, 110.0)
    var zzy = Student("zzy", "zzy", true, 400.0)
    var fgt = Student("fgt", "fgt", true, 123.0)
    var efr = Student("efr", "efr", true, 118.0)
    var lok = Student("lok", "lok", true, 191.0)
    var hgt = Student("hgt", "hgt", true, 864.0)
    var jnd = Student("jnd", "jnd", true, 765.0)
    var tpc = Student("tpc", "tpc", true, 491.0)
    var hpz = Student("hpz", "hpz", true, 691.0)

    var fhy = Student("fhy", "fhy", false, 1.0)
    var yly = Student("yly", "yly", false, 00.0)
    var weg = Student("weg", "weg", false, 2.2)
    var tyu = Student("tyu", "tyu", false, 2.5)
    var qwe = Student("qwe", "qwe", false, 3.2)
    var oiu = Student("oiu", "oiu", false, 1.6)
    var iuh = Student("iuh", "iuh", false, 3.1)
    var pnh = Student("pnh", "pnh", false, 1.9)

    var students = arrayListOf<Student>(zzh, zyw, lj, lcg, zzy, fgt, efr, lok, hgt, jnd, tpc, hpz,
            fhy, yly, weg, tyu, qwe, oiu, iuh, pnh)

    var seleteTen = students.filter { it.passing && it.averageGrade > 4 }
            .sortedBy { it.averageGrade }
            .take(10)
            .sortedWith(compareBy({it.surname}, {it.name}))
//            .toList()
    seleteTen.forEach {
        println(it)
    }

    println("----------------")
    var obtainOrder = students.filter { it.passing && it.averageGrade > 4.0 }
            .withIndex() // 1
            .sortedBy { (_, s) -> s.averageGrade } // 2
            .take(10)
            .sortedBy { (i, _) -> i } // 3
            .map { (_, s) -> s } // 4
    obtainOrder.forEach {
        println(it)
    }
}

data class Student(
        val name: String,
        val surname: String,
        val passing: Boolean,
        val averageGrade: Double // > 4
)
image.png

幂集

高中数学我们学习了集合的概念,应该还记得有个幂集(Powerset)的知识点:所谓幂集,就是原集合S中所有的子集(包括全集和空集)构成的一个集合P(S)或者称作2S,P(S) = { U | U ⊆ S }。

假设我们有这样的一个集合:S = {1,2,3},那么S的幂集就是:P(S) = {{}, {1}, {2}, {3}, {1,2}, {1,3}, {2,3}, {1,2,3}}

问题来啦,怎么使用Kotlin代码实现求幂集的算法呢?如果你想挑战自己,那么请停止阅读本文,自己实现这个算法之后再来继续阅读。

我们要通过观察来发现规律,比如对于幂集P(S)的每一个不可拆分的元素比如1,所有包含这个元素的集合是{1}, {1,2}, {1,3}, {1,2,3},不包含这个元素的集合是:{}, {2}, {3}, {2,3}

注意第二个集合是集合{2,3}的幂集,第一个集合是这个幂集的每个元素加上1形成的集合。方法来了:先取出一个元素e,计算出其余元素组成的集合的幂集A,将A内的每个集合加上元素e形成集合B,集合A与B的并集就是最终我们要求的幂集。

fun <T> powerset(set: Set<T>): Set<Set<T>> {
    val first = set.first()
    val powersetOfRest = powerset(set.drop(1).toSet())
    return powersetOfRest.map { it + first }.toSet() + powersetOfRest
}

上面的代码还有个问题,因为第一行的first()方法不允许空集合调用,否则会抛出异常,那么我们就定义空集合的幂集还是空集合:

fun <T> powerset(set: Set<T>): Set<Set<T>> {
    return if (set.isEmpty()) setOf(setOf())
    else {
        val first = set.first()
        val powersetOfRest = powerset(set.drop(1).toSet())
        powersetOfRest.map { it + first }.toSet() + powersetOfRest
    }
}
image.png

我们分析下算法流程,比如要计算集合{1,2,3}的幂集:

  1. powerset({1,2,3}) = powerset({2,3}) + powerset({2,3}).map { it + 1 }
  2. powerset({2,3}) = powerset({3}) + powerset({3}).map { it + 2}
  3. powerset({3}) = powerset({}) + powerset({}).map { it + 3}
  4. powerset({}) = {{}}
  5. powerset({3}) = {{}, {3}}
  6. powerset({2,3}) = {{}, {3}} + {{2}, {2, 3}} = {{}, {2}, {3}, {2, 3}}
  7. powerset({1,2,3}) = {{}, {2}, {3}, {2, 3}} + {{1}, {1, 2}, {1, 3}, {1, 2, 3}} = {{}, {1}, {2}, {3}, {1,2}, {1,3}, {2,3}, {1,2,3}}

上面的代码不够kotlinc,什么叫做kotlinc?我们使用Kotlin提供的操作符let进行优化,不懂let操作符的同学可以看下这篇文章,下面优化后的代码就比较kotlinc,感受下:

fun <T> powerset(set: Set<T>): Set<Set<T>> {
    return if (set.isEmpty()) setOf(setOf())
    else {
        powerset(set.drop(1).toSet())
                .let { it + it.map { it + set.first() } }
    }
}

只能对集合才能求幂集,那么我们可以使用Kotlin的一个特性:扩展函数,我们对集合类Set添加扩展函数:

fun <T> Collection<T>.powerset(): Set<Set<T>> =
        if (isEmpty()) setOf(setOf())
        else drop(1)
                .powerset()
                .let { it + it.map { it + first() } }

众所周知,上面的递归方法有很大的风险,因为每次递归的上一次递归都要保存在内存中,对于很大的集合用递归的方法求其幂集的话肯定会出现堆栈溢出的问题。

Kotlin支持一种叫做尾递归的编程风格。这允许一些通常意义上的使用递归函数替代循环的算法,当一个函数被tailrec修饰并遇到符合的形式,编译器会优化递归,替代为一个快速并有效率的基于循环的版本来避免堆栈溢出的风险。

因此,我们需要用尾递归进行优化。tailrec要求函数调用自身必须是最后才能做的事情,否则tailrec这个关键字就形同虚设并会有语法警告,而且tailrec不能与 try/cathch/finally块一起使用,这样的话其实内存只保存最后一次的迭代,不存在堆栈溢出的问题:

fun <T> Collection<T>.powerset(): Set<Set<T>> =
        powerset(this, setOf(setOf()))

private tailrec fun <T> powerset(left: Collection<T>, acc: Set<Set<T>>): Set<Set<T>> =
        if (left.isEmpty()) acc
        else powerset(left.drop(1), acc + acc.map { it + left.first() })

求幂集还有第二种方法,利用幂集和二进制之间的关系:

fun <T> Collection<T>.findPowersetByBinary(): Set<Set<T>> =
        mutableSetOf(setOf<T>()).apply {
            for (i in 0 until Math.pow(2.0, this@findPowersetByBinary.size.toDouble()).toInt()) {
                mutableSetOf<T>().run {
                    var temp = i
                    var index = 0
                    do {
                        takeIf { temp and 1 == 1 }?.run {
                            add(this@findPowersetByBinary.elementAt(index))
                        }
                        temp = temp shr 1
                        index++
                    } while (temp > 0)
                    this@apply.add(this)
                }
            }
        }

上面代码由两层循环完成。假设集合S的个数为n,那么它的幂集元素的个数就是2n,在[0,2n)的整数区间上任取一个值x,x的二进制表示可以用来表示S的一个子集:对于x的第i位,如果为1,则此子集包含S的第i个元素,否则不包含。因此,只要遍历[0,2n)的整数区间,就能列举出S的所有子集,这些子集的集合就是S的幂集。

我们分析下算法流程,比如要计算集合{1,2,3}的幂集,遍历[0,23)的整数区间:

  • 0 ----> 对应的二进制0 ----> {}
  • 1 ----> 对应的二进制1 ----> {1}
  • 2 ----> 对应的二进制10 ----> {2}
  • 3 ----> 对应的二进制11 ----> {1,2}
  • 4 ----> 对应的二进制100 ----> {3}
  • 5 ----> 对应的二进制101 ----> {1,3}
  • 6 ----> 对应的二进制110 ----> {2,3}
  • 7 ----> 对应的二进制111 ----> {1,2,3}

把这8个集合组合起来的结果{{}, {2}, {3}, {2, 3}} + {{1}, {1, 2}, {1, 3}, {1, 2, 3}} = {{}, {1}, {2}, {3}, {1,2}, {1,3}, {2,3}, {1,2,3}}就是最终的幂集。

代码来自KotlinDiscreteMathToolkit库,它提供了离散数学很多有用的工具。

快速排序

我们将实现Quicksort(快速排序)算法。快排很简单:我们从源列表中挑选一个哨兵元素pivot,对元素小于pivot的列表A和元素不小于pivot的列表B这两个列表分别进行快排,最后的A + povit + B就是排好序的列表:

fun <T : Comparable<T>> List<T>.quickSort(): List<T> =
        if(size < 2) this
        else {
            val pivot = first()
            val (smaller, greater) = drop(1).partition { it <= pivot}
            smaller.quickSort() + pivot + greater.quickSort()
        }
// Usage
listOf(2,5,1).quickSort() // [1,2,5]

这代码真是比java简单太多了,而且易读,这就是函数式编程的美。
image.png

这个方法的一个注意点是算法性能,必须承认算法性能需要优化,但是算法很短而且易读。

如果你想要一个高度优化的算法,那么可以使用Java标准库。它是根据不同情形使用不同的算法,效率更高。效率究竟有多高?我们来比较两个算法:

val r = Random()
listOf(100_000, 1_000_000, 10_000_000)
    .asSequence()
    .map { (1..it).map { r.nextInt(1000000000) } }
    .forEach { list: List<Int> ->
        println("Java stdlib sorting of ${list.size} elements took ${measureTimeMillis { list.sorted() }}")
        println("quickSort sorting of ${list.size} elements took ${measureTimeMillis { list.quickSort() }}")
    }

在我的机器得到以下结果:

Java stdlib sorting of 100000 elements took 83
quickSort sorting of 100000 elements took 163
Java stdlib sorting of 1000000 elements took 558
quickSort sorting of 1000000 elements took 859
Java stdlib sorting of 10000000 elements took 6182
quickSort sorting of 10000000 elements took 12133

可以看到quickSort方法慢了2倍。对大列表来说,它有同样的可伸缩性。通常情况下,结果会有0.1ms到0.2ms的误差。算法更加简单易读,因此在一些情况下我们可以使用简单易读的算法,虽然性能可能不是最优的。


如果你对Kotlin感兴趣,可以参加Kotlin Academy。它是致力于Kotlin的开放社区。我在我的Twitter也发布很多资源。我的联系方式:@marcinmoskala。如果你需要我的帮助,请记住:I am open for consultations

翻译自原文地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容