C++手撕堆结构


一、基础知识

堆结构是一个用数组实现的完全二叉树结构,分为大根堆和小根堆两种,其性质分别为:
(1)大根堆:将数组还原成完全二叉树结构后,每个节点的值都不大于其父节点的值 ;
(2)小根堆:将数组还原成完全二叉树结构后,每个节点的值都不小于其父节点的值。
本文假设堆中实际记录元素的数组从下标为0的位置开始存储堆中的数据。完全二叉树中的父子关系可以转换为数组的下标变换关系,比如对于节点i(数组中对应nums[i]位置的元素),其左孩子节点应为数组中的nums[2*i + 1],其右孩子节点应为数组中的nums[2*(i + 1)],父节点应为数组中的nums[(i - 1) / 2]。

二、堆结构实现

1.接口定义

堆结构需要实现两个接口:
(1)push:将一个新元素插入堆结构中,并维护堆结构(保证修改后仍满足堆的性质)
(2)pop:(大根堆)弹出并返回堆中最大的元素,并维护堆结构。(小根堆)弹出并返回堆中最小的元素,并维护堆结构。

分析这两个函数实现的具体细节

  • push

先将插入的元素放在内部数组的末端,实现数据的插入。为了维护大根堆结构,首先将新插入元素与其父节点比较大小,若比父节点大则交换这两个节点,循环与父节点比较的过程,两个停止条件:
(1)新插入的节点移动到根节点则停止;
(2)新插入的节点移动到某个位置,其值不再大于此时的父节点值则停止。
push函数中维护堆结构的部分调用heapInsert函数实现。最后,更新堆中元素个数。

  • pop

当前数组中首元素即最大元素,记录该值,选取当前数组末尾元素替换首元素,并收缩堆元素的范围。由于补位的末尾元素可能破坏堆的结构,需要对该元素进行判断,判断的方法如下:
(1)若补位元素的值大于其左子节点和右子节点中的较大值,满足堆结构,判断结束;
(2)若补位元素无左子节点和右子节点,满足堆结构,判断结束;
(3)除上述两种情况,该元素至少存在左子节点,将补位元素与当前的左子节点和右子节点(若存在)中的较大值进行比较,记录较大值所在的下标,交换较大值节点与该补位节点;
循环判断上述过程至满足结束判断条件。

2.代码实现

以下以大根堆为例,介绍大根堆的实现方法:

#include <vector>
#include <iostream>
using namespace std;

template <typename T>
class heap{
public:
    heap():m_size(0){}
    ~heap() = default;
	//在大根堆中插入新元素
    void push(T elem){
        nums.push_back(elem);  //元素下标为m_size,其父节点下标为(m_size - 1) / 2
        heapInsert(m_size);    //维护堆结构
        ++m_size; 
    }
   //返回并删除大根堆中的最大值
    T pop(){
        T res = nums[0];
        nums[0] = nums[m_size - 1]; --m_size;
        heapify(0);           //维护堆结构
        return res;
    }
    //打印验证函数
    void print(){
        for(int i = 0; i < m_size; ++i){
            cout << nums[i] << " ";
        }
        cout << endl;
    }
private:
    vector<T> nums;
    int m_size;
    
    //heapInsert: 插入新元素后维护堆结构的函数, 其功能是判断指定下标的元素是否需要在树结构中向上移动,如需要将其移动到相应位置
    void heapInsert(int index){
        int parent = (index - 1) / 2;
        while(parent != index && nums[index] > nums[parent]){
            swap(nums[index], nums[parent]);
            index = parent;
            parent = (index - 1) / 2;
        }
    }
	
	//heapify: 删除元素后维护堆结构的函数,其功能是判断指定下标的元素是否需要向在树结构中向下移动,如需要将其移动到相应位置
	 void heapify(int index){
        //其左节点的下标为index * 2 +1 其右节点的下标为2*(index + 1)
        int left = index * 2 + 1;
        while(left < m_size){
            int largerIndex = -1;
            if(left + 1 < m_size){
                largerIndex = nums[index] >= max(nums[left], nums[left + 1]) ?
                        index : (nums[left] > nums[left + 1] ? left : left + 1);
            }else{  //没有右子节点
                largerIndex = nums[index] >= nums[left] ? index: left;
            }
            if(largerIndex == index) break;
            swap(nums[largerIndex], nums[index]);
            index = largerIndex;
            left = index * 2 + 1;
        }
    }
};

插入操作的时间复杂度为O(logN),返回并删除最大元素的时间复杂度为O(logN)。

三、堆排序

  • 方法一:利用priority_queue结构实现堆排序

可以借助C++中的priority_queue容器实现堆排序,默认为大根堆,具体代码如下:

#include <queue>
using namespace std;

void heapSort(vector<int>& nums){
    priority_queue<int, vector<int>> q;
    for(int i = 0; i < nums.size(); ++i){
        q.push(nums[i]);
    }
    for(int i = nums.size() - 1; i >= 0; --i){
        nums[i] = q.top(); q.pop();
    }
}

时间复杂度为O(NlogN),该方法中使用额外容器实现堆结构,空间复杂度为O(N)。

  • 方法二:借助堆结构性质实现空间复杂度为O(1)的堆排序
void heapify(vector<int>&nums, int index, int n){
    //其左节点的下标为index * 2 +1 其右节点的下标为2*(index + 1)
    int left = index * 2 + 1;
    while(left < n){
        int largerIndex = -1;
        if(left + 1 < n){
            largerIndex = nums[index] >= max(nums[left], nums[left + 1]) ? index : (nums[left] > nums[left + 1] ? left : left + 1);
        }else{  //没有右子节点
            largerIndex = nums[index] >= nums[left] ? index: left;
        }
        if(largerIndex == index) break;
        swap(nums[largerIndex], nums[index]);
        index = largerIndex;
        left = index * 2 + 1;
    }
}

void heapSort(vector<int>& nums){
    int n = nums.size();
    for(int i = n - 1; i >= 0; --i){
        heapify(nums, i, n);
    }
    for(int i = 1; i < n; ++i){
        swap(nums[0], nums[n - i]);
        heapify(nums, 0, n - i);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值