堆的各种操作与堆排序 Java&Golang


本文以大顶堆为例,给出堆中各种常见的操作与堆排序的 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)/21
  • 在一个大小为 s i z e size size 的堆中,第一个(从下往上、从右往左)非叶子节点的位置是 s i z e / 2 − 1 size / 2 - 1 size/21

堆操作

首先,在所有的堆操作中,都一定会用到工具函数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)
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZBH4444

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值