比快排更加复杂的一种排序
如果要想彻底理解这种排序方法的话,肯定得了解前导知识:树
无向图
一个无向图可以表示成G=(V,E),其中V是非空有限节点集,称V中元素为结点,E是边集,其中的元素是由V中元素组成的无序对,称E中的元素为边。
根据离散数学中对树的定义,一个联通且无回路的无向图即是一颗无向树。
二叉树(二元树)
在根树中每个结点的出度(儿子数)小于或等于2,就称这颗树为二叉树。
今天要复习的堆其实是一种特殊的树,即完全二叉树
完全二叉树
首先得了解满二叉树的定义,满二叉树即一棵深度为(层数)k且有2^k -1个结点的二叉树称为满二叉树。那么我们对这些结点进行编号,从末尾拿掉一些就是一颗完全二叉树,满二叉树是特殊的完全二叉树。这里注意,只能从末尾按顺序取下,即取完后所有结点的标号还是连贯的。
堆分为两种即大顶堆与小顶堆:
大顶堆:完全树,任意结点都比其孩子大
小顶堆:完全树,任意结点都比其孩子小
今天就是要利用大顶堆的特性来进行排序(学会大顶堆小顶堆自然随意拿捏),本文采用顺序存储(数组)的方式模拟树(链式估计不久后再更)。
堆排序:
1.理解顺序存储模拟树的操作
其实很简单,就是将树的每个结点标号,按照编号存入数组
需要了解的是:
任意结点的左儿子是该结点下标乘二加一,即设该点坐标为i,那么左儿子为i*2+1,右儿子即为左儿子加一,即i*2+2。
在知道如何利用顺序存储模拟树结构之后就完成了20%。
2.向下调整(关键)
之前我们说过利用大顶堆的性质对数组进行排序,那么我们的首要任务就是将一个乱序的堆变成一个合格的大顶堆。
下面我们引入一个假设,假设有上图这么一颗树,根的左右子树都是一个大顶堆,我们将这颗树也变成一个大顶堆的过程就叫向下调整。
首先我们把2拿出来
将2的孩子中的较大者放在堆顶,继续比较发现8和5也比2大,那么将大的8放在之前9的位置,继续比较发现6和4也都比2大,那么把6放在原来8的位置,最后将2放在原来6的位置。
这样之后我们就得到了一个合格的大顶堆了。
3.挨个出数
在循环调用2步骤之后我们便能得到一个大顶堆了,那么接下来的操作就是利用它的性质得到一个有序数组了。如果我们开辟一个新的数组不断的将堆顶元素拿走然后向下调整必然是可行的。但这样会增大空间开销,这时我们采用一种换位出数的方法得到有序数组。
①堆顶元素与末尾元素换位,将末尾元素前一个元素设置为末尾元素(相当于将内个堆顶放到堆外
②向下调整,重新得到大顶堆
不断重复上述两步即可得到有序数组
代码:
#include <iostream>
#include <stdlib.h>
#include <cmath>
#include <cstring>
using namespace std;
void sift (int a[], int low , int high) //向下调整函数,low和high为头尾下标
{
int temp = a[low]; //将顶部元素保存
int i = low; //i为父指针
int j = 2*i+1; //j为子指针
while(j<=high) //如果孩子没有超过下标就继续调整
{
if(a[j+1]>a[j]&&j+1<=high) //比较左右孩子谁大,孩子指针指向大的
j = j+1;
if(a[j]>temp) //孩子比顶部元素大就让孩子往上移动,父子指针更新
{
a[i]=a[j];
i = j;
j = 2*j+1;
}
else //如果孩子指针比顶部元素小说明顶部元素应该在这一层
{
a[i] = temp; //将顶部元素赋值给父指针位置
break;
}
}
if(j>high) //如果调整到最后超了,也要进行赋值
a[i]=temp;
}
void swap(int &a,int &b) //简单的置换元素函数
{
int temp = a;
a = b;
b = temp;
}
void heap_sort(int a[], int size) //堆排序本体
{
for(int i = (size-2)/2 ; i > -1 ;i-- )
{
sift(a,i,size-1); //从最后一个父亲向上进行向下调整
}
for(int j = size - 1; j>-1 ; j--)
{
swap(a[0],a[j]); //头尾元素换位
sift(a,0,j-1); //更新尾部下标后向下调整
}
int main()
{
int a[] = {1,4,5,6,7,3,2,8,9};
heap_sort(a,sizeof(a)/sizeof(int));
for(int i = 0;i<sizeof(a)/sizeof(int);i++)
{
cout<<a[i];
}
}