什么是堆?
堆是利用完全二叉树实现的一种数据结构,分为大根堆和小根堆:
大根堆:每个节点的值都大于等于其左右孩子的值。
小根堆:每个节点的值都小于等于其左右孩子的值。
堆在数组中的存储
- 根节点存储在数组
a[0]
的位置,后面的数值按照广度优先顺序依次填入数组中。 - 已知父节点的索引为
i
,其左孩子坐标为2*i+1
,右孩子的坐标为2*i+2
。
堆排序
排序步骤
- 首先将待排序的数组构造成一个大根堆,此时数组中最大的值应该是堆顶端的值,也就是数组下标为
a[0]
的值。 - 将堆顶的元素与最末尾的数字交换。此时最大值的位置转移到了数组的末尾处。
- 将剩余的 n − 1 n-1 n−1个元素重新构建大根堆,重复步骤2,将堆顶元素与这 n − 1 n-1 n−1个元素的最后一个交换。
- 重复上述步骤,就能得到一个升序的数组。
- 如果想要得到降序的数组,就构建小根堆。
构建大根堆
构建的大根堆需要自下而上进行构建
-
首先定义一个
parent
游标从后向前移动,找到最后一个有孩子的节点。 -
定义
child
游标,指向parent
的左孩子。 -
判断当前
parent
是否存在右孩子。(根据完全二叉树的性质,有孩子就一定有左孩子,右孩子有没有不一定) -
如果有右孩子,比较左右孩子的大小,
child
指向左右孩子中大的那个。
-
parent
与child
节点进行比较,如果父节点大,parent
继续向上移动。 -
如果父节点小,父子节点进行交换,
child
游标继续指向child
的左右孩子中的最大值进行对比,直至child
指向null
或者parent
的值大于child
。
-
重复上述步骤直至
parent
游标指向堆顶元素。
堆顶和堆底元素交换
堆顶和堆底元素交换之后,去掉最后一个元素重新构建一个大根堆。
代码实现
//检查一个节点是否符合大顶堆
public static void adjust(int[] arr, int parent, int length) {
int child = 2 * parent + 1;
while (child < length){
int rchild = child + 1;
if (rchild < length && arr[child] < arr[rchild]){ //如果右孩子的索引小于数组的有效长度,并且右孩子大,child已经指向左右孩子中的最大值
child ++;
}
if(arr[parent] < arr[child]) {//如果父节点小于孩子节点
int temp = arr[parent];
arr[parent] = arr[child];
arr[child] = temp;
//递归处理孩子的孩子节点
parent = child;
child = 2 * child + 1;
} else {
break;
}
}
}
public static void main(String[] args) {
int[] arr = {5,7,4,2,0,3,1,6};
//1.构建大顶堆
for (int p = arr.length - 1; p >= 0; p --){
adjust(arr,p,arr.length);
}
System.out.println(Arrays.toString(arr));
for(int i = arr.length - 1; i >= 0; i --){
//2.堆顶堆底进行交换
int temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
System.out.println(Arrays.toString(arr));
//3.剩余元素继续构建大顶堆
adjust(arr,0,i);
}
}
复杂度分析
- 在构建大顶堆的过程中,假设每个元素都需要调整,根据二叉树的性质,最顶端的节点最坏的情况下也不过调整 l o g n logn logn 次,因此一共 n n n 个元素,复杂度不会超过 O ( n l o g n ) O(nlogn) O(nlogn)。
- 在将堆顶堆底元素进行交换的过程中,假设每个元素都会移动 l o g n logn logn 次,共 n n n 个元素,复杂度也不会超过 O ( n l o g n ) O(nlogn) O(nlogn)。
- 这两次操作是平行的而不是嵌套的,因此最终排序的复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。