排序算法之堆(heap)

本文深入探讨了堆排序算法,包括堆的性质、最大堆和最小堆的概念,以及如何调整堆和建立堆。堆排序的时间复杂度为O(nlog2n),在排序过程中使用了堆这种数据结构。此外,还介绍了堆排序在优先队列中的应用,如在C++ STL中的优先队列实现,并提供了关于如何创建大顶堆和小顶堆的指导。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  排序时,实际中,待排序的数很少是单独的数值,它们通常是称为记录(record)的数据集的一部分。每一个记录包含一个关键字(key),就是排序中要重排的值。记录的剩余部分有卫星数据(satellite data)组成,通常与关键字是一同存取的。

算法最坏情况运行时间平均情况/期望运行时间
插入排序Θ(n2)Θ(n2)
归并排序Θ(nlog2n)Θ(nlog2n)
堆排序О(nlog2n)——
快速排序Θ(n2)Θ(nlog2n)(期望)
计数排序Θ(k+n)Θ(k+n)
基数排序Θ(d(n+k))Θ(d(n+k))
桶排序Θ(n2)Θ(n)(平均情况)

一、堆

  堆排序的时间复杂度是O(nlog2n),堆排序具有空间原址性:任何时候都只需要常数个额外的元素空间存储临时数据,堆排序引入了一种算法设计技巧:使用一种我们称为“堆”的数据结构来进行信息管理。堆不仅用在堆排序中,而且它也可以用来构造一种有效地优先队列。在Java和Lisp中它被引申为垃圾收集存储机制。
  如图所示,(二叉)堆是一个数组,它可以被看成一个近似的完全二叉树,树上的每一个结点对应数组中的一个元素。除了最底层外,该树是个完全充满的,而且是从左到右填充。表示堆的数组A包括两个属性:A.length(通常)给出数组元素的个数,A.heap-size表示堆元素(即堆结点)的个数,有多少个堆元素存储在该数组中,这里0A.heapsizeA.length。树的根节点是A[1],给出一个结点的下标i很容易计算其它父节点、左孩子和右孩子。
  计算结点下标 给出结点i,其父节点为i2 ,左孩子结点为2i,右孩子结点为2i+1,堆的高度log2n
  树状图
  二叉树堆可以分为两种形式:最大堆和最小堆。在这两种堆中,结点的值都要满足堆的性质,最大堆是除了根以外的所有结点i都要满足:A[parent(i)]A[i]也就是说某个结点的值至多与其父节点一样大。因此,堆中的最大元素存放在根结点中。在任一子树中,该子树所包含的所有结点的值都不大于该子树根结点的值。同理,最小堆的组织方式正好相反,最小堆是指除了根结点以外的所有结点i都有:A[parent(i)]A[i]。最小堆的最小元素存放在根节点中。在堆排序算法中,我们使用的是最大堆。最小堆通常用于构造优先队列。

二、调整堆

  MAX-HEAPIFY是用于维护堆性质的重要过程。它的输入为一个数组A和一个下标i。在调用MAX-HEAPIFY的时候,我们假定下标i的以左子树left(i)和右子树right(i)为根节点的二叉树都是最大堆,但这时A[i]有可能小于其孩子,这样就违背了最大堆的性质,MAX-HEAPIFY通过让A[i]的值在最大堆中逐级下降,从而使得以下标i为根节点的子树重新遵守最大堆原则。
  

MAX-HEAPIFY(A,i)://最大堆的调整的伪代码
1 l=LEFT(i)//i的左节点
2 r=RIGHT(i) //i的右节点
3 if l<=A.heap-size and A[l]>A[i] 
4        largest=l
5 else  largest=i
6 if r<=A.heap-size and A[r]>A[largest]
7        largest=r
//3-7行代码求出A[i],A[LEFT(i)]和A[RIGHT(i)]的最大的,并将其下标存储在largest中。
8 if largest !=r
9      exchange A[i] with A[largest] 
10     MAX-HEAPIFY(A,largest)

在程序的每一步中,从A[i],A[LEFT(i)]和A[RIGHT(i)]中选出最大的,并将其下标存储在largest中。如果A[i]是最大的那么以i为结点的子树已经是最大的堆,程序结束运行。否则,最大元素是i的某个孩子节点,则交换A[i]和A[largest]的值。从而使得i及其孩子都满足最大堆的性质。在交换后,下标为largest的结点的值就是原来的A[i],于是以该结点为根的子树又可能会违法最大堆的性质。因此需要递归调用MAX-HEAPIFY。
这里写图片描述

三、建堆

  我们可以用自低向上的方法利用过程MAX-HEAPIFY把一个大小为n=A.length的数组A[1..n]转换为最大堆。子数组A[n2+1n]中的元素都是树的叶结点。每一个叶结点可以看成只包含一个元素的堆。第n个结点的父节点为n2,因此在初始数组情况下我们需要从第n2 结点开始递归调用MAX-HEAPIFY调整堆,一直到第1个结点,则建堆完成。关键点是,n个结点的完全二叉树,最后一个结点是第n2个结点的子树。

