《算法导论》学习(七)----堆排序和优先队列(C语言)

系列文章

《算法导论》学习(一)---- 插入排序和归并排序
《算法导论》学习(七)----堆排序和优先队列(C语言)
《算法导论》学习(八)----快速排序(C语言)
《算法导论》学习(九)----为什么比较排序算法时间复杂度的下界是确定的?
《算法学习》学习(十)----计数排序,基数排序,桶排序(C语言)


前言

本文主要讲解了堆的数据结构,并且用C语言实现了堆排序和优先队列


一、堆

1.什么是堆?

(二叉)堆从存储空间的本质上来讲是一个数组,但是在进行数据处理的时候,我们将它当作是一个近似的完全二叉树。
在这里插入图片描述
1.关于堆的结构,有以下的要点:

1.树的结点里面是存储的值,每个结点上方标记的是元素在数组中的索引。
2.根据图我们可以看出,堆的数据排列方式是从数组的第一个元素开始,自上到下,从左到右。
3.每个结点最多与下面的两个结点相连。
4.对于第4,8,9数据,它们组成了一个子堆。在这个子堆中,4是8,9的父结点,8是4的左孩子;9是4的右孩子。
5.最顶端的结点是根结点;最底层的结点是叶子结点。
6.n个元素的堆的高度是

2.那么根据堆的结构,我们可以得到如下结论

1.对于一个给定结点的序号为i(非根结点)
	(1)父结点的序号:i/2(向下取整)。
	(2)左孩子结点的序号:2i。
	(3)右孩子结点的序号:2i+1。

2.最大堆和最小堆

最大堆和最小堆的性质:

1.最大堆的性质:除了根结点以外的所有结点都满足:
			父结点都大于它们的左右孩子结点
2.最小堆的性质:除了根结点以外的所有结点都满足:
			父结点都小于它们的左右孩子结点

二、堆排序

1.什么是堆排序?

堆排序就是运用一种叫“”的数据结构来进行排序。它是集合了插入排序和归并排序优点的一种算法。

1.与插入排序相似的是:它具有空间原址性
2.与归并排序相似的是:它的时间复杂度是O(nlgn)

2.最大堆的两种操作函数

维护最大堆函数

void MAXHEAP_IFY(int *x,int n,int size)
{
	//得到n结点的左右孩子结点 
	int l=2*n;
	int r=2*n+1;
	
	int largest=n;//假设n结点比它的左右孩子都大,为它赋初值
	
	/*
	如果左右孩子存在的情况下,找到n的左右孩子和n结点三者之间的最大值 
	*/ 
	if(l<=size&&x[l-1]>x[n-1])
	{
		largest=l;
	}
	if(r<=size&&x[r-1]>x[largest-1])
	{
		largest=r;
	}
	
	/*
	如果n,n的左孩子,n的右孩子
	三者中的最大值不是n的话
	我们需要调整三者的关系
	使满足最大堆的性质 
	*/
	if(largest!=n)
	{
		int temp;
		temp=x[n-1];
		x[n-1]=x[largest-1];
		x[largest-1]=temp;
		//如果改变了结构
		//那么可能导致下一层的最大堆性质不满足
		//需要进一步维护 
		MAXHEAP_IFY(x,largest,size);
	}
} 

构造最大堆

void BUILD_MAXHEAP(int *x,int size)
{
	int heap_size=size;
	int first_node=heap_size/2;//得到编号最大的父结点 
	int i=0;
	//从编号最大的父结点开始,自底向上地构建最大堆
	//这个过程可以循环调用维护函数来实现 
	for(i=first_node;i>=1;i--)
	{
		MAXHEAP_IFY(x,i,size);
	}
}

3.堆排序

C语言实现

#include<stdio.h>
#include<stdlib.h>
#include<time.h>


#define SIZE 10



void MAXHEAP_IFY(int *x,int n,int size)
{
	//得到n结点的左右孩子结点 
	int l=2*n;
	int r=2*n+1;
	
	int largest=n;//假设n结点比它的左右孩子都大,为它赋初值
	
	/*
	如果左右孩子存在的情况下,找到n的左右孩子和n结点三者之间的最大值 
	*/ 
	if(l<=size&&x[l-1]>x[n-1])
	{
		largest=l;
	}
	if(r<=size&&x[r-1]>x[largest-1])
	{
		largest=r;
	}
	
	/*
	如果n,n的左孩子,n的右孩子
	三者中的最大值不是n的话
	我们需要调整三者的关系
	使满足最大堆的性质 
	*/
	if(largest!=n)
	{
		int temp;
		temp=x[n-1];
		x[n-1]=x[largest-1];
		x[largest-1]=temp;
		//如果改变了结构
		//那么可能导致下一层的最大堆性质不满足
		//需要进一步维护 
		MAXHEAP_IFY(x,largest,size);
	}
} 


