功能性快速排序是否有可能比命令式快速排序运行得更快?

时间:2018-04-06 07:30:09

标签: sorting functional-programming kotlin quicksort

我正在尝试理解函数式编程,虽然代码看起来很漂亮,但我担心与命令式实现相比会有性能损失。

然而,我完全惊讶地发现功能实现比我的命令式实现(看起来很难看)要快得多。

现在,我确信在我的命令式实施中存在一些错误,但是,我不确定这个错误是什么..

一些基准: 功能大小为35个元素: 152954779 ns

迫害35: 198337749325 ns

即使我在列表中添加10个元素,这也会恶化

代码在kotlin:

势在必行:

fun quickSort(numbers: IntArray, l: Int, r: Int): IntArray {
if (l >= r)
    return numbers

fun swap(m: Int, n: Int) {
    val temp = numbers[m]
    numbers[m] = numbers[n]
    numbers[n] = temp
}

var i = l + 1
var j = l + 1
val pivot = numbers[l]
while (j < r) {
    if (numbers[j] < pivot) {
        if (numbers[i] > pivot) {
            swap(i, j)
        }
        i++
    }
    j++
}
swap(l, i - 1)
quickSort(numbers, 0, i - 1)
quickSort(numbers, i, r)
return numbers
}

我相信我可以重构并改进它,但是,这不是我现在的目标。

势在必行2:

fun partitionTest(arr: IntArray, left: Int, right: Int): Int {

var i = left
var j = right
var tmp: Int

val pivot = arr[(left + right) / 2]

while (i <= j) {
    while (arr[i] < pivot)
        i++
    while (arr[j] > pivot)
        j--
    if (i <= j) {
        tmp = arr[i]
        arr[i] = arr[j]
        arr[j] = tmp
        i++
        j--
    }
}
return i
}


fun quickSortTest(arr: IntArray, left: Int, right: Int) {

val index = partitionTest(arr, left, right)

if (left < index - 1)

    quickSort(arr, left, index - 1)

if (index < right)

    quickSort(arr, index, right)

}

功能:

fun functionalQuickSort(numbers: IntArray): IntArray {
return when {
    numbers.size <= 1 -> numbers
    else -> {
        val pivotIndex = 0
        functionalQuickSort(numbers.filter { it < numbers[pivotIndex] }.toIntArray()) + numbers[pivotIndex] + functionalQuickSort(
            numbers.filter { it > numbers[pivotIndex] }.toIntArray()
        )
    }
  }
}

主:

val numbers = Random().ints(10).toArray()
var start = System.nanoTime()
functionalQuickSort(numbers).also { println(it.contentToString()) }
var end = System.nanoTime()
println("Took ${end - start}")


start = System.nanoTime()
quickSort(numbers,0,numbers.size).also { println(it.contentToString()) }
end = System.nanoTime()
println("Took ${end - start}")

2 个答案:

答案 0 :(得分:4)

我使用了一个已知良好的命令式QuickSort算法而不是你的算法,这看起来很糟糕。我的分区代码在结构上与您的不同,因为它使用C.A.R. Hoare的原始方案,而你的似乎是使用Lomuto方案(因其简单而非流行而非流行)。

我还编写了代码来处理大多数JVM微基准测试问题。这是:

import java.util.concurrent.ThreadLocalRandom
import kotlin.system.measureTimeMillis

const val PROBLEM_SIZE = 1_000_000L

fun quickSort(array: IntArray, lo: Int, hi: Int) {
    if (lo >= hi) {
        return
    }
    val p = partition(array, lo, hi)
    quickSort(array, lo, p)
    quickSort(array, p + 1, hi)
}

private fun partition(array: IntArray, lo: Int, hi: Int): Int {
    val pivot = array[(lo + hi) / 2]
    var i = lo - 1
    var j = hi + 1
    while (true) {
        do {
            i++
        } while (array[i] < pivot)
        do {
            j--
        } while (array[j] > pivot)
        if (i >= j) {
            return j
        }
        array[i] = array[j].also { array[j] = array[i] }
    }
}

fun functionalQuickSort(numbers: IntArray): IntArray {
    return when {
        numbers.size <= 1 -> numbers
        else -> {
            val pivotIndex = 0
            functionalQuickSort(numbers.filter { it < numbers[pivotIndex] }.toIntArray()) +
                    numbers[pivotIndex] +
                    functionalQuickSort(numbers.filter { it > numbers[pivotIndex] }.toIntArray()
                    )
        }
    }
}

fun main(args: Array<String>) {
    benchmark("imperative", ::runImperativeQuickSort)
    benchmark("functional", ::functionalQuickSort)
}

fun benchmark(name: String, block : (IntArray) -> IntArray) {
    println("Warming up $name")
    (1..4).forEach {
        validate(block(randomInts()))
    }
    println("Measuring")
    val average = (1..10).map {
        var result: IntArray? = null
        val input = randomInts()
        val took = measureTimeMillis {
            result = block(input)
        }
        validate(result!!)
        took
    }.average()
    println("An average $name run took $average ms")
}

private fun runImperativeQuickSort(array: IntArray): IntArray {
    quickSort(array, 0, array.size - 1)
    return array
}

private fun randomInts() = ThreadLocalRandom.current().ints(PROBLEM_SIZE).toArray()

private fun validate(array: IntArray) {
    var prev = array[0]
    (1 until array.size).forEach {
        array[it].also { curr ->
            require(curr >= prev)
            prev = curr
        }
    }
}

典型输出:

Warming up imperative
Measuring
An average imperative run took 106.6 ms
Warming up functional
Measuring
An average functional run took 537.4 ms

所以......不,功能版本不会更快。

答案 1 :(得分:2)

我花了一段时间才找到它,但你的递归调用中有一个错误:

  quickSort(numbers, 0, i - 1)

这应该是:

  quickSort(numbers, l, i - 1)
                     ^

作为一个小优化,您也可以提前返回长度为1的段(除了长度为0):

  if (l + 1 >= r)
    return numbers

似乎还有一些我没有详细研究过的问题。 if循环中的嵌套while看起来很狡猾;我认为可以删除内部if

  while (j < r) {
    if (numbers[j] < pivot) {
      swap(i, j)
      i++
    }
    j++
  }

仔细考虑你的不变量是什么以及每个语句是否维护它们。

通过这些调整,命令式版本在100000个元素上的运行速度提高了约10倍。

还要考虑如果两个元素相等会发生什么,这对于如此小的数组是不太可能的,但对于100000个元素的数组(生日悖论)会发生。在这种情况下,您会发现您的功能实现已被破坏。

关于基准测试的主题:

  • 确保您的输入足够大以获取数据而不是噪音。超过一秒的运行时间是好的。
  • 不包括输入数据的生成。
  • 绝对不包括输出的打印。 I / O相对较慢。