【堆的应用】堆排序

简介: 向上调整建堆通常使用在堆中一开始没有数据,后来数据才进堆中,需要不断的往堆中插入,这样使用向上调整建堆很方便。因为数据只有在堆上面,下面也没有数据,使用向下调整很困难,而且低效。

本篇总结堆排序是如何实现的,以及各种细节问题,两种不同的建堆方法以及各自的使用场景,注意排序利用堆删除思想进行处理


⏰堆排序


🕚建堆


建什么样的堆根据需求来决定

如果要排升序则建大堆

如果要排降序则建小堆


8b1006da0b2f45ba890c8e6d589d44a0.png


而建堆的方法有两种方法可以使用,一种是利用向上调整法建堆,即模拟插入建堆,一种是向下调整法建堆。不过通常堆排序中使用的建堆方法是向下调整建堆,因为向下调整建堆的方法效率要高。


并且在排序中也是使用向下调整思想进行排序,所以我们如果掌握了向下调整的思想,也就可以学会堆排序了。


向上调整建堆通常使用在堆中一开始没有数据,后来数据才进堆中,需要不断的往堆中插入,这样使用向上调整建堆很方便。因为数据只有在堆上面,下面也没有数据,使用向下调整很困难,而且低效。


而向下调整建堆长使用在已有数据,对已有数据进行建堆,也就是给你一个数组,让你进行排序就是很适合利用向下调整法进行建堆。


0d38bcaf044a4708a1cdbc2f43baf2e1.png


◽向上调整建堆


如何使用向上调整法建堆呢?


首先我们要理解向上调整的思想,即堆最后一个元素,顺着双亲往上调整即可


【建大堆】如果要建大堆则要求父结点元素大于子结点元素,如果不满足则两个结点值交换。

【建小堆】如果要建小堆则要求父节点元素小于子结点元素,如果不满足则两个结点值交换。


所以如果给定一个数组,我们如何使用向上调整法对这个数组建堆呢?


我们可以默认让数组第一个元素为堆顶元素,从数组第二个元素开始进行向上调整,调整完再调整数组第三个元素,……就这样调整n-1个元素就可以了。


