排序
快速排序是一种非常高效的排序算法,当表现良好时,快速排序的速度要比其他主要对手(归并排序)快2~3倍。
排序过程
- 在输入的数组中随机选取中间值(pivot)作为中间值。
- 根据中间值对数组进行分区(partition),比中间值小的数据移到数组左侧,比中间值大的数据移到数组右侧。
- 对中间值左右两侧的数组进行上述重复排序的过程,直到子数组只包含一个数字。
上述过程递归的话可以做如下表示
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】来进行解析。
- 我们随机选取的中间值为3,所以先把3移动到数组最后。初始化两个指针,P1(红色)P2(黑色),P1始终指向最后一个比3小的位置,所以初始化P1的位置为-1,P2位置为0;如图1;
- 移动指针P2找到第一个比3小的数字,此时找到了1,之后P1指针+1,到位置0,然后交换P1和P2的值;如图2;
- 接着移动指针P2找到第二个比3小的数字,此时找到了2,之后P1指针+1,到位置1,然后交换P1和P2的值;如图3;
- 最后指针继续往后移动发现没有比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)。
思路:
- 如果分区函数(partition)选取的中间值在分区之后的下标刚好是n-k,此时中间值就是第k大的值。
- 如果分区函数(partition)选取的中间值在分区之后的下标大于n-k,则第k大的值一定在分区左侧,在对左侧进行分区。
- 如果分区函数(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]
}
🙆♀️。欢迎技术探讨噢!