堆与堆排序
再将堆排序之前,我们先引入二叉树概念
一.二叉树初探
1).基本概念
二叉树是一种数据结构,二叉树形如:
1.其中A节点被称为根节点,B和C对应的两个树分别被称为左子树和右子树
2.度:节点上连接的子树的个数被称为这个节点的度,二叉树不存在度大于2的节点
3.度为0的节点被称为叶子节点。
4.该节点的孩子节点为该节点链接的下一个节点,如B的孩子节点为D和E。
5.该节点的父亲节点为该节点的上一个节点,如B的父亲节点为A。
6.树的度:该数中所有节点的度的最大值。
其中二叉树还有两种特殊的结构:
7.树的层数,字面意思,根节点记为一层
2).满二叉树和完全二叉树
1.满二叉树:
若每一层的节点都是满的的情况,即层数为n,节点个数为2^n-1
2.完全二叉树
当二叉树共有n层,其中n-1层是满二叉树,并且第n层的节点分布是从左到右依次排列的:
1.度为0的节点的个数==度为2的节点的个数+1
2.有n个节点的满二叉树的高h=log2(n-1);
3.)二叉树的存储方式
在这里我们重点探究二叉树的顺序储存(ps:适用于满二叉树和完全二叉树)
将完全二叉树的每一层按顺序依次排列到数组中去,可以发现以下特点:
1.父亲节点的脚标n,儿子节点的脚标m,可以得出m=n * 2+1或m=n * -2+2,同理n=(m-1)/2。
通过这种特性,我们可以用顺序结构去访问我们的完全二叉树
二.堆与堆排序
1.堆(完全二叉树的特例)
堆分为大堆和小堆:
大堆表示为一棵完全二叉树 ,其每一个父亲节点都比他的孩子节点要大
小堆表示为一棵完全二叉树 ,其每一个父亲节点都比他的孩子节点要小
1).建堆(向下调整法)
1.向下调整法:
typedef struct Heap
{
HPDataType* _a;
int _size;
int _capacity;
}Heap;
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = 0;
tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustDown(HPDataType* a, int n, int root)
{
int parent = root;
int child = root * 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;
}
}
}
不难得出向下调整法的时间复杂度为O(logn)
** 2.建堆**
这是处理一个节点无序的情况,如果整个给的数组完全无顺,我们该怎么办呢
接下来,我们来讲一下建堆的过程:
我们找到最后一个叶子节点M,可知M的父亲节点N是最后一个父亲节点,所以我们只需要从这个父亲节点开始向上依次遍历一遍,其中每一次遍历都在经历一次向下调整法,就可以实现建堆
eg:建小堆
typedef struct Heap
{
HPDataType* _a;
int _size;
int _capacity;
}Heap;
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = 0;
tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustDown(HPDataType* a, int n, int root)
{
int parent = root;
int child = root * 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;
}
}
}
void HeapInit(Heap* php, HPDataType* a, int n)
{
php->_a = (HPDataType*)malloc(sizeof(HPDataType) * n);
memcpy(php->_a, a, n*sizeof(HPDataType));
php->_size = n;
php->_capacity = n;
int child_ini = (n - 1-1) / 2;
while (child_ini>=0)
{
AdjustDown(php->_a, n, child_ini);
child_ini--;
}
int main()
{
int a[] = { 27,15,22,33,44,23,43,2,32,12 };
Heap HP;
HeapInit(&HP, a, sizeof(a)/sizeof(a[0]));
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
printf("%d ", HP._a[i]);
}
return 0;
}
我们来思考一下,建堆的时间复杂度O是多少
已知共有n个节点(假设为满二叉树)
树高h=log2(n+1)
每一层的个数=2^(h’-1) h’为当前层数
遍历次数S=h*2^(0)+(h-1)*2 ^(1)+(h-2)2 ^(2)…+12 ^(h-1)
这是一个等差数列乘上等比数列,用错位相减法
2S-S最后的结果是
S=2^h -h -1=n-log2(n+1)
时间复杂度为O(n)
2).堆排序
接下来进入真正的重头戏------堆排序
如果我们随意给出一个数组,里面的数全是无序的
那么我们如何去排序呢,直接使用冒泡排序,选择排序,插入排序?
如果我们给的数组足够大,那么使用这种排序方法效率就会非常的低下
这时我们强大的堆排序就要派出用场了:
(ps:这边我们以升序为例,升序建大堆,降序建小堆)
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = 0;
tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustDown(HPDataType* a, int n, int root)
{
int parent = root;
int child = root * 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;
}
}
}
void HeapSort(HPDataType* php, int n)
{
int child_ini = (n - 1 - 1) / 2;
while (child_ini >= 0)
{
AdjustDown(php, n, child_ini);
child_ini--;
}//先建堆
Swap(php, php + n-1);
n--;//第一次先交换
for (;n>0;n--)
{
AdjustDown(php, n, 0);//每一次向下调整
Swap(php, php + n - 1);
}
}
nt main()
{
int a[] = { 20,15,16,13,12,19 };
HeapSort(a, sizeof(a) / sizeof(a[0]));
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
printf("%d ", a[i]);
}
return 0;
}
我们来思考一下,堆排序的时间复杂度是多少
第一次的建堆O(n)
往后的n次遍历,每一次都向下调整O(logn)
我们可以近似的去处理,时间复杂度为
O(n+nlogn)=O(nlogn)
可以看出这是一种极为高效的排列方式,
稳定:在最好和最坏的情况下时间复杂度都是O(nlogn)
不稳定:堆排序有可能会改变两个值相等的数的相对位置,这也是他的不稳定性
创作不易,恳请留赞ヾ(^▽^*)))