这次在刷Leetcode时,在求解数组中的第K大问题时,想到了使用堆排序,因此本篇文章用于巩固对堆排序的学习以及代码实现。

题目描述:在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

输入:[3,2,3,1,2,4,5,5,6] 和 k = 4

输出:4

当时看到此题时,第一反应就是想到使用大顶堆来求解,在第K次调堆后,就能够得到最K大元素。后续以大顶堆进行为例。

先来简单回顾堆排序:堆是一棵完全二叉树。如果是一个大顶堆,则根节点递归的大于其左右孩子节点的值。

以大顶堆为例:(3,2,3,1,2,4,5,5,6),对该待排序列进行堆排序。

首先,将该序列建立一棵完全二叉树。(这里可以假定建立完全二叉树,可以以数组的位序模拟对二叉树的操作)。

image-20210318110035310

从第一个(从右向左,从下向上看起)非叶子节点为根节点的子树开始,将其调整为大根堆。因为6>5,所以6和1进行交换。

image-20210318110418419

开始调整第二个非叶子节点作为根节点的子树,这里第二个非叶子节点为3。因为5>4,所以根节点3和5进行交换。

image-20210318110937984

接下来,来到非叶节点2,因为 6 > 2,所以根节点2与6进行交换。此时,该子树因为根的调整,以2为根节点的子树不满足大顶堆的性质,因此需要递归的调整子树,将5和2进行调换。

image-20210318111210360

此时来到最后一个非叶节点,即根节点。因为6 > 5,所以3和6进行调换,同时需要递归修改此时以3为根节点的子树进行调整为大顶堆。

image-20210318111603757

至此,最后得到的一个二叉树就为大顶堆,每个节点的值都大于其左右子树的值。其中,根节点为最大值,就是第一大元素。

将根节点输出,以最后一个叶子节点将其补上,然后重复上述的步骤。

image-20210318111752758

备注:当左、右节点值相同时,替换哪一个依据代码的具体形式。

该题解的具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class Solution {
public int findKthLargest(int[] nums, int k) {
return maxHeapMethod(nums,k);
}

public int maxHeapMethod(int[] nums,int k) {
/**
采用堆排序该方法
利用数组进行建堆:
如果根为 i,则其左子树为 i*2+1,则其右子树为 i*2+2
建堆的过程为:
从下至上进行建堆,一边建堆,一边调整堆
调堆的过程为:
其左右孩子节点,依次和其父亲节点的值进行比较;如果大于父亲节点则和父亲节点的值进行交换。
调堆的整个过程为递归过程

模拟完全二叉树
这里用来建立最大堆.
**/
int heapLength = nums.length;
generateHeap(nums); // 建堆
for(int i = nums.length - 1;i > nums.length - k;--i){ // 调整K次堆以后,则此时数组中的第一个元素,即为第K个最大元素
swapNums(nums,0,i);
heapLength -= 1;
adjustHeap(nums,0,heapLength);
}
return nums[0];
}

private void generateHeap(int[] splitNums){
/**
该函数用于建堆:
如果根为 i,则其左子树为 i*2+1,则其右子树为 i*2+2
建堆的过程为:
从下至上进行建堆,一边建堆,一边调整堆
从最后一个非叶节(最近)点依次向上进行建堆并且调整
**/

int heapLength = splitNums.length;
for(int i = heapLength / 2;i >= 0;--i) { // 从下至上进行建堆,最后一个非叶节点开始。
adjustHeap(splitNums,i,heapLength);
}

}

private void adjustHeap(int[] splitNums,int rootIndex,int heapLength) {

/**
从下至上进行调堆
比较root和左右孩子的大小,
如果是调换的root和左孩子,调换完以后。那么再递归该左孩子(以左孩子为root)的调堆。
**/


int leftIndex = rootIndex * 2 + 1; // 左孩子
int rightIndex = rootIndex * 2 + 2; // 右孩子
int maxIndex = rootIndex;

// 以下两个条件,将左右孩子中的最大值与根节点做比较,使得根为最大值
if(leftIndex < heapLength && splitNums[leftIndex] > splitNums[maxIndex]) {
maxIndex = leftIndex;
}
if(rightIndex < heapLength && splitNums[rightIndex] > splitNums[maxIndex]) {
maxIndex = rightIndex;
}

if(maxIndex != rootIndex) { // 递归对堆进行调整
swapNums(splitNums,rootIndex,maxIndex); // 将左、右孩子中的最大一个与父节点值进行交换
adjustHeap(splitNums,maxIndex,heapLength); // 以调换的孩子节点为root节点,继续递归调整堆
}
}

private void swapNums(int[] nums,int indexA,int indexB) {

int temp = nums[indexA];
nums[indexA] = nums[indexB];
nums[indexB] = temp;

}
}

Comment