堆排序

不问归期 提交于 2020-02-03 13:45:18

堆排序

预备知识

满二叉树: 除最后一层结点均无任何子节点外,每一层的所有结点都有两个子结点的树。也就是每一层节点个数都是最大值的二叉树(每层kk的节点个数=2k12^{k-1}),也可以说除了叶子结点之外的每一个结点都有两个孩子,每一层(当然包含最后一层)都被完全填充。
在这里插入图片描述

完全二叉树: 如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。
在这里插入图片描述
所以一个满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。

堆: 堆是一个完全二叉树,可以分为大根堆(最大堆)和小根堆(最小堆)

  • 大根堆:对于任意一个子树,父节点的值大于等于其左右孩子的值
  • 小根堆:对于任意一个子树,父节点的值小于等于其左右孩子的值
    在这里插入图片描述

一些性质: 假如用一个数组存储一个堆,那么节点 ii 的左右孩子的下标分别为 2i+12i+12i+22i+2,其父节点的下标为 i12\frac{i-1}{2}。如果设叶子节点的高度为1,那么一个长度为 nn 的数字可以构建的完全二叉树的高度为 h=lognh = logn

数组构建堆

根据上面的预备知识,一个堆可以分为大根堆和小根堆,如何使用一个数组构建一个堆呢(大根堆为例)。如给定一个数组 arr = [2,1,3,6,0,4],构建的目标大根堆为 arr = [6,3,4,1,0,2]。下面描述一下构建步骤:

  • 首先把下标为2作为二叉树的根节点 ,heap = [2]
image-20200202102608619
  • 添加1作为2的左子树,此时符合大根堆的要求,heap = [2,1]
image-20200202102654175
  • 添加3作为2的右子树,heap = [2,1,3],此时不符合,因为根节点2小于右孩子3,那么交换根节点和右孩子的数值,变为 heap = [3,1,2]
  • 添加6作为1的左孩子,heap = [ 3,1,2,6],1的左孩子6比较大,所以交换1和6的位置,heap = [3,6,2,1],此时2的左孩子6比较大,交换二者的位置,heap = [6,3,2,1]
    在这里插入图片描述
  • 添加0作为数字3的右孩子,heap = [6,3,2,1,0],符合要求
image-20200202104743603
  • 添加4作为数字2的左孩子,heap = [6,3,2,1,0,4],2的左孩子4比较大,交换位置,heap = [6,3,4,1,0,2]
  • 构建完成

下图就是上面的构建过程
在这里插入图片描述

代码实现

#include <iostream>
#include <vector>

using namespace std;

/*
* 交换两个数字
*/
void mySwap(vector<int>& vec, int index1, int index2) {
	int temp = vec[index1];
	vec[index1] = vec[index2];
	vec[index2] = temp;
}

/*
* 在已有的大根堆中插入一个数字
*/
void heapInsert(vector<int>& vec, int index) {
    // 如果孩子节点大于父节点,那么就交换二者的位置
	while (vec[index] > vec[(index - 1) / 2]) {
		mySwap(vec, index, (index - 1) / 2);
		index = (index - 1) / 2;  // 更新孩子节点的位置
	}
}
/*
* 构建大根堆
*/
void maxHeap(vector<int>& vec) {
    // 遍历每一个元素,插入到已有的大根堆中
	for (int i = 0; i < vec.size(); i++) {
		heapInsert(vec, i);
	}
}

int main() {

	vector<int> arr = {2,1,3,6,0,4};
	maxHeap(arr);
	for (int i = 0; i < arr.size(); i++) {
		cout << arr[i] << " ";
	}
	cout << endl;

	return 0;
}

构建大根堆的时间复杂度