void BUILD_MAXHEAP(int *x,int size)
{
	int heap_size=size;
	int first_node=heap_size/2;//得到编号最大的父结点 
	int i=0;
	//从编号最大的父结点开始,自底向上地构建最大堆
	//这个过程可以循环调用维护函数来实现 
	for(i=first_node;i>=1;i--)
	{
		MAXHEAP_IFY(x,i,size);
	}
}


void HEAP_SORT(int *x,int size)
{
	int i=0;
	int j=0;
	//先构建一个最大堆
	//那么堆顶的元素就是所有元素中最大的 
	BUILD_MAXHEAP(x,size);
	int temp;
	//循环地取出堆顶元素,将该元素放到数组尾部,将尾部的元素放到堆顶 
	//一方面在每次取出后就针对堆顶元素进行维护堆
	//另一方面将取到数组尾的“原堆顶” 排除在维护之外
	//这样就会按大小次序将堆顶排列于数组尾
	//等循环操作完成,排序也就完成了 
	for(i=size;i>=2;i--)
	{
		temp=x[i-1];
		x[i-1]=x[0];
		x[0]=temp;
		//维护堆 
		MAXHEAP_IFY(x,1,i-1);
	}
}



int main()
{
	int a[SIZE];
	srand((unsigned)time(NULL));
	int i=0;
	//得到随机数组 
	for(i=0;i<SIZE;i++)
	{
		a[i]=rand()%1000;
	}
	//将随机数组打印出来 
	for(i=0;i<SIZE;i++)
	{
		printf("%5d",a[i]);
	}
	printf("\n");
	//对数组进行堆排序 
	HEAP_SORT(a,SIZE);
	//将排序后的新数组打印出来 
	for(i=0;i<SIZE;i++)
	{
		printf("%5d",a[i]);
	}
	printf("\n");	
	return 0;
}

堆排序的逻辑分析

堆排序的核心关键在:

1.最大堆的堆顶是所有元素中的最大值
2.最大堆维护了一个相对从大到小的一个层级关系
		(1)相对大的元素概率上靠前
		(2)相对小的元素概率上靠后
3.从概率上讲不断维护堆的时间代价相对低

通过不断地取堆顶最大值和维护堆,是可以达到排序的目的

堆排序的时间复杂度分析

涉及的主方法可以参考文章:
《算法导论》学习(五)---- 分治策略(递归)的时间复杂度求解

(1)维护堆操作

对于维护堆的代码,我们可以有下面的时间表达式:
T ( n ) ⩽ T ( 2 n 3 ) + Θ ( 1 ) T(n)\leqslant T(\frac{2n}{3})+\Theta(1) T(n)T(32n)+Θ(1)
根据主方法, T ( n ) = O ( l g n ) = O ( h ) , 其中 h = l g n ,是堆的高度。注意:这里的 l g n = l o g 2 n , 不是 l o g 10 n T(n)=O(lgn)=O(h),其中h=lgn,是堆的高度。注意:这里的lgn=log_2{n},不是log_{10}n T(n)=O(lgn)=O(h),其中h=lgn,是堆的高度。注意:这里的lgn=log2n,不是log10n

(2)建立堆操作

根据代码分析,我们可以得到时间的表达式:
T ( n ) = ∑ h = 0 ⌊ l g n ⌋ ⌈ n 2 h + 1 ⌉ O ( h ) = O ( n ∑ h = 0 ⌊ l g n ⌋ h 2 h ) 又因为: ∑ h = 0 ∞ h 2 h = 1 / 2 ( 1 − 1 / 2 ) 2 = 2 于是我们可以化简时间表达式: T ( n ) = O ( n ∑ h = 0 ⌊ l g n ⌋ h 2 h ) = O ( n ∑ h = 0 ∞ h 2 h ) = O ( n ) T(n)=\sum^{\lfloor{lgn}\rfloor}_{h=0}\lceil{\frac{n}{2^{h+1}}}\rceil O(h)=O(n\sum_{h=0}^{\lfloor{lgn}\rfloor}\frac{h}{2^h})\\又因为:\sum^{\infty}_{h=0}\frac{h}{2^h}=\frac{1/2}{(1-1/2)^2}=2\\于是我们可以化简时间表达式:T(n)=O(n\sum_{h=0}^{\lfloor{lgn}\rfloor}\frac{h}{2^h})=O(n\sum^{\infty}_{h=0}\frac{h}{2^h})=O(n) T(n)=h=0lgn2h+1nO(h)=O(nh=0lgn2hh)又因为:h=02hh=(11/2)21/2=2于是我们可以化简时间表达式:T(n)=O(nh=0lgn2hh)=O(nh=02hh)=O(n)

