算法基础之五(堆排序)

堆排序,首先要了解一下这里的堆是什么,这里的堆其实就是二叉树,很形象是不是,完整的二叉树从头看起,就是一个三角形,也可以看成一个“堆”。

一、数组转换成堆

那么首先要解决的问题就是给数组排序,如何转换成二叉树的?转换方法如图:

数组 int a[],包含元素a[0],a[1],a[2],a[3].....等等。

转换成二叉树:

图还是比较形象的,其实就是依次从堆顶向下排,但是有一个原则,就是上一层没有排满的时候,下一层不会有元素,有限排满上一层,才考虑下一层。

二、如何进行堆排序

那么第二个问题来了,我们怎么才能对其进行排序呢。

在考虑这个问题之前,我们需要先进行一些前序工作:

1、已知一个堆中的元素a[i],它的父节点是谁?左右子节点呢(如果有)?

通过观察不难得出,如果像我们上图这样,数组序号从0开始排起的话,元素a[i]的父节点(当然i不等于0时)是a[(i+1)/2 - 1],而左右子节点分别是a[2*i+1]和a[2*i+2];

2、引入大根堆的概念。

大根堆:除根节点以外的所有及诶单a[i]都要满足:a[PARENT(i)]>=a[i],这里parent(i)表示i的父节点的下标值;显然如果我们将一个树变成了一个大根堆,我们就可以得到一个当前树的最大值,也就是这个堆的顶部节点的值,由大根堆的性质可知。

3、对于一个左右子树已经是大根堆的根节点,怎样将以该节点为根的二叉树转换成大根堆?

显然,如果根节点比左右子节点要大,那么已经是大根堆

如果根节点比左右子节点要小,或者比其中一个要小?我们就需要将最大的那个子节点与根节点交换位置;但是这还没有完,因为原根节点交换到它的直接子节点位置之后,还不一定会满足大根堆的条件限制,如果不满足,我们就需要继续刚才的工作;

看看下面的代码:

/**
 * 在左右子树都是大根堆的情况下,形成新的大根堆
 */
void maxHeapIndex(int a[], int root, int maxSize) {
	//得到左子节点的下标
	int l = leftChildIndex(root);
	//得到右子节点的下标
	int r = rightChildIndex(root);
	//先默认最大节点的下标值为当前根节点
	int largest = root;

	//如果左节点存在且左节点的值大于根节点,则标记最大值为左节点
	if (l <= maxSize && a[l] > a[root]) {
		largest = l;
	}

	//如果左节点存在且右节点的值大于根节点,则标记最大值为右节点
	if (r <= maxSize && a[r] > a[largest]) {
		largest = r;
	}

	//经过标记判断,如果发现最大值确实不是根节点,则进行交换值,且进行递归调用看看交换之后的根节点是否满足大根堆性质
	if (root != largest) {
		swap(a + root, a + largest);
		maxHeapIndex(a, largest, maxSize);
	}
}

/*
 * 交换
 */
