题目:给定一个整数数组nums
和一个整数k
,要求返回数组中第k
个大的元素。
例如:输入[3, 2, 1, 5, 6, 4]
,k=2
则输出为5。
解题思路
只求AC笔试必挂的解题思路:
public int findKthLargest(int[] nums, int k) {
Arrays.sort(nums);
return nums[nums.length-k];
}
哈哈~ 实际上本题考查的是对排序的理解。下面介绍一下几种排序:
插入排序
插入排序的思想是:在待排序的元素中,假设前面的n-1个数已经排好序,现将第n个数插入已排好的序列中,使得插入后的序列依然有序。根据插入排序本题的代码为:
// 插入排序
public static int findKthLargest(int[] nums, int k) {
if(nums.length==1&&k==1) return nums[0];
for(int i=1;i<nums.length;i++){
int temp = nums[i];
int index = i-1;
while(index>=0&&nums[index]>temp){
nums[index+1] = nums[index];
index--;
}
nums[index+1] = temp;
}
return nums[nums.length-k];
}
插入排序的时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)。
快速排序
快速排序的思想是每次找一个基准值,此基准值将数组分为左右两部分,使得数组在基准值左侧的数据小于基准值,在基准值右侧的数据大于基准值。之后分别对数组的左右两部分进行同样的处理。
最简单的快速排序是使用数组的第一个元素作为基准值,之后首先从数组的右边往左边扫描,如果遇到数组的元素小于基准值,则停下。之后从数组的左边向右边扫描,找到第一个大于基准值的数据。交换左右两个值。循环中始终保持左指针数值小于右指针。当两个指针相遇的时候即为基准值应该在的位置。然后递归左右两部分即可,可得代码如下:
public void quicksort(int[] nums, int left, int right){
if(left>right) return;
int temp = nums[left];
int i = left, j = right;
while(i!=j){
while(i<j&&nums[j]>=temp) j--;
while (i<j&&nums[i]<=temp) i++;
swap(nums, i, j);
}
nums[left] = nums[i];
nums[i] = temp;
quicksort(nums,left, i-1);
quicksort(nums,i+1, right);
}
上述代码的时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)。快速排序是可以优化的,根据基准值的选取不同,时间复杂度也不同。我们可以设置基准值随机选取,这样时间复杂度可降到 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),代码如下:
public void quicksort2(int[] nums, int left, int right){
if(left>right) return;
int index = new Random().nextInt(right-left+1)+left;
int temp = nums[index];
swap(nums, left, index); // 精髓!!!此处必须交换
int i = left, j = right;
while(i!=j){
while(i<j&&nums[j]>=temp) j--;
while (i<j&&nums[i]<=temp) i++;
swap(nums, i, j);
}
swap(nums, i, left);
quicksort2(nums,left, i-1);
quicksort2(nums,i+1, right);
}
还有些方法使用的是使用数组中间的值作为基准值,这几种变体的核心都是将所选基准值和数组最左边元素进行替换!这一步很重要。由变体的快速排序可获得代码,但题目需要返回的是数组中第K个最大的元素,因此可以对结果进行剪枝,代码如下:
// 快速排序
public static int findKthLargest2(int[] nums, int k) {
int left=0, right = nums.length-1;
int index = nums.length-k;
return quicksort3(nums, left, right, index);
}
public static int quicksort3(int[] nums, int left, int right, int k){
int index = new Random().nextInt(right-left+1)+left;
int temp = nums[index];
swap(nums, left, index); // 精髓!!!此处必须交换
int i = left, j = right;
while(i!=j){
while(i<j&&nums[j]>=temp) j--;
while (i<j&&nums[i]<=temp) i++;
swap(nums, i, j);
}
swap(nums, i, left);
if(i==k) return nums[i];
else if(i>k) return quicksort3(nums,left, i-1, k);
else return quicksort3(nums,i+1, right, k);
}
此时不会遇到left>right
的情况了。
堆排序
堆排序包括堆的建立和堆中元素的删除,其中堆的建立较为简单,因为堆的本质是由数组实现的,因此对于索引为i
的元素,其左子树的索引在数组中为2*i+1
,而右子树则为2*i+2
,其父亲结点为(i-1)/2
,因此每次插入元素只需跟其父亲结点进行比对即可,代码如下:
public void buildHeap(int[] heap, int index){
while(heap[index]>heap[(index-1)/2]){
swap(heap, index, (index-1)/2);
index = (index-1)/2 ;
}
}
堆中元素的删除则较为复杂,删除堆中的元素,我们首先需要将堆的根节点和最后一个叶子结点进行互换,之后需要将此堆再次调整为大根堆,调整的步骤为首先找到根节点的左右子树结点中较大的那个,注意此时不一定比根节点大,因此需要比对,如果大则和根节点互换,之后再次找到互换后的结点的左右子树结点,依次循环,代码如下:
public void heapify(int[] heap, int index, int size){
int left = 2*index+1;
while(left<size){
int largest = left+1<size && heap[left]<heap[left+1] ? left+1:left;
largest = heap[largest] > heap[index] ? largest : index; // 这一步要注意
if(largest==index) break; // 这一步也要注意
swap(heap, index, largest);
index = largest;
left = 2*index+1;
}
}
对于本题,我们首先根据堆排序获得一个大根堆,之后依次删除元素,保证删除k-1
个元素就停止,此时输出的nums[0]
即为最终答案,代码如下:
public int findKthLargest3(int[] nums, int k) {
int len = nums.length;
for(int i=1;i<len;i++){
buildHeap(nums,i);
}
if(k==1) return nums[0];
swap(nums, 0, --len);
while(len>0){
heapify(nums,0, len);
if(len!=nums.length-k+1) {
swap(nums, 0, --len);
}else {
break;
}
}
return nums[0];
}
注意看截断的那里。