排序算法3-堆排序与优先队列

首先说明一点,不要被名字所误导,优先队列并不是一种排序饿算法,而是一种数据结构。这个结构与一般的先入先出的队列不同,它每次出队的是优先级最大的元素。当一个指定优先级的元素入队时,能够很快的把它排到队列中。把这两个放在一起,是因为优先队列是用堆排序的方法设计的。

首先先看堆排序吧。堆是一个完全二叉树。完全二叉树我们之前已提过,就是它的前面是满的,直到最后一层除外。最后一层从左边起一直都有叶子,但是从某一个节点开始,这个节点的后面全都没有叶子了。(用嘴说这个数据结构真费心,如果我没说清楚,大家自行百度吧!)这个结构有一个明显的好处在于,他可以方便的使用数组(而不是指针)来实现,如果对整个完全二叉树从左向右,从上大小,从0开始编号的话,数组的下标i对应着某一个节点,那么这个节点的左右孩子分别为:2*i+1、2*i+2;它的父节点为(i-2)/2。

讲完了完全二叉树,我们再看看最大堆与最小堆。最大堆是这样一种结构:它的孩子节点的key都小于等于父节点;如果孩子节点的key都大于等于父节点,就称为最小堆。

有了这个写概念,我们就可以讲堆排序了。

堆排序的过程大致分为4大步:1.利用原来的数组建立一个最大堆。此时,数组中最大的元素肯定是在data[0]中保存的。2.把最大堆的data[0]与data[n-1]元素交换,此时最大的元素就跑到data[n-1]中了。3.忽略最后一个元素,调整整个堆,是得元素交换以后又是一个最大堆。4.将最大堆的data[0]与data[n-2]进行交换,以此类推。

整个程序的难点在于:如何调整一个堆,让他变为最大堆。这个问题可以递归的解决:假设某个节点的左右子树已经是最大堆了,那么如需要将根节点与左右子树相比较,然后(如果需要的话)与左右子树中最大的交换;但是交换完以后,可能会引起左右子树中的一颗不再是最大推,那么就对于以左(或者右)子树为根的子树继续重复前面的过程,直到遇见叶子节点。这个部分的程序如下:

//调整堆,使以index为根的子树成为最大堆
void heapAdjust(pArrayList list, int index,int length)
{
	//左孩子
	int lchild = index * 2 + 1;
	//右孩子
	int rchild = index * 2 + 2;
	//最大的下标
	int largest ;
	//找出最大值的下标,存放在largest中
	if(lchild <= length && list->data[lchild] > list->data[index])
		largest = lchild;
	else
		largest = index;
	if(rchild <= length && list->data[rchild] > list->data[largest])
		largest = rchild;

	//如果需要交换
	if(largest != index)
	{
		//交换它们
		int tmp = 	list->data[index];
		list->data[index] = list->data[largest];
		list->data[largest] = tmp;
		//当交换以后,需要判断下一级的3个节点是否需要调整
		heapAdjust(list,largest,length);
	}

}


 

有了这个函数,其他问题就好办多了:比如利用数组初始化一个最大堆,就需要从最后一个叶子节点的父节点开始,依次调用上面的heapAdjust函数就行了:

//自底而上的调用heapAdjust将数组变成一个最大堆
void BuildMaxHeap(pArrayList list)
{
	//从最后一个父亲节点开始,直到树根
	for(int i = (list->length-2)/2; i >= 0; --i)
		heapAdjust(list,i,list->length);

}


 

剩下的内容就如前面所述:将最大堆的跟与数组的最后一个元素交换,然后忽略最后一个元素,重新调整堆。然后取倒数第二个元素重复上述步骤:

//堆排序
void heapSort(pArrayList list)
{
	BuildMaxHeap(list);
	printf("构成最大堆:");
	printArrayList(list);
	int len = list->length-1;
	for(int i = list->length-1; i > 0; --i)
	{
		int tmp = list->data[0];
		list->data[0] = list->data[i];
		list->data[i] = tmp;
		printf("交换以后");
		printArrayList(list);
		//排好以后就该忽略最后一个元素,然后考虑前n-1个元素了
		--len;
		heapAdjust(list,0, len);
		
	}
	printf("最终结果:");
	printArrayList(list);

}


 

这样,就完成了堆排序。

个人觉得,这个算法虽然设计精巧,但是与前面讲到的快速排序和归并排序相比,运算量着实不小。它的一个重要的应用在于:实现优先队列。

让我们简单的思考一下思路:对于一个优先队列,需要支持如下几种关键的操作:(其他什么判断队列是否为空,返回队列长度之类的就省略了)

(1)push:将元素插入优先队列。

(2)top:返回优先级最高的元素

(3)pop:删除优先级最高的元素

(4)increase:将某个元素的优先级增加到某个数

有了前面的堆排序,我们就不会对这些操作一脸茫然了,当我们已经有一个最大堆时:

(2)操作:就是返回一个最大推的第一个元素A[0];

(3)操作:与堆排序类似,把A[0]用A[n-1]代替,然后将数组的长度减1(忽略最后一个元素),然后调整堆就可以了。

(4)操作:将A[i]的设为指定的优先级。然后在i>1且i的父节点一直小于i的情况下,一直做两件事:a.交换A[i]与它的父节点的值。b.将i递增为父节点的序号。

(1)操作:有了(4)操作,其实(1)就很容易实现了,给堆中插入一个优先级非常低的节点,然后通过increase函数将它的优先级提升的指定值就可以了。

下面看一下具体的函数:

//返回队首元素
int top(pArrayList list)
{
	return list->data[0];
}

//出队
int pop(pArrayList list)
{
	int head = list->data[0];
	list->data[0] = list->data[list->length-1];
	--list->length;
	//调整堆为最大堆
	heapAdjust(list,0,list->length);
	return head;
}


//将指定位置的优先级升高为一个数
void increaseKey(pArrayList list, int index, int key)
{
	if(list->data[index] > key)
	{
		printf("new key must bigger than old key!\n");
		return ;
	}
	else
		list->data[index] = key;
	int tmp;
	//当子节点不为树根且父节点小于子节点时
	while(index > 0 && list->data[(index-2)/2] < list->data[index])
	{
		//交换父节点与子节点的元素
		tmp = list->data[index];
		list->data[index] = list->data[(index-2)/2];
		list->data[(index-2)/2] = tmp;
		//继续向上一层走
		index = ( index - 2 ) / 2;
	}

}
//入队
void push(pArrayList list, int key)
{
	if(list->length == list->size)
	{
		list->data = (int*)realloc(list->data,sizeof(int) * list->size * 2);
		list->size = list->size*2;
	}
	//初始优先级设为最小
	list->data[list->length] = -1;
	increaseKey(list,list->length,key);
	++list->length;
}


 

为了更“像”STL库中的风格,这里并没有使用传统的enqueue、dequeue之类的来表明入队出队。

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值