小鑫的算法之路:堆

背景

队列的特点是先进先出(FIFO),在日常生活中很常见,比如说食堂中就餐的队列,基本都是谁先排队,谁就先打餐吃饭。

然而,还有其他一些场景在生活中也很常见,比如说在医院排队求诊的病人,如果来了一位严重的病人,那么就需要急诊,否者如果按照普通队列排队等待。如果其他病人求诊完,那就会危及这位病人的生命,因此这位病人需要立即就医,优先级很高。就这引出了优先队列,优先队列的特点不是先进先出,而是基于优先级出队

对于优先队列,在数据结构上不能依赖普通队列进行实现,而需要基于实现,才能高效实现入队和基于优先级出队的时间复杂度均为O(logn)

二叉堆

特点

为了实现优先队列出队和入列的O(logn)的时间复杂度,往往通过二叉堆实现。

二叉堆是一颗完全二叉树,完全二叉树中的元素按照层级依次从左到右进行排列。

二叉堆中某个节点的优先级不能高于其父节点的优先级。在最大堆中,节点的值不大于父节点的值。在最小堆中,节点的值不小于父节点的值。

存储结构

由于二叉堆是一颗完全二叉树,可通过数组而非二叉树来存储二叉堆。在数组中,如果根节点的起始索引为0,节点的索引为i,那么其父节点和子节点的索引如下:

parent(i) = (i - 1) / 2 
left_child(i) = 2 * i + 1
right_child(i) = 2 * i + 2
代码示例

在二叉堆中,最主要的操作是节点上移节点下移操作。以最大堆为例,代码如下:

template<typename T>
class maxheap {
public:
    maxheap() {}

    maxheap(size_t capacity) 
    {
        data_.reserve(capacity);
    }

    ~maxheap() = default;

    size_t size() const 
    {
        return data_.size();
    }

    bool empty() const
    {
        return data_.empty();
    }

    void add(const T& element) 
    {
        data_.emplace_back(element);
        sift_up(data_.size() - 1);
    }

    // 注意:如果二叉堆中无数据,获取max()是未定义的
    const T& max() const
    {
        return data_.front();
    }

    void pop()
    {
        if (data_.empty()) {
            return;
        }

        std::swap(data_.front(), data_.back());
        data_.pop_back();

        sift_down(0);
    }

private:
    void sift_up(std::ptrdiff_t index)
    {
        auto parent_index = parent(index);
        // 如果当前节点大于父亲节点的值,需要进行上移操作
        while (parent_index.has_value() && parent_index.value() >= 0 && (data_[index] > data_[parent_index.value()])) {
            std::swap(data_[parent_index.value()], data_[index]);
            index = parent_index.value();
            parent_index = parent(index);   
        } 
    }

    void sift_down(std::ptrdiff_t index)
    {
        auto left_child_index = left_child(index);
        // 如果当前节点小于其左右孩子节点的最大值,需要进行下移操作
        while (left_child_index.has_value() && left_child_index.value() < data_.size()) {
            std::ptrdiff_t max_child_index = left_child_index.value();
            if ((max_child_index + 1 < data_.size()) && (data_[max_child_index] < data_[max_child_index + 1])) {
                ++max_child_index;
            }
            if (data_[index] >= data_[max_child_index]) {
                break;
            }
            std::swap(data_[max_child_index], data_[index]);
            index = max_child_index;
            left_child_index = left_child(index);
        }
    }

    std::optional<std::ptrdiff_t> parent(std::ptrdiff_t index) const
    {
        if (index <= 0) {
            return std::nullopt;
        }

        return (index - 1) / 2;
    }

    std::optional<std::ptrdiff_t> left_child(std::ptrdiff_t index) const
    {
        if (index < 0) {
            return std::nullopt;
        }

        return 2 * index + 1;
    }

    std::optional<std::ptrdiff_t> right_child(std::ptrdiff_t index) const
    {
        if (index < 0) {
            return std::nullopt;
        }

        return 2 * index + 2;
    }
   

private:
    std::vector<T> data_;  // 存储堆内部数据
};

堆排序

在最大堆中,数组的首个元素为整个数组中的最大值。根据这个性质,不断将最大值交换到数组尾部,然后将除开尾部已排序的数据进行堆化,继续将最大值交换到尾部,可以实现堆排序。

堆排序是原地排序,时间复杂度为O(nlogn),空间复杂度为O(1)。代码如下:

template<typename T>
class heapSort {
public:
    void sort(std::vector<T>& data)
    {
        // 如果元素个数不超过1,那么无需处理
        if (data.size() <= 1u) {
            return;
        }

        heapify(data); // 将数据进行堆化处理
        for (int i = static_cast<int>(data.size()) - 1; i > 0; --i) {
            std::swap(data[i], data[0]);  // 交换最大值到数组尾部  
            sift_down(data, 0, i);  // data[0, i)区间进行堆化
        }
    }

private:
    void heapify(std::vector<T>& data)
    {
        for (int i = (static_cast<int>(data.size()) - 2) / 2; i >= 0; --i) {
            sift_down(data, i, static_cast<int>(data.size()));
        }
    }

    void sift_down(std::vector<T>& data, int index, int size)
    {
        int left_child = 2 * index + 1;
        while (left_child < size) {
            int max_child = left_child;
            if ((left_child + 1 < size) && (data[left_child] < data[left_child + 1])) {
                ++max_child;
            }
            if (data[index] >= data[max_child]) {
                break;
            }
            std::swap(data[index], data[max_child]);
            index = max_child;
            left_child = 2 * index + 1;
        }
    }
};

优先队列

堆排序应用不是很多,堆应用最多的场景是优先队列

例如在规模很大的数据集合中,如果要找到前K个最大的元素,那么无需对所有元素进行排序(某些场景下内存不足无法支持排序),此时只需要一个能存储K个元素的优先队列即可解决问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值