堆
堆逻辑上是一棵完全二叉树。
其任何一非叶节点的关键字不大于或者不小于其左右孩子节点的关键字。
大顶堆:堆的每个父节点都大于其孩子节点,因此根节点为堆中最大值;
小顶堆:堆的每个父节点都小于其孩子节点,根节点为堆中最小值;
堆的存储:
一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2i+1和2i + 2。
如下为堆和其数组的表达(堆的根节点以下标0开始,层序遍历):
堆:
数组:[12,8,9,5,3]
建堆:
给定一个数组 [8,9,5,3,12],把它转化为大顶堆
可以看到堆建好之后堆中第0个数据是堆中最大的数据。那么堆建好了,如何对其进行排序呢?
堆排序:
基本思想:
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样根节点会是n个元素中的次小值,将其与n-1的元素互换,剩下n-2个元素继续建堆。如此反复执行,便能得到一个有序序列了。(反之,降序则建立小顶堆)
此时我们接着使用上面建立好的堆,按照此思想进行排序:
最后数组为[3,5,8,9,12],可见是一个升序数组
再次总结下堆排序的基本思路:
a.将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大(小)元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
实现代码
void AdjustDown(int arr[], int i, int n)//小顶堆 降序堆排序
{
int j = i * 2 + 1;//子节点
while (j<n)
{
if (j+1<n && arr[j] > arr[j + 1])//子节点中找较小的
{
j++;
}
if (arr[i] < arr[j])
{
break;
}
swap(arr[i],arr[j]);
i = j;
j = i * 2 + 1;
}
}
void MakeHeap(int arr[], int n)//建堆
{
int i = 0;
//最后一个非叶节点 ((n-1)-1)/2 = n/2 -1
for (i = n / 2 - 1; i >= 0; i--)
{
AdjustDown(arr, i, n);
}
}
void HeapSort(int arr[],int len)
{
int i = 0;
MakeHeap(arr, len);
for (i = len - 1; i >= 0; i--)
{
swap(arr[i], arr[0]);
AdjustDown(arr, 0, i);
}
}
堆排序时间复杂度:O(nlog2n) 空间复杂度:O(1)
关于空间复杂度为O(1),实际上堆排序的所有操作都建立在原始数组或者链表上,不需要再建立一个二叉树。也就是说堆的逻辑上是二叉树,但底层实现上其实还是数组。
问:有 1000 个无序的整数,希望使用最快的方式找出前 50 个最大的,最佳的选择是( )
- A.快速排序
- B.堆排序
- C.基数排序
- D.冒泡排序
答:B。
解析:首先A的快排不是普通意义上的快排,它只做了快排递归的半边,平均时间复杂度为O(n),最坏时间复杂度为O(n^2)。
而堆排序无论什么情况都是o(nlogn),当然还有建堆的时间o(n),所以为n+nlogn。按照题意可以建一个50个节点的小顶堆,还剩950个节点,每个节点依次与小根堆根节点比较,若大于根节点值则替换根节点,在调整成小顶堆,比较完所有节点,堆中即为50个最大元素,这种做法一般适用于数据无法全部放入内存中的查找最值,比如1TB的整数数据。
所以找出若干个数中最大/最小的前K个数,用堆排序是最好的。找K个最大数,用小顶堆;找K个最小数,用大顶堆。