堆排序和优先队列(C实现)

lvnux

1. 二叉堆

1.1 堆简介

  二叉堆是一个完全二叉树,分为最大堆和最小堆。在最大堆中,父节点的值大于等于其左右子节点的值,即最大堆中根节点的值最大。在最小堆中,父节点的值小于等于其左右子节点的值,即最小堆中根节点的值最小
  本文以最大堆为例。
  二叉堆一般用数组表示,本文直接用int型数组存储堆数据(为了能动态扩展也可以使用C++STL的vector实现),主要是为了能讲解清楚堆原理,不考虑代码的扩展性和封装。本文采用最大堆结构为:

// 定义一个最大堆结构,主要是要保存堆大小
struct Tmaxheap
{
    int* array;  // 数组首元素地址
    int length;  // 数组长度(也是堆可扩展的最大容量)
    int heap_size;  // 堆大小(该范围内的数据才是最大堆)
};

    17-48-01.jpg

  上图展示了最大堆的数组表示法, 树的根节点是数组的第一个元素(下标为0),这样给定一个节点的下标i,很容易计算得到它的父节点、左右孩子的下标:

// 注意: 这里我们的计算是基于根节点在数组中的下标为0
// 计算下标为index的节点的父节点下标
int get_parent_index(int index)
{
    return (index - 1) / 2;
}

// 计算下标为index的节点的左孩子下标
int get_left_child_index(int index)
{
    return 2 * index + 1;
}

// 计算下标为index的节点的右孩子下标
int get_right_child_index(int index)
{
    return 2 * index + 2;
}

1.2 最大堆向下调整

1.2.1 向下调整方法

  下面的函数展示了最大堆的向下调整,使用该函数的前提是index位置的左右子树都是最大堆。原理也很简单,只需要找到当前节点和左右子节点中的最大值,然后决定当前节点值是否下沉,下沉到哪个位置。具体见代码:

// max_heapify_down 采用向下调整的方法不断向下调整index位置的值,使整个堆依然是最大堆。
// 注意:使用该函数的前提是index位置的左右子树都是最大堆。
void max_heapify_down(Tmaxheap* maxheap, int index)
{
    int largest = index;
    int left_child_index = get_left_child_index(index);
    int right_child_index = get_right_child_index(index);

    // 找出左右孩子中的值最大的那个
    if ((left_child_index < maxheap->heap_size) && (maxheap->array[left_child_index] > maxheap->array[index]))
    {
        largest = left_child_index;
    }

    if ((right_child_index < maxheap->heap_size) && ((maxheap->array)[right_child_index] > (maxheap->array)[largest]))
    {
        largest = right_child_index;
    }

    // 如果当前节点的值小于左右子节点的值,则将其与最大的那个子节点交换,
    // 交换完成后index对应的值已经在正确的位置上了,但是 largest 位置的值是原来的index的值,不一定在正确的位置上,
    // 还要继续调整,知道该值在正确的位置上。也就是说最终是整棵树依然是最大堆
    if (largest != index)
    {
        swap((maxheap->array)+index, (maxheap->array)+largest);

        max_heapify_down(maxheap, largest);
    }
}

其中的swap函数交换传入的两个变量的值,如下:

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

1.2.2 使用向下调整方法建堆

  最大堆向下调整方法要求待调整位置的左右子树都是最大堆,所以利用该方法构建最大堆需要先构建叶子节点,再慢慢构建至根节点,这样才能保证每次加入的元素的左右子树都是最大堆。

