堆:
堆本质上就是一棵用数组存储的完全二叉树。
因此用数组存储的完全二叉树的性质,堆也具备。
性质1: 对于i结点 他的左孩子是2i 他的右孩子是2i+1
性质2: 如果 2i < n ,则代表该结点没有左孩子
性质3: 如果 2i+1 < n , 则代表该结点没有右孩子
性质4: 1<=i<=n/2 i所指结点均为分支结点。
性质5: 当i>n/2时 i所指结点是叶子结点。
堆还具备自己的一些性质:
1.对于堆这棵完全二叉树 、 根结点的值>= 左孩子的值、 右孩子的值
如果完全二叉树的分支结点都满足这个特性,那这棵完全二叉树用数组存储就是一个标标准准的大根堆
必须要掌握的技能:
1. 在堆这节我们需要根据给定的数组,来初始化一个大根堆
2. 我们需要根据大根堆排序 --> 生成一个递增序列
3. 分析堆排序算法的稳定性
4. 堆的插入算法
5. 堆的删除算法
6. 学会手动分析大根堆算法的时间复杂度、空间复杂度
分析堆排序的稳定性
2 2 1 符合最大堆的定义,排序时将堆顶元素和数组末尾元素交换然后调整堆顶元素的位置
交换之后 1 2 2 调整堆顶元素之后 2 1 2 再交换 1 2 2 此时两个2的相对位置发生了变化。
所以堆排序算法是一种不稳定的排序算法。
代码展示:
#include<stdio.h>
#include<stdlib.h>
#define MaxSize 20
/*
堆:
堆本质上就是一棵用数组存储的完全二叉树。
因此用数组存储的完全二叉树的性质,堆也具备。
性质1: 对于i结点 他的左孩子是2i 他的右孩子是2i+1
性质2: 如果 2i < n ,则代表该结点没有左孩子
性质3: 如果 2i+1 < n , 则代表该结点没有右孩子
性质4: 1<=i<=n/2 i所指结点均为分支结点。
性质5: 当i>n/2时 i所指结点是叶子结点。
堆还具备自己的一些性质:
1.对于堆这棵完全二叉树 、 根结点的值>= 左孩子的值、 右孩子的值
如果完全二叉树的分支结点都满足这个特性,那这棵完全二叉树用数组存储就是一个标标准准的大根堆
必须要掌握的技能:
1. 在堆这节我们需要根据给定的数组,来初始化一个大根堆
2. 我们需要根据大根堆排序 --> 生成一个递增序列
3. 分析堆排序算法的稳定性
4. 堆的插入算法
5. 堆的删除算法
6. 学会手动分析大根堆算法的时间复杂度、空间复杂度
分析堆排序的稳定性
2 2 1 符合最大堆的定义,排序时将堆顶元素和数组末尾元素交换然后调整堆顶元素的位置
交换之后 1 2 2 调整堆顶元素之后 2 1 2 再交换 1 2 2 此时两个2的相对位置发生了变化。
所以堆排序算法是一种不稳定的排序算法。
*/
void HeadAdjust(int a[],int k , int len);
//构建一个大根堆 //建立初始堆O(4n) --> O(n) --> 先求所有结点的最大下坠层数而每次下坠最多对比关键字两次
//把这些相加,就是构建一个初始堆的最坏的比较关键字的次数(时间复杂度)。
//得到一个差比数列, 错位相减 得到的结果是 : 4n-4-2log2n 所以初始堆的时间复杂度为O(n)级别。
void BulidBigHeap(int a[], int len){
//对数组中所有的分支结点进行"下坠"操作
for(int i = len/2 ; i >=1 ; i--){
HeadAdjust(a,i,len);
}
}
//将以k为根的子树调整为大根堆 len就是数组中元素的个数
void HeadAdjust(int a[],int k , int len){
a[0]=a[k];
//i就是k的左孩子
for(int i = 2*k;i <= len ;i = i*2){
//将根节点的值保留在a[0]中
// i<len 证明i结点有右兄弟 保证i指针始终指向最大的那个孩子
if(i<len && a[i]<a[i+1]){
i++;
}
//根节点的值比左右孩子的值都大,满足大根堆的特性,不需要调整
if(a[0] >= a[i]) break;
else{
//将大的元素赋值给根节点
a[k]=a[i];
//但是仍然不确定a[0]的值放到i上合不合适,有可能a[0]的值放到i上之后,比左右孩子小了,还得在需要调整
k=i;
}
}
//找到了合适的位置
a[k]=a[0];
}
void swap(int a[],int index1,int index2){
//因为真正的堆元素都是在a[1-len]中,所以a[0]充当temp临时变量
a[0]=a[index1];
a[index1]=a[index2];
a[index2]=a[0];
}
//大根堆排序算法 --> 产生一个递增序列 大根堆排序算法的时间复杂度为O(n)+O(nlogn) --> O(nlogn)
void HeapSort(int a[] ,int len){
//先将数组初始化为大根堆
//O(n)
BulidBigHeap(a,len);
//如果堆中有n个元素,我们只需要进行n-1次排序即可
//n-1轮循 所以for循环的时间复杂度为O(nlogn)
for(int i = len ; i > 1 ; i--){
//将最大的元素放到数组末尾
swap(a,i,1);
//调整堆根元素
//每次根节点下坠到合适的位置,由于是一个完全二叉树、所以下坠的层数最大为h-1
//而 h = log2n+1 所以每次调用HeadAdjust函数下坠都不能超过log2n层,而下坠一层对比关键字两次
//所以每次调用此函数的时间复杂度最大为O(2log2n) --> O(log2N)
HeadAdjust(a,1,i-1);
}
}
void HeadAdjustSmall(int a[],int k,int len);
//根据数组初始化小根堆
void BuildSmallHeap(int a[] ,int len){
//针对分支结点从后往前进行小根堆调整
for(int i = len/2 ;i >= 1 ;i--){
HeadAdjustSmall(a,i,len);
}
}
//调整以k为根的子树
void HeadAdjustSmall(int a[],int k,int len){
a[0]=a[k];
for(int i =2*k;i <= len;i=i*2){
//让i指针始终指向 最小的那个孩子
if(i<len && a[i] > a[i+1]){
i++;
}
if(a[0] <= a[i]) break;
else{
//把最小的孩子放上来
a[k]=a[i];
k=i;
}
}
a[k]=a[0];
}
// 小根堆排序算法 --> 产生一个递减序列
void SmallHeapSort(int a[] , int len){
//对于n个元素的小根堆,只需要进行n-1次调整就可以得到一个递减序列
for(int i=len ; i>1 ; i--){
//交换堆顶元素和堆尾元素
swap(a,i,1);
HeadAdjustSmall(a,1,i-1);
}
}
/*
以小根堆为例
*/
//堆的插入 len为数组中堆元素的个数, data为要插入的元素 ,返回的是数组长度,如果数组长度没变,则代表插入失败,如果数组长度+1,则代表插入成功
//最坏情况插入到树根 比较h-1次关键字 而h=log2n+1 所以插入一个元素最多比较log2n 次关键字
//所以插入方法的时间复杂度是O(logn) 级别
int HeapInsert(int a[], int len , int data){
//数组中没有多余空间了
if(len >= MaxSize){
return len;
}
a[len+1]=data;
int k=len+1;
//a[0]充当临时变量,暂存新插入的元素
a[0]=a[k];
//找新插入结点的父结点 新插入的结点不断上升的过程
for(int i = (len+1)/2;i>=1;i=i/2){
if(a[i] <= a[0]) break;
else{
a[k]=a[i];
k=i;
}
}
a[k]=a[0];
return len+1;
}
//堆的删除 len为数组中堆元素的个数、 index为要删除元素的下标。 返回的是数组长度,如果数组长度没变,则代表删除失败、如果数组长度减一则代表删除成功
//最坏情况删除树根元素, 元素需要进行h-1次下坠操作,而每次下坠操作需要进行2次关键字对比,并且h=log2n+1 所以最坏情况需对比关键字2log2n次。
//所以删除元素的时间复杂度为O(logn)级别
int HeapDelete(int a[],int len,int index){
//堆中只剩一个元素了
if(len == 1){
return len-1;
}
//数组中最后一个元素放到被删除的位置
a[index]=a[len];
//数组长度减一
len--;
a[0]=a[index];
//要满足堆的属性,最后一个元素要下坠到合适的位置
for(int i = 2*index;i <= len; i=i*2){
if(i < len && a[i] > a[i+1]){
i++;
}
if(a[0] <= a[i]) break;
else{
a[index] = a[i];
index = i;
}
}
a[index]=a[0];
return len;
}
int main(int argc, char *argv[])
{
//要构建的堆元素从数组下标1开始存储
int array[MaxSize] = {-1,53,17,78,9,45,65,87,32};
//len是堆元素的个数
//根据已有的数组序列构建大根堆
// BulidBigHeap(array,8);
// for(int i = 1;i<=8 ; i++){
// printf("%d ",array[i]);
// }
// printf("\n");
//对大根堆进行堆排序 --> 生成递增序列
// HeapSort(array,8);
// for(int i = 1;i<=8 ; i++){
// printf("%d ",array[i]);
// }
// printf("\n");
//根据已有的数组序列构建小根堆
BuildSmallHeap(array,8);
printf("构建小根堆的序列为:\n");
for(int i = 1;i<=8 ; i++){
printf("%d ",array[i]);
}
printf("\n");
//往小根堆中插入一个元素
int len = HeapInsert(array,8,8);
printf("往小根堆中插入元素8之后的序列为:\n");
for(int i = 1;i<=9 ; i++){
printf("%d ",array[i]);
}
printf("\n");
//在小根堆中删除一个元素
printf("小根堆中删除堆顶元素之后的序列为:\n");
HeapDelete(array,len,1);
for(int i = 1;i<=8 ; i++){
printf("%d ",array[i]);
}
printf("\n");
//根据小根堆进行堆排序 --> 生成递减序列
SmallHeapSort(array,8);
printf("根据小根堆排序生成递减序列为:\n");
for(int i = 1;i <= 8 ; i++){
printf("%d ",array[i]);
}
printf("\n");
return 0;
}
结果测试:
重点:
对排序算法的难点就是对时间复杂度的分析。
思路:
先求所有结点的最大下坠层数而每次下坠最多对比关键字两次
把这些相加,就是构建一个初始堆的最坏的比较关键字的次数(时间复杂度)。
得到一个差比数列, 错位相减 得到的结果是 : 4n-4-2log2n 所以初始堆的时间复杂度为O(n)级别。
堆排序的时间复杂度和数组的初始状态无关,始终都是O(nlogn);