参考左程云的视频
1.完全二叉树的概念
在了解堆排序的最开始,需要明白什么是完全二叉树
对于这样一棵用编号代表节点的树,若这棵树的节点严格按照图中的顺序填充(不必填满),即称为完全二叉树
也就是说,除了最后一层之外的每一层都被完全填满,而最后一层的所有节点,都需要保持从左到右的顺序
以上面这个图举例:若去掉节点12 13 14 15,该树满足完全二叉树
但是若去掉节点8 其他不动,则不满足
简单的说就是从左到右的排列中,不允许出现 “插队”的节点
2.大根堆的概念
首先我们要有一棵完全二叉树,这棵完全二叉树需要满足以下情况:
在该完全二叉树的任意一棵子树中,父节点都是这棵子树的最大值
仍用上图举例(图中的数字代表序号 而不代表实际值)
在1-15号节点的树中 1号节点应是1-15号所有节点的最大值
在这棵子树中 2号节点的值也应该是最大值
在中,4号节点的值也应该是最大值
其他节点间的大小关系不作任何约束
这样一棵 最大值为父节点的完全二叉树 称为大根堆
相反为小根堆
3.由数组来“建立”大根堆
现在我们有数组 5 7 0 6 8
我们要将它搭成一个大根堆,那么首先要将它变换成一棵完全二叉树
这里有一个非常重要的概念:
这棵完全二叉树并不是真实存在的,它只是我们脑海中的排列方式
我们常见的二叉树是这种形式来表现的:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
而此时我们需要搭建的完全二叉树并不是这样,它的本体仍然是数组,也就是说,树中不同节点需要用数组下标来表示
我们仍然按照下标从0到4的顺序,来填满这棵完全二叉树
假设父节点的下标为 i
那么它的左子节点为 2*i+1
它的右子节点为 2*i+2
对于某个节点,它的父节点的下标即为 (i-1)/2
节点间的对应关系清楚之后 我们发现这样一棵完全二叉树,并不符合大根堆的概念,是因为我们并没有按照规则来填满完全二叉树
正确的步骤是:
第一步:填入第一个数字 5
第二步:填入第二个数字7
此时7>5 不符合大根堆性质 我们将5与7的位置交换
注意:交换实际是发生在数组中的
之后数组变为 7 5 0 6 8
树变为
第三步:填入第三个数字0
不需要交换
第四步:填入第四个数字6
需要将5和6交换 交换前数组为:7 5 0 6 8 交换后数组即为 7 6 0 5 8
最后一步:将8填入
8填入之后需要将6与8交换 交换前数组为:7 6 0 5 8 交换后数组为:7 8 0 5 6
但是交换后发现 7和8也需要交换 其实每一次交换完之后 都需要将换上去的父节点 与这个换上去的父节点的父节点进行一次对比
交换前数组:7 8 0 5 6
交换后数组: 8 7 0 5 6
这样 由数组建立大根堆的步骤就完成了
用代码来实现也非常简单:
传入数组arr 对他的每个元素进行“填入”
for(int i=0;i<arr.length;i++)
{
heapInsert(arr,i);
}
public static void heapInsert(int[] arr,int index)
{
//直到当前节点不再大于它的父节点
while(arr[index]>arr[(index-1)/2])
{
swap(arr,index,(index-1)/2);
index=(index-1)/2;
}
}
4.由大根堆来完成排序
建完大根堆之后的数组为 8 7 0 5 6
树中的形式为:
由于大根堆中父节点最大的性质,此时8一定是数组中最大的元素
由于整棵树的父节点,在数组中的下标一定是0 所以下标为0的这个元素即为数组中的最大值
接下来将这个最大值,与最后一个元素交换
在这个例子中,即将8与6交换
交换后的数组为 6 7 0 5 8
交换后:数组的最后一个数即为数组中的最大元素,此时这个数不再参与接下来的排序
那么如何让最后的元素不再参与接下来的排序?
在排除之前
在生成大根堆时,是如何判断X位置没有元素的?
是因为X位置 在数组中的下标为5 而我们数组实际最大下标只到4 数组越界 所以X位置没有值
那么我们在将8排除后,同样可以设置一个指针,这个指针初始指向数组实际的最大下标 即为4
排除8后,这个指针前移一位 指向3 这个指针就是是否越界的标志 指向3后,相当于8被排除了
此时数为上图
在每一次排除一个元素后,树都应该做一次检查,检查是否符合大根堆的性质,若不符合则进行变换
在上图中,元素6换到下标0位置后,显然小于7 ,不符合大根堆性质
此时应该进行交换,通俗的做法是:
从下标0的节点开始,即从整棵树的父节点开始
若这个父节点有左孩子,那么去看他是否有右孩子,在父节点/左孩子/右孩子 三者中(可能不存在孩子)选择出最大的元素的下标
若这个最大元素的下标就是父节点的下标 说明父节点是最大值 不需要调整
其他情况则交换
交换后继续向下判断是否符合大根堆性质 直到某个元素没有左孩子(没有左孩子就没有右孩子)
本次调整完成
此时7为最大值 与数组最后一个元素5交换 并排除 指针前移一位 指向2 数组为 5 6 0 7 8
树为:
之后将5 6 0进行调整 调整后为 6 5 0
调整完成 交换6与0 排除6 指针前移一位 指向1
此时数组为0 5 6 7 8 此时看似排序已经完成 但是指针下标并没有指向0 所以继续
树为
调整后为
交换5 与 0 排除 5 此时指针下标指向0 排序完成
完整代码
import java.util.List;
public class Main
{
public static void main(String[] args)
{
int[] arr={5,7,0,6,8};
heapSort(arr);
for(int i=0;i<arr.length;i++)
{
System.out.println(arr[i]);
}
}
public static void heapSort(int[] arr)
{
if(arr==null||arr.length<2)
{
return;
}
for(int i=0;i<arr.length;i++)
{
heapInsert(arr,i);
}
int size=arr.length;
swap(arr,0,--size);
while(size>0)
{
heapify(arr,0,size);
swap(arr,0,--size);
}
}
public static void heapInsert(int[] arr,int index)
{
//建立大根堆 每个元素往上走
while(arr[index]>arr[(index-1)/2])
{
swap(arr,index,(index-1)/2);
index=(index-1)/2;
}
}
public static void heapify(int[] arr,int index,int size)
{
int left=index*2+1;
//有左孩子时
while(left<size)
{
//在左孩子和右孩子中选出较大者
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
//返回当前三个节点中最大的下标
largest=arr[largest]>arr[index] ? largest : index;
//若最大者是自己 则不用调整
if(largest==index)
break;
//某个孩子大于我 所以交换
swap(arr,largest,index);
//每个元素都往下走
index=largest;
left=index*2+1;
}
}
public static void swap(int[] nums,int i,int j)
{
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}