BUILT-MAX-HEAP(A):
1 A.heap-size=A.length
2 for i=[A.length/2] downto 1//向下取整
3          MAX-HEAPIFY(A,i)

这里写图片描述
从第5个结点开始调整,依次4,3,2,1直到建堆完成。

四、堆排序算法

  初始时,堆排序算法利用BUILD-MAX-HEAP将数组A[1..n]建成最大堆,其中n=A.length。因为数组中的最大元素总是在根结点A[1]中,通过把它和A[n]进行互换,我们可以让该元素放到正确的位置。这时候,如果我们从堆中去掉结点n(这一操作可以通过减少A.heap-szie的值来实现),剩余的结点中,原来根的孩子结点仍然是最大堆,而新的根节点可能违背最大堆的性质。为了维护最大堆的性质,我们调用MAX-HEAPIFY(A,1),从而在A[1..n-1]上构造一个新的最大堆。排序算法会不断重复这一过程,直到堆的大小从n-1降到2。
  

HEAPSORT(A)
1 BUILD-MAX-HEAP(A)
2 for i=A.length downto 2
3      exchange A[1] with A[i]
4      A.heap-size=A.heap-size-1
5      MAX-HEAPIFY(A,1)

五、优先队列

  堆排序算法是一个优秀的算法,堆排序的一个最常应用为:最大优先队列和最小优先队列。最大优先队列应用:一个为在共享计算机系统的作业调度,最大优先队列记录将要执行的各个作业以及它们之间的相对优先级,当一个作业完成或中断后,调度器调用EXTRACT-MAX从所有的等待作业中,选出具有高优先级的作业来执行。在任何时候调度器可以调用INSERT把一个新的作业加入到队列中来。
c++STL中提供了一个优先队列priority-queue。

template <class T,class Container=vector<T>,class Compare=less<typename Container::value_type> >   class priority_queue
//T:Type of the elements.Aliased as member type priority_queue::value_type.
//Container:Type of the internal underlying container object where the elements are stored.Its value_type shall be T.
//Compare:The expression comp(a,b), where comp is an object of this type and a and b are elements in the container
priority_queue调用 STL里面的 make_heap(), pop_heap(), push_heap() 算法实现,也算是堆的另外一种形式。priority_queue 对于基本类型的使用方法相对简单。他的模板声明带有三个参数: 
priority_queue<Type, Container, Functional>:其中Type 为数据类型, Container 为保存数据的容器,Functional 为元素比较方式。Container 必须是用数组实现的容器,比如 vector, deque 但不能用 list。STL里面默认用的是 vector。

比较方式默认用 operator< , 所以如果你把后面俩个参数缺省的话,优先队列就是大顶堆,队头元素最大。如果要使用小顶堆,则一般要把模板的三个参数都带进去。STL里面定义了一个仿函数greater<>,对于基本类型可以使用这个仿函数声明小顶堆。对于自定义类型则必须自己重载operator<或者自己写仿函数。

六、示例程序

  K链表合并 Merge k Sorted Lists:

#include <iostream>
#include <vector>
#include <queue>
#include <cstdlib>
#include <ctime>
using namespace std; 
 struct ListNode 
{ 
     int val; 
     ListNode *next; 
     ListNode(int x) : val(x), next(NULL) {} 
};
struct compare
{
    bool operator()(ListNode* list1,ListNode* list2)
    {
        return list1->val>list2->val;
    }
};
ListNode *mergeKLists(vector<ListNode *> &lists)
{
    priority_queue<ListNode*,vector<ListNode*>,compare>Myqueue;
    int mLength=lists.size();
    if(mLength==0) return NULL;
    if(mLength==1) return lists[0];
    ListNode* headPtr=new ListNode(-1);
    ListNode* movePtr=headPtr;
    ListNode* tempElement=NULL;
    for (int i=0;i<mLength;i++)
    {
        if(lists[i]!=NULL) Myqueue.push(lists[i]);
    }
    while(!Myqueue.empty())
    {
        tempElement=Myqueue.top();
        movePtr->next=tempElement;
        movePtr=movePtr->next;
        Myqueue.pop();
        if(tempElement->next!=NULL) Myqueue.push(tempElement->next);
    }
    return headPtr->next;
}
int main()
{
   int max=1000;
   srand(time(NULL));
   vector<ListNode*> lists;
   ListNode* head=NULL;
   for (int i=0;i<10;i++)
   {
       ListNode* temp=new ListNode(1+rand()%max);
       lists.push_back(temp);
   }
   head=mergeKLists(lists);
   return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值