void AdjustUp(int* a, int child)//向上调整的前提就是child之前的数是堆
{
  int parent = (child - 1) / 2;
  while (child > 0)
  {
    if (a[child] > a[parent])//这里决定建什么样的堆,大堆还是小堆
    {
      Swap(&a[child], &a[parent]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}


建堆:


//向上调整建堆--模拟插入建堆,时间复杂度为N*logN
for (int i = 1; i <10; i++)//从第二个元素开始调整,默认第一个元素为堆顶元素
  {
    AdjustUp(a, i);//i就是每次要调整的元素下标
  }


◽向下调整建堆


如何使用向下调整法进行建堆呢?


首先还是要理解向下调整法的思想


从堆顶元素开始往下调整,如果不满足要调整的堆性质,就要交换。


在已有数据的基础上我们进行建堆


下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。


int a[] = {1,5,3,8,7,6}; 


倒数第一个非叶子,或者最后一个叶子的父亲。


为什么这样调?


我们知道,向下调整有前提条件,要求左右子树都为大堆或小堆。


我们从后面开始调,从最后一个叶子的父亲开始调,调完父亲,再调父亲的兄弟,那父亲和兄弟都调整完,父亲的父亲就可以调整了,因为父亲和兄弟那串子树都为堆了已经。 只要左右子树为堆我们就可以用向下调整。


c5fc73f50c674f52aa9c53c8f4c140a1.png


要从最后一个子叶的父节点开始调整,首先我们需要找到它


对于数组来说,最后一个子叶的父亲的位置在哪呢?


最后一个结点的位置是n-1,而它的父节点的位置是((n-1)-1)/2 ,即(n-2)/2.


从这个位置开始向下调整,直到堆顶元素调整完即可


void AdjustDown(int* a, int n, int parent)//实现的前提是左右子树都是堆
{
  int child = parent * 2 + 1;
  while (child < n)
  {
    //选出左右孩子中比较大的孩子,假设child为左边,假设左边孩子比较大
    if (child + 1 < n && a[child] < a[child + 1])//不过这里存在越界的风险,不能保证右边的孩子一定存在
    {//右边的孩子要存在的话也需要小于n才可以所以我们再加上去
      ++child;//让右边的孩子成为比较大的child
    }
    //然后让根(父亲)与较大儿子比较,这里是大堆,父亲要大于儿子的
    if (a[parent] >a[child])
    {
      Swap(&a[parent], &a[child]);
      //交互完后,让parent跳到儿子位置上去,儿子继续往下找
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}


建堆:


    //通常建堆是使用向下调整建堆--怎么使用向下调整建堆呢?
  //从最后一个非空子叶开始向下调整,调整完后,再调整它的兄弟,再调整它们的父辈
  for (int i = (n -2) / 2; i >= 0; i--)
  {
    AdjustDown(a, n, i);
  }


◽两者区别:时间复杂度不同


向上调整的时间复杂度是O(N*logN)


而向下调整的时间复杂度是O(N)


也正是它们效率的不同,通常建堆常使用向下调整建堆,效率高。


🕐排序


利用堆删除思想进行排序


建完堆后,就要对堆中数据进行排序了。


那怎么排序呢?


比如我们排升序,我们要求的是第一个元素应该是最小的才对。


最后一个元素才是最大的。但我们建的是大堆,而大堆的堆顶数据是最大的,也就是数组第一个元素最大,完全不符合排序嘛。


我们是这样做:将堆顶数据与最后一个数据交换,并不删除最后一个元素,只是将堆的范围缩小,从堆顶开始往下调整使它的性质恢复。


然后再接着将堆顶与倒数第二个交换,交换完后,堆的范围缩小,从堆顶向下调整。


8ed023d518764a87acf4eab2b8134c30.png


//将堆顶数据与堆尾部数据交换,然后不要删除最后这个数据,让剩下的n-1个结点继续变成堆,然后继续交换
  int end = n - 1;//堆尾数据
  for (int i = 0; i < n; i++)
  {
    Swap(&a[0], &a[end]);
    AdjustDown(a, end, 0);//end就是每次要调整堆的大小,每次都会缩小1
    end--;
  }


⏰整体代码


void HeapSort(int* a, int n)//堆排序
{
  //建堆
  for (int i = (n - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(a, n, i); 
  }
    //排序
  int end = n - 1;
  for (int i = 0; i < n; i++)
  {
    Swap(&a[0], &a[end]);
    AdjustDown(a, end, 0);
    end--;
  }
}
int main()
{
  int a[20] = { 9,5,2,3,6,7,8,4,1,0 };
  HeapSort(a, 10);
  return 0;
}


6934d1343730440db448e0b0e190e6f4.png

相关文章
|
2月前
|
前端开发 算法 JavaScript
最小堆最大堆了解吗?一文了解堆在前端中的应用
该文章详细解释了堆数据结构(特别是最小堆)的概念与性质,并提供了使用JavaScript实现最小堆的具体代码示例,包括堆的插入、删除等操作方法。
最小堆最大堆了解吗?一文了解堆在前端中的应用
|
5月前
|
存储 算法
面试必知必会|理解堆和堆排序
面试必知必会|理解堆和堆排序
|
存储 算法
优先级队列(堆)&&  堆排序
优先级队列(堆)&&  堆排序
50 0
|
6月前
|
算法 C语言
堆的应用:堆排序
堆的应用:堆排序
97 0
|
11月前
|
存储 C语言
堆与堆排序操作详解
堆与堆排序操作详解
堆/选择/插入/希尔排序
堆/选择/插入/希尔排序
51 0
|
存储 算法
【二叉树的顺序结构:堆 && 堆排序 && TopK](一)
【二叉树的顺序结构:堆 && 堆排序 && TopK](一)
72 0
|
算法
【数据结构与算法】堆的应用:堆排序和topk问题
【数据结构与算法】堆的应用:堆排序和topk问题
80 0