【LeetCode】快排-无序整数数组中找第k大的数(或者最小的k个数)

一个有代表性的题目:无序整数数组中找第k大的数,对快排进行优化。

这里先不说这个题目怎么解答,先仔细回顾回顾快排,掰开了揉碎了理解理解这个排序算法:时间复杂度、空间复杂度;什么情况下是复杂度最高的情况。

1.主要思想

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。

快排是冒泡排序的改进,改进点:冒泡排序每次只能交换相邻的两个元素,而快速排序是跳跃式的交换,交换的距离很大,因此总的比较和交换次数少了很多,速度也快了不少。

2.具体排序例子

步骤:

  1. 要找一个数字作为基准数(这只是个专用名词)。为了方便,我们一般选择第 1 个数字作为基准数(其实选择第几个并没有关系)。
  2. 我们需要把这个待排序的数列中小于基准数的元素移动到待排序的数列的左边,把大于基准数的元素移动到待排序的数列的右边。这时,左右两个分区的元素就相对有序了;
  3. 接着把两个分区的元素分别按照上面两种方法继续对每个分区找出基准数,然后移动,直到各个分区只有一个数时为止。这是典型的分治思想,即分治法。

举个例子,放个图理解理解:

以 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");
		}
	}

}

2.复杂度

时间复杂度

快速排序在最坏情况下的同冒泡排序,是 O(n2),每一次取到的元素就是数组中最小/最大的,这种情况其实就是冒泡排序了(每一次都排好一个元素的顺序),冒泡排序的时间复杂度:T[n] = n * (n-1) = n^2 + n。

数列的平均时间复杂度是 O(nlogn).

空间复杂度

从代码来看,仅定义了几个变量,占用常数空间,使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据,所以空间复杂度不是O(1)。

在最差的情况下,退化为冒泡排序的情况,若每次只完成了一个元素,那么空间复杂度为 O(n)。

快速排序只是使用数组原本的空间进行排序,所以所占用的空间应该是常量级的,但是由于每次划分之后是递归调用,所以递归调用在运行的过程中会消耗一定的空间,在一般情况下的空间复杂度为 O(logn)。

稳定性:

快速排序是一个不稳定的算法,在经过排序之后,可能会对相同值的元素的相对位置造成改变。快速排序基本上被认为是相同数量级的所有排序算法中,平均性能最好的。

思考:真正理解一个东西,是具备举一反三的能力,如果不能,需要再去理解理解了。当然,在说时候,也不要紧张,容易大脑一片空白,质疑自己[🤦‍♀️]。

题目:无序整数数组中找第k大的数

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];
	}

解法二:键值索引方法:将每个数作为辅助数组的索引,计算每个数出现的次数。统计所有的次数,找到第K个数。

时间复杂度: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;
	}

解法三:改进的快速排序方法:避免对所有的数排序,利用快速排序分堆,然后递归另外一半(不需要两半都递归),直到最终所有小于基准数的个数为K【尚可】

平均时间复杂度 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

  • 3
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
给定一个整数组 nums 和一个目标值 target,要求在数组中出两个数的和等于目标值,并返回这两个数的索引。 思路1:暴力法 最简单的思路是使用两层循环遍历组的所有组合,判断两个数的和是否等于目标值。如果等于目标值,则返回这两个数的索引。 此方法的时间复杂度为O(n^2),空间复杂度为O(1)。 思路2:哈希表 为了优化时间复杂度,可以使用哈希表来存储数组中的元素和对应的索引。遍历组,对于每个元素nums[i],我们可以通过计算target - nums[i]的值,查哈希表中是否存在这个差值。 如果存在,则说明到了两个数的和等于目标值,返回它们的索引。如果不存在,将当前元素nums[i]和它的索引存入哈希表中。 此方法的时间复杂度为O(n),空间复杂度为O(n)。 思路3:双指针 如果组已经排序,可以使用双指针的方法来求解。假设组从小到大排序,定义左指针left指向组的第一个元素,右指针right指向组的最后一个元素。 如果当前两个指针指向的的和等于目标值,则返回它们的索引。如果和小于目标值,则将左指针右移一位,使得和增大;如果和大于目标值,则将右指针左移一位,使得和减小。 继续移动指针,直到到两个数的和等于目标值或者左指针超过了右指针。 此方法的时间复杂度为O(nlogn),空间复杂度为O(1)。 以上三种方法都可以解决问题,选择合适的方法取决于具体的应用场景和要求。如果组规模较小并且不需要考虑额外的空间使用,则暴力法是最简单的方法。如果组较大或者需要优化时间复杂度,则哈希表或双指针方法更合适。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值