数据结构与算法之堆排序

1、先修知识点

        看懂本篇文章的前提是要对这种数据结构有所了解,堆排序其实是树结构的一个应用,和冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序类似,它也是一种排序方法。

        要想继续往下学习,先去学习树和完全二叉树的基本概念、判断方法和存储方式。或者你私信我,我发简单知识点的视频给你,不然就去网上找一找。

2、大顶堆和小顶堆的理论介绍

  • 堆排序是利用堆这种数据结构(其实就是树结构)而设计的一种排序算法,堆排序是一种树形选择排序,不使用遍历的方式查找待排序区间最大数,而是通过堆来选择待排序区间最大数,它也是不稳定排序。(PS:或许这样说有些抽象,等我后面讲解完,你再来看这些理论的东西!)
  • 堆是具有以下性质的完全二叉树:每个结点的值都大于或者等于其左右孩子结点的值,称为大顶堆,注意:没有要求结点的左孩子的值和右孩子的值的大小关系。
  • 每个结点的值都小于或者等于其左右孩子结点的值,称为小顶堆

举一个大顶堆的例子,如下图: 首先是一棵完全二叉树,且每个结点的值都大于等于左右孩子结点的值。

对于以上的大顶堆而言,如果使用顺序存储,那我们可以这样存:对堆中的结点进行编号,映射到数组中,观察数组有如下特点:arr[i] >= arr[2*i+1] 且 arr[i] >= arr[2*i+2],其中i代表结点的编号,编号从0开始。

 

 再举一个小顶堆的例子,如下图: 首先是一棵完全二叉树,且每个结点的值都小于等于左右孩子结点的值。和大顶堆类似,小顶堆需要满足条件:arr[i]<=arr[2*i+1] 且 arr[i] <= arr[2*i+2],其中i代表结点的编号,编号从0开始。

 说明:如果使用堆排序,一般是通过构建大顶堆来完成升序排列,构建小顶堆来完成降序排列。

3、堆排序的基本思想

1、将待排序序列构造成一个大顶堆(大顶堆用数组来存储)

2、此时,整个序列的最大值就是堆顶的根节点。

3、将其与末尾元素进行交换,此时末尾就为最大值。

4、然后将剩余的n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。

4、堆排序的案例演示

看完以上的4个步骤,或者你还是一脸茫然,别说你了,我也是,我们来举一个具体的案例来看一下。

要求:给你一个数组{4,6,8,5,9},要求使用堆排序,将数组升序排列

 第一大步:构造初始堆。将给定的无序序列{4,6,8,5,9}构造成一个大顶堆(因为是升序排列,所以这里可以构造大顶堆)

 1、此时我们从最后一个非叶子结点开始(叶结点不用调整,第一个非叶子结点:长度/2-1,以此图为例,就是5/2-1=1号节点,此结点为第一个非叶子结点),通过观察,发现第一个非叶子结点的值,比右子结点的值还小,于是将6和9进行交换,得到如右所示的图。

经过第一次调整,局部大顶堆就构建完成了,紧接着继续构建。

2、找到第二个非叶子结点4,由于[4,9,8]中9元素最大,4和9交换,如下图所示:

注意,这时观察,整个二叉树,此时它还不是一个大顶堆,因为本次交换导致了[4,5,6]结构混乱,因此继续调整。[4,5,6]中6最大,交换4和6,如下图所示:

此时,我们就将一个无序序列构造成了一个大顶堆。

第二大步:将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换,重建,交换。

1、将堆顶元素9和末尾元素4进行交换

 

 2、重新调整树结构,使其继续满足大顶堆的定义

 3、再将堆顶元素8与末尾元素5进行交换,得到第二大元素8

 

4、后续过程,继续进行调整、交换,如此反复进行,最终使得整个序列有序

 以上就完成了序列的升序排列,先从宏观上理解一下堆排序大概的一个过程是怎样的,当然,其中还有很多细节,在代码中才能体现得更加清楚。

 4、堆排序的代码实现

堆排序的代码确实比较难理解,我们需要把每一句代码的含义都搞清楚,我不妨以上面这个序列{4,6,8,5,9} 为例来编写代码。

其实堆排序最关键的核心步骤就是:如何进行大根堆调整?先看代码吧!然后按照我的分析一步一步理解

// 将一个数组(二叉树)调整成一个大顶堆 
// 举例:int arr[] = {4,6,8,5,9}; => i=1 => adjustHeap => 得到{4,9,8,5,6}
// 如果我们再次调用adjustHeap 传入的是i=0 => 得到 {9,6,8,5,4} 
// arr表示待调整的数组, 
// i表示非叶子结点的在数组中的索引 
// length表示对多少个元素进行调整,length 表示在逐渐减少 
// 完成将以i对应的叶子结点的树调整大顶堆 
void adjustHeap(int arr[],int i,int length) {
	int temp = arr[i];  //先取出当前元素的值保存在一个临时变量temp中
	//开始调整
	for(int k=2*i+1;k<length;k=k*2+1) {
		if(k+1<length && arr[k]<arr[k+1]) {  //说明左子结点的值小于右子结点的值 
			k++; //k就指向右子结点 
		}
		//如果子结点大于父结点 
		if(arr[k]>temp) {
			arr[i] = arr[k];  //把较大的值赋给当前i结点
			i = k; //让i指向k,继续循环比较  
		}
		else {
			break; 
		} 
	} 
	//当for循环结束后,已经将以i为父结点的树的最大值放在了最顶上 
	arr[i] = temp;  //将temp赋值放到调整后的位置 
}

思路:首先告诉要调整的数据—数组arr,以上述序列为例,然后找到要调整的结点i,i代表结点的编号,最后要知道对多少个元素length进行调整,在初始化情况下,数组的长度length为5,首先根据i=(len/2-1)公式得到i=1,于是对1号结点进行调整。怎么调整呢?

① 取出当前i=1号元素6保存到临时变量temp中,然后找到i=1号结点的左孩子结点编号3将其赋值给k变量,即k=3,然后与编号为k+1=4的右孩子结点进行比较,如果发现左孩子结点的值大于右孩子结点的值,就先将k的值自增1,在这里我们发现arr[3]=5 > arr[4]=9,于是将k的值自增1,k变为4,即k=4。

② 如果发现arr[k]的值大于temp,说明需要进行调整,执行arr[i]=arr[k]语句,并且执行i=k语句,否则的话,直接退出for循环;以本例子为例,发现arr[4]等于9大于temp6,于是执行arr[1]=arr[4]的操作,此时将9赋值给arr[1],还需要将k=4这个值赋值给i,此时i=4,代表即将以4为根结点,准备继续大顶堆的调整,但是,在for循环的时候发现k=2*4+1=9,不满足小于length的条件,退出for循环

③ 当for循环结束后,将temp赋值给arr[i],此时所有的arr[i]都会大于等于左右孩子的值。

经过以上三步,以[9,5,6]为元素的大顶堆就调整好了,接下来我们继续举例子来理解。

以上的代码需要多琢磨,多研究,不然一时半会真的很难读懂。

接下来继续进行调整,找到第2个非叶子结点4,如下图所示:

 

 按照刚刚的步骤继续进行调整(你可以对照着代码理解)

① 取出当前i=0号元素4保存到临时变量temp中,然后找到i=0号结点的左孩子结点编号1将其赋值给k变量,即k=1,然后与编号为k+1=2的右孩子结点进行比较,如果发现左孩子结点的值大于右孩子结点的值,就先将k的值自增1,在这里我们发现arr[1]并没有大于arr[2],所以k的值还是保持原来的1不变。注意,元素4已经保存在temp变量中了。

 

② 如果发现arr[k]的值大于temp,即大于arr[i],那就需要进行调整,执行arr[i]=arr[k]语句,即arr[0]=arr[1]语句,即arr[0]=9,此时继续让k赋值给i,即i=k,那么此时i=1,注意,temp中依然存储的是4这个数。

 

③ 因为i=0的结点调整后有可能会影响之前已经调整好的大顶堆,因此继续执行for循环,重新将k赋值为k=2*k+1,继续调整以k为根结点的树,即k=3,,然后比较arr[3]和arr[4]的值,发现arr[3]>arr[4],于是让k自增1,k变为4。发现arr[4]==6 >temp==4,于是将6赋值给arr[1],此时将k=4赋值给i,i此时为4。继续执行for循环,发现条件不满足,退出for循环,此时需要将原来temp的值赋值给arr[i],即arr[4]=4。

 以上是整个堆排序当中的核心代码 — 调整堆。

我们再来回顾一下堆排序的整体步骤:

1).将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

2).将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

3).重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

从主函数出发,我们可以这样来编写代码:

int main() {
	//要求将数组进行升序排列 
	int arr[] = {4,6,8,5,9}; 
	heapSort(arr,5);
	return 0;
}

 其中,heapSort代表要用堆排序的方法堆arr数组进行升序排列,5代表数组元素的长度。

然后,再来看一下heapSort的具体代码细节

//编写一个堆排序的方法,
void heapSort(int arr[],int len) {
    //1.初始堆,先把无序序列调成一个大顶堆
	for(int i=len/2-1;i>=0;i--) {
		adjustHeap(arr,i,5);
	} 
	
	//2.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端
	for(int j=5-1;j>0;j--) {
		//交换
		int temp = arr[j];
		arr[j] = arr[0];
		arr[0] = temp; 
		adjustHeap(arr,0,j);    
	} 
	
	for(int i=0;i<5;i++) cout << arr[i] << "  ";
	cout << endl;	
} 

首先,先把无序序列进行初始化,调整成一个大顶堆,然后进行堆顶元素和末尾元素的交换,每次交换完毕之后需要对堆重新进行调整,这里,我只有5个元素,因此,需要交换4次。

 5、堆排序的完整代码实现

#include <iostream>
using namespace std;

// 将一个数组(二叉树)调整成一个大顶堆 
// 举例:int arr[] = {4,6,8,5,9}; => i=1 => adjustHeap => 得到{4,9,8,5,6}
// 如果我们再次调用adjustHeap 传入的是i=0 => 得到 {9,6,8,5,4} 
// arr表示待调整的数组, 
// i表示非叶子结点的在数组中的索引 
// length表示对多少个元素进行调整,length 表示在逐渐减少 
// 完成将以i对应的叶子结点的树调整大顶堆 
void adjustHeap(int arr[],int i,int length) {
	int temp = arr[i];  //先取出当前元素的值保存在一个临时变量temp中
	//开始调整
	for(int k=2*i+1;k<length;k=k*2+1) {
		if(k+1<length && arr[k]<arr[k+1]) {  //说明左子结点的值小于右子结点的值 
			k++; //k就指向右子结点 
		}
		//如果子结点大于父结点 
		if(arr[k]>temp) {
			arr[i] = arr[k];  //把较大的值赋给当前i结点
			i = k; //让i指向k,继续循环比较  
		}
		else {
			break; 
		} 
	} 
	//当for循环结束后,已经将以i为父结点的树的最大值放在了最顶上 
	arr[i] = temp;  //将temp赋值放到调整后的位置 
}
//编写一个堆排序的方法,
void heapSort(int arr[],int len) {
	cout << "堆排序" << endl;
	//分步完成
	/*
	adjustHeap(arr,1,len);
	for(int i=0;i<5;i++) cout << arr[i] << "  ";
	cout << endl;
	
	adjustHeap(arr,0,len);
	for(int i=0;i<5;i++) cout << arr[i] << "  ";
	cout << endl;
	*/
	//将无序序列构建一个堆,根据升序降序需求选择大顶堆或者小顶堆
	for(int i=len/2-1;i>=0;i--) {
		adjustHeap(arr,i,5);
	} 
	
	//2.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端
	for(int j=5-1;j>0;j--) {
		//交换
		int temp = arr[j];
		arr[j] = arr[0];
		arr[0] = temp; 
		adjustHeap(arr,0,j);    //adjustHeap(arr,0,3) 
	} 
	
	
	for(int i=0;i<5;i++) cout << arr[i] << "  ";
	cout << endl;	
} 
int main() {
	//要求将数组进行升序排列 
	int arr[] = {4,6,8,5,9}; 
	heapSort(arr,5);
	return 0;
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
排序是计算机科学中常见的操作,它将一组元素按照特定的顺序重新排列。排序算法的目标通常是将元素按照升序或降序排列。 常见的排序算法有很多种,每种算法都有不同的时间复杂度和空间复杂度。以下是几种常见的排序算法: 1. 冒泡排序(Bubble Sort):比较相邻的两个元素,如果顺序不正确就交换位置,每次遍历将一个最大(最小)的元素移到最后(最前)。时间复杂度为O(n^2)。 2. 插入排序(Insertion Sort):将数组分为已排序和未排序两部分,每次从未排序部分取出一个元素,插入已排序部分的适当位置。时间复杂度为O(n^2)。 3. 选择排序(Selection Sort):每次从未排序部分选择一个最小(最大)的元素放到已排序部分的末尾。时间复杂度为O(n^2)。 4. 快速排序(Quick Sort):选取一个基准元素,将数组划分为两个子数组,小于基准元素的放在左边,大于基准元素的放在右边,然后对子数组进行递归排序。时间复杂度平均情况下为O(nlogn),最坏情况下为O(n^2)。 5. 归并排序(Merge Sort):将数组递归分成两个子数组,然后对子数组进行排序,最后将两个已排序的子数组合并成一个有序数组。时间复杂度为O(nlogn)。 6. 堆排序(Heap Sort):将数组构建成一个最大(最小)堆,每次从堆顶取出最大(最小)元素放到已排序部分的末尾,然后调整堆使其满足堆的性质。时间复杂度为O(nlogn)。 这里只介绍了几种常见的排序算法,每种算法都有其适用的场景和优缺点。在实际应用中,根据数据规模和性能要求选择合适的排序算法非常重要。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值