假设有 nn 个节点,设叶节点的高度为1,那么构建的完全二叉树的高度 h=logn+1h=logn+1,那么叶节点的父节点最多需要调整 11 次(倒数第一层的父节点),最后一层总共包括 2h12^{h-1} 个节点,在最坏的情况下,每个节点都需要调整,需要调整的次数就为 1×2h11 \times 2^{h-1};倒数第二层的父节点最多需要调整 22 次,包含的节点个数为 2h22^{h-2},总共需要调整 2×2h22\times2^{h-2}次,以此类推,根节点需要调整 h1h-1 次,总共需要调整 (h1)×21(h-1) \times 2^{1} 次。那么整个的时间复杂度为
s=1×2h1+2×2h2++(h1)×21 s = 1 \times 2^{h-1} + 2\times2^{h-2} + \cdots + (h-1)\times2^1
两侧乘以2,并相减
s=1×2h1+2×2h2++(h1)×202s=1×2h+2×2h1++(h1)×212ss=2h+2h1+2h2++21(h1)s=2h+2h1+2h2++21+1hs=2×2h2+1hs=4×2h1h1 \begin{array}{l} s = 1 \times {2^{h - 1}} + 2 \times {2^{h - 2}} + \cdots + \left( {h - 1} \right) \times {2^0}\\ 2s = 1 \times {2^h} + 2 \times {2^{h - 1}} + \cdots + \left( {h - 1} \right) \times {2^1}\\ 2s - s = {2^h} + {2^{h - 1}} + {2^{h - 2}} + \cdots + {2^1} - (h - 1)\\ s = {2^h} + {2^{h - 1}} + {2^{h - 2}} + \cdots + {2^1} + 1 - h\\ s = 2 \times {2^h} - 2 + 1 - h\\ s = 4 \times {2^{h - 1}} - h - 1 \end{array}
h=logn+1h=logn+1 带入到 ss 中的 s=4nlogns=4n-logn ,所以构建大根度需要的时间复杂度为 O(n)O(n)

调整大根堆

假设给定的一个数组已经是大根堆,arr = [6, 3, 4, 1, 0, 2],如果某个下标的数字**向下变小**,那么原来的大根堆就有可能被破坏,如下标为0的数字变为0,arr = [0, 3, 4, 1, 0, 2],那么原来的大根堆就被破坏了。那么如何再把它调整为大根堆,如果需要调整的数的下标index=0,堆的长度heapSize=6,采用如下步骤:

  • 首先计算出来下标0的左孩子和右孩子的下标,并判断他们是否越界,如果都不存在,表示是叶子节点,那么直接结束整个流程;如果任意一个存在,那么就进行下一步的操作,此例中,index=0的left=1,right=2
image-20200202204845703
  • 选择arr[index]、arr[left]、arr[right]三个数中最大的数,如果vec[index]是最大的那么直接结束整个流程,此例中,vec[right]比较大,所以交换过后 arr=[4, 3, 0, 1, 0, 2],把right的值更新给index
    在这里插入图片描述
  • 此时index=2,那么left=5,right=6,right越界,舍弃right;在arr[index],arr[left]两个数中选择最大的数,此例中,arr[left]比较大,所以交换index中位置 arr = [4, 3, 2, 1, 0, 0],把left的值更新给index
    在这里插入图片描述
  • 此时index=5,其为叶子节点,arr = [4, 3, 2, 1, 0, 0]所以结束流程
image-20200202215138607

以上就是调整的过程,其调整的核心思想就是,判断当前的节点与其左右孩子比较,选择较大的,然后下沉,直到比较到叶子节点,如果中间的任何一个过程,index的值最大,也是结束该过程。通过上述的描述,如果有 nn 个节点,那么可以构建的完全二叉树高度为 logn+1logn+1,那么如果调整的是根节点,最坏的情况需要调整 lognlogn 次,所以这个过程的时间复杂度为 O(logn)O(logn)

代码实现

#include <iostream>
#include <vector>

using namespace std;
/*
*  交换两个数
*/
void mySwap(vector<int>& vec, int index1, int index2) {
	int temp = vec[index1];
	vec[index1] = vec[index2];
	vec[index2] = temp;
}

/*
*  根据给定的index和heapSize调整,使其成为大根堆
*/
void heapify(vector<int>& vec, int index, int heapSize) {
	int left = index * 2 + 1;  // 计算左孩子的下标
	while (left < heapSize) {  // 如果左孩子下标超过heapSize,那么就直接结束
		int largest = left;  // 最大数的下标初始化为左孩子的下标
        // 当右孩子没有越界,切右孩子大于左孩子的时候,更新largest
		if ((left + 1) < heapSize && vec[left + 1] > vec[left]) {
			largest = left + 1;
		}
        // 把largest和index对应的数字比较,即比较三者的值
		largest = (vec[largest] > vec[index] ? largest : index);
        // 如果最大数的下标还是index,那么表示现在的堆已经是最大堆,直接结束
		if (largest == index) {
			break;
		}
        // 交换两个数
		mySwap(vec, largest, index);
		index = largest;  // index 下沉,即更新index的值
		left = index * 2 + 1;
	}
}


