数据结构--堆排序

堆排序

思路:首先把一维数组视为一棵完全二叉树,这样才有接下来的的一系列操作和默认的定义

序列满足如下条件

1、L[i]>=L[2i]&&L[i]>=L[2i+1] (大根堆)或者

2、L[i]<=L[2i]&&L[i]<=L[2i+1] (小根堆)

以下只讨论大根堆的情况,小根堆同理

大根堆:任何一个非叶子节点的值都不小于其左右孩子的值,即父亲大孩子小

根节点即为最大的元素

构造

大根堆的排序中的关键操作就是把无序的序列调整成堆

建立大根堆的思想为:
一次对每个节点的为根的子树进行筛选,看节点值是否大于其左右子节点的值,
若不大于,这将其与左右节点中更大的交换,
且交换后可能会破坏下一级的堆,于是要继续进行比较,直到该值大于其左右节点

王道书上的代码是这样的:

// 建立大根堆, A[0]不存储数据, A[1...len]初始为无序序列,
// 且对于堆的定义默认是只有叶子节点满足,所以是从len/2开始一个一个往上构造堆
void BuildMaxHeap(int A[], int len){
	for(int i = len/2; i >= 1; i--)
		HeadAdjust(A,i,len);
}
// 把节点插入到堆中
void HeadAdjust(int A[], int k, int len){	
	A[0] = A[k];	// 暂存A[k]
	for(int i = 2*k; i <= len; i *= 2){		// i*2为其左孩子的节点号
		if(i<len&&A[i]<A[i+1]){	// 取其左右孩子的最大值
			i++;
		}
		if(A[0]>=A[i]) break;	// 如果该值大于其左右节点的最大值,则构成堆,退出循环
		else {					// 否则,进行交换,把A[k]的值变为其中更大的值,同时A[i]的值与A[k]交换
			A[k] = A[i];
			k = i;				// 同时更新要交换的节点
		}
	}
	A[k] = A[0];	// 最后进行交换,在循环中进行交换也可以,但是那样的话会比较麻烦
}

但这种代码应该更容易看懂一点

void Sift(int A[], int k, int len){
	int root = k, child = 2*root;	// root表示以当前节点为根,child表示当前节点的左孩子
	int tmp = A[k];		// tmp暂时存储根节点的值
	while(child <= len){	// child范围不超过len
		if(child+1<=len && A[child]<A[child+1]){	// 取左右孩子的最大值
			child++;
		}
		if(tmp < A[child]){		// 如果根节点的值小于其孩子的最大值,则进行交换
			A[root] = A[child];	// 交换值
			root = child;		// 同时下个要比较的堆的根变为当前的孩子的节点号
			child = root*2;		// 孩子的节点号也同时改变
		}else break;			// 否则跳出循环
	}
	A[root] = tmp;				// 最后把被调整的节点的值放入最终位置,也就是合理位置
}

排序

要从大根堆中得到一个不下降的序列,只需要把根节点1与最后一个节点n互换即可,然后对以1为根的堆在进行一次调整即可,以此类推,即可得到一个不下降的序列

void HeapSort(int A[], int len){
	BuildMaxHeap(A, len);
	for(int i = len; i >= 2; i--){  //最后一个即为最小的元素,故只需要到2即可
		Swap(A[i], A[1]);	//与最后一个元素交换
		HeadAdjust(A,1,i-1);	//然后对以1为根的堆进行调整
	}
}

插入

插入节点放在最底层的最右边,然后依次往上调整即可,实现起来也并不困难

// 插入
// 其实差不多和之前的一样,只是一个是往下调整,而插入是往上调整
void HeapInsert(int A[], int key, int len){
	len++;
	int root = len/2, child = len;	// 与之前的同理,root表示以当前节点为根,child表示当前节点的左孩子
	while(root>=1){			// 因为是往上找节点故退出循环的条件为root>=1,而不是之前的往下找节点child<=len
		if(key>A[root]){	// 只需要判断该值是否大于其根节点即可,不需要取左右孩子的最大值
			A[child] = A[root];		// 如果大于的话进行交换,之后继续往上寻找
			child = root;			// 之后孩子节点变成了当前的根节点
			root = child/2;			// child/2表示该孩子的父亲节点
		}else break;
	}
	A[child] = key;
}

删除

删除一个节点的话会使堆中出现一个孔,解决办法是把最后一个节点与其互换位置,然后对以这个位置为根的堆进行调整即可

// 删除
// 和排序差不多了
void HeapDelete(int A[], int pos, int len){
	swap(A[pos], A[len]);	// 和最后一个节点进行交换
	HeapAdjust(A, A[pos], --len);	// 然后对以该节点为根的堆进行调整即可,注意len--
}

