本文以大顶堆为例,给出堆中各种常见的操作与堆排序的 Java 代码。
首先,在堆(用数组实现)中有如下几个关键知识需要掌握:
- 若一个节点的下标为 i i i ,则其左右子节点的下标分别为 2 i + 1 2i + 1 2i+1 和 2 i + 2 2i + 2 2i+2 。
- 若一个节点的下标为 i i i ,则其父节点的下标为 ( i + 1 ) / 2 − 1 (i + 1) / 2 - 1 (i+1)/2−1 。
- 在一个大小为 s i z e size size 的堆中,第一个(从下往上、从右往左)非叶子节点的位置是 s i z e / 2 − 1 size / 2 - 1 size/2−1 。
堆操作
首先,在所有的堆操作中,都一定会用到工具函数swap()
,以交换堆中的两个节点。
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
另外,需要一个变量heapSize
用以记录堆中当前的元素的数量(而不是堆的最大容量)。
private int heapSize = 0;
上浮
将堆中一个指定位置的节点进行上浮,从而维护堆。
private void floatNode(int[] maxHeap, int node) {
int parent = ((node + 1) >> 1) - 1; // 计算 node 的父节点的位置
// 只要 node 还大于它的父节点,就不断上浮
while (parent >= 0 && maxHeap[parent] < maxHeap[node]) {
swap(maxHeap, parent, node);
node = parent;
parent = ((node + 1) >> 1) - 1;
}
}
下沉
将堆中一个指定位置的节点进行下沉,从而维护堆。
private void sinkNode(int[] maxHeap, int node) {
int leftChild = 2 * node + 1, rightChild = 2 * node + 2; // 计算 node 的左右子节点的位置
int biggerChild = node; // node 的两个子节点中较大者的下标
// 不断进行下沉,直到 node 节点的两个子节点都小于它
while (true) {
// 选出左右两个子节点中较大的那个
if (leftChild < heapSize && maxHeap[biggerChild] < maxHeap[leftChild]) biggerChild = leftChild;
if (rightChild < heapSize && maxHeap[biggerChild] < maxHeap[rightChild]) biggerChild = rightChild;
// 若存在大于 node 节点的子节点则继续下沉,否则结束下沉
if (biggerChild != node) {
swap(maxHeap, node, biggerChild);
node = biggerChild;
leftChild = 2 * node + 1;
rightChild = 2 * node + 2;
} else break;
}
}
堆的插入
往堆中插入元素时,只需先将新元素插入堆底,然后对其进行上浮操作即可。
private void insertIntoMaxHeap(int[] maxHeap, int newNodeVal) {
// 若堆未满则可以添加
if (heapSize < maxHeap.length) {
// 将新元素放入堆底
maxHeap[heapSize] = newNodeVal;
// 进行上浮
floatNode(maxHeap, heapSize);
heapSize++;
}
}
堆的删除
堆只能删除其顶部的元素,删除堆顶元素需先将堆顶元素与堆底元素互换位置,然后对换过来的堆顶元素进行下沉操作。
注意: 堆顶元素不是真的被删除了,而是被放入了堆底,由于heapSize
进行了减一操作,所以这个元素的存在不会影响后续的堆操作,这一点使得堆排序得以实现!
private void pollFromMaxHeap(int[] maxHeap) {
swap(maxHeap, 0, heapSize - 1);
heapSize--;
sinkNode(maxHeap, 0);
}
堆化数组
将一个无序的数组转化为堆,即称为将数组堆化。
首先将这个数组视为一个堆(虽然是无序的)。
然后对堆中的每个非叶子节点进行下沉操作,即可完成堆化。
private void heaping(int[] nums) {
// 从第一个非叶子节点到最后一个非叶子节点(从下往上数),将它们依次进行下沉,使得这些以它们为顶的子堆合法
for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
sinkNode(nums, i);
}
}
堆排序
利用各种堆操作,即可实现堆排序。
首先将数组堆化,然后将所有元素 “删除” 一遍,即可得到一个排好序的数组。
private void heapSort(int[] nums) {
heapSize = nums.length; // 在堆排序中,heapSize 可以理解为尚未排序的元素的数量
// 先将数组堆化为一个最大堆
heaping(nums);
// 不断将堆顶元素删除(实际上是把它放入堆底),直到堆为空(实际上是堆中不再有尚未排序的元素)
while (heapSize > 0) {
pollFromMaxHeap(nums);
}
}
完整代码 - Java
import java.util.Arrays;
public class HeapTest {
private int heapSize;
public HeapTest() {
this.heapSize = 0;
}
public static void main(String[] args) {
int[] nums = new int[8];
HeapTest heapTest = new HeapTest();
// 添加元素
heapTest.insertIntoMaxHeap(nums, 3);
System.out.println("添加后:" + Arrays.toString(nums));
heapTest.insertIntoMaxHeap(nums, 2);
System.out.println("添加后:" + Arrays.toString(nums));
heapTest.insertIntoMaxHeap(nums, 1);
System.out.println("添加后:" + Arrays.toString(nums));
heapTest.insertIntoMaxHeap(nums, 5);
System.out.println("添加后:" + Arrays.toString(nums));
heapTest.insertIntoMaxHeap(nums, 6);
System.out.println("添加后:" + Arrays.toString(nums));
heapTest.insertIntoMaxHeap(nums, 4);
System.out.println("添加后:" + Arrays.toString(nums));
heapTest.insertIntoMaxHeap(nums, 7);
System.out.println("添加后:" + Arrays.toString(nums));
heapTest.insertIntoMaxHeap(nums, 4);
System.out.println("添加后:" + Arrays.toString(nums));
// 删除元素
heapTest.pollFromMaxHeap(nums);
System.out.println("删除后:" + Arrays.toString(nums));
//堆排序
heapTest.heapSort(nums);
System.out.println("排序后:" + Arrays.toString(nums));
}
private void heapSort(int[] nums) {
heapSize = nums.length; // 在堆排序中,heapSize 可以理解为尚未排序的元素的数量
// 先将数组堆化为一个最大堆
heaping(nums);
// 不断将堆顶元素删除(实际上是把它放入堆底),直到堆为空(实际上是堆中不再有尚未排序的元素)
while (heapSize > 0) {
pollFromMaxHeap(nums);
}
}
private void heaping(int[] nums) {
// 从第一个非叶子节点到最后一个非叶子节点(从下往上数),将它们依次进行下沉,使得这些以它们为顶的子堆合法
for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
sinkNode(nums, i);
}
}
private void insertIntoMaxHeap(int[] maxHeap, int newNodeVal) {
// 若堆未满则可以添加
if (heapSize < maxHeap.length) {
// 将新元素放入堆底
maxHeap[heapSize] = newNodeVal;
// 进行上浮
floatNode(maxHeap, heapSize);
heapSize++;
}
}
private void pollFromMaxHeap(int[] maxHeap) {
swap(maxHeap, 0, heapSize - 1);
heapSize--;
sinkNode(maxHeap, 0);
}
private void floatNode(int[] maxHeap, int node) {
int parent = ((node + 1) >> 1) - 1; // 计算 node 的父节点的位置
// 只要 node 还大于它的父节点,就不断上浮
while (parent >= 0 && maxHeap[parent] < maxHeap[node]) {
swap(maxHeap, parent, node);
node = parent;
parent = ((node + 1) >> 1) - 1;
}
}
private void sinkNode(int[] maxHeap, int node) {
int leftChild = 2 * node + 1, rightChild = 2 * node + 2; // 计算 node 的左右子节点的位置
int biggerChild = node; // node 的两个子节点中较大者的下标
// 不断进行下沉,直到 node 节点的两个子节点都小于它
while (true) {
// 选出左右两个子节点中较大的那个
if (leftChild < heapSize && maxHeap[biggerChild] < maxHeap[leftChild]) biggerChild = leftChild;
if (rightChild < heapSize && maxHeap[biggerChild] < maxHeap[rightChild]) biggerChild = rightChild;
// 若存在大于 node 节点的子节点则继续下沉,否则结束下沉
if (biggerChild != node) {
swap(maxHeap, node, biggerChild);
node = biggerChild;
leftChild = 2 * node + 1;
rightChild = 2 * node + 2;
} else break;
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
完整代码 - Golang(面向对象)
package main
import "fmt"
// 912. 排序数组
/*
前置知识(nums []int 表示堆):
1. 下标为 i 的节点的左右子节点的下标分别为 i * 2 + 1 和 i * 2 + 1
2. 下标为 i 的节点的父节点的下标为 (i + 1) / 2 - 1
3. 整个堆下标最大的非叶子节点的 size / 2 - 1
*/
type Heap struct {
heap []int // 最大堆数组
heapSize int // 堆的当前大小
}
func NewHeap(num []int) *Heap {
heapInstance := &Heap{
heap: num,
heapSize: len(num),
}
heapInstance.heaping()
return heapInstance
}
// 将一个指定位置的节点上浮,直到达到正确位置
func (h *Heap) floatNode(nodeIndex int) {
for true {
parentNodeIndex := (nodeIndex+1)>>1 - 1
if parentNodeIndex >= 0 && h.heap[parentNodeIndex] < h.heap[nodeIndex] { // 尚未到顶且尚未到达应到的位置
h.heap[parentNodeIndex], h.heap[nodeIndex] = h.heap[nodeIndex], h.heap[parentNodeIndex]
nodeIndex = parentNodeIndex
} else { // 已到达正确位置
break
}
}
}
// 将一个指定位置的节点下沉,直到达到正确位置
func (h *Heap) sinkNode(nodeIndex int) {
for true {
leftChildIndex, rightChildIndex := (nodeIndex<<1)+1, (nodeIndex<<1)+2
biggestChildIndex := nodeIndex
// 尝试找出最大的(且大于该节点的)子节点
if leftChildIndex < h.heapSize && h.heap[leftChildIndex] > h.heap[biggestChildIndex] {
biggestChildIndex = leftChildIndex
}
if rightChildIndex < h.heapSize && h.heap[rightChildIndex] > h.heap[biggestChildIndex] {
biggestChildIndex = rightChildIndex
}
if nodeIndex != biggestChildIndex { // 该节点存在最大的(且大于该节点的)子节点
// 互换 nodeIndex 和 biggestChildIndex,然后继续下沉
h.heap[nodeIndex], h.heap[biggestChildIndex] = h.heap[biggestChildIndex], h.heap[nodeIndex]
nodeIndex = biggestChildIndex
} else { // 已经下沉到正确的位置
break // 结束下沉
}
}
}
// 向堆中插入一个节点
func (h *Heap) addNode(newNodeValue int) {
if h.heapSize <= len(h.heap) {
// 新节点先放在堆底
h.heap[h.heapSize] = newNodeValue
h.heapSize++
// 然后将其上浮到它应该在的位置
h.floatNode(h.heapSize)
}
}
// 从堆顶移除一个节点(实际是放入最大堆数组的最后并将 heapSize - 1,达到从逻辑堆中移除的效果,便于实现堆排序)
func (h *Heap) pollTopNode() {
h.heap[0], h.heap[h.heapSize-1] = h.heap[h.heapSize-1], h.heap[0] // 堆顶堆底对调
h.heapSize-- // 更新堆的大小
h.sinkNode(0) // 重新整理堆
}
// 将最大堆数组堆化
func (h *Heap) heaping() {
// 把所有的非叶子节点都执行一次下沉操作,即可完成对堆化
for i := (h.heapSize >> 1) - 1; i >= 0; i-- {
h.sinkNode(i)
}
}
func heapSort(nums []int) []int {
heapInstance := NewHeap(nums)
// 堆排序即为不断将堆顶节点移除,直到堆的逻辑大小为空
for heapInstance.heapSize > 0 {
heapInstance.pollTopNode()
}
return heapInstance.heap
}
func main() {
arr := []int{12, 6, 13, 11, 5, 9}
fmt.Println("Given array is:", arr)
heapSort(arr)
fmt.Println("Sorted array is:", arr)
}