数据和结构与算法学习之路15--堆与堆排序

244 阅读3分钟

堆的概念

堆(Heap)是一种特殊的树,它有两个需要满足的点:首先必须是一个完全二叉树;其次堆中每个节点的值都必须大于等于(或者小于等于)其左右子节点的值。对于每个节点的值都大于等于子树中每个节点值的堆,我们叫作“大顶堆”。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫作“小顶堆”。

上图中1和2是大顶堆,3是小顶堆,4不是堆。

堆的存储

因为堆是一个完全二叉树所以比较适合用数组来存储,这样我们不需要存储左右子节点对应的指针,能够节省存储空间。堆我们默认从下标1开始存储数据。

从图中我们可以看出下标为i的节点对应的左子节点的下标为2i,对应右子节点的下标为2i+1。

堆的核心操作(以大顶堆为例)

往堆中插入一个数据

往堆中添加数据需要继续堆的两个特性,这个过程称为堆化。堆化有两种方式:一种是自下到上,一种是自上到下,堆化的过程可以理解为顺着节点的方向向上或者向下对比然后交换。
由下往上的方式是先将要插入的数据放到数组的底部然后新插入的节点与父节点对比大小。如果不满足子节点小于等于父节点的大小关系,我们就互换两个节点。一直重复这个过程,直到父子节点之间满足子节点小于等于父节点。

删除堆顶元素

删除堆顶元素后为了保持堆的特性我们把最后一个节点放到堆顶,然后利用同样的父子节点对比方法。对于不满足父子节点大小关系的,互换两个节点,并且重复进行这个过程,直到父子节点之间满足大小关系为止。这就是从上往下的堆化方法。

PHP伪代码实现(大顶堆)

/***
* 增加堆中的元素
*$data 插入的值
*$arr  堆对应的数组
*$count 堆中已经存储的数据个数
***/
function addData($data, $arr, $count){
    $count++;
    $arr[$count] = $data;
    $i = $count;
    while($i/2 >0 && $a[$i] > $a[$i/2]) {
        array_swap($arr, $i, $i/2);
        $i = $i/2;
    }
}

/***
* 删除堆顶元素
*$arr  堆对应的数组
*$count 堆中已经存储的数据个数
***/
function removeMax($count, $arr) {
  if ($count == 0) {
      return false; // 堆中没有数据
  }
  $arr[1] = $arr[$count];
  $count--;
  heapify($arr, $count, 1);
}

function heapify($arr, $count, $i) { // 自上往下堆化
  while (true) {
    $maxPos = $i;
    if ($i*2 <= $count && $arr[$i] < $arr[$i*2]) {
        $maxPos = $i*2;
    } 
    if ($i*2+1 <= $count && $arr[$maxPos] < $arr[$i*2+1]) {
        $maxPos = $i*2+1;
    }
    if ($maxPos == $i) {
        break;
    }
    array_swap($arr, $i, $maxPos);
    $i = $maxPos;
  }
}

/***
* 交换数组元素
*$arr  堆对应的数组
*$swap_a 需要交换的元素
*$swap_b 需要交换的元素
**/
function array_swap(&$array,$swap_a,$swap_b){
   list($array[$swap_a],$array[$swap_b]) = array($array[$swap_b],$array[$swap_a]);
}

堆排序

基于堆这种数据结构实现的排序算法称为堆排序,它的时间复杂度为O(nlogn)。堆排序的过程分为建堆和排序两步。

建堆

我们首先将数组原地建成一个堆。所谓“原地”就是,不借助另一个数组,就在原数组上操作。建堆的过程,有两种思路。

  • 第一种是借助我们前面讲的,在堆中插入一个元素的思路。我们可以假设,起初堆中只包含一个数据,就是下标为 1 的数据。然后,我们调用前面讲的插入操作,将下标从 2 到 n 的数据依次插入到堆中。这样我们就将包含 n 个数据的数组,组织成了堆。
  • 第二种实现思路,跟第一种截然相反,第一种建堆思路的处理过程是从前往后处理数组数据,并且每个数据插入堆中时,都是从下往上堆化。而第二种实现思路,是从后往前处理数组,并且每个数据都是从上往下堆化。

排序

建堆结束之后,数组中中的第一个值就是最大的值,将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

PHP伪代码实现(大顶堆)

/**
 * 堆排序
 * 取出最大数
 * @param $arr
 * @return mixed
 */
function heapSort($arr)
{
    buildHeap($arr);
    $length = count($arr);
    while ($length > 1) { // 长度大于一才需要排序
        swap($arr, $length - 1, 0); // 将堆顶放到数组尾部(完成一次排序(取出一个最大数))
        $length --;
        adjustHeap($arr, $length, 0);
    }
    return $arr;
}

/**
 * 创建一个大顶堆
 * @param $arr
 */
function buildHeap(&$arr)
{
    $node = floor(count($arr) / 2) - 1 ; // 取得最后非叶子节点(数组0为开始故减1)
    for ($i = $node; $i >= 0; $i--) {
        adjustHeap($arr, count($arr), $i);
    }
}

/**
 * 调整堆
 * @param $arr
 * @param $length
 * @param $node
 */
function  adjustHeap(&$arr, $length, $node)
{
    list($lchild, $rchild) = getChildNode($node);
    $max = $node; // 将改节点设为节点子树最大节点
    while ($lchild < $length || $rchild < $length) { // 左右子节点是否存在
        if ($lchild < $length && $arr[$lchild] > $arr[$max]) { // 左节点大于父节点
            $max = $lchild;
        }
        if ($rchild < $length && $arr[$rchild] > $arr[$max]) { // 右节点大于父节点
            $max = $rchild;
        }
        if ($max != $node) { // 父节点是否是最大节点
            swap($arr, $max, $node); // 若不是交换最大节点和父节点
            $node = $max; // 当前节点被最大节点替换,查出最大节点两个子节点
            list($lchild, $rchild) = getChildNode($node);
        } else {
            break;
        }

    }
}


function swap(&$arr, $a, $b)
{
    list($arr[$a], $arr[$b]) = [$arr[$b], $arr[$a]];
}

/**
 * 获取左右子节点
 * @param $node
 * @return array
 */
function getChildNode($node)
{
    return [$node * 2 + 1, $node * 2 + 2];
}