任务3
- 题目
堆是一棵完全二叉树,是二叉树众多应用中一个最适合用数组方式存储的树形,所以必须要完全掌握。堆的主要用途是构建优先队列ADT,除此之外,高效的从若干个数据中连续找到剩余元素中最小(或最大)都是堆的应用场景,比如构建 Huffman 树时,需要从候选频率中选择最小的两个;比如最短路径 Dijkstra 算法中要从最短路径估计值数组中选取当前最小值等,所有的这些应用都因为使用了堆而如虎添翼。本任务从几个方面对堆进行实践:
1) 参看书中的代码,撰写一个具有 insert、delete、getMin 等方法的 Min_Heap;
2) 完成一个使用 1)编写的 Min_Heap 的排序算法;
3) 堆是二叉树的一个应用,如果将堆的概念扩展到三叉树(树中每个结点的子结点数最多是 3个),此时将形成三叉堆。除了树形和二叉堆不一致外,堆序的要求是完全一致的。使用完全三叉树形成堆,在 n 个结点下显然会降低整棵树的高度,但是在维持某个结点的堆序时需要比较的次数会增加(这个时候会是 3 个子结点之间进行互相比较求最大或最小),这也是一个权衡问题。现在要求将 1)中实现的二叉堆改造成三叉堆,并通过 2)中实现的排序验证这个三叉堆是否正确。
- 任务1
- 数据结构设计
堆是一种树,堆中的每一个节点值都大于等于(或小于等于)子树中所有节点的值。或者说,任意一个节点的值都大于等于(或小于等于)所有子节点的值。根据大于或者小于区分为大顶堆和小顶堆。堆不一定是完全二叉树,只是为了方便存储和索引,我们通常用完全二叉树的形式来表示堆。
相对于有序数组而言,堆的主要优势在于插入和删除数据效率较高。 因为堆是基于完全二叉树实现的,所以在插入和删除数据时,只需要在二叉树中上下移动节点,时间复杂度为 O(log(n)),相比有序数组的 O(n),效率更高。Heap 初始化的时间复杂度为 O(n),而非O(nlogn)。
我建立的小顶堆数组索引0的位置为空,这样可以更轻易找到子节点和父节点,类中包括一个 public double[] array用于存储堆元素的数组和一个public int usedSize表示堆中已使用的元素数量。还有两个构造方法,分别是外界传入数组的构造方法和一个没有传入数组的构造方法,默认容量为10,后续可以扩容。
- public class Min_Heap_BinaryTree {
- //二叉树中的数组索引0的位置为空,这样可以更轻易找到子节点和父节点
- public double[] array; // 存储堆元素的数组
- public int usedSize; // 堆中已使用的元素数量
- public Min_Heap_BinaryTree(double[] array) {
- this.array = new double[array.length + 1];
- this.usedSize = array.length; // 初始化已使用元素数量为传入数组的长度
- buildHeap(array);
- }
- public Min_Heap_BinaryTree() {
- this.array = new double[11]; // 创建长度为11的数组
- this.usedSize = 0; // 初始化已使用元素数量为0
- }
- }
Insert方法接受一个double类型的值作为参数,表示要插入堆中的新元素;接着检查当前堆的容量是否不足,即usedSize + 1等于数组的长度,如果容量不足,就调用resizeArray()方法进行数组的扩容;接下来,将usedSize + 1作为索引,将新元素插入到堆的末尾位置。这里的usedSize + 1是因为数组索引0的位置为空,所以实际使用的索引从1开始;增加usedSize的值,表示堆中已使用的元素数量增加了一个;最后,调用heapifyUp(index)方法,向上调整堆。这是为了确保新插入的元素在堆中找到合适的位置并满足最小堆的性质。
heapifyUp方法接受一个索引值作为参数,表示需要向上调整的节点的索引;然后,计算父节点的索引,即index除以2;进入一个循环,条件是当前节点的索引大于1且当前节点的值小于其父节点的值。这是因为最小堆要求父节点的值小于等于子节点的值,在循环中,首先调用swap(index, parentIndex)方法交换当前节点和父节点的值,将较小的值移动到上层位置;然后,更新当前节点的索引为父节点的索引,即index = parentIndex;再次计算父节点的索引,即parentIndex = index / 2;重复上述步骤,直到当前节点的值大于等于其父节点的值,或者当前节点已经到达堆的根节点(索引为1)为止。
- public void insert(double value) {
- if (usedSize + 1 == array.length) { // 如果数组容量不足,则调整数组大小
- resizeArray();
- }
- int index = usedSize + 1;
- array[index] = value; //
- usedSize++;
- heapifyUp(index);
- }
- private void heapifyUp(int index) {
- int parentIndex = index / 2;
- while (index > 1 && array[index] < array[parentIndex]) {
- swap(index, parentIndex);
- index = parentIndex;
- parentIndex = index / 2;
- }
- }
delete方法检查堆是否为空,即usedSize等于0,如果堆为空,抛出一个IllegalStateException异常,表示堆为空,无法执行删除操作;然后,将堆顶元素的值赋给minValue,即最小值;将堆的最后一个元素移动到堆顶(索引为1的位置),即array[1] = array[usedSize];减少usedSize的值,表示堆中已使用的元素数量减少了一个;最后,调用heapifyDown(1)方法,向下调整堆。这是为了确保新的堆顶元素找到合适的位置并满足最小堆的性质。
heapifyDown方法接受一个索引值作为参数,表示需要向下调整的节点的索引;初始化smallest变量为当前节点的索引;计算左子节点和右子节点的索引;接着,进行一系列条件判断。首先判断左子节点的索引是否在堆范围内,并且左子节点的值是否小于当前最小值节点的值。如果满足条件,将smallest更新为左子节点的索引;然后,判断右子节点的索引是否在堆范围内,并且右子节点的值是否小于当前最小值节点的值。如果满足条件,将smallest更新为右子节点的索引;最后,如果smallest不等于当前节点的索引,说明需要进行交换。调用swap(index, smallest)方法交换当前节点和最小值节点的值;递归调用heapifyDown(smallest)方法,以确保被交换的节点在下层继续向下调整,以满足最小堆的性质。
- public double delete() {
- if (usedSize == 0) {
- throw new IllegalStateException("Heap is empty");
- }
- double minValue = array[1];
- array[1] = array[usedSize]; // 将最后一个元素移动到堆顶
- usedSize--;
- heapifyDown(1); // 向下调整堆
- return minValue;
- }
- private void heapifyDown(int index) {
- int smallest = index;
- int leftChild = 2 * index;
- int rightChild = 2 * index + 1;
- if (leftChild <= usedSize && array[leftChild] < array[smallest]) {
- smallest = leftChild;
- }
- if (rightChild <= usedSize && array[rightChild] < array[smallest]) {
- smallest = rightChild;
- }
- if (smallest != index) { // 如果最小值索引不等于当前节点索引,说明需要进行交换
- swap(index, smallest);
- heapifyDown(smallest); // 递归向下调整堆
- }
- }
buildHeap方法接受一个double类型的数组initialArray作为参数,表示初始数组;使用一个循环,将初始数组的元素复制到堆数组中;接下来,使用另一个循环,从最后一个非叶子节点开始向下调整堆。循环的起始索引是usedSize / 2在循环中,调用heapifyDown(i)方法,向下调整堆。这是为了确保从当前节点开始的子树满足最小堆的性质。
- public double getMin() {
- if (usedSize == 0) { // 如果堆为空,则抛出异常
- throw new IllegalStateException("Heap is empty");
- }
- return array[1]; // 返回堆顶元素,即最小值
- }
- private void buildHeap(double[] initialArray) {
- for (int i = 0; i < usedSize; i++) {
- array[i + 1] = initialArray[i]; // 将传入数组的元素复制到堆数组中
- }
- for (int i = (usedSize / 2); i >= 1; i--) {
- heapifyDown(i); // 从最后一个非叶子节点开始向下调整堆
- }
- }
两个私有方法比较简单,不再赘述。
- private void swap(int i, int j) {
- double temp = array[i];
- array[i] = array[j];
- array[j] = temp;
- }
- private void resizeArray() {
- double[] newArray= new double[array.length * 2]; // 创建新的数组,长度是原数组的两倍
- System.arraycopy(array, 0, newArray, 0, array.length); // 将原数组的元素复制到新数组中
- array = newArray; // 更新数组引用为新数组
- }
- 测试
测试代码如下:
- package Homework03;
- public class Min_HeapTest_BinaryTree {
- public static void main(String[] args) {
- // 创建一个空的小堆
- Min_Heap_BinaryTree heap = new Min_Heap_BinaryTree();
- // 插入元素
- heap.insert(5);
- heap.insert(3);
- heap.insert(8);
- heap.insert(1);
- heap.insert(10);
- // 获取最小值
- System.out.println("最小值:" + heap.getMin()); // 1
- // 删除最小值
- double minValue = heap.delete();
- System.out.println("删除的最小值:" + minValue); // 1
- // 再次获取最小值
- System.out.println("最小值:" + heap.getMin()); // 3
- // 打印堆中剩余元素
- System.out.println("堆中剩余元素:");
- while (heap.usedSize > 0) {
- double value = heap.delete();
- System.out.println(value);
- }
- }
- }
- 测试结果:

图4 二叉树构建的小顶堆文件测试结果
- 任务2
- 堆排序算法
堆排序算法很简单,创建一个Min_Heap_BinaryTree对象heap,并将待排序的数组传递给它。使用一个循环,从数组的末尾开始,依次将堆顶元素删除,并将其放入已排序部分的末尾。循环的起始索引是n - 1,循环条件是i >= 0,每次循环结束后,将i减1。在循环中,调用heap.delete()方法,该方法会删除堆顶元素并返回该元素的值。将返回的值赋给数组中对应的位置。
- public static void heapSort(double[] array) {
- int n = array.length;
- // 构建小堆
- Min_Heap_BinaryTree heap = new Min_Heap_BinaryTree(array);
- // 依次删除堆顶元素,放入已排序部分的末尾
- for (int i = n - 1; i >= 0; i--) {
- array[i] = heap.delete();
- }
- }
- 测试
- public static void main(String[] args) {
- double[] array = { 4, 10, 6, 5, 9 };
- System.out.println("排序前:");
- printArray(array);
- heapSort(array);
- System.out.println("排序后:");
- printArray(array);
- }
- 测试结果:

图5 二叉树构建的小顶堆堆排序结果
- 任务3
- 数据结构设计
二叉树构建的堆和三叉树构建的堆其实有很多相似的地方,区别有如下几个:
子结点位置:在二叉树构建的堆中,每个结点有左子结点和右子结点。左子结点在数组中的索引位置为2i-1,右子结点在数组中的索引位置为2i,其中i是父结点在数组中的索引位置。而在三叉树构建的堆中,每个结点有左子结点、中子结点和右子结点。左子结点在数组中的索引位置为3i-1,中子结点在数组中的索引位置为3i,右子结点在数组中的索引位置为3i+1,其中i是父结点在数组中的索引位置。
向下调整堆的方式:在二叉树构建的堆中,我们需要比较父节点和两个子节点,考虑是否交换位置以及和哪个子节点交换。在三叉树构建的堆中,我们需要比较四个节点,考虑是否交换位置以及和哪个子节点交换。
三叉树构建的堆的详细代码贴在附录,这里仅仅列出向下调整堆的方法。
- private void heapifyDown(int index) {
- int child1Index = 3 * index - 1;
- int child2Index = 3 * index;
- int child3Index = 3 * index + 1;
- int smallestChildIndex = index;
- if (child1Index <= usedSize && array[child1Index] < array[smallestChildIndex]) {
- smallestChildIndex = child1Index;
- }
- if (child2Index <= usedSize && array[child2Index] < array[smallestChildIndex]) {
- smallestChildIndex = child2Index;
- }
- if (child3Index <= usedSize && array[child3Index] < array[smallestChildIndex]) {
- smallestChildIndex = child3Index;
- }
- if (smallestChildIndex != index) {
- swap(index, smallestChildIndex);
- heapifyDown(smallestChildIndex);
- }
- }
heapifyDown方法接受一个索引index作为参数,表示需要进行向下调整的结点在数组中的索引位置。根据三叉树的性质,计算出当前结点的三个子结点的索引位置,然后初始化smallestChildIndex变量为当前结点的索引,表示当前结点是目前最小的结点通过一系列的条件判断,找到当前结点的三个子结点中值最小的子结点,并将其索引赋给smallestChildIndex,如果smallestChildIndex与index不相等,说明最小子结点的值小于当前结点的值,需要进行交换,递归调用heapifyDown方法,以最小子结点的索引作为参数,继续向下调整堆。
使用三叉树构建的小顶堆进行堆排序很简单,类似上一问。
- public static void heapSort(double[] array) {
- int n = array.length;
- // 构建小堆
- Min_Heap_TernaryTree heap = new Min_Heap_TernaryTree(array);
- // 依次删除堆顶元素,放入已排序部分的末尾
- for (int i = n - 1; i >= 0; i--) {
- array[i] = heap.delete();
- }
- }
- 测试
测试三叉树构建的小顶堆的方法的代码:
- package Homework03;
- public class Min_HeapTernaryTreeTest {
- public static void main(String[] args) {
- // 创建一个空的小堆
- Min_Heap_TernaryTree heap = new Min_Heap_TernaryTree();
- // 插入元素
- heap.insert(5);
- heap.insert(3);
- heap.insert(8);
- heap.insert(1);
- heap.insert(10);
- // 获取最小值
- System.out.println("最小值:" + heap.getMin()); // 1.0
- // 删除最小值
- double minValue = heap.delete();
- System.out.println("删除的最小值:" + minValue); // 1.0
- // 再次获取最小值
- System.out.println("最小值:" + heap.getMin()); // 3.0
- // 打印堆中剩余元素
- System.out.println("堆中剩余元素:");
- while (heap.usedSize > 0) {
- double value = heap.delete();
- System.out.println(value);
- }
- }
- }
测试利用三叉树构建的小顶堆进行堆排序的代码:
- public static void main(String[] args) {
- double[] array = { 4, 10, 6, 5, 9 };
- System.out.println("排序前:");
- printArray(array);
- heapSort(array);
- System.out.println("排序后:");
- printArray(array);
- }
- 测试结果:

图6 三叉树构建的小顶堆测试结果

图7 三叉树构建的小顶堆堆排序结果
- 总结和收获
复习了堆以及堆排序的算法,收获很大。
附录:
- 任务3
- Min_Heap_BinaryTree:
- package Homework03;
- public class Min_Heap_BinaryTree {
- //二叉树中的数组索引0的位置为空,这样可以更轻易找到子节点和父节点
- public double[] array; // 存储堆元素的数组
- public int usedSize; // 堆中已使用的元素数量
- public Min_Heap_BinaryTree(double[] array) {
- this.array = new double[array.length + 1];
- this.usedSize = array.length; // 初始化已使用元素数量为传入数组的长度
- buildHeap(array);
- }
- public Min_Heap_BinaryTree() {
- this.array = new double[11]; // 创建长度为11的数组
- this.usedSize = 0; // 初始化已使用元素数量为0
- }
- public void insert(double value) {
- if (usedSize + 1 == array.length) { // 如果数组容量不足,则调整数组大小
- resizeArray();
- }
- int index = usedSize + 1;
- array[index] = value; //
- usedSize++;
- heapifyUp(index);
- }
- public double delete() {
- if (usedSize == 0) {
- throw new IllegalStateException("Heap is empty");
- }
- double minValue = array[1];
- array[1] = array[usedSize]; // 将最后一个元素移动到堆顶
- usedSize--;
- heapifyDown(1); // 向下调整堆
- return minValue;
- }
- public double getMin() {
- if (usedSize == 0) { // 如果堆为空,则抛出异常
- throw new IllegalStateException("Heap is empty");
- }
- return array[1]; // 返回堆顶元素,即最小值
- }
- private void buildHeap(double[] initialArray) {
- for (int i = 0; i < usedSize; i++) {
- array[i + 1] = initialArray[i]; // 将传入数组的元素复制到堆数组中
- }
- for (int i = (usedSize / 2); i >= 1; i--) {
- heapifyDown(i); // 从最后一个非叶子节点开始向下调整堆
- }
- }
- private void heapifyUp(int index) {
- int parentIndex = index / 2;
- while (index > 1 && array[index] < array[parentIndex]) {
- swap(index, parentIndex);
- index = parentIndex;
- parentIndex = index / 2;
- }
- }
- private void heapifyDown(int index) {
- int smallest = index;
- int leftChild = 2 * index;
- int rightChild = 2 * index + 1;
- if (leftChild <= usedSize && array[leftChild] < array[smallest]) {
- smallest = leftChild;
- }
- if (rightChild <= usedSize && array[rightChild] < array[smallest]) {
- smallest = rightChild;
- }
- if (smallest != index) { // 如果最小值索引不等于当前节点索引,说明需要进行交换
- swap(index, smallest);
- heapifyDown(smallest); // 递归向下调整堆
- }
- }
- private void swap(int i, int j) {
- double temp = array[i];
- array[i] = array[j];
- array[j] = temp;
- }
- private void resizeArray() {
- double[] newArray= new double[array.length * 2]; // 创建新的数组,长度是原数组的两倍
- System.arraycopy(array, 0, newArray, 0, array.length); // 将原数组的元素复制到新数组中
- array = newArray; // 更新数组引用为新数组
- }
- }
- Min_Heap_TernaryTree:
- package Homework03;
- public class Min_Heap_TernaryTree {
- public double[] array;
- public int usedSize;
- public Min_Heap_TernaryTree(double[] array) {
- this.array = new double[array.length + 1];
- this.usedSize = array.length;
- buildHeap(array);
- }
- public Min_Heap_TernaryTree() {
- this.array = new double[11];
- this.usedSize = 0;
- }
- public void insert(double value) {
- if (usedSize + 1 == array.length) {
- resizeArray();
- }
- int index = usedSize + 1;
- array[index] = value;
- usedSize++;
- heapifyUp(index);
- }
- public double delete() {
- if (usedSize == 0) {
- throw new IllegalStateException("Heap is empty");
- }
- double minValue = array[1];
- array[1] = array[usedSize];
- usedSize--;
- heapifyDown(1);
- return minValue;
- }
- public double getMin() {
- if (usedSize == 0) {
- throw new IllegalStateException("Heap is empty");
- }
- return array[1];
- }
- private void buildHeap(double[] initialArray) {
- for (int i = 0; i < usedSize; i++) {
- array[i + 1] = initialArray[i];
- }
- for (int i = usedSize / 3; i >= 1; i--) {
- heapifyDown(i);
- }
- }
- private void heapifyUp(int index) {
- int parentIndex = (index - 1) / 3;
- while (index > 1 && array[index] < array[parentIndex]) {
- swap(index, parentIndex);
- index = parentIndex;
- parentIndex = (index - 1) / 3;
- }
- }
- private void heapifyDown(int index) {
- int child1Index = 3 * index - 1;
- int child2Index = 3 * index;
- int child3Index = 3 * index + 1;
- int smallestChildIndex = index;
- if (child1Index <= usedSize && array[child1Index] < array[smallestChildIndex]) {
- smallestChildIndex = child1Index;
- }
- if (child2Index <= usedSize && array[child2Index] < array[smallestChildIndex]) {
- smallestChildIndex = child2Index;
- }
- if (child3Index <= usedSize && array[child3Index] < array[smallestChildIndex]) {
- smallestChildIndex = child3Index;
- }
- if (smallestChildIndex != index) {
- swap(index, smallestChildIndex);
- heapifyDown(smallestChildIndex);
- }
- }
- private void swap(int i, int j) {
- double temp = array[i];
- array[i] = array[j];
- array[j] = temp;
- }
- private void resizeArray() {
- double[] newArray = new double[array.length * 2];
- System.arraycopy(array, 0, newArray, 0, array.length);
- array = newArray;
- }
- }
- Min_HeapTest_BinaryTree:
- package Homework03;
- public class Min_HeapTest_BinaryTree {
- public static void main(String[] args) {
- // 创建一个空的小堆
- Min_Heap_BinaryTree heap = new Min_Heap_BinaryTree();
- // 插入元素
- heap.insert(5);
- heap.insert(3);
- heap.insert(8);
- heap.insert(1);
- heap.insert(10);
- // 获取最小值
- System.out.println("最小值:" + heap.getMin()); // 1
- // 删除最小值
- double minValue = heap.delete();
- System.out.println("删除的最小值:" + minValue); // 1
- // 再次获取最小值
- System.out.println("最小值:" + heap.getMin()); // 3
- // 打印堆中剩余元素
- System.out.println("堆中剩余元素:");
- while (heap.usedSize > 0) {
- double value = heap.delete();
- System.out.println(value);
- }
- }
- }
- Min_HeapTernaryTreeTest:
- package Homework03;
- public class Min_HeapTernaryTreeTest {
- public static void main(String[] args) {
- // 创建一个空的小堆
- Min_Heap_TernaryTree heap = new Min_Heap_TernaryTree();
- // 插入元素
- heap.insert(5);
- heap.insert(3);
- heap.insert(8);
- heap.insert(1);
- heap.insert(10);
- // 获取最小值
- System.out.println("最小值:" + heap.getMin()); // 1.0
- // 删除最小值
- double minValue = heap.delete();
- System.out.println("删除的最小值:" + minValue); // 1.0
- // 再次获取最小值
- System.out.println("最小值:" + heap.getMin()); // 3.0
- // 打印堆中剩余元素
- System.out.println("堆中剩余元素:");
- while (heap.usedSize > 0) {
- double value = heap.delete();
- System.out.println(value);
- }
- }
- }
- HeapSortBinaryTree:
- package Homework03;
- public class HeapSortBinaryTree {
- public static void heapSort(double[] array) {
- int n = array.length;
- // 构建小堆
- Min_Heap_BinaryTree heap = new Min_Heap_BinaryTree(array);
- // 依次删除堆顶元素,放入已排序部分的末尾
- for (int i = n - 1; i >= 0; i--) {
- array[i] = heap.delete();
- }
- }
- public static void printArray(double[] array) {
- for (double num : array) {
- System.out.print(num + " ");
- }
- System.out.println();
- }
- public static void main(String[] args) {
- double[] array = { 4, 10, 6, 5, 9 };
- System.out.println("排序前:");
- printArray(array);
- heapSort(array);
- System.out.println("排序后:");
- printArray(array);
- }
- }
- HeapSortTernaryTree:
- package Homework03;
- public class HeapSortTernaryTree {
- public static void heapSort(double[] array) {
- int n = array.length;
- // 构建小堆
- Min_Heap_TernaryTree heap = new Min_Heap_TernaryTree(array);
- // 依次删除堆顶元素,放入已排序部分的末尾
- for (int i = n - 1; i >= 0; i--) {
- array[i] = heap.delete();
- }
- }
- public static void printArray(double[] array) {
- for (double num : array) {
- System.out.print(num + " ");
- }
- System.out.println();
- }
- public static void main(String[] args) {
- double[] array = { 4, 10, 6, 5, 9 };
- System.out.println("排序前:");
- printArray(array);
- heapSort(array);
- System.out.println("排序后:");
- printArray(array);
- }
- }
2714

被折叠的 条评论
为什么被折叠?



