一、基础知识
堆的结构可以分为大根堆和小根堆,是一个完全二叉树,而堆排序是根据堆的这种数据结构设计的一种排序。
- 完全二叉树(Complete Binary Tree): 除了最后一层之外的其他每一层都被完全填充,并且所有结点都保持向左对齐。
1.1 大根堆和小根堆
大根堆:每个结点的值都大于其左孩子和右孩子结点的值。
小根堆:每个结点的值都小于其左孩子和右孩子结点的值。
2.2 查找数组中某个数的父结点和左右孩子结点
对于 i 点,有:
父亲节点为:
(
i
−
1
)
/
2
(i-1)/2
(i−1)/2;
左儿子为:
2
∗
i
+
1
2*i+1
2∗i+1;
右儿子为:
2
∗
i
+
2
2*i+2
2∗i+2;
对于大小根堆映射的数组,他们满足堆的定义性质。所以有:
大根堆: a r r ( i ) > a r r ( 2 ∗ i + 1 ) arr(i)>arr(2*i+1) arr(i)>arr(2∗i+1) && a r r ( i ) > a r r ( 2 ∗ i + 2 ) arr(i)>arr(2*i+2) arr(i)>arr(2∗i+2)
小根堆: a r r ( i ) < a r r ( 2 ∗ i + 1 ) arr(i)<arr(2*i+1) arr(i)<arr(2∗i+1) && a r r ( i ) < a r r ( 2 ∗ i + 2 ) arr(i)<arr(2*i+2) arr(i)<arr(2∗i+2)
二、堆排序
2.1 基本思想
- 将待排序的数组构造成一个大根堆,此时整个数组的最大值就是堆结的顶端
- 将顶端的数与末尾的数交换,此时末尾的数为最大值,剩余待排序数组个数为n-1
- 将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
2.2 模拟过程
我们以数组 [ 7 , 14 , 9 , 21 , 9 , 10 , 12 ] [ 7,14,9,21,9,10,12 ] [7,14,9,21,9,10,12] 为例:
初建堆为:
我们会发现根节点的数并不符合,明显 7 小于 14,所以我们要进行交换,将7与14 交换后,7 所在的树又不符合大根堆的性质,所以我们也要将右子树上最大的数交换到右子树的根节点上(左子树同理)
可以发现的是:一次堆建立完之后,我们的最大值就在了堆的根节点上
随后将堆顶最大值和数组最后的元素进行替换,我们就完成了一趟排序了。
接下来,剩下的数不断进行建堆,交换就可以完成我们的堆排序了
2.3 动态图演示
三、代码
堆排序的时间复杂度O(N*logN), 额外空间复杂度O(1),是一个不稳定性的排序
//交换数组中两个元素的值
void swap(int[] arr, int i, int j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//堆排序
void heapSort(int[] arr)
{
//构造大根堆
heapInsert(arr);
int size = arr.length;
while (size > 1)
{
//固定最大值
swap(arr, 0, size - 1);
size--;
//构造大根堆
heapify(arr, 0, size);
}
}
//构造大根堆(通过新插入的数上升)
void heapInsert(int[] arr)
{
for (int i = 0; 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;
}
}
}
//将剩余的数构造成大根堆(通过顶端的数下降)
void heapify(int[] arr, int index, int size)
{
int left = 2 * index + 1;
int right = 2 * index + 2;
while (left < size)
{
int largestIndex;
//判断孩子中较大的值的索引(要确保右孩子在size范围之内)
if (arr[left] < arr[right] && right < size)
{
largestIndex = right;
}
else
{
largestIndex = left;
}
//比较父结点的值与孩子中较大的值,并确定最大值的索引
if (arr[index] > arr[largestIndex])
{
largestIndex = index;
}
//如果父结点索引是最大值的索引,那已经是大根堆了,则退出循环
if (index == largestIndex)
{
break;
}
//父结点不是最大值,与孩子中较大的值交换
swap(arr, largestIndex, index);
//将索引指向孩子中较大的值的索引
index = largestIndex;
//重新计算交换之后的孩子的索引
left = 2 * index + 1;
right = 2 * index + 2;
}
}