(3)堆排序

由于建立堆执行了一次,维护堆执行了n-1次

很容易我们可以得到
T ( n ) = O ( n l g n ) + O ( n ) = O ( n l g n ) T(n)=O(nlgn)+O(n)=O(nlgn) T(n)=O(nlgn)+O(n)=O(nlgn)

三、优先队列

1.什么是优先队列?

优先队列是一种用来维护由一组元素构成的集合S的数据结构。优先队列的每一个元素都有一个数值,被称为关键字—key(根据场景被赋予意义)。它可以用堆来实现

像堆一样,优先队列也分为最大优先队列和最小优先队列,可以用于持续保障数据的优先级。

下面是用C语言实现的优先队列的数据结构体:(包含了内存和数据大小的一些关系)

//由于存在插入操作,因此需要预留一定的存储空间 
//假设存储空间的单元树是数据个数的2倍
#define MSIZE 30 
#define DSIZE 15//数据的个数 



typedef struct priority_queue
{
	int *a;//指向队列的顺序存储空间 
	int heap_size;//队列中元素的个数	
}pq;

2.优先队列的应用

最大优先队列的应用有很多。其中一个就是共享计算机系统的作业调度。最大优先队列记录将要执行的各个作业以及它们之间的相对优先级。当一个作业完成,调度器通过最大优先队列的操作函数,选出具有最高优先级的作业来执行。同时,任何时候,调度器可以通过最大优先队列的操作函数来加入新的作业。

最小优先队列可以被用于基于事件驱动的模拟器,队列中保存着要模拟的事件,每个事件都有一个发生事件作为其关键字。时间必须按照时间发生的次序进行模拟,在模拟过程中,一个事件的发生可能又会触及其它事件的发生模拟。模拟器会通过最小优先队列的操作函数来选择下一个要模拟的事件,以及插入新事件

下面我们仅仅介绍最大优先队列的实现函数

3.最大优先队列的四种操作函数

注意:

要用到之前堆的操作函数

插入元素并入队

void MAX_HEAP_INSERT(pq *x,int key)
{
	x->heap_size=x->heap_size+1;
	x->a[x->heap_size-1]=-1000;//我们假设-1000对于key值的所有可能来说是负无穷
	//相当于增大最后一个元素的key值从负无穷到指定key值 
	HEAP_increase_key(x,x->heap_size,key); 
}

返回优先队列的最值

int HEAP_MAXIMUM(pq *x)
{
	//先判断队列是否还有值 
	if(x->heap_size<1)
	{
		printf("priority queue is underflow\n");
		exit(0);
	}	
	return x->a[0];//堆顶就是最大值所在 
}

返回优先队列的最值并将其出队

int HEAP_EXTRACT_MAX(pq *x)
{
	//先判断队列是否还有值 
	if(x->heap_size<1)
	{
		printf("priority queue is underflow\n");
		exit(0);
	}  
	int max=x->a[0];//找到最大值
	
	//用最小的叶子结点填充堆顶的空缺
	//之后维护堆 
	x->a[0]=x->a[x->heap_size-1];
	x->heap_size=x->heap_size-1; 
	MAXHEAP_IFY(x->a,1,x->heap_size);
	//返回最大值 
	return max;
} 

增大优先队列元素的key至指定值

void HEAP_increase_key(pq *x,int n,int key)
{
	//该函数是将队列中的key值增大到某一个值
	//因此若新key值小于原key值,那么报错结束 
	if(key<x->a[n-1])
	{
		printf("new key is smaller than current key\n");
		exit(0);
	}
	//修改key值 
	x->a[n-1]=key;
	//准备循环变量 
	int i=0;
	i=n;
	int p_i=i/2;//父结点的编号 
	int temp=0;
	//以修改的那个点为起点,向上与父结点进行循环比较
	//直到达到最大优先队列的性质 
	while(i>1&&x->a[p_i-1]<x->a[i-1])
	{
		temp=x->a[p_i-1];
		x->a[p_i-1]=x->a[i-1];
		x->a[i-1]=temp;
		i=p_i;
		p_i=i/2;
	}
}

