动图演示堆排序

Posted 捕获一只小肚皮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动图演示堆排序相关的知识,希望对你有一定的参考价值。

前言

上一小节,博主介绍了树状数据结构,其中提到了二叉树的顺序存储与链式存储.而二叉树的顺序存储我们用的最多的就是 堆排序以及 选出前k个数(最小或最大),但是今天博主要介绍的就是 堆排序


顺序存储特点

用一个数组从每一层开始,从上到下,从左到右进行存储二叉树中每一个结点中的值.如果有空结点,也需要占一个位置.

有人会问,这样进行存储我们怎么知道双亲结点(父节点)与其左右孩子的关系呢?答案是 二叉树中双亲节点与孩子结点下标满足一定的关系,可以用公式进行表示出来

左右孩子结点与双亲结点的下标关系如下:

  • leftchild = parent * 2 + 1
  • rightchild = parent * 2 + 2

双亲结点与孩子结点的下标的关系如下

  • parent = ( leftchild - 1 ) / 2
  • parent = ( rightchild - 2 ) / 2

但是大家仔细想想双亲结点与孩子结点的关系是否可以优化一下?没错,优化如下:

  • parent = (child - 1) / 2,理由是在C语言中,运算符/遵循向下取整,所以上面的两个式子可以用下面一个式子代替.

清楚数据结构----堆

堆是二叉树中的一些特殊数据,其中分为大堆与小堆,并且只有符合大堆或者小堆的特性的二叉树才能叫做堆.

  • 大堆 二叉树中所有的双亲结点值(父结点)都大于其对应的孩子结点的值.
  • 小堆 二叉树中所有的双亲结点值(父结点)都小于其对应的孩子结点的值.

如下图所示,展示两种结构:

大家发现无论是大堆还是小堆,都有一个特性吗?什么特性呢?

最值都是在数组索引为0处. 大家记住这个特点,后面会用到哦.


堆排序

即随机给出一个数组,我们想要利用堆的特性进行排序,该怎么进行排序呢?

比如有数组num[] = {4,5,1,3,9,7,8,6,2,8,4};

我们既然想要利用堆特性进行对此数组排序,那么我们的第一步一定是把此数组变成堆(该过程称为建堆),大家想想有什么办法把它变成堆?

答案是:向下调整法


向下调整法

向下调整法的前提是除了根结点以外,其所有子树都符合小堆或者大堆的特性.

比如数组 num[] = {6,7,8,6,2,1,3};其树状结构如图:

对于向上面的数组便可以使用向下调整法,我们下面以要构建小堆为例.

向下调整法的介绍:

假设有一个数组num[] = {6,3,2,5,4,3,7,8,9,6,8,5,9,8,8,9,9,8,7,7,9,7,6,6};(符合向下调整法的前提),因为除了根结点之外,其余都符合小堆特性,所以为了构建一个完整的堆,我们就需要把根结点移动到相应的位置,其移动过程示意图如下:

我们仔细观察上述动图过程,发现向下调整法步骤如下:

  1. 判断左右孩子中谁最小.

  2. 如果双亲结点比最小孩子大,就交换两个结点值,否则调整完毕

  3. 一直重复此操作,若调整到已没有孩子结点,则调整结束

第一步:判断左右孩子结点谁更小.

child = parent*2+1; //我们先假设左孩子更小.
if(child+1<n && num[child+1] < num[child])   //n是数组长度
{
    child++;
} 
//如果右孩子更小,就把最小孩子更新到右孩子.
//之所以有条件child+1<n,是考虑上图二叉树最后只有一个孩子结点时候,那么默认最小值就只要左孩子,不需要与右孩子比较

第二步:判断是否交换双亲结点与孩子结点值

void Swap(int*a,int*b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

if(num[parent] > num[child])
{
    Swap(&num[parent] , &num[child]);
    parent = child;              //交换值以后,重新更新双亲结点
    child = parent*2+1;          //交换值以后,重新默认新的左孩子为最小值.
}
else
{
    break; //否则结束循环.
}

第三步:重复上述步骤

while(child < n) //child<n 代表没有子节点了,就结束调整
{
    //伪代码
    第一步代码;
    第二步代码;
}

所以向下调整法的代码步骤为:

void Swap(int*a,int*b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
    
void AdjustDown(int num[],int n,int parent)   //n是数组长度
{
    child = parent*2+1; //我们先假设左孩子更小.
    
	while(child<n)
    {
        if(child+1<n && num[child+1] < num[child])
        {
            child++;
        } 
        
        if(num[parent] > num[child])
        {
            Swap(&num[parent] , &num[child]);
            parent = child;              //交换值以后,重新更新双亲结点
            child = parent*2+1;          //交换值以后,重新默认新的左孩子为最小值.
        }
        else
        {
            break; //否则结束循环.
        }
    }
}

建堆

向下调整法中,我们已经发现了,想要向下调整必须满足向下调整的条件,但是我们想要的是对随机数组进行排序,所以除了向下调整外,我们还应该怎样做,才能达到真正的建堆操作??.

答案: 我们反向行走,从最底层,最右边的子树开始进行调整,然后从右向左,从下到上.

比如有数组num[] = {4,3,9,8,1,6,7,5,2,9,7,6,1,3};,我们的向下调整数据步骤如下:

所以建堆代码如下:

for(int parent = (n-1-1)/2;parent>=0;parent--)  //n-1是最后一层最右边结点的索引.(n-1-1)/2就是其双亲结点索引
{
    AdjustDown(num,n,parent);
}

排序

既然我们已经对随机数组建立好堆结构,那么剩下的就是进行**堆排序.**但是到这一步后就产生了一个误区,什么误区呢?

我们想要排升序,就应该建立小堆.

我们想要排降序,就应该建立大堆. 对吗?答案是不对,这会让时间复杂度变得极高.

正确的答案是,如果想要排升序,就应该建立大堆,想要排降序,就应该建立小堆.如果不相信的人,大家可以试试升序建小堆,看看怎样进行排序,就会发现及其复杂,由于篇幅有限,博主就不再赘述.

由于我们已经建立好小堆,我们就以排列降序为例,仍是数组num[] = {4,3,9,8,1,6,7,5,2,9,7,6,1,3};

我们利用堆结构中最值总是在索引为0处特点进行排序.首先把最小值和最后一个值进行交换,然后又对[0,n-2]索引中的数进行向下调整,不断重读此步骤,如图(下面只是演示了排序过程中的部分步骤,因为后续步骤都是一样的进行重复):

所以按照上述步骤,我们的堆排序(降序)过程代码如下:

void HeapSort(int num[],int n)
{
    //建小堆
    for(int parent = (n-1-1)/2;parent>=0;parent--)
    {
         AdjustDown(num,n,parent);
    }
    
    for(int end = n-1;end>0;end--)  //end不用为0是因为最后还剩一个时候不用再交换.
    {
        Swap(&num[0],&num[end]);
      	AdjustDown(num,end,0);
    }
}

堆排序总代码

void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

void AdjustDown(int num[],int n,int parent)
{
	int leftchild = parent * 2 + 1;
	while (leftchild < n)
	{
		if (leftchild + 1 < n && num[leftchild] > num[leftchild + 1])
		{
			leftchild++;
		}

		if (num[parent] > num[leftchild])
		{
			Swap(&num[parent],&num[leftchild]);
			parent = leftchild;
			leftchild = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int num[], int n)
{
	//建小堆
	for (int parent = (n - 1 - 1) / 2; parent >= 0; parent--)
	{
		AdjustDown(num, n, parent);
	}

    
    //交换首位值,然后排除最后一个位置,重新向下调整
	for (int end = n - 1; end > 0; end--)  //end不用为0是因为最后还剩一个时候不用再交换.
	{
		Swap(&num[0], &num[end]);
		AdjustDown(num, end, 0);
	}
}

测试

数组num[] = {4,3,9,8,1,6,7,5,2,9,7,6,1,3};,使用上述代码进行堆排序以后的结果为:

排序成功!!!

以上是关于动图演示堆排序的主要内容,如果未能解决你的问题,请参考以下文章

动图演示堆排序

花一个晚上时间整理,十大经典排序算法(Python版本),拿起就用

万字手撕七大排序(代码+动图演示)

八大排序(详细分析+动图演示)

十大经典排序算法(下)

算法漫游指北(第七篇):冒泡排序冒泡排序算法描述动图演示代码实现过程分析时间复杂度和选择排序算法描述动图演示代码实现过程分析时间复杂度