// build_max_heap_with_down 利用最大堆的向下调整法够建最大堆
Tmaxheap* build_max_heap_with_down(int* array, int length)
{
    Tmaxheap* maxheap = new Tmaxheap;

    maxheap->array = array;
    maxheap->length = length;
    maxheap->heap_size = length;

    // 错误:for (int i = 0; i < length; ++i)
    // 这里一定要注意函数 max_heapify_down 的前提是待调整位置的左右子树都是最大堆,所以这里不能从第一个元素开始调整(
    // 因为第一个元素是根节点,而当前数组还未调整,跟节点的子节点自然还不是最大堆)

    // 可以从最后一个元素开始调整,因为最有一个元素是叶子节点,不存在子节点了,所以它本身就是一个最大堆(只有一个节点的最大堆)
    for (int i = length - 1; i >= 0; --i)
    {
        max_heapify_down(maxheap, i);
    }

    // 因为最大堆是完全二叉树,所以 数组长度的后一半都是叶子节点了(也就是说都是最大堆)。
    // 原理很简单,假设 length / 2 下标的节点不是叶子节点,那么它至少有左子树,根据前面的计算,其左子树下标应为 length+1(2i+1)个,
    // 显然已经越界了,所以 length / 2 下标之后的节点都是叶子节点。
    // for (int i = length / 2; i >= 0; --i)

    return maxheap;
}

1.3 最大堆向上调整

1.3.1 向上调整方法

  下面展示采用向上调整的方法不断向上调整index位置的值,使整个堆依然是最大堆。使用该函数的前提从根节点到index位置的父节点的子树是最大堆。原理也很简单,只需要将当前节点值与其父节点值做比较,判断当前节点值是否需要上浮。

// max_heapify_up 采用向上调整的方法不断向上调整index位置的值,使整个堆依然是最大堆。
// 注意:使用该函数的前提从根节点到index位置的父节点的子树是最大堆。
void max_heapify_up(Tmaxheap* maxheap, int index)
{
    int parent_index = get_parent_index(index);

    if ((parent_index >=0 ) && ((maxheap->array)[index] > (maxheap->array)[parent_index]))
    {
        swap((maxheap->array) + index, (maxheap->array) + parent_index);

        max_heapify_up(maxheap, parent_index);
    }
}

1.3.2 使用向上调整方法建堆

  最大堆向上调整方法要求从根节点到待调整位置的父节点的子树是最大堆,所以利用该方法构建最大堆需要先构建根节点节点,这样才能保证每次构建完成的子树是最大堆。

// build_max_heap_with_up 利用最大堆的向上调整法够建最大堆
Tmaxheap* build_max_heap_with_up(int* array, int length)
{
    Tmaxheap* maxheap = new Tmaxheap;

    maxheap->array = array;
    maxheap->length = length;
    maxheap->heap_size = length;

    // 注意:函数 max_heapify_up 的前提是从根节点到index位置的父节点的子树是最大堆,所以这里只能从第一个元素开始调整(
    // 因为第一个元素是根节点,跟节点自然是最大堆)
    for (int i = 0; i < length; ++i)
    {
        max_heapify_up(maxheap, i);
    }

    return maxheap;
}

2. 推排序

  有了前面的介绍,堆排序就很简单了,因为是最大堆,每次最容易获取的是最大值(即根节点),这里我们采用“原址排序(只借助常数个额外的地址空间)”来实现推排序(实际上我们这里只是在数据交互的时候借助了一个额外的地址空间)。

  1. 将根节点的值和最后一个子节点的值替换(替换后最大值在数组的最后面,而数据的第一个元素可能回导致整个堆不是最大堆);
  2. 将堆的大小减少一个(因为最大值是已经排序好的了,从堆里剔除);
  3. 然后利用向下调整法调整根节点,使整个堆依然是最大堆;
  4. 重复第1步,继续将剩下的元素里面的最大值替换出来。

  下面给出代码和帮助理解的示意图:

// 堆排序,这种方法采用的是“原址排序”:只借助常数个额外的地址空间。
void heap_sort(int* array, int length)
{
    Tmaxheap* maxheap;

    // 由现有数组创建最大堆, 下面两种方法都可以

    // 采用向下调整法够建最大堆
    maxheap = build_max_heap_with_down(array, 16);

    // 采用向上调整法够建最大堆
    // maxheap = build_max_heap_with_up(array, 16);

    // 不停地将最大堆地根节点提出来,并将最大堆地大小减少一,直到最大堆只剩一个元素(最后一个元素就是最小的了,不用再做处理)
    while (maxheap->heap_size > 0)
    {
        // 这里没有开辟新的空间来存放排序后的数据,而是直接使用原有数组的空间
        swap(array, array + maxheap->heap_size - 1);

        --maxheap->heap_size;

        // 因为上面只是把根节点的值更改了,但是根节点的左右子树都是最大堆,所以这里需要用向下调整法将目前根节点的值调整到合适的位置以使整个堆仍然是最大堆
        max_heapify_down(maxheap, 0);
    }
}

  堆排序过程示意图:
  19-46-39.jpg

