29.最小的K个数
题目描述
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
思路一:
1.. 建立最大堆,对数组前 k 个元素建立最大堆,对 k 后面的每个元素,与堆顶元素进行大小比较,如果小于堆顶元素,则用当前元素替换为堆顶元素,并进行一次向下过滤,如果不小于堆顶元素,则直接跳过。
始终保证堆中的元素是小的 k 个
1 import java.util.ArrayList; 2 public class Solution { 3 4 public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) { 5 6 // 建立最大堆 7 ArrayList<Integer> list = new ArrayList<Integer>(k); 8 9 if (input == null || input.length < k) { 10 return list; 11 } 12 // 建堆 13 int n = k; 14 for (int i = (n / 2 - 1); i >= 0; i--) { 15 downAdj(input, i, n); 16 } 17 18 19 // 每当读入一个元素,与堆顶元素比较大小, 20 for (int i = k; i < input.length; i++) { 21 // 如果小于大堆顶元素,则用当前数替换堆顶元素,进行一次向下过滤 22 // 如果小于的话,直接丢弃这个数 23 if (input[i] < input[0]) { 24 input[0] = input[i]; 25 downAdj(input, 0, n); 26 } 27 28 } 29 30 // 最后对这个堆进行堆排序,输出 31 for (int i = n - 1; i >= 0; i--) { 32 int temp = input[0]; 33 input[0] = input[i]; 34 input[i] = temp; 35 downAdj(input, 0, i); 36 } 37 38 // 把 k 个数放到集合中 39 for(int i = 0; i < n; i++){ 40 list.add(input[i]); 41 } 42 return list; 43 } 44 45 // 向下过滤 46 public void downAdj(int[] A, int p, int n) { 47 int parent, child = p; 48 int x = A[p]; 49 for (parent = p; 2 * parent < n; parent = child) { 50 child = 2 * parent; 51 if (child + 1 < n && A[child + 1] > A[child]) { 52 child++; 53 } 54 // 与最大的孩子结点比较,如果父节点大,说明找到了位置, 55 if (x >= A[child]) { 56 break; 57 } 58 // 否则让最大的孩子上位 59 else { 60 A[parent] = A[child]; 61 } 62 63 } 64 A[parent] = x; 65 } 66 }
使用PriorityQueue代替手动建堆过程
1 class Solution { 2 // 使用最小堆,每次从堆中取一个元素 3 public int[] getLeastNumbers(int[] arr, int k) { 4 PriorityQueue<Integer> minHeap = new PriorityQueue<Integer>(K+1); 5 int[] res = new int[k]; 6 int len = arr.length; 7 for(int i = 0; i < len; i++){ 8 minHeap.offer(arr[i]); 9 } 10 for(int i = 0; i < k; i++){ 11 res[i] = minHeap.poll(); 12 } 13 return res; 14 } 15 }
leetcode运行时间为19ms, 空间为39.8MB
复杂度分析:
时间复杂度:建堆过程花费的时间为O(nlog(k+1)), 取前k 个元素花费的时间为O(klogk)
空间复杂度:堆的大小即为空间复杂度,所以空间复杂度为O(k+1)
思路二: 快速排序
利用快速排序,经过若干轮轮快排后,如果主元位置刚好为k-1, 那么前半段元素即使数组中最小的k个元素
1 class Solution { 2 // 利用快速排序的过程,经过若干轮轮快排后,如果主元位置刚好为k-1, 那么前半段元素即使数组中最小的k个元素 3 public int[] getLeastNumbers(int[] arr, int k) { 4 5 // arr进行若干轮快排 6 quickSort(arr, 0, arr.length-1, k); 7 8 // 返回前k个元素的范围数组 9 return Arrays.copyOfRange(arr, 0, k); 10 } 11 12 // 快排 13 public void quickSort(int[] arr, int left, int right, int k){ 14 // 如果left < right继续快排 15 if(left < right){ 16 // 对[left, right]区间的元素进行一轮快排,获取到快排后的主元位置 17 int pivotIndex = partition(arr, left, right); 18 if(pivotIndex == k-1){ 19 return; // 如果主元位置刚好是k-1, 直接返回 20 } 21 // 否则,继续递归快排左右子区间 22 quickSort(arr, left, pivotIndex - 1, k); 23 quickSort(arr, pivotIndex + 1, right, k); 24 } 25 } 26 27 // 一次快排操作 28 public int partition(int[] arr, int left, int right){ 29 // 选定第一个元素作为主元 30 int pivot = arr[left]; 31 // 当left < right时一直循环 32 while(left < right){ 33 while(left < right && arr[right] >= pivot){ 34 right--; 35 } 36 arr[left] = arr[right]; 37 while(left < right && arr[left] <= pivot){ 38 left++; 39 } 40 arr[right] = arr[left]; 41 } 42 arr[left] = pivot; 43 return left; 44 } 45 }
leetcode运行时间为9ms, 空间为40.3MB,可以看到比思路一快很多
复杂度分析:
时间复杂度:若干次快排,复杂度为O(n), 具体不会证明,参考:具体证明可以参考《算法导论》第 9 章第 2 小节。
空间复杂度:递归栈的深度最小为O(logn), 最大为O(n)