堆的相关操作

     堆:
     堆本质上就是一棵用数组存储的完全二叉树。
    因此用数组存储的完全二叉树的性质,堆也具备。
    性质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);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值