一个有代表性的题目:无序整数数组中找第k大的数,对快排进行优化。
这里先不说这个题目怎么解答,先仔细回顾回顾快排,掰开了揉碎了理解理解这个排序算法:时间复杂度、空间复杂度;什么情况下是复杂度最高的情况。
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。
快排是冒泡排序的改进,改进点:冒泡排序每次只能交换相邻的两个元素,而快速排序是跳跃式的交换,交换的距离很大,因此总的比较和交换次数少了很多,速度也快了不少。
步骤:
举个例子,放个图理解理解:
以 47、29、71、99、78、19、24 的待排序的数列为例进行排序,为了方便区分两个 47,我们对后面的 47 增加一个下画线,即待排序的数列为 47、29、71、99、78、19、24。
言而总之:小于基准数的,放在左边,大于基准数的,放在右边,最后,基准数放在二者中间即可。
代码:
public class Sort_quick_sort {
public void quick(int[] src, int begin, int end) {
if (begin < end) {
//基准数
int key = src[begin];
int i = begin;
int j = end;
while (i < j){
//如果右边大于基准数,j--
while(i < j && src[j] > key){
j--;
}
//上面循环结束,说明右边不大于基准数了,换位置
if (i < j){
swap(src, i, j);
i++;
}
//如果左边的小于基准,i++
while (i < j && src[i] < key) {
i++;
}
//上面循环结束,说明左边不小于基准数了,换位置
if (i < j){
swap(src, i, j);
j--;
}
}
//当i== j的时候,基准数停留在了它应该在的位置,分而治之的递归下去
quick(src, begin, i - 1);
quick(src, i + 1, end);
}
}
public void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String args[]) {
Sort_quick_sort obj = new Sort_quick_sort();
int[] num = {2, 7, 11, 15, 1, 0, 0,15};
obj.quick(num, 0, num.length - 1);
for (int n: num){
System.out.print(n+ "\n");
}
}
}
快速排序在最坏情况下的同冒泡排序,是 O(n2),
每一次取到的元素就是数组中最小/最大的,这种情况其实就是冒泡排序了(每一次都排好一个元素的顺序),冒泡排序的时间复杂度:T[n] = n * (n-1) = n^2 + n。
数列的平均时间复杂度是 O(nlogn).
从代码来看,仅定义了几个变量,占用常数空间,使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据,所以空间复杂度不是O(1)。
在最差的情况下,退化为冒泡排序的情况,若每次只完成了一个元素,那么空间复杂度为 O(n)。
快速排序只是使用数组原本的空间进行排序,所以所占用的空间应该是常量级的,但是由于每次划分之后是递归调用,所以递归调用在运行的过程中会消耗一定的空间,在一般情况下的空间复杂度为 O(logn)。
快速排序是一个不稳定的算法,在经过排序之后,可能会对相同值的元素的相对位置造成改变。快速排序基本上被认为是相同数量级的所有排序算法中,平均性能最好的。
思考:真正理解一个东西,是具备举一反三的能力,如果不能,需要再去理解理解了。当然,在说时候,也不要紧张,容易大脑一片空白,质疑自己[♀️]。
LeetCode有一个类似的题目链接,
设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。
示例:
输入: arr = [1,3,5,7,2,4,6,8], k = 4
输出: [1,2,3,4]
提示:
0 <= len(arr) <= 100000
0 <= k <= min(100000, len(arr))
方法1:改进的快排,同最下面解法三,时间复杂度O(n),空间复杂度O(1)。
这是qrqhuang大佬的解答:
class Solution {
public int[] smallestK(int[] arr, int k) {
if (k >= arr.length) {
return arr;
}
int low = 0;
int high = arr.length - 1;
while (low < high) {
int pos = partition(arr, low, high);
if (pos == k - 1) {
break;
} else if (pos < k - 1) {
low = pos + 1;
} else {
high = pos - 1;
}
}
int[] dest = new int[k];
System.arraycopy(arr, 0, dest, 0, k);
return dest;
}
private int partition(int[] arr, int low, int high) {
int pivot = arr[low];
while (low < high) {
while (low < high && arr[high] >= pivot) {
high--;
}
arr[low] = arr[high];
while (low < high && arr[low] <= pivot) {
low++;
}
arr[high] = arr[low];
}
arr[low] = pivot;
return low;
}
}
方法2:构建小顶堆。
时间复杂度: O(n + kLogn), 其中建初始堆: O(n),取top: O(kLogn)
空间复杂度:O(1)
class Solution {
public int[] smallestK(int[] arr, int k) {
int len = arr.length;
if (k >= len) {
return arr;
}
if (k ==0) return new int[0];
buildMinHeap(arr, len);
int pos = len - k;
//只对最小堆的前k个进行heapify,此时返回的数组中最后k个为先大后小排列,最后一个元素最小
for (int i = len - 1; i >= pos; i--) {
//将根节点与最后一个元素换位置,砍断最后一个节点;然后对剩下的节点进行heapify
swap(arr, 0, i);
heapify(arr, 0, i);
}
int[] ret = new int[k];
int j = 0;
//倒着将arr中的元素写到返回结果中
for (int i = len - 1; i >= pos; i--) {
ret[j++] = arr[i];
}
return ret;
}
private void buildMinHeap(int[] arr, int len) {
for (int i = (len - 1) / 2; i >= 0; i--) {
heapify(arr, i, len);
}
}
private void heapify(int[] arr, int i, int len) {
if (i >= len) return;
int min = i;
int c1 = 2 * i + 1;
int c2 = 2 * i + 2;
if (c1 < len && arr[c1] < arr[min]) {
min = c1;
}
if (c2 < len && arr[c2] < arr[min]) {
min = c2;
}
if (min != i) {
swap(arr, i, min);
heapify(arr, min, len);
}
}
private void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
最初的努力,脑回路仍然没有打通,刷题时,碰到了上述妙招。
时间复杂度:O(N * logN)
调用上面的快排函数:
public static void main(String args[]) {
Sort_quick_sort obj = new Sort_quick_sort();
int[] num = {2, 7, 11, 15, 1, 0, 0,15};
obj.quick(num, 0, num.length - 1);
int k = 3;
int Kth = num[k - 1];//索引从0开始
System.out.print(Kth); //输出 1
}
如果我们的k很小,可以将时间复杂度降低为O(N * K),只对前k个数排序,可以进行选择排序。
简单实现下选择排序:
public int select (int[] nums, int k) {
int i = 0;
for (; i < k; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] < nums[i]) {
swap(nums, i, j);
}
}
}
return nums[i - 1];
}
时间复杂度:O(n)。
空间复杂度:O(maxvalue(nums)),适用于数据的取值范围不太大的情景。内存多的话,空间换时间也可以。
public int kv (int[] nums, int k) {
//数组长度自定义,nums的最大值就是长度
int[] kv = new int[20];
for (int num: nums) {
kv[num]++;
}
int sum = 0;
int res = 0;
for (int i = 0; i < 20; i++) {
sum += kv[i];
if (sum >= k) {
res = i;
break;
}
}
return res;
}
平均时间复杂度 O(N *logK)
快排中的每次递归,将待排数据分做两组,其中一组的数据的任何一个数都比另一组中的任何一个大,然后再对两组分别做类似的操作;在本问题中,假设 N 个数存储在数组 S 中,我们从数组 S 中随机找出一个元素 X,把数组分为两部分 Sa 和 Sb。Sa 中的元素大于等于 X,Sb 中元素小于 X。
这时,有两种可能性:
1. Sa中元素的个数小于K,Sa中所有的数和Sb中最大的K-|Sa|个元素(|Sa|指Sa中元素的个数)就是数组S中最大的K个数。
2. Sa中元素的个数大于或等于K,则需要返回Sa中最大的K个元素。
int res = 0;
public void quickModify(int[] src, int begin, int end, int k - 1) {
if (begin < end) {
int key = src[begin];
int i = begin;
int j = end;
while (i < j){
while(i < j && src[j] > key){
j--;
}
if (i < j){
swap(src, i, j);
i++;
}
while (i < j && src[i] < key) {
i++;
}
if (i < j){
swap(src, i, j);
j--;
}
}
if (i > k - 1) {
quickModify(src, begin, i - 1, k);
}
if (i == k - 1) {
res = src[i];
}
if (i < k - 1) {
quickModify(src, i - 1, end, k - i);
}
}
}
2020 5 21
参考:
1.无序数组找第k大的数:https://blog.csdn.net/wangbaochu/article/details/52949443
2.快排:http://data.biancheng.net/view/117.html