3. 优先队列

  优先队列(priority queue)里的元素具有优先级,访问优先队列中的元素时,最有最高优先级的元素先出队。优先队列一般用我们上面介绍的堆来实现。优先队列也有两种类型:

  1. 最大优先队列:利用最大堆实现,最大值元素先出队。
  2. 最小优先队列:利用最小堆实现,最小值元素先出队。

  下面以最大优先队列进行介绍。这里给出四种优先队列的操作:返回队列最大值、入队、出队、更新队列某位置数据。

// 返回最大优先队列优先级最高元素
int heap_maximun(Tmaxheap* maxheap)
{
    return (maxheap->array)[0];
}

// max_heap_insert 像最大堆中插入元素,相对于入队操作
bool max_heap_insert(Tmaxheap* maxheap, int value)
{
    if (maxheap->heap_size + 1 >= maxheap->length)
    {
        // 队列已满
        return false;
    }

    // 将堆大小扩展一位存放新添加的元素,然后向上调整该元素值
    ++maxheap->heap_size;
    (maxheap->array)[maxheap->heap_size - 1] = value;

    max_heapify_up(maxheap, maxheap->heap_size - 1);
    return true;
}

// 返回并删掉最大优先队列优先级最高元素,相当于出队操作
int heap_extract_max(Tmaxheap* maxheap)
{
    int max = (maxheap->array)[0];

    (maxheap->array)[0] = (maxheap->array)[maxheap->heap_size-1];
    --maxheap->heap_size;

    // 根节点的左右子树都是最大堆,可以调用向下调整法
    max_heapify_down(maxheap, 0);

    return max;
}

// 更新index位置的元素值
void heap_increase_key(Tmaxheap* maxheap, int index, int value)
{
    if ((maxheap->array)[index] == value)
    {
        return;
    }

    int old_value = (maxheap->array)[index];
    (maxheap->array)[index] = value;

    // 如果新值比旧值大,向上调整。否则,向下调整。
    if (value > old_value)
    {
        max_heapify_up(maxheap, index);
    }
    else if (value < old_value)
    {
        max_heapify_down(maxheap, index);
    }
}

  最大优先队列测试:

void test_priority_queue()
{
    int array[20] = { 0 };

    // 生成最大堆
    Tmaxheap* maxheap = build_max_heap_with_down(array, 20);

    // 清空堆
    maxheap->heap_size = 0;

    // 入队元素
    max_heap_insert(maxheap, 2);
    max_heap_insert(maxheap, 99);
    max_heap_insert(maxheap, 23);
    max_heap_insert(maxheap, 16);
    max_heap_insert(maxheap, 55);
    max_heap_insert(maxheap, 78);
    max_heap_insert(maxheap, 1);
    max_heap_insert(maxheap, 199);

    // 修改下标为6的元素的值
    heap_increase_key(maxheap, 6, 2222);

    // 依次出队
    printf("test_priority_queue result: ");
    while (maxheap->heap_size > 0)
    {
        
        printf("%d ", heap_extract_max(maxheap));
    }

    printf("\n");
}

输出:

test_priority_queue result: 2222 199 99 78 55 23 16 2

可见不管入队顺序是怎样的,每次出队的元素都是队列中的最大值。

4. 参考文献

  1. 《算法导论(第3版)》第6章
阅读 1.4k

后端到架构
后端、架构技术分享
19 声望
2 粉丝
0 条评论
19 声望
2 粉丝
文章目录
宣传栏