int main() {

	vector<int> arr = {0,3,4,1,0,2};
	
	heapify(arr, 0, arr.size());
	for (int i = 0; i < arr.size(); i++) {
		cout << arr[i] << " ";
	}
	cout << endl;

	return 0;
}

堆排序

有了上面的知识以后,怎么实现堆排序的呢?

算法思想如下:

  1. 把给定的序列调整为大根堆,定义heapSize的长度(整个数组的长度)
  2. 把堆heapSize-1对于元素与根节点位置的元素交换,此时heapSize-1对于的是最大的数
  3. heapSize的长度减小1,调整现有长度的堆为最大堆
  4. 重复1~3过程,直到heapSize的长度为0
    通过下图的动画了解一下堆排序的整体过程:
    在这里插入图片描述
    只要熟练了上文的内容,下面堆排序的代码就很好写了

代码实现

#include <iostream>
#include <vector>

using namespace std;

void mySwap(vector<int>& vec, int index1, int index2) {
	int temp = vec[index1];
	vec[index1] = vec[index2];
	vec[index2] = temp;
}

/*
* 在已有的大根堆中插入一个数字
*/
void heapInsert(vector<int>& vec, int index) {
    // 如果孩子节点大于父节点,那么就交换二者的位置
	while (vec[index] > vec[(index - 1) / 2]) {
		mySwap(vec, index, (index - 1) / 2);
		index = (index - 1) / 2;  // 更新孩子节点的位置
	}
}

/*
*  根据给定的index和heapSize调整,使其成为大根堆
*/
void heapify(vector<int>& vec, int index, int heapSize) {
	int left = index * 2 + 1;  // 计算左孩子的下标
	while (left < heapSize) {  // 如果左孩子下标超过heapSize,那么就直接结束
		int largest = left;  // 最大数的下标初始化为左孩子的下标
        // 当右孩子没有越界,切右孩子大于左孩子的时候,更新largest
		if ((left + 1) < heapSize && vec[left + 1] > vec[left]) {
			largest = left + 1;
		}
        // 把largest和index对应的数字比较,即比较三者的值
		largest = (vec[largest] > vec[index] ? largest : index);
        // 如果最大数的下标还是index,那么表示现在的堆已经是最大堆,直接结束
		if (largest == index) {
			break;
		}
        // 交换两个数
		mySwap(vec, largest, index);
		index = largest;  // index 下沉,即更新index的值
		left = index * 2 + 1;
	}
}

/*
* 堆排序函数
*/
void heapSort(vector<int>& vec) {
	// 给定的序列长度少于2个直接返回
    if (vec.size() < 2) {
		return;
	}
	// 把数组调整为大根堆
	for (int i = 0; i < vec.size(); i++){
		heapInsert(vec, i);
	}
    
	int heapSize = vec.size();  // 定义堆的长度
    // 堆的长度为0时,排序完毕
	while (heapSize > 0) {
		mySwap(vec, 0, heapSize - 1); // 交换根节点和堆尾
		heapSize--;                   // 堆的长度减少一
		heapify(vec, 0, heapSize);    // 调整现有的数组为大根堆
	}
}

int main() {

	vector<int> arr = { 6, 4, 8, 9, 2, 3, 1};
	
	heapSort(arr);
	for (int i = 0; i < arr.size(); i++) {
		cout << arr[i] << " ";
	}
	cout << endl;

	return 0;
}

根据上述的代码,产生大根堆的时间复杂度为 O(n)O(n) ,在堆排序的过程中,需要对数组遍历 nn 次,每次都需要调整,最坏的情况需要调整 lognlogn 次,所以整个的时间复杂度为 O(n×logn)+O(n)O(n\times logn)+O(n),取最高大项,所以整体堆排序的时间复杂度为 O(n×logn)O(n \times logn)。在堆排序的过程中,只用了常数个的变量,所以空间复杂度为 O(1)O(1)。堆排序是一种不稳定的排序算法,如 arr=[8,6,6],是一个大根堆,根节点index=0需要和right=2交换,即arr=[6,6,8],两个6的下标顺序发生了改变,所以是不稳定的排序算法。

总结

  • 稳定性:不稳定
  • 时间复杂度:O(n×logn)O(n\times logn)
  • 空间复杂度:O(1)O(1)

欢迎大家关注我的个人公众号,同样的也是和该博客账号一样,专注分享技术问题,我们一起学习进步
在这里插入图片描述

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!