剑指29.最小的K个数
题目描述
思路
思路1:利用排序。排序后最前面的K个数就是最小的K个数。 (时间复杂度O(nlogn))
思路2:快排Partition思想(Partition函数既是快排的基础,也可以用来查找n个数中第k大的数) (时间复杂度为O(n),缺点是需要修改输入的数组)
基于数组的第k个数字来调整,使得比第k个数字小的所有数字都位于数组的左边,比第k个数字大的所有数字都位于数组的右边。调整之后,位于数组左边的k个数字就是最小的k个数字(这k个数字不一定是排序的)。这种思路虽时间复杂度最优,但会修改输入数组。
思路3:最大堆。用最大堆保存k个数,每次只和堆顶比,如果比堆顶小,删除堆顶,新数入堆。 (时间复杂度O(nlogk),特别适合处理从海量数据中找出最小的k个数)
先创建一个大小为k的容器,如果容器中已有的数字小于k个,则直接把这次读入的数据放入容器中;如果容器中已有k个数了,我们找出这已有k个数中的最大值,如果接下来遍历的数值比容器中的最大值小,那么用这个数替换容器中的最大值。可以看出,容器满了之后需要做3件事。1)在k个数中找到最大数;2)有可能在容器中删除最大数;3)有可能要插入一个新数字。当需要在某个数据容器内频繁查找及替换最大值时,我们可以使用最大堆,最大堆中根节点的值总是大于它子树中任意节点的值。每次可以在O(1)的时间内得到已有k个数字的最大值,但需要O(logk)时间完成删除以及插入操作。
☆解法2(Partition思想)
import java.util.ArrayList; public class Solution { // 采用快排的partition思想 public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) { ArrayList<Integer> res = new ArrayList<>(); if(input == null || k < 1 || k > input.length) return res; // 在数组中寻找位置为k - 1 的pivot int start = 0, end = input.length - 1; int index = partition(input,start,end);// 任取一个数字为基准,如果index=k-1,说明左边有k个数了 while(index != k - 1){ if(index < k - 1) start = index + 1; // 说明左边不够k个数,因此对右边进一步划分 else end = index - 1; // 说明左边大于k个数,因此对左边进一步划分 index = partition(input,start,end); } for(int i = 0; i < k; i++) res.add(input[i]); return res; } // partition版本2:进阶版,相对版本1优化不必要的交换 private int partition(int[] arr, int start, int end){ int pivot = arr[start]; while(start < end){ // 顺序很重要,要先从右往左找,如果右边的数比标准数大 while(start < end && arr[end] >= pivot) end--; arr[start] = arr[end]; // 采用替换而不是交换的方式进行操作,使用右边的数字替换左边的数 // 再从左往右找,如果左边的数比标准数小 while(start < end && arr[start] <= pivot) start++; arr[end] = arr[start]; //使用左边的数字替换右边的数 } // 此时,low和high重合了,标准数归位。 arr[start] = pivot; return start; //返回枢纽所在位置 } // partition版本1 :需要交换数据 // 任取一个数字为基准,进行比较和划分。 返回基准点的index private int partition(int[] arr, int start, int end){ int pivot = arr[start]; while(start < end){ while(start < end && arr[end] >= pivot) // 需要加上等号,大于等于pivot才能通过 end--; swap(arr,start,end); //交换位置,使得大于pivot的值位于数组右边 while(start < end && arr[start] <= pivot) // 需要加上等号,小于等于pivot才能通过 start++; swap(arr,start,end); //交换位置,使得小于pivot的值位于数组左边 } return start; } private void swap(int[] arr, int i, int j){ int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
Note:举个例子理解一下上述过程~ 参考:lron欣
例如输入k=4,数组为(9,5,1,3,8,7,13,11,12)。以9为基准划分后,为(5,1,3,8,7,9,13,11,12),左边小于9的有5个数(9的索引不为3),仍需划分,再对(5,1,3,8,7)进行划分此时以5为划分,则形成(1,3,5,8,7),此时左边小5的只有2个(5的索引不为3),因此对右边进行划分,右边划分(7,8)。此时拿到的值为7的索引,整体上来说,原数组变成了(1,3,5,7,8,9,13,11,12)而7这个地方的索引是3,也就是说,左边的4个较小的数已经拿到了。
解法3(最大堆)
import java.util.*; public class Solution { // 用优先队列构造最大堆 public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) { ArrayList<Integer> res = new ArrayList<>(); if (input == null || k < 1 || k > input.length) return res; //PriorityQueue<Integer> maxHeap = new PriorityQueue<>(((o1, o2) -> o2.compareTo(o1))); //o1.compareTo(o2) 从小到大;反过来 从大到小,因此这里建立的是最大堆。 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k, Collections.reverseOrder()); for (int num : input){ if (maxHeap.size() < k){ maxHeap.add(num); }else{ if (maxHeap.peek() > num){ maxHeap.poll(); maxHeap.add(num); } } } while (!maxHeap.isEmpty()) res.add(maxHeap.poll()); return res; } }
Note:以后复习时手写一个最大堆进行动态调整,,不使用优先队列。
参考: