数据结构与算法-08堆

Posted 北京-临渊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法-08堆相关的知识,希望对你有一定的参考价值。

堆(Heap)是一种特殊的树形数据结构,它满足以下两个条件:

堆是一棵完全二叉树,即除了最后一层,其他层都是满的,最后一层从左到右填满。

堆中每个节点的值都大于等于(或小于等于)其子节点的值,这种性质称为堆序性。

根据堆序性,堆可以分为两种类型:

  • 大根堆(Max Heap):每个节点的值都大于等于其子节点的值。
  • 小根堆(Min Heap):每个节点的值都小于等于其子节点的值。

堆的主要应用是在排序算法中,例如堆排序(Heap Sort)优先队列(Priority Queue)。堆排序是一种基于堆的排序算法,它的时间复杂度为O(nlogn),空间复杂度为O(1)。优先队列是一种数据结构,它可以用堆来实现,用于维护一组元素中的最大值或最小值。

堆可以使用数组来实现,具体实现方式为:

对于一个节点i,它的左子节点为2i+1,右子节点为2i+2。

对于一个节点i,它的父节点为(i-1)/2。

以下是一个简单的Python示例代码,演示了如何使用数组实现大根堆:

class MaxHeap:
    def __init__(self):
        self.heap = []

    def push(self, value):
        self.heap.append(value)
        self._sift_up(len(self.heap) - 1)

    def pop(self):
        if len(self.heap) == 0:
            raise ValueError("Heap is empty")
        value = self.heap[0]
        last_value = self.heap.pop()
        if len(self.heap) > 0:
            self.heap[0] = last_value
            self._sift_down(0)
        return value

    def _sift_up(self, index):
        parent_index = (index - 1) // 2
        while index > 0 and self.heap[index] > self.heap[parent_index]:
            self.heap[index], self.heap[parent_index] = self.heap[parent_index], self.heap[index]
            index = parent_index
            parent_index = (index - 1) // 2

    def _sift_down(self, index):
        left_child_index = 2 * index + 1
        right_child_index = 2 * index + 2
        largest_index = index
        if left_child_index < len(self.heap) and self.heap[left_child_index] > self.heap[largest_index]:
            largest_index = left_child_index
        if right_child_index < len(self.heap) and self.heap[right_child_index

Python中的heapq模块

Python中的heapq模块提供了一些堆操作的函数,包括将列表转换为堆、将元素添加到堆、从堆中删除元素、获取堆中的最小值或最大值等。heapq模块使用的是小根堆,即堆中的最小值在堆顶。

常用的heapq函数:

  • heapify(iterable):将可迭代对象转换为堆。
  • heappush(heap, item):将元素添加到堆中。
  • heappop(heap):从堆中删除并返回最小值。
  • heappushpop(heap, item):将元素添加到堆中,并返回堆中的最小值。
  • heapreplace(heap, item):从堆中删除并返回最小值,并将元素添加到堆中。
  • nlargest(n, iterable[, key]):返回可迭代对象中最大的n个元素。
  • nsmallest(n, iterable[, key]):返回可迭代对象中最小的n个元素。

使用示例:

import heapq

# 将列表转换为堆
heap = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3

# 将列表转换为堆
heap = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
heapq.heapify(heap)
print(heap)

# 将元素添加到堆中
heapq.heappush(heap, 0)
print(heap)

# 从堆中删除并返回最小值
min_value = heapq.heappop(heap)
print(min_value)
print(heap)

# 将元素添加到堆中,并返回堆中的最小值
min_value = heapq.heappushpop(heap, 7)
print(min_value)
print(heap)

# 从堆中删除并返回最小值,并将元素添加到堆中
min_value = heapq.heapreplace(heap, 8)
print(min_value)
print(heap)

# 返回可迭代对象中最大的n个元素
largest = heapq.nlargest(3, heap)
print(largest)

# 返回可迭代对象中最小的n个元素
smallest = heapq.nsmallest(3, heap)
print(smallest)

在这个示例代码中,首先定义了一个列表heap,然后使用heapq.heapify函数将其转换为堆。接着使用heapq.heappush函数将元素0添加到堆中,使用heapq.heappop函数从堆中删除并返回最小值,使用heapq.heappushpop函数将元素7添加到堆中,并返回堆中的最小值,使用heapq.heapreplace函数从堆中删除并返回最小值,并将元素8添加到堆中。最后使用heapq.nlargest函数返回堆中最大的3个元素,使用heapq.nsmallest函数返回堆中最小的3个元素。

数据结构与算法堆排序总结与实现

本博客总结学习堆排序算法,以一个数组为例,采用大根堆进行升序排序,附有代码实现。

堆排序的思想

堆排序的逻辑是建立在完全二叉树的基础上。

有两个概念必须要了解:

  • 大根堆:每个结点值都大于等于左右孩子结点值
  • 小根堆:每个结点值都小于等于左右孩子结点值

以大根堆为例,将根结点与最后一个结点交换,弹出根结点,即可得到整个树中的最大值。继续,将剩下的n-1个结点的树再调整为大根堆,再弹出根结点,以此类推,可得到一个有序序列。

问题的关键在于,如何进行堆调整?

我们把二叉树中每一簇“父结点、左孩子、右孩子”当成一个三元组,从二叉树底层开始,由下往上,依次对每一个三元组进行调整,套一两层循环,即可完成堆调整。这是直观的总体思路。

存在一个问题:如何根据父或子结点快速获取三元组?

说白了就是需要建立父结点和孩子结点之间的联系。可通过完全二叉树的性质来解决。完全二叉树中,若按照层序遍历对每个结点进行编号(从1开始),父节点为 k ,则左右孩子结点编号一定为 2 * k 和 2 * k + 1 。根据此性质可在父子结点之间快速互相访问。

把待排序的数组看做完全二叉树层序遍历的结果,即可应用这个性质。如下图所示:
技术图片

代码示例

先上代码:

    private void heapSort(int[] arr) {
        int len = arr.length;
        //将乱序数组调整为大根堆
        for (int i = len / 2 - 1; i > -1; --i) {
            heapAdjust(arr, i, len);
        }
        //元素出堆、循环堆调整
        for (int i = len - 1; i > 0; --i) {
            //交换i和0两个元素,使用位运算完成
            arr[i] ^= arr[0];
            arr[0] ^= arr[i];
            arr[i] ^= arr[0];
            heapAdjust(arr, 0, i);
        }
        //arr排序完毕
    }
    /**
     * 堆调整
     */
    private void heapAdjust(int[] arr, int s, int length) {
        int temp = arr[s];
        for (int j = 2 * s + 1; j < length; j *= 2) {
            if (j + 1 < length && arr[j + 1] > arr[j]) {
                ++j;
            }
            if (temp > arr[j]) break;
            arr[s] = arr[j];
            s = j;
        }
        arr[s] = temp;
    }

堆排序流程

1.将乱序数组调整为大根堆

对于一个杂乱无章的数组而言,一层循环不足以将其调整为大根堆,需要两层。

  • 外层循环:相当于从下往上遍历所有的三元组;
  • 内层循环:用子函数heapAdjust实现。按照直观思路,此处不应该有循环,直接调整三元组即可(将父结点与某个孩子结点交换)。但是,每次调整后,孩子结点的值发生改变,该孩子结点值可能比下层结点小。因此需要循环对每一个发生改变的孩子结点的下层三元组进行修正。

2.元素出堆、循环堆调整

交换根节点与最后一个结点,把最大值移到了数组的末尾。再对前 n-1 个数进行堆调整,再次将最大值移到末尾,依次循环,即可得到升序排序结果。

注意:此处的堆调整不需要第一步中的两层循环,只需要一层,调用heapAdjust即可。因为前 n-1 个数中,只有arr[0]这一个位置不正确,并不是完全乱序,只需要调整这一个位置即可。

堆调整

堆调整是本算法中最核心的部分。即调整以 s 为根的三元组为正确的大根堆/小根堆,并对下层结点进行循环修正。

注意:此方法并不会遍历整颗二叉树,也不能将一棵杂乱的二叉树调整为大/小根堆

本部分代码很巧妙,需要细细品读。每次调整时,并不是直接交换父结点值和子结点值,那样会徒增赋值次数。


以上是关于数据结构与算法-08堆的主要内容,如果未能解决你的问题,请参考以下文章

408数据结构与算法—堆排序(二十一)

JavaScript数据结构与算法 - 二叉堆和堆排序

万字总结图解堆算法链表栈与队列(多图预警)

数据结构:堆 的详解

数据结构与算法堆排序总结与实现

算法与数据结构常见排序算法8——堆排序