4.最大优先队列的C语言实现

注意:
·
这里的最大优先队列是通过最大堆来实现的,因此最大堆的两个操作函数也是最大优先队列的基础

#include<stdio.h>
#include<stdlib.h>
#include<time.h>


//由于存在插入操作,因此需要预留一定的存储空间 
//假设存储空间的单元树是数据个数的2倍
#define MSIZE 30 
#define DSIZE 15//数据的个数 



typedef struct priority_queue
{
	int *a;//指向队列的顺序存储空间 
	int heap_size;//队列中元素的个数	
}pq;



void MAXHEAP_IFY(int *x,int n,int size)
{
	//得到n结点的左右孩子结点 
	int l=2*n;
	int r=2*n+1;
	
	int largest=n;//假设n结点比它的左右孩子都大,为它赋初值
	
	/*
	如果左右孩子存在的情况下,找到n的左右孩子和n结点三者之间的最大值 
	*/ 
	if(l<=size&&x[l-1]>x[n-1])
	{
		largest=l;
	}
	if(r<=size&&x[r-1]>x[largest-1])
	{
		largest=r;
	}
	
	/*
	如果n,n的左孩子,n的右孩子
	三者中的最大值不是n的话
	我们需要调整三者的关系
	使满足最大堆的性质 
	*/
	if(largest!=n)
	{
		int temp;
		temp=x[n-1];
		x[n-1]=x[largest-1];
		x[largest-1]=temp;
		//如果改变了结构
		//那么可能导致下一层的最大堆性质不满足
		//需要进一步维护 
		MAXHEAP_IFY(x,largest,size);
	}
} 



void BUILD_MAXHEAP(int *x,int size)
{
	int heap_size=size;
	int first_node=heap_size/2;//得到编号最大的父结点 
	int i=0;
	//从编号最大的父结点开始,自底向上地构建最大堆
	//这个过程可以循环调用维护函数来实现 
	for(i=first_node;i>=1;i--)
	{
		MAXHEAP_IFY(x,i,size);
	}
}



void HEAP_SORT(int *x,int size)
{
	int i=0;
	int j=0;
	//先构建一个最大堆
	//那么堆顶的元素就是所有元素中最大的 
	BUILD_MAXHEAP(x,size);
	int temp;
	//循环地取出堆顶元素,将该元素放到数组尾部,将尾部的元素放到堆顶 
	//一方面在每次取出后就针对堆顶元素进行维护堆
	//另一方面将取到数组尾的“原堆顶” 排除在维护之外
	//这样就会按大小次序将堆顶排列于数组尾
	//等循环操作完成,排序也就完成了 
	for(i=size;i>=2;i--)
	{
		temp=x[i-1];
		x[i-1]=x[0];
		x[0]=temp;
		//维护堆 
		MAXHEAP_IFY(x,1,i-1);
	}
}



int HEAP_MAXIMUM(pq *x)
{
	//先判断队列是否还有值 
	if(x->heap_size<1)
	{
		printf("priority queue is underflow\n");
		exit(0);
	}	
	return x->a[0];//堆顶就是最大值所在 
}



int HEAP_EXTRACT_MAX(pq *x)
{
	//先判断队列是否还有值 
	if(x->heap_size<1)
	{
		printf("priority queue is underflow\n");
		exit(0);
	}  
	int max=x->a[0];//找到最大值
	
	//用最小的叶子结点填充堆顶的空缺
	//之后维护堆 
	x->a[0]=x->a[x->heap_size-1];
	x->heap_size=x->heap_size-1; 
	MAXHEAP_IFY(x->a,1,x->heap_size);
	//返回最大值 
	return max;
} 



void HEAP_increase_key(pq *x,int n,int key)
{
	//该函数是将队列中的key值增大到某一个值
	//因此若新key值小于原key值,那么报错结束 
	if(key<x->a[n-1])
	{
		printf("new key is smaller than current key\n");
		exit(0);
	}
	//修改key值 
	x->a[n-1]=key;
	//准备循环变量 
	int i=0;
	i=n;
	int p_i=i/2;//父结点的编号 
	int temp=0;
	//以修改的那个点为起点,向上与父结点进行循环比较
	//直到达到最大优先队列的性质 
	while(i>1&&x->a[p_i-1]<x->a[i-1])
	{
		temp=x->a[p_i-1];
		x->a[p_i-1]=x->a[i-1];
		x->a[i-1]=temp;
		i=p_i;
		p_i=i/2;
	}
}