void swap(int* a, int* b) {
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
/*
 * 得到左子节点的下标
 */
int leftChildIndex(int i) {
	return (i + 1) * 2 - 1;
}

/*
 * 得到右子节点的下标
 */
int rightChildIndex(int i) {
	return (i + 1) * 2;
}

4、堆中节点的下标关系。

经过第3点,已经搞定了在左右子树都是大根堆的情况下,形成一个新的大根堆;那么我们如何让一颗没有处理过的堆来形成一个大根堆呢?如何找到左右子树都是大根堆的情况呢?找不到,那么不如先不要左右子树,那就是叶子节点,叶子节点没有左右子树,换言之它们就是天然的单节点大根堆;所以叶子节点的父节点都是满足3所假设的条件的,那么对于一个堆而言,叶子节点的父节点的下标的范围是多少呢?

对于一个满节点二叉树而言,如果有h层,比如上图中到a[15]处,有三层;对于h层的满节点二叉树,它的节点个数是2*2^h-1,2^h即2的h次幂,叶子节点的个数为2^h,而除叶子节点之外的其他节点的个数为2^h-1个。我们不难得出,如果数组下标从0开始算起的话,除叶子节点以外的其他节点的下标范围应该是[0,2^h-2]。

所以我们只需要从2^h-2位置的节点开始,也就是倒数第二层的最右边的节点开始。而对于高为h层,节点数为n的堆而言,h和n有如下关系:h=不小于lgn的最接近的整数;知道了上面这些,我们就可以从2^h-2位置开始一直到0位置一直循环执行第3点来实现形成一个大根堆:

/**
 * 生成一个大根堆,思路:
 * 从倒数第二层的最右边的节点开始,依次调用maxHeapIndex
 *
 */
void maxHeapBuild(int a[], int len) {
	//得到高度
	int h = log2(len);

	//得到可用的最大的下标值
	int max = (int) (pow(2, h)) - 2;

	//循环调用
	for (int i = max; i >= 0; i--) {
		maxHeapIndex(a, i, len - 1);
	}
}

/**
 * 得到log2为底的值
 */
int log2(int i) {
	//ceil函数返回一个不小于某值的整数
	return (int) ceil(log(i) / log(2));
}

三、堆排序过程

经过上面四步,我们已经可以得到一个大根堆了,接下来的工作就是如何利用大根堆来进行排序。

对于大根堆而言,我们唯一可以确定的是,它的顶部的那个元素一定是当前堆中最大的元素,于是我们可以每次将顶部的元素和最右边的叶子节点也就是最后一个叶子节点进行交换,然后拿出这个最大值,并且将堆的节点数减一,然后重新对顶点也就是刚才那个交换到顶点位置的叶子节点进行上面3、中的操作,来重新让新堆变成一个大根堆,然后我们依次循环执行上面的操作,直到取出堆中的所有元素,这样,我们从开始到最后取出的元素是按照从大到小的顺序,也就完成了排序。

这一步的代码很简单:

/*
 * 堆排序,思路:
 * 先进行一次maxHeapBuild,使得root节点为最大的值
 * 然后将root节点和最后一个节点的值进行交换
 * 然后将最后一个节点断开(maxsize-=1)
 * 然后进行maxheapify,因为这时候根节点的左右子树依旧是大根堆,所以只需要maxheapify而不需要maxHeapBuild
 * 这样从n-1开始循环到2的时候即可
 */

void maxHeapSort(int a[], int maxSize, int tmp[]) {
	maxHeapBuild(a, maxSize); //n

	for (int i = maxSize; i > 0;) { //循环n-1次
		swap(a, a + i);
		tmp[i] = a[i];
		i--;

		if (i == 0) {
			tmp[0] = a[0];
		} else {
			maxHeapIndex(a, 0, i); //logn 执行了n-2次
		}
	}

	for (int i = 0; i <= maxSize; i++) {
		a[i] = tmp[i];
	}
}

四、堆排序时间复杂度

对于堆排序算法,我们简单的看一下它的时间复杂度,一次maxHeapBuild的时间复杂度是n,然后循环n-1的maxHeapIndex时间复杂度为nlogn,循环赋值操作复杂度为n,所以算起来,时间复杂度为nlogn。

时间: 2024-11-09 03:11:42

算法基础之五(堆排序)的相关文章

一步一步写算法(之堆排序)

原文:一步一步写算法(之堆排序) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 堆排序是另外一种常用的递归排序.因为堆排序有着优秀的排序性能,所以在软件设计中也经常使用.堆排序有着属于自己的特殊性质,和二叉平衡树基本是一致的.打一个比方说,处于大堆中的每一个数据都必须满足这样一个特性: (1)每一个array[n] 不小于array[2*n] (2)每一个array[n]不小于array[2 * n + 1] 构建这样一个堆只是基础,后

数据结构与算法基础学习笔记

*********************************************            ---算法与数据机结构--- 数据结构:由于计算机技术的发展,需要处理的对象不再是纯粹的数值,还有像字符,表,图像等具有一定结构的数据,需要用好的算法来处理这些数据. 我们把现实中大量而又复杂的问题以特定的数据类型的特定的存储结构保存到主存储器中,以及在此基础上为实现某个功能而执行的相应操作(查找排序),这个相应的操作也叫算法. 数据结构 = 个体 +个体的关系算法 =对存储数据的操

排序算法学习之堆排序

一.堆与堆排序的产生及定义 在简单选择排序中,每次从n个元素中比较n-1次选取最小的元素,这很好理解,但是前面比较过的数据在之后还要重新比较,这将花费大量的运算时间.堆排序算法就很好的解决了这个问题,堆排序在每次选择到最小记录的同时会根据比较结果对其他数据进行调整,堆排序的时间复杂度为O(NlogN). 堆通常是指二叉堆,即堆是一颗完全二叉树,同时其满足一定性质:每个节点的值大于等于其左右孩子的值(大顶堆),或者每个节点的值小于等于其左右孩子的值(小顶堆).堆在本质上是一个数组,根节点即为a[0

白话经典算法系列之五 归并排序的实现(转)

归并排序是建立在归并操作上的一种有效的排序算法.该算法是采用分治法(Divide and Conquer)的一个非常典型的应用. 首先考虑下如何将将二个有序数列合并.这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数.然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可. //将有序数组a[]和b[]合并到c[]中 void MemeryArray(int a[], int n, int b[], int m, int c[]) { int

算法基础——经典八大排序算法的Java及Python实现

概述 八大排序算法不用多说了,程序员算法基础必须要掌握的,现在总结一下加深记忆.下图是这八大排序算法的分类.名称.时间空间复杂度,以及稳定性. 代码 以下是经典八大排序算法的Java及Python代码,都是基于经典算法书籍<算法导论>里的伪代码实现的,我在关键语句部分附上了注释. 按照上图中的顺序分别介绍八大排序算法的实现(升序),前面是Java,后面是Python.Java的排序函数写在了一个类里,Python的排序函数则直接写出来了. 直接插入排序 public class InsertS

算法 基础

算法复杂度 时间复杂度:用来估计算法运行时间的一个单位:O(n).O(1) 常见于for循环, 或者log(n)—常见于while循环.循环减半时复杂度为log(n) 常见时间复杂度排序  O(1) < O(logn)< O(n) < O(n^log n) < O(n^2) < O(n^2 log n ) < O(n^3) 空间复杂度:估算算法内存占用大小的单位:"空间换时间"; 算法:完成算法基础模块后,算法优化是关键点(方法:查找算法模块中重复或

python小白-day4递归和算法基础

递归&算法基础 一.递归 递归函数的优点是定义简单,逻辑清晰.理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰. 使用递归函数需要注意防止栈溢出.在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧.由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出. 1 2 3 4 5 6 7 8 def calc(n):     print(n)     if n/2>1:         r

算法——基础篇——快速排序

快速排序是一个经常使用的算法,由于每次用的时候,都感觉没有理解清楚,特写一篇文章记录一下. 算法介绍 快速排序有点类似有冒泡排序,冒泡排序从相邻的两个元素比较,小的在左边,大的在右边,这个算法很容易理解.而快速排序它相当于是在一头一尾两边分别排序比较,比较的对象是当前元素值,和一个选定的key值,主题的思想就是通过跟key值比较,把大于key的值放在右边,小于的放在左边这样就完成了一次排序,接着在对key值左边的序列进行同样的操作,右边也是,最后便能将所有的元素给排好序,由于它每次排序,都会分成

算法三之堆排序

一.堆(Heap)定义 (1)n个关键字序列Kl,K2,-,Kn称为(Heap),当且仅当该序列满足如下性质(简称为堆性质): k(i)<=k(2i)且k(i)<=k(2i+1)(1≤i≤ n/2), 当然,这是小根堆,大根堆则换成>=号. (2)k(i)相当于二叉树的非叶子结点,K(2i)则是左子节点,k(2i+1)是右子节点 若将此序列所存储的向量R[1..n]看做是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:树中任一非叶子结点的关键字均不大于(或不小于)其左右孩