堆排序
一.堆排序
1.堆的概念及性质
1.1堆的概念
a. 堆是一种基本的数据结构。在这里我用数组来形容,在一个二叉堆的数组中,每一个元素(根)都要保证大于等于或小于等于另外两个特定位置的元素(左右子树)。同时相应的,这些元素(根)又要大于等于或小于等于另外两个相应位置的元素(左右子树),整个数据结构以此类推。如果我们将整个数据结构画成树状结构,就能够清晰地看出整个结构的样子。
1.2堆的性质
a. 堆的逻辑结构一定是完全二叉树,物理结构为顺序表,用数组实现。
b. 任一根结点的值是其子树所有结点的最大值或最小值
最大值时,称为“最大堆”,也称大根堆,如1.1中图一;
在完全二叉树中,任何一个子树的最大值都在这个子树的根结点
最小值时,称为“最小堆”,也称小根堆,如1.1中图二;
在完全二叉树中,任何一个子树的最小值都在这个子树的根结点。
c. 在物理结构上,如果父亲节点的位置为k,那么它的左右孩子节点分别为2k+1和 2k+2,那么如果孩子节点位置为k,则父亲节点为(k-1) / 2。
二.向下调整和向上调整两大算法
1. 向下调整算法
2.1 向下调整算法基本思路
a.若想将其调整为小堆,那么根节点的左右子树必须是小堆。
b.若想将其调整为大堆,那么根节点的左右子树必须是大堆。
c.向下调整算法的基本思路(小堆)
1.从根节点处开始,选出左右孩子中值较小的孩子。
2.让小的孩子与其父亲进行比较。
若小孩子比父亲小,则该孩子与其父亲的位置进行交换。并将原来小的孩子的位置当做父亲继续向下进行调整,直到调整到叶子节点为止。
若调整中间小的孩子比父亲大,则不需要继续向下调整了,整个树已经为小堆了,调整完成。
图片示例:
堆(小堆)的向下调整算法代码实现:
void AdjustDown(HPDataType* a,int size,int root)
{
int parent = root;
int child = 2 * parent + 1;
//假设左孩子为较小的孩子
while (child < size)
{
if (child + 1 < size && a[child] > a[child + 1])
{
child++;
}
//如果右孩子存在,且右孩子小于左孩子,则假设不成立,右孩子为较小的孩子
if (a[parent] > a[child])
{
Swap(&a[child], &a[parent]);
//交换
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
使用堆的向下调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1次(h为树的高度)。而h = log2(N+1)(N为树的总结点数)。所以堆的向下调整算法的时间复杂度为:O(logN) 。
2. 向上调整算法
2.2 向上调整的基本思路
a.若想将其调整为小堆,那么除该节点外,整个树必须为小堆。
b.若想将其调整为大堆,那么除该节点外,整个树必须为大堆。
c.向上调整 的基本思路(大堆)
1.由该节点找到其父亲节点。
若该节点(孩子节点)的值大于其父亲节点的值,两个节点的位置发生交换,再把原来父亲节点的位置当做孩子节点,继续向上调整,直到树的根为止。
若中间调整过程中,孩子节点的值小于其父亲节点的值,则不需要在向上调整了,整个树已经为大堆了,调整完成。
图片示例:
堆(大堆)的向下调整算法代码实现:
void AdjustUp(HPDataType* 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;
}
}
}
使用堆的向上调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1次(h为树的高度)。而h = log2(N+1)(N为树的总结点数)。所以堆的向上调整算法的时间复杂度为:O(logN) 。
三.堆的实现(小堆)
1.头文件包含
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}Heap;
//堆的初始化
void HeapInit(Heap* HP);
//堆的销毁
void HeapDestroy(Heap* HP);
//堆的打印
void HeapPrint(Heap* php)
//向堆存放数据
void HeapPush(Heap* HP,HPDataType x);
//删除堆中元素
void HeapPop(Heap* HP);
//获取堆顶元素
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* HP);
// 堆的判空
int HeapEmpty(Heap* HP);
2. 接口实现
#include"heap.h"
void HeapInit(Heap* HP)
{
assert(HP);
HP->a = NULL;
HP->size = HP->capacity = 0;
}
void HeapDestroy(Heap* HP)
{
assert(HP);
assert(HP->a);
free(HP->a);
HP->a = NULL;
HP->capacity = HP->size = 0;
}
void HeapPrint(Heap* php)
{
assert(php);
for (size_t i = 0; i < php->size; ++i)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
void Swap(HPDataType* child, HPDataType* parent)
{
HPDataType tmp = *child;
*child = *parent;
*parent = tmp;
}
void AdjustUp(HPDataType* 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;
}
}
}
void HeapPush(Heap* HP,HPDataType x)
{
assert(HP);
if (HP->size == HP->capacity)
{
int newcapacity = HP->capacity == 0 ? 4 : 2 * HP->capacity;
HPDataType* tmp = (HPDataType*)realloc(HP->a,newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
printf("malloc is fail\n");
exit(-1);
}
HP->a = tmp;
HP->capacity = newcapacity;
}
HP->a[HP->size] = x;
HP->size++;
AdjustUp(HP->a, HP->size - 1);
}
void AdjustDown(HPDataType* a,int size,int root)
{
int parent = root;
int child = 2 * parent + 1;
while (child < size)
{
if (child + 1 < size && a[child] > a[child + 1])
{
child++;
}
if (a[parent] > a[child])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
void HeapPop(Heap* HP)
{
assert(HP);
Swap(&HP->a[0], &HP->a[HP->size - 1]);
HP->size--;
AdjustDown(HP->a,HP->size,0);
}
HPDataType HeapTop(Heap* HP)
{
assert(HP);
return HP->a[0];
}
int HeapSize(Heap* HP)
{
assert(HP);
return HP->size;
}
int HeapEmpty(Heap* HP)
{
assert(HP);
return HP->size == 0;
}
四.任意树调整为堆(小堆为例)
1.向下调整法建堆
如果左右子树不是小堆,就不能直接使用向下调整算法了!那么如何才能将一个任意的树调整为堆呢?该怎么办???
其实答案很简单,我们只需要从倒数第一个非叶子结点开始,从后往前,按下标,依次作为根去向下调整即可。(倒数第一个非叶子节点一定是最后一个叶子节点的父亲)
a.逻辑结构
b.物理结构
int a[] = {3,5,2,7,8,6,1,9,4,0};
c.建堆代码:
for(int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a,n,i);
}
那么建堆的时间复杂度又是多少呢?
当结点数无穷大时,完全二叉树与其层数相同的满二叉树相比较来说,它们相差的结点数可以忽略不计,所以计算时间复杂度的时候我们可以将完全二叉树看作与其层数相同的满二叉树来进行计算。
我们计算建堆过程中总共交换的次数:
T ( n ) = 1 × ( h − 1 ) + 2 × ( h − 2 ) + . . . + 2h-3 × 2 + 2h-2 × 1
两边同时乘2得:
2 T ( n ) = 2 × ( h − 1 ) + 2 2 × ( h − 2 ) + . . . + 2h-2× 2 + 2h-1 × 1
两式相减得:
T ( n ) = 1 − h + 2 1 + 2 2 + . . . + 2 h-2 + 2 h-1
运用等比数列求和得:
T ( n ) = 2h − h − 1
由二叉树的性质,有N = 2h − 1和 h = log 2 ( N + 1 ), 于是
T ( n ) = N − log 2 ( N + 1 )
用大O的渐进表示法:
T ( n ) = O ( N )
总结一下:
堆的向下调整算法的时间复杂度:T ( n ) = O ( log N ) 。
建堆的时间复杂度:T ( n ) = O ( N ) 。
2.堆(小堆)排序
那么堆建好后,如何进行堆排序呢?
步骤如下:
(1) 将堆顶数据与堆的最后一个数据交换,然后对根位置进行一次堆的向下调整,但是调整时被交换到最后的那个最小的数不参与向下调整。
(2) 完成步骤1后,这棵树除最后一个数之外,其余数又成一个小堆,然后又将堆顶数据与堆的最后一个数据交换,这样一来,第二小的数就被放到了倒数第二个位置上,然后该数又不参与堆的向下调整…反复执行下去,直到堆中只有一个数据时便结束。此时该序列就是一个降序。
void HeapSort1(int* a, int n)
{
//建小堆排降序
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
// 向下调整算法
HeapDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
HeapDown(a, end, 0);
end--;
}
}
时间复杂度O(N*logN)