void MAX_HEAP_INSERT(pq *x,int key)
{
	x->heap_size=x->heap_size+1;
	x->a[x->heap_size-1]=-1000;//我们假设-1000对于key值的所有可能来说是负无穷
	//相当于增大最后一个元素的key值从负无穷到指定key值 
	HEAP_increase_key(x,x->heap_size,key); 
}	




int main()
{
	pq x;
	//申请空间并且初始化 
	x.a=(int *)malloc(MSIZE*sizeof(int));
	x.heap_size=DSIZE;
	srand((unsigned)time(NULL));
	int i=0;
	//得到随机数组 
	for(i=0;i<x.heap_size;i++)
	{
		x.a[i]=rand()%1000;
	}
	//将随机数组打印出来 
	for(i=0;i<x.heap_size;i++)
	{
		printf("%5d",x.a[i]);
	}
	printf("\n");
	//根据数组建立最大优先队列(基于最大堆) 
	BUILD_MAXHEAP(x.a,x.heap_size);
	//顺序打印最大优先队列 
	for(i=0;i<x.heap_size;i++)
	{
		printf("%5d",x.a[i]);
	}
	printf("\n");
	/*
	先调用返回最值函数;
	再连续调用两次最值出队函数,看看是否有改变 
	*/
	printf("优先队列的最大key:%5d\n",HEAP_MAXIMUM(&x));	
	printf("优先队列的最大key:%5d\n",HEAP_EXTRACT_MAX(&x));
	printf("优先队列的最大key:%5d\n",HEAP_EXTRACT_MAX(&x));
	
	//调用增大值函数 
	HEAP_increase_key(&x,x.heap_size,1001);
	//顺序打印最大优先队列 
	for(i=0;i<x.heap_size;i++)
	{
		printf("%5d",x.a[i]);
	}
	printf("\n");
	
	//调用插入函数 
	MAX_HEAP_INSERT(&x,1002);
	for(i=0;i<x.heap_size;i++)
	{
		printf("%5d",x.a[i]);
	}
	printf("\n");
	//释放动态内存	
	free(x.a);		
	return 0;
}

总结

本文主要关注堆排序算法,对于优先队列仅仅停留于实现层次,并且仅仅实现了最大堆和最大优先队列
最小堆和最小优先队列读者可以举一反三,自行实现。
对于文章内容的不妥之处,请广大读者谅解并且指正。

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C语言是一种广泛应用于系统编程和嵌入式开发的高级编程语言。函数指针是C语言中的一个重要概念,它可以指向函数,并且可以通过函数指针来调用函数。快速排序算法是一种常用的排序算法,它的核心思想是通过分治的策略将一个大问题分解为多个小问题,并通过递归的方式解决这些小问题。 下面是C语言中使用函数指针实现快速排序算法的示例代码: ```c #include <stdio.h> // 交换两个元素的值 void swap(int* a, int* b) { int temp = *a; *a = *b; *b = temp; } // 快速排序的分区函数 int partition(int arr[], int low, int high) { int pivot = arr[high]; // 选取最后一个元素作为基准 int i = (low - 1); // 定义一个指针,用于指向小于基准的元素 for (int j = low; j <= high - 1; j++) { if (arr[j] < pivot) { i++; swap(&arr[i], &arr[j]); } } swap(&arr[i + 1], &arr[high]); return (i + 1); } // 快速排序函数 void quickSort(int arr[], int low, int high) { if (low < high) { int pi = partition(arr, low, high); // 将数组分区,并获取分区点的位置 quickSort(arr, low, pi - 1); // 对分区点左边的子数组进行快速排序 quickSort(arr, pi + 1, high); // 对分区点右边的子数组进行快速排序 } } // 打印数组元素 void printArray(int arr[], int size) { for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } printf("\n"); } int main() { int arr[] = {10, 7, 8, 9, 1, 5}; int n = sizeof(arr) / sizeof(arr[0]); printf("原始数组:"); printArray(arr, n); quickSort(arr, 0, n - 1); printf("排序后的数组:"); printArray(arr, n); return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SigmaBull

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值