我们知道,堆的物理存储形式其实就是数组存储。
堆有大堆和小堆之分。
而对数组进行堆排序其实本质就是把一个数组在逻辑层面上变成堆的形式。也就是变成大堆或小堆。
目录
什么是堆?
这里我们首先要了解到堆的定义与结构。
堆:如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足: = 且 >= ) i = 0,1, 2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
这里结构以小堆为例:
如何实现的呢?
实现的本质是将原本的数组里的数据按照堆数据的储存方式进行位置改变,变成一个堆。
这里其实有两种方法去进行调整,一种是向上调整法,一种是向下调整法。
这里我先介绍向下调整法
首先我们先来看一组数据:
由此可见,向下调整的前提是调整位置的左右树必须满足是堆才行。
换言之,要想用向下调整,就必须从最后叶结点的上一个结点开始调整,然后依次对前面的节点用向下调整法,才能保证每次对节点用向下调整法时满足使用条件。
接下来上代码!
#include<stdio.h>
#include<assert.h>
//交换数据
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//向下调整函数
void AdjustDown(int* a, size_t size, size_t root) //传数组地址、数组大小、调整节点下标
{
assert(a);
int parent = root;
int child = parent * 2 + 1;
while (child < size)
{
if (child + 1 < size && a[child + 1] < a[child])
{
++child;
}
if (a[parent] > a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// 对数组进行堆排序
void HeapSort(int* a, int n)
{
assert(a);
for (int i = (n - 1) / 2; i >= 0; i--) //从最后叶节点的父节点开始调整
{
AdjustDown(a, n, i);
}
}
//打印函数
void HeapPrint(int* a, size_t n)
{
assert(a);
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
//代码的应用实例
int main()
{
int a[] = { 4,3,5,1,2,8,7,6};
int n = sizeof(a) / sizeof(a[0]);
HeapSort(a, n);
HeapPrint(a, n);
return 0;
}
下面是代码的实现效果:
向下调整法的时间复杂度和空间复杂度:
向下调整法是在数组本身上进行调整,所以他的的空间复杂的很明显是O(1)。
接下来计算时间复杂度:
以最坏的情况考虑,假设一个堆有h层,且是一个每一层的是满数据,每进行一次向下调整都要调整到最低层(步数最大化情况)
对于第h-1层来说,最多调整2^(h-2)*1步
h-2层:2^(h-3)*2步
....
1层:2^0 * h步
总步数:T(h)=2^0*(h-1) + 2^1*(h-2) + 2^2*(h-3) + ....+2^(h-2)*1
错位相减法:2T(h)= 2^1*(h-1) + 2^2*(h-2) + ....+2^(h-2)*2 + 2^(h-1)*1
两式相减:T(h)= -h + 2^0 + 2^1 + 2^2 + ...+ 2^(h-2) + 2^(h-1) - 2^0=2^(h-1)+1-h
又h=log(n+1)(以2为底n+1的对数)
故T(h)=n+1-log(n+1)
即时间复杂度为O(N);
向上调整法:
有了上面的讲解,这里我就不再多赘述了,直接看代码估计各位老爷们就能理解了。
注意!
这里用向上调整法不再有限制条件,可以直接从最后一个节点依次往前向上调整就行。
#include<stdio.h>
#include<assert.h>
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void Adjustup(int* a, size_t size)
{
assert(a);
int child = size;
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;
}
}
}
// 对数组进行堆排序
void HeapSort(int* a, int n)
{
assert(a);
for (int i = n; i > 0; i--)
{
Adjustup(a, i-1);
}
}
void HeapPrint(int* a, size_t n)
{
assert(a);
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
int main()
{
int a[] = { 4,3,5,1,2,8,7,6};
int n = sizeof(a) / sizeof(a[0]);
HeapSort(a, n);
HeapPrint(a, n);
return 0;
}
向上调整法的时间复杂的和空间复杂度:
向下调整法是在数组本身上进行调整,所以他的的空间复杂的很明显也是O(1)。
时间复杂度:进行了i次循环,每次可看作走了log i 步,故时间复杂度可近似看作O(nlogn)。
总结:
相比而言,向下调整法更优!
总之,数组堆的排序无非就是这两种方法,根据自己的需要可以灵活应用。