堆排序
堆的概念
堆本身可以是一个数组,关键在于我们将其看成一个二叉树结构。比如下面
核心在于利用二叉树的结构特点:即根结点为最大值或最小值,来进行排序。
注:大堆:父亲比孩子大的堆。小堆反之。
堆排序(默认小堆,大堆同理)
向上调整算法
向上调整是指,对一个数据a要插入一个小堆,要不断向上与父节点比较,比父亲小则与之交换,并继续往上比较;由于默认除了a之外已是小堆,故若比父亲大则无需继续比较。如此,插入a后,小堆仍为小堆。
void AdjustUp(int* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
向下调整算法
向下调整则与前者相反。是指,对于小堆中的数据a,与其两个子节点较小的那一个比较,比孩子大则与之交换;由于已经是小堆,比孩子小则停下即可。
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] > a[child + 1])
{
child++;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
堆排序的原理(小堆对应降序)
堆排序实际上很巧妙。我们通过将堆顶数据与最后一个数据(即数组的最后一个)交换,实现将最小数放在数组末尾。再将前n-1个数据重新建堆,再重复前面过程,便实现了数组从大到小排列,而且不消耗额外空间。
堆排序的代码附上
void HeapSort(int* a, int n)
{
for (int i = 0; i < n; i++)//先向上调整建堆
{
AdjustUp(a, i);
}
//for (int i = (n - 1) / 2; i >= 0; i--)//此为向下调整建堆
//{
// AdjustDown(a, n, i);
//}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
堆排的时间复杂度
- 首先建堆部分:如果是向上调整,则是一个等比数列:对于第k层,有2k-1个节点,最坏每个都需要向上挪动k-1次。从第一层累加到第h(堆的高度)层即可。又满二叉树h与N的关系:2h-1=N。结论是O(Nlog2N);
如果是向下调整建堆,因为只需从倒数第二层开始调整,所以是对2k-1乘(h-k),k从1到h-1累加即可,结论是O(N)。
- 向下调整排序部分:第k层交换需要2k-1次,每次向下调整最坏需要调k-1次。则计算过程与向上调整建堆的一样。均为O(Nlog2N)。
综上,堆排时间复杂度为O(Nlog2N),与快排,希尔属同一段位。
堆的应用----top k问题
问:如何在N(比如1亿)个数据里选取最大的(或最小的)k个数,k远小于N。
答:当然是堆排了!假设选最大的,只需建立有k个数的小堆,遍历一遍数组,遇到比堆顶元素大的数据就把堆顶换成该数据,再向下调整小堆,最终堆里面留下的就是top大的k个数。
关于时间复杂度,遍历需要N,建堆应该为klog2k,因为k<<N,故O(N)=N。