优先队列之最大堆代码实现及其时间复杂度分析

    在我们的计算机操作系统中,经常使用到优先队列来存储数据。我们计算机中的CPU按一定的调度算法从就绪队列中选择一个进程,把CPU的使用权交给被选中的进程。这个需求是很频繁的。其实我们常说的优先队列就是最大最小堆。

    堆分为最大堆和最小堆,其实就是完全二叉树。最大堆要求节点的元素都要大于等于其孩子,最小堆要求节点元素都小于等于其左右孩子,两者对左右孩子的大小关系不做任何要求,其实很好理解。小编今天分享下有关最大堆的算法实现及对其时间复杂度做简单分析。

    首先,最大堆的定义意味着最大元素就在堆的树根。如果元素都是不相同的,树根就保存着最大的元素。小编在算法实现上,采用了数组来实现最大堆【这里实现的是二叉树堆】。

    我们先来看看这样的一个数组:Array[6]={45,36,18,53,72,30}

    一般来说,有两种方式来实现最大堆的创建。包括自顶向下的建堆方式和自下向上的建堆方式。我们先来看第一种。

一、自顶向下

    从根结点开始,然后一个一个的把结点插入堆中。当把一个新的结点插入堆中时,需要对结点进行调整,以保证插入结点后的堆依然是大根堆。一起看看是如何通过插入的方式来建立一个最大堆的【按数组中的元素顺序来插入,这里最大堆用数组heap[n]表示,且元素从1开始存放】。

插入过程1-1
插入过程1-2
插入过程1-3

    最终得到的堆中的元素顺序为72、53、30、36、45、18。

    使用自上而下建立二叉堆,那么插入每个节点都和树的深度有关,并且都是不断的把树折半来实现插入,因此是典型的递归,而非叠加。其中树的h =-1,第k层结点个数为个(当然最后一层结点个数可能小于)。第k层的一个结点插入之后需要进行的比较(移动)次数为k。于是总的比较(移动)次数为∑k*2*(k = 0,1,2,...,h)。可以求得

    ∑k*(k = 0,1,2,...,h)=(-2)*(n+1)+2 = O(n*)

    因此这种插入方法的时间复杂度为O(n)。

二、自底向上

    除了通过插入的方法,我们还可以通过别的方法来构建这个最大堆。先把array数组里的值赋值给heap[1]到heap[n],这时得到的堆数组不是最大堆,是没有经过调整的,需要进行调整。我们需要做的是从第一个非叶子结点开始进行判断该子树是否满足堆的性质。如果满足就继续判断下一个点。否则,如果子树里面某个子结点有最大元素,则交换他们,并依次递归判断其子树是否仍满足堆性质。下图第一个图中的非叶子节点元素为18【数组长度为6,6/2=3获得第一个非叶子节点在堆中位置heap[3]】,可以发现该子树不是最大堆,要交换18和30两个元素。接下来找到倒数第二个非叶子节点,其元素为36,发现子树不是最大堆,调整。然后找到最后的根节点,发现子树不满足最大堆性质,调整。最后得到的最大堆就是最后的图所示。

调整

    这个时候得到的最大堆元素排列为72、53、30、45、36、18。

    初始化建堆只需要对二叉树的非叶子节点调用Adjust()函数,由下至上,由右至左选取非叶子节点来调用Adjust()函数。那么倒数第二层的最右边的非叶子节点就是最后一个非叶子结点。

    如果仅从下面展示的代码上直观观察,会得出构造二叉堆的时间复杂度为O(n)的结果,这个结果是错的,虽然该算法外层套一个n次循环,而内层套一个分治策略下的复杂度的循环,该思考方法犯了一个原则性错误,那就是构建二叉堆是自下而上的构建,每一层的最大纵深总是小于等于树的深度的,因此,该问题是叠加问题,而非递归问题。

    假设高度为k,则从倒数第二层右边的节点开始,这一层的节点都要执行子节点比较然后交换(如果顺序是对的就不用交换);倒数第三层呢,则会选择其子节点进行比较和交换,如果没交换就可以不用再执行下去了。如果交换了,那么又要选择一支子树进行比较和交换;高层也是这样逐渐递归。

推导

    因此自底向上的建堆时间复杂度为O(n)。

    两种方法得到的都是最大堆,接下来就是小编自己写的代码,截图如下:

C++实现最大堆

    在代码中,insert函数就是自顶向上的的方法。ArrToHeap函数使用的是用自底向上的调整方法。关于 最大堆代码中的其它函数功能是怎样的,小编已经做了注释。对于函数代码的逻辑,这里小编就不分析了。核心逻辑就是上面的那几幅图。

    小编能力有限,可能会有些错误,欢迎指正。

你可能感兴趣的:(优先队列之最大堆代码实现及其时间复杂度分析)