堆排序法的使用

堆排序法的使用

相关背景:

堆排序法被喻为十大经典排序算法中最难掌握的一种算法,这是因为这种算法基于堆这种数据结构。在学习这种算法之前,我们需要先复习一下堆这个数据结构。

堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。

完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树
在这里插入图片描述

大根堆的概念:

在使用堆排序法的时候,我们引入一个重要概念大根堆

  • 该树为完全二叉树。
  • 该树中父节点的内容大于子节点的内容。
    即满足:arr(i) > arr(2 * i + 1) && arr(i) > arr(2 * i + 2)

补充:将数组化为堆有一个好处,就是对于任意已知结点 arr(i),可以根据其索引推出其父节点与子节点。

  • 子节点为:arr(2 * i + 1)(左),arr(2 * i + 1)(右)
  • 父节点为:arr((i - 1) / 2)

算法思想:

1.构造大根堆:

第一步要做的,就是把我们的无序数组( 无序数组如图所示 )构造成一个大根堆。如何构造呢?我们首先将拿到的无序数组以堆的形式表示出来:
在这里插入图片描述
以便进行后面的操作。

之后我们首先将 0 ~ 1 的元素构造成大根堆,然后构造 0 ~ 2 的元素 … 直到 0 ~ ( size - 1 )( size 表示数组长度 ) 。这种方法类似于降序的插入排序法

构造的过程是:对于每一次插入的新索引,我们都会将这个新索引的内容与其父节点的内容进行比较,若这个新索引的内容更大一些,则将这个新元素的内容与其父节点的内容交换,继续比较,直到这个新索引的内容找到一个专属它的位置

接下来我将一步步演示构造过程:

0 ~ 1 构造:(判断节点1的内容2与其父节点0的内容55的大小,2 < 55,故无需继续构造)
在这里插入图片描述
0 ~ 2 构造:(判断节点2的内容6与其父节点0的内容55的大小,6 < 55,故无需继续构造)

0 ~ 3 构造:(判断节点3的内容4与其父节点1的内容2的大小,4 > 2,交换。4 < 55,故无需继续构造)
在这里插入图片描述
0 ~ 4 构造:(判断节点4的内容32与其父节点1的内容4的大小,32 > 4,。32 < 55,故无需继续构造)
在这里插入图片描述
0 ~ 5 构造:(判断节点5的内容12与其父节点2的内容6的大小,12 > 6,交换。12 < 55,故无需继续构造)
在这里插入图片描述
0 ~ 6 构造:(判断节点6的内容9与其父节点2的内容12的大小,9 < 12,故无需继续构造)

0 ~ 7 构造:(判断节点7的内容73与其父节点3的内容2的大小,73 > 2,交换。73 > 32,交换。73 > 55,交换)
在这里插入图片描述

最终我们得到一个大根堆:
在这里插入图片描述

2.固定最大值并调整堆:

此时我们只需反复通过将大根堆的第一个内容(最大值)与最后一个内容交换,就能得到一个有序数组。我们做的事,相当于找最大值然后归位。

(固定后的内容为黑色,不参与后续操作)

不过,不要忘了,经过一次交换之后,我们的堆就已经不是大根堆了。这时,我们就要调整堆了,使它再变成一个大根堆(此时堆的大小减一,被固定的最大值不参与调整)。

这里我们用到的方法 heapify 也会与降序的插入排序法有几分相似只不过我们这次要插入的元素为交换后数组的第一个元素,也就是4。
在这里插入图片描述
heapify 的作用是循环比较当前索引内容与其左右孩子的大小,若当前索引内容大于其左右孩子内容,说明已经构成大根堆,break。若左右孩子其中一个的内容大于当前索引,则交换当前索引的内容与左右孩子中的最大内容,更新当前索引为这个最大值的索引,然后继续比较,交换,直到构成大根堆。

下面我将演示一次 heapify:

当前索引0的内容4与其左右孩子中的最大值55交换,更新索引为1:
在这里插入图片描述
当前索引1的内容4与左右孩子中的最大值37交换,更新当前索引为4:
在这里插入图片描述
此时得到一个大根堆:
在这里插入图片描述
之后将大根堆的第一个内容(最大值)与最后一个内容交换,size(需调整堆中元素的个数)–,继续 heapify,重复多次后,完成排序。

代码实现:

public class HeapSort
{
 public static void main(String[] args)
 {
  int[] arr = { 55, 2, 6, 4, 32, 12, 9, 73, 26, 37 };
  System.out.println(Arrays.toString(arr)); // 输出原数组
  heapSort(arr);
  System.out.println(Arrays.toString(arr)); // 输出排序后数组
 }
 public static void heapSort(int[] arr) // 堆排序法本体
 {
  heapInsert(arr); // 构造大根堆
  int size = arr.length;
  
  while (size > 1)
  {
   swap(arr, 0, size - 1); // 将顶端与最后一个数交换
   size--;                 // 需调整堆中元素个数
   heapify(arr, 0, size);  // 反复更新大根堆
  }
 }
 public static void heapInsert(int[] arr) // 构造大根堆(通过新插入的数上升)
 {
  for(int i = 1; i < arr.length; i++)
  {
   int currentIndex = i;
   int fatherIndex = (currentIndex - 1) / 2;
   while (arr[currentIndex] > arr[fatherIndex]) // 若当前节点大于其父节点
   {
    swap(arr, currentIndex, fatherIndex);    // 交换两节点的值
    currentIndex = fatherIndex;              // 更新当前结点
    fatherIndex = (currentIndex - 1) / 2;    // 更新父节点
   }
  }
 }
 public static void heapify(int[] arr, int index, int size) // 调整大根堆(通过顶端的数下降)
 {
  int left  = 2 * index + 1; // 左孩子的索引
  int right = 2 * index + 2; // 右孩子的索引
  while (left < size)        // 左孩子的索引小于数组长度
  {
   int largestIndex;      //最大孩子的索引
   if (arr[left] < arr[right] && right < len) // 左孩子小于右孩子且右孩子索引小于数组长度
   {
    largestIndex = right; 
   } else {
    largestIndex = left;
   }
   if (arr[index] > arr[largestIndex]) // 父节点大于左右孩子中的最大值
   {
    break;
   }
   swap(arr, largestIndex, index); // 交换
   index = largestIndex;  // 更新父节点(左右孩子中的最大值索引)
   left  = 2 * index + 1; // 更新左孩子(根据父节点来)
   right = 2 * index + 2; // 更新右孩子(根据父节点来)
  }
 }
 public static void swap(int[] arr, int i, int j)
 {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
 }
}

友情提示:手机观看,可以左右滑动

时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
空间复杂度: O ( 1 ) O(1) O(1)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值