注意插入和删除的基础是该数组为堆,但是还未排序的状态

本博客中所说的最后一个元素均是对于数组而言,当然也可以对树,但是树单纯的说最后一个不太妥当,应说最底层最右边的元素

算法性能

n个节点,对于完全二叉树而言高度为log(n),即对每个节点的调整的时间复杂度为O(logn),循环次数为n/2

故建立一个大根堆的基本操作次数为O(nlogn)

对于排序为调整O(logn),循环次数为n-1

故排序的操作次数为O(nlogn)

加起来总的时间复杂度为O(nlogn)

空间复杂度,为借助辅助空间,故空间复杂度为O(1)

且堆排序最坏时间复杂度也为O(nlogn),不稳定算法

应用

堆排序适合关键字较多的情况

完整代码

#include <cstdio>
#include <bits/stdc++.h>
using namespace std;
/*
	建立大根堆的思想为:
		一次对每个节点的为根的子树进行筛选,看节点值是否大于其左右子节点的值,
		若不大于,这将其与左右节点中更大的交换,
		且交换后可能会破坏下一级的堆,于是要继续进行比较,直到该值大于其左右节点
 */


// 建立大根堆, A[0]不存储数据, A[1...len]初始为无序序列,
// 且对于堆的定义默认是只有叶子节点满足,所以是从len/2开始一个一个往上构造堆
// 把节点插入到堆中
void HeadAdjust(int A[], int k, int len){	
	A[0] = A[k];	// 暂存A[k]
	for(int i = 2*k; i <= len; i *= 2){		// i*2为其左孩子的节点号
		if(i<len&&A[i]<A[i+1]){	// 取其左右孩子的最大值
			i++;
		}
		if(A[0]>=A[i]) break;	// 如果该值大于其左右节点的最大值,则构成堆,退出循环
		else {					// 否则,进行交换,把A[k]的值变为其中更大的值,同时A[i]的值与A[k]交换
			A[k] = A[i];
			k = i;				// 同时更新要交换的节点
		}
	}
	A[k] = A[0];	// 最后进行交换,在循环中进行交换也可以
}

void BuildMaxHeap(int A[], int len){
	for(int i = len/2; i >= 1; i--)
		HeadAdjust(A,i,len);
}

// 这种实现方法应该会更好理解一点
void Sift(int A[], int k, int len){
	int root = k, child = 2*root;	// root表示以当前节点为根,child表示当前节点的左孩子
	int tmp = A[k];		// tmp暂时存储根节点的值
	while(child <= len){	// child范围不超过len
		if(child+1<=len && A[child]<A[child+1]){	// 取左右孩子的最大值
			child++;
		}
		if(tmp < A[child]){		// 如果根节点的值小于其孩子的最大值,则进行交换
			A[root] = A[child];	// 交换值
			root = child;		// 同时下个要比较的堆的根变为当前的孩子的节点号
			child = root*2;		// 孩子的节点号也同时改变
		}else break;			// 否则跳出循环
	}
	A[root] = tmp;				// 最后把被调整的节点的值放入最终位置,也就是合理位置
}

// 插入
// 其实差不多和之前的一样,只是一个是往下调整,而插入是往上调整
void HeapInsert(int A[], int key, int len){
	len++;
	int root = len/2, child = len;	// 与之前的同理,root表示以当前节点为根,child表示当前节点的左孩子
	while(root>=1){			// 因为是往上找节点故退出循环的条件为root>=1,而不是之前的往下找节点child<=len
		if(key>A[root]){	// 只需要判断该值是否大于其根节点即可,不需要取左右孩子的最大值
			A[child] = A[root];		// 如果大于的话进行交换,之后继续往上寻找
			child = root;			// 之后孩子节点变成了当前的根节点
			root = child/2;			// child/2表示该孩子的父亲节点
		}else break;
	}
	A[child] = key;
}

// 删除
// 和排序差不多了
void HeapDelete(int A[], int pos, int len){
	swap(A[pos], A[len]);
	HeadAdjust(A, A[pos], --len);
}

// 堆排序算法
void HeapSort(int A[], int len){
	BuildMaxHeap(A, len);
	// 构建完堆后进行插入
	HeapInsert(A, 66, len);	
	len++;
	// 进行删除
	HeapDelete(A, 7, len);
	len--;
	for(int i = len; i >= 2; i--){
		swap(A[i], A[1]);
		HeadAdjust(A,1,i-1);
	}
}


int main(){
	int A[20], len = 10;
	for(int i = 1; i <= 10; i++) A[i] = 100-i*3;
	for(int i = 1; i <= 10; i++) printf("%d ", A[i]);printf("\n");
	HeapSort(A, len);
	for(int i = 1; i <= 10; i++){
		printf("%d ", A[i]);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值