堆排序目录
- 1、什么是堆?
- 1.1、什么是大顶堆
- 1.2、什么是小顶堆
- 2、堆排序的过程
- 3、堆排序的图解
- 3.1、将数组映射成一个完全二叉树
- 3.2、将数组转变为一个大顶堆
- 3.3、开始进行堆排序
- 4、堆排序代码
1、什么是堆?
堆的定义:在一棵完全二叉树中,每一棵子树的根节点值均大于或小于其左右子树的所有根节点值,被称为堆。其中每一棵子树的根节点值均大于左右子树的节点时,这棵树被称为大顶堆,反之,被称为小顶堆。
堆的特殊定义导致了堆具有特殊的结构,也就是说,一个堆的根节点,肯定是最大/最小
的,因此,堆排序就是要将一串数组放进一个堆中去,先将这个堆构建好,然后我们就可以肯定堆顶
元素是这串数组中最大/最小
的了,之后我们就取走堆顶元素
,将堆中最后一个元素
放到堆顶
,然后再维护这个堆,让它重新成为一个合法的堆即可。
1.1、什么是大顶堆
我们对堆中的节点按层进行编号,映射到数组中就是下面这个样子:
大顶堆特点:
a
r
r
[
i
]
>
=
a
r
r
[
2
i
+
1
]
&
&
a
r
r
[
i
]
>
=
a
r
r
[
2
i
+
2
]
arr[i]>=arr[2i+1]\&\&arr[i]>=arr[2i+2]
arr[i]>=arr[2i+1]&&arr[i]>=arr[2i+2]
1.2、什么是小顶堆
小顶堆特点: a r r [ i ] < = a r r [ 2 i + 1 ] & & a r r [ i ] < = a r r [ 2 i + 2 ] arr[i]<=arr[2i+1]\&\&arr[i]<=arr[2i+2] arr[i]<=arr[2i+1]&&arr[i]<=arr[2i+2] //i对应第几个节点,i从0开始标号
一般升序采用大顶堆,降序采用小顶堆
2、堆排序的过程
堆排序的过程:
-
根据拿到的数组构建大顶堆/小顶堆;
-
从堆顶取走元素,放到其应该存在的位置中去。从堆底拿到堆中最后一个元素,放到堆顶,此时这个堆很可能不再合法也就是说不再是一个堆;
-
维护这个堆,通过自己写的方法调整堆中节点结构,让它重新变成一个堆;
-
重复2,3过程,直到堆被取空,此时数组也被完全排列好;
3、堆排序的图解
3.1、将数组映射成一个完全二叉树
我们先自己写了一个无序的数组,如图所示,这个数组是很没有规律的。然后我们既然想把这个数组构建成堆
接下来,我们根据这个数组的结构映射出一个完全二叉树的结构,如下图所示:
3.2、将数组转变为一个大顶堆
在这里我们使用大顶堆
进行排序,基于小顶堆的堆排序和这里的算法思路相同,只是实现起来有微小的差异,不过需要声明的是:基于数组的堆排序使用大顶堆排序更加方便,写起来代码量更少一些。
大顶堆的构建,我们可以使用递归
的方法来构建,但是这里我们暂且不深入研究递归,因此我们使用从堆底元素一个个排查构建的基础手段进行堆的构建,也就是从数组尾部一个一个往前找
,直到找到第一个不是叶子结点的节点后,我们对以它为根节点的子树进行整改,让其成为一个堆
,这样一个个的往前遍历整改,就能够使得这棵树完全成为一个堆。这里只是简述了我的堆构建算法的大体思路,其中有很多细节将在下面进行详细的展示,同时使用我的算法在构建堆的时候存在一个非常重要的细节,它关系到这个堆能否被成功构建,接下来我们开始详细讲解如何构建一个堆。
-
后往前找,找到了第一个不是叶子结点的节点,如图所示:
可见该节点的叶子节点都小于根节点,也就是15
,因此这棵子树本身就是一个大顶堆,我们继续向前遍历。 -
这时我们遍历到了值为7的节点,这个节点也大于其左子树和右子树,因此它也是一个大顶堆,我们继续向前遍历:
-
我们这时遍历到了值为1的节点,很不幸,它小于它的左子树和右子树,它不再是堆了:
这时我们应该怎么做?我们应该从它的
左右孩子中挑选出最大的一个,然后和根节点进行交换
,这样就足以保证这棵子树的根节点大于它的所有直接孩子了。这里我们进行交换,会得到一个这样的新结构: -
我们这时遍历到了以值为3的节点为根的子树,这个子树显然也不是一个大顶堆,3比它的左右孩子都小,如图所示:
这时我们需要将其变成一个大顶堆,有了前面的经验,我们做起来轻车熟路,选择根节点孩子节点中的最大值和根节点交换就行了,如图所示:等等!这,这不对吧?这棵子树的根节点确确实实已经大于它的左右
直接孩子
了,但是,由于3和15的交换,导致了该子树的右子树不再是一个大顶堆
。现在,3是该子树的右子树的根节点,而这个根节点的右孩子是12,12大于3,它已经不符合大顶堆的概念了。这时,重要的知识点来了:由于大顶堆构建导致的一次节点值互换,有极大的可能直接导致以参与值交换的孩子节点为根节点的子树不再是一个大顶堆,简而言之,大顶堆的构建过程中的值互换操作,会导致一个更小的子树不再是大顶堆,放到这里就是,节点值为3的子树,为了变成大顶堆,和它的右孩子节点,也就是值为15的节点发生了交换,这时这颗子树的根节点值不再是3了,而是15,如上图所示,而这时,这个子树的右子树的根节点不再是15了,而变成了3,这就直接导致这个右子树不再是堆了,其有序性遭到了破坏。这时,我们要继续深入,攘外必先安内,解决掉这个问题。我们将当前的游标指向当前树的右子树根节点,也就是现在值为3的节点:
我们将此刻标红的节点继续处理,变成大顶堆,它没有右孩子,只有左孩子,因此不需要找最大孩子,直接交换就行:
现在我们将解决了刚才的问题,现在以15为根节点(也就是最初以3为根节点)的那个子树,彻彻底底确确实实的变成大顶堆了。可见,关于堆的维护,其实是穿插在堆的构建中的,构建堆的操作可能导致一个子树不再是堆,这时我们就应该在一次交换操作之后检索以参与交换的孩子节点为根节点的子树是否还是一个堆,如果不是了,那么我们必须要将当前的根节点游标指向它,将以它为根节点的子树作为新的问题规模,重复堆构建操作。现在我们解决了这个问题,就要回退到之前的位置,并继续向前遍历。 -
现在我们终于遍历到首节点了,也就是堆顶,或者说这棵树的根节点,也就是值为6的节点:
我们通过观察发现,现在这棵树显然不是一个大顶堆,根节点的左右孩子都比根节点大,我们挑选最大的
直接孩子
也就是15,和6交换:
有了之前的经历,我们已经见怪不怪了,由于6和15的交换,导致了这棵树的左子树不再是一个堆,因为现在以6为根节点的子树小于它的直接左右孩子的值,也就是6和12,好事多磨,我们没有办法,只得向下深入,将游标重新指向当前值为6的节点,将其重新进行堆构建:
我们发现当前子树的根节点的左右孩子最大的是12,因此我们做一个交换,并且在交换后我们将游标指向参与交换的孩子节点位置,检测这次交换是否造成了子树堆的破坏:
好在没有,现在我们将游标回退到之前的位置,一个大顶堆也宣告完成:
以上就是堆构建的过程,然而,你以为这就完了吗?答案是还没有,这只是把一个堆构建了出来,我们实际上还没有开始进行排序,但是实际上整个过程我们已经完成了大半了,堆的构建以及维护就是上面讲的内容了,而这些内容就是最为核心的内容,并且这个构建算法需要在堆排序中反复使用,因此大家要多加学习。接下来,我们开始进行堆排序。
3.3、开始进行堆排序
那么,堆排序的过程是怎样的呢?现在我们已经得到了构建一个大顶堆的算法,因此我们现在可以对这个堆进行一些操作并有自信将其变回一个新的大顶堆了。所以我们先将堆顶元素和堆底元素进行替换:
也就是将15和3进行互换,如上图所示。在互换后我们会得到如下图所示的一个新树,此时它已经不是大顶堆了:
之后,我们将堆底元素拿走,放到数组的最末端去,有:
在这个操作之后,我们发现以该树根节点为根节点的子树不再是大顶堆了,因为根节点发生了变化,因此我们对这棵树进行维护,使用上文“将数组转变为一个大顶堆”中提到的方法,让这棵新树重现变为大顶堆:
先让3和12交换:
然后我们发现有一棵左子树不再是大顶堆了,我们继续交换:
在此之后,又发现更小的一棵子树不是大顶堆了,我们继续进行堆维护操作:
至此,一棵新的大顶堆树又完成了,我们重复上文提到的堆顶元素和堆底元素交换的过程,并同样重复拿走交换后的堆底元素的操作:
至此,我们又拿到了除15外最大的数字并排列到了15之后,重复这个过程,我们最终将得到一个从小到大的有序数组。在每次交换取数之后,再次进行堆的维护,这样一来我们就可以不断的取走当前剩余数字中最大的,并维持大顶堆,保证我们下一次取数也能立刻找到最大的。以上就是堆排序的过程。
4、堆排序代码
public class HeapSort {
public static void main(String[] args) {
int[] arr = new int[] { 5, 2, 1, 6, 4, 8, 11, 34, 56, 17, 26 };// 测试数组
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void heapSort(int[] arr) {
for (int i = arr.length - 1; i >= 0; i--) {
adjustSort(arr, i, arr.length);
}
for (int i = arr.length - 1; i >= 0; i--) {
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
adjustSort(arr, 0, i);
}
}
public static void adjustSort(int[] arr, int parent, int lenght) {
int temp = arr[parent];
int Child = 2 * parent + 1;
while (Child < lenght) {
// 找到左右子节点中较大的那个节点,如果左边大就用child,如果右边大就用child++
if (Child + 1 < lenght && arr[Child] < arr[Child + 1]) {
Child++;
}
if (temp >= arr[Child]) {
break;
}
arr[parent] = arr[Child];
parent = Child;
// 查找当前节点的子节点,如果有子节点,继续调整
Child = parent * 2 + 1;
}
// 交换数据
arr[parent] = temp;
}
}
adjustSort
负责维护大顶堆,heapSort
负责将堆顶元素与最后一个数对调,然后重复前面的数
码字不易,如果有收获不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️