快速排序介绍、详解、案例

9 篇文章 0 订阅

排序

快速排序是一种非常高效的排序算法,当表现良好时,快速排序的速度要比其他主要对手(归并排序)快2~3倍。

排序过程

  1. 在输入的数组中随机选取中间值(pivot)作为中间值。
  2. 根据中间值对数组进行分区(partition),比中间值小的数据移到数组左侧,比中间值大的数据移到数组右侧。
  3. 对中间值左右两侧的数组进行上述重复排序的过程,直到子数组只包含一个数字。

上述过程递归的话可以做如下表示

fun fastSortArray(array: IntArray) {
    fastSortArray(array, 0, array.size - 1)
}

fun fastSortArray(array: IntArray, start: Int, end: Int) {
    if (end > start) {
    		//分区
        val pivot = partition(start, end, array)
        //pivot 支点 已排序的位置已经固定 所以可以越过它 继续排序
        fastSortArray(array, start, pivot - 1)
        fastSortArray(array, pivot + 1, end)
    }
}

接下来就是对分区的理解,随机选取中间值,以中间值的大小将数组分为左右两部分。下面以数组【4,1,5,3,6,2,7,8】来进行解析。

在这里插入图片描述

  1. 我们随机选取的中间值为3,所以先把3移动到数组最后。初始化两个指针,P1(红色)P2(黑色),P1始终指向最后一个比3小的位置,所以初始化P1的位置为-1,P2位置为0;如图1;
  2. 移动指针P2找到第一个比3小的数字,此时找到了1,之后P1指针+1,到位置0,然后交换P1和P2的值;如图2;
  3. 接着移动指针P2找到第二个比3小的数字,此时找到了2,之后P1指针+1,到位置1,然后交换P1和P2的值;如图3;
  4. 最后指针继续往后移动发现没有比3小的数字,结束循环,但是最后仍需要做最后一步交换,指针P1+1,到位置2,将数组最后的3和其进行交换;如图4;

步骤4的核心目的是将第一步移动到数组最后的中间值移动到中间位置。

如此就完成了一个分区的过程,用代码则可以进行如下表示:

fun partition(start: Int, end: Int, array: IntArray): Int {
    //随机选取中间值
    val randomPivot = Random(System.currentTimeMillis()).nextInt(end - start + 1) + start
    //将该值交换到数组末尾
    swap(array, randomPivot, end)

    var small = start - 1
    for (i in start until end) {
        //找到一个比末尾值小的值,进行交换
        if (array[i] < array[end]) {
            small++
            swap(array, small, i)
        }
    }
		
		//将末尾的值交换到中间  这样比它小的都在它的左边,比他大的都在他的右边。
    small++
    swap(array, small, end)

    return small
}
//交换
fun swap(array: IntArray, position1: Int, position2: Int) {
    if (position1 != position2) {
        val temporary = array[position1]
        array[position1] = array[position2]
        array[position2] = temporary
    }
}

时间复杂度:如果每次选取的中间值都在排序数组的中间位置,则快速排序的时间复杂度为O(nlogn),但是如果每次选取的中间值都是排序数组的头部或者尾部,那么快速排序的时间复杂度为O(n^2)。这也就是随机选取的原因,所以再随机选取的前提下,快排的平均时间复杂度为O(nlogn)。

空间复杂度:快速排序是递归调用的,故上述代码的空间复杂度即为递归调用深度,所以平均空间复杂度为O(nlogn)。

示例

从一个乱序数组中找出第k大的数字。例如,数组[3,1,2,4,5,5,6]中第3大的数字是5。

解法1:最小堆解法

思路:确保最小堆的容量为K,每次从数组中读取一个数字时都和堆顶的元素进行比较,如果比堆顶的元素大,则移除堆顶元素并且将该元素添加到最小堆之中。

代码

fun kthLargestValue(array: IntArray, k: Int): Int {
    val minPriorityQueue = PriorityQueue<Int>()
    array.forEach {
        if (minPriorityQueue.size < k) {
            minPriorityQueue.add(it)
        } else if (minPriorityQueue.peek() < it) {
            minPriorityQueue.poll()
            minPriorityQueue.add(it)
        }
    }
    return minPriorityQueue.peek()
}

时间复杂度为O(nlogk),空间复杂度为O(k)。

解法2:利用分区解

解法1适用于数据都位于一个数据流之中,且无法一次性全部读入内存之中。

在长度为n的排序数组之中,第k大的值在数组中的位置为(n-k)。

思路:

  1. 如果分区函数(partition)选取的中间值在分区之后的下标刚好是n-k,此时中间值就是第k大的值。
  2. 如果分区函数(partition)选取的中间值在分区之后的下标大于n-k,则第k大的值一定在分区左侧,在对左侧进行分区。
  3. 如果分区函数(partition)选取的中间值在分区之后的下标小于n-k,则第k大的值一定在分区的右侧,在对右侧进行分区。

代码:

fun kthLargestValueByPartition2(array: IntArray, k: Int): Int {
    val kIndex = array.size - k
    var start = 0
    var end = array.size - 1
    var index = partition(start, end, array)
    while (kIndex != index) {
        if (kIndex > index) {
            start = index + 1
        } else {
            end = index - 1
        }
        index = partition(start, end, array)
    }
    return array[index]
}

🙆‍♀️。欢迎技术探讨噢!

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

pumpkin的玄学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值