二叉堆实现优先队列和堆排序

二叉堆

二叉堆是一棵完全二叉树,树上的每一个节点对应数组的每个元素。除了最底层外,该树是完全充满的,而且是从左向右填充。

满足两个性质:
1)堆中任意节点的值总是大于或小于其子节点的值;
2)堆是一棵完全树。

二叉堆又被称为优先队列,任意节点不大于其子节点的堆称为最小堆或最小优先队列,反之称为最大堆或最大优先队列。

一般用数组存储,且数组下标0的位置不存数据,直接从1开始
在这里插入图片描述

优先队列

优先级队列在插⼊或者删除元素的时候,需要调整元素位置, 保证底层二叉堆的性质
优先级队列有两个主要 API, 分别是插⼊⼀个元素(insert) 和删除堆顶元素(delHeapTop)

  • C++实现代码:

采用模板实现,实例化时,向构造函数传入 bool cmp(t1,t2);
当函数 bool cmp(t1,t2),若t1>t2,返回true, 则是大顶堆
当函数 bool cmp(t1,t2),若t1<t2,返回true, 则是小顶堆

关于函数包装器function,参考:https://www.cnblogs.com/Braveliu/p/12387684.html

#include<iostream>
#include<string>
#include<functional>
#include<vector>
#include<exception>
using namespace std;

/*说明:
* cmp可以是一个函数/函数模板,或者一个函数对象/函数对象模板
* 函数对象指定义了operator()操作符重载的类实例化的对象
* cmp的参数(T,T),返回值bool
* 对于cmp(t1,t2),若t1>t2,返回true,则是大顶堆
* 对于cmp(t1,t2),若t1<t2,返回true,则是小顶堆
*/

template<typename T>
class priorityQueue{
public:
    vector<T> Q;
    function<bool(T,T)> CMP;

public:
    explicit priorityQueue(function<bool(T,T)> cmp):Q(1,0),CMP(cmp)
    {
    }
    ~priorityQueue(){
    }

public:
    /*返回当前队列队首元素,但不删除它*/
    T heapTop(){
        return Q[1];
    }

    /*插入元素*/
    void insert(T x){
        //先将元素插到最后
        Q.push_back(x);
        //然后让他上浮到正确的位置
        swim(qsize());//qsize()返回最后位置
    }

    /*删除并返回当前队列中队首的元素
    * ⽅法先把堆顶元素 A 和堆底最后的元素 B 对调, 然后删除 A,
    * 最后让 B 下沉到正确位置
    */
    T delHeapTop(){
        if(qsize()==0)//越界
            throw out_of_range("priorityQueue is empty");
        //堆顶元素
        T max=Q[1];
        //把堆顶元素换到最后,删除之
        swap(1,qsize());
        Q.pop_back();
        //让Q[1]下沉到正确位置
        sink(1);
        return max;
    }

    /*返回堆大小*/
    int qsize(){
        return Q.size()-1;//去掉头一个位置
    }

private:
    /*上浮第k个元素,以维护堆性质*/
    void swim(int k){
         while(k>1 && CMP(Q[k],Q[parent(k)])){
            //大顶堆:如果第k个元素比上层大 //小顶堆:如果第k个元素比上层小
            //将k换上去
            swap(parent(k),k);
            k=parent(k);
         }
    }

    /*下沉第k个元素,以维护堆性质*/
    void sink(int k){
        while(left(k)<=qsize()){
            //先假设左孩子
            int older=left(k);
            //如果右孩子存在,比一下大小
            if(right(k)<=qsize() && CMP(Q[right(k)],Q[older]))
                older=right(k);
            //大顶堆:结点k比两孩子都大,就不必下沉了//小顶堆:结点k比两孩子都小,就不必下沉了
            if(CMP(Q[k],Q[older]))
                break;
            //否则,下沉k结点
            swap(k,older);
            k=older;
        }
    }
    /*交换两个元素*/
    void swap(int i,int j){
        T temp=Q[i];
        Q[i]=Q[j];
        Q[j]=temp;
    }
    /*父结点索引*/
    int parent(int root){
        return root/2;
    }
    /*左孩子结点索引*/
    int left(int root){
        return root*2;
    }
    /*右孩子结点索引*/
    int right(int root){
        return root*2+1;
    }
};

template<class T>
bool myMore(T t1,T t2){
    return t1>t2?true:false;
}
template<class T>
bool myLess(T t1,T t2){
    return t1<t2?true:false;
}

int main(){
//注意:在标准库中的头文件<queue>中的priority_queue,默认情况下用less表示大顶堆,
//而用greater表示小顶堆,比如priority_queue<int,vector<int>,greater<int>> least;这在标准库中表示小顶堆
//关于priority_queue的用法  
//参考:https://blog.csdn.net/qq_35987777/article/details/106438221

    priorityQueue<int> maxPriorityQueue(myMore<int>);//大顶堆
    for(int i=1;i<10;i++)
        maxPriorityQueue.insert(i);
    int pSize=maxPriorityQueue.qsize();
    for(int j=0;j<pSize;j++)
        cout<<maxPriorityQueue.delHeapTop()<<" ";

    try{
        maxPriorityQueue.delHeapTop();//越界
    }catch(exception const& ex) {
        cerr <<endl<< "Exception: " << ex.what() <<endl;
    }

    priorityQueue<int> minPriorityQueue(myLess<int>);//小顶堆
    for(int i=1;i<10;i++)
        minPriorityQueue.insert(i);
    pSize=minPriorityQueue.qsize();
    for(int j=0;j<pSize;j++)
        cout<<minPriorityQueue.delHeapTop()<<" ";

    system("pause");
    return 0;
}

堆排序

  • 利用上面实现的优先队列很容易实现堆排序,把上面的main函数改为如下:
    时间复杂度: O ( N ∗ l o g N ) O(N*logN) O(NlogN)
    额外空间复杂度: O ( N ) O(N) O(N)
int main(){

    priorityQueue<int> minPriorityQueue(myLess<int>);//小顶堆
    vector<int>nums{2,3,5,3,4,4,5,2,5};
    
    cout<<" before sort,nums= ";
    for(int i=0;i<nums.size();i++){
        cout<<nums[i]<<" ";
        minPriorityQueue.insert(nums[i]);
    }
    cout<<endl<<" before heapSort,nums= ";
    for(int j=0;j<nums.size();j++){
        nums[j]=minPriorityQueue.delHeapTop();
        cout<<nums[j]<<" ";
    }
    system("pause");
    return 0;
}

在这里插入图片描述

  • 这里还有一份左程云简化版的堆排序实现,
    时间复杂度O(N*logN),额外空间复杂度O(1)。
    给定一个数组,原地建大顶堆,同时对数组原地堆排序,

这里堆是从下标0开始的,关于堆从下标0还是下标1开始,都可以,

  • 从0开始,i结点的左孩子2*i+1,右孩子2*i+2, index结点的父亲:(index-1)/2;
  • 从1开始,i结点的左孩子2*i,右孩子2*i+1, index结点的父亲:index)/2;

堆排序的细节和复杂度分析
时间复杂度O(N*logN),额外空间复杂度O(1)
1,堆结构的heapInsert与heapify
2,堆结构的增大和减少
3,如果只是建立堆的过程,时间复杂度为O(N)
4,优先级队列结构,就是堆结构

建最大堆过程: 遍历数组,每次与自己父节点比较,大于父节点就与父节点交换,一直上浮到根节点为止

提示:
下标i节点的父节点下标(i-1)/2;
下标为i的节点的左孩子为(2i+1),右孩子为2i+2;

//函数含义:将数组arr中下标index的元素上浮到正确位置。
void heapInsert(int arr[], int index)
{
	while (arr[index] > arr[(index - 1 )/ 2]) {
		swap(arr[index], arr[(index - 1) / 2]);
		index = (index - 1) / 2;
	}
}

heapify函数将数组arr中下标为index处的元素下潜到正确位置

//函数含义:将数组arr中下标为index处的元素下潜到正确位置
void heapify(int arr[], int index, int heapSize) 
{
	int left = index * 2 + 1;
	while (left < heapSize) {
		int largest = left + 1 < heapSize && arr[left + 1] > arr[left]
			? left + 1
			: left;
		largest = arr[largest] > arr[index] ? largest : index;
		if (largest == index) {
			break;
		}
		//某个孩子比较大,那个孩子的位置是largest
		swap(arr[largest], arr[index]);
		index = largest;//下潜到较大孩子节点
		left = index * 2 + 1;//准备下次循环
	}
}

  • 对数组原地堆排序,额外空间复杂度O(1),时间复杂度O(n*logn)
  1. 先利用heapInsert原地建立大顶堆,根节点即为max
  2. 然后将根节点(max)与最后一个元素交换,同时将堆的尾部前移(因为第一次的max已经放到正确位置了),
  3. 然后比较新的根节点与左右孩子节点中的最大值,新根节点小于左右节点最大值,则交换;若交换发生,就一直下潜到最下层;(这个方法用于删除头节点也可以)

这里有个值得初学者注意的点:

一个数组int arr[]不是作为形参的话,sizeof(arr) / sizeof(*arr)就返回数组大小。

一个数组若作为形参的话,因为数组本身不能复制,形参只是一个指针,所以无法在函数里面通过上面的方式计算数组大小,所以一般得传入一个形参arrSize表示数组大小

void heapSort(int arr[],int arrSize)
{
   	if (arrSize< 2)
		return;
	for (int i = 0; i < arrSize; i++) {
		heapInsert(arr, i);//原地建立大顶堆
	}

	int heapSize = arrSize;
	swap(arr[0], arr[--heapSize]);//首次max,换到数组尾部
	while (heapSize > 0) {
		heapify(arr, 0, heapSize);//在数组arr的前heapSize个元素中,将下标0处元素下潜
		swap(arr[0], arr[--heapSize]);//下潜完毕,将头部元素换到下标--heapSize处
	}
}
  • 完整测试代码:
#include<iostream>
#include<vector>
using namespace std;

//建最大堆过程: 遍历数组,每次与自己父节点比较,大于父节点就与父节点交换,一直上浮到根节点为止
//提示:下标i节点的父节点下标(i-1)/2;
//下标为i的节点的左孩子为(2*i+1),右孩子为2*i+2;

//函数含义:将数组arr中下标index的元素上浮到正确位置。
void heapInsert(int arr[], int index)
{
	while (arr[index] > arr[(index - 1 )/ 2]) {
		swap(arr[index], arr[(index - 1) / 2]);
		index = (index - 1) / 2;
	}
}
//函数含义:将数组arr中下标为index处的元素下潜到正确位置
void heapify(int arr[], int index, int heapSize)
{
	int left = index * 2 + 1;
	while (left < heapSize) {
		int largest = left + 1 < heapSize && arr[left + 1] > arr[left]
			? left + 1
			: left;
		largest = arr[largest] > arr[index] ? largest : index;
		if (largest == index) {
			break;
		}
		//某个孩子比较大,那个孩子的位置是largest
		swap(arr[largest], arr[index]);
		index = largest;//下潜到较大孩子节点
		left = index * 2 + 1;//准备下次循环
	}
}


void heapSort(int arr[],int arrSize)
{
   	if (arrSize< 2)
		return;
	for (int i = 0; i < arrSize; i++) {
		heapInsert(arr, i);//原地建立大顶堆
	}

	int heapSize = arrSize;
	swap(arr[0], arr[--heapSize]);//首次max,换到数组尾部
	while (heapSize > 0) {
		heapify(arr, 0, heapSize);//在数组arr的前heapSize个元素中,将下标0处元素下潜
		swap(arr[0], arr[--heapSize]);//下潜完毕,将头部元素换到下标--heapSize处
	}
}

int main(){
    int nums[]={2,3,5,3,4,4,5,2,5};
    int numsSize=sizeof(nums)/sizeof(nums[0]);//数组元素个数

    cout<<" before sort,nums= ";
    for(int i=0;i<numsSize;i++){
        cout<<nums[i]<<" ";
    }
    cout<<endl<<" after heapSort,nums= ";
    heapSort(nums,numsSize);
    for(int j=0;j<numsSize;j++){
        cout<<nums[j]<<" ";
    }

    system("pause");
    return 0;
}

在这里插入图片描述

标准库的make_heap(), pop_heap(), push_heap()

头文件

#include <algorithm> 
  • make_heap()是生成一个堆,大顶堆或小顶堆
//默认生成大顶堆
template <class RandomAccessIterator>
  void make_heap (RandomAccessIterator first, RandomAccessIterator last);
//comp是greater时生成小顶堆,less时生成大顶堆
template <class RandomAccessIterator, class Compare>
  void make_heap (RandomAccessIterator first, RandomAccessIterator last,
                  Compare comp );
  • push_heap()是向堆中插入一个元素,并且使堆的规则依然成立
//默认为大顶堆
template <class RandomAccessIterator>
  void push_heap (RandomAccessIterator first, RandomAccessIterator last);
//comp是greater时为小顶堆,less时为大顶堆
template <class RandomAccessIterator, class Compare>
  void push_heap (RandomAccessIterator first, RandomAccessIterator last,
                   Compare comp);

调用push_heap之前必须调用make_heap创建一个堆,首先调用push_back向容器中尾插入元素,然后再调用push_heap,它会使最后一个元素插到合适位置

注意,push_heap中的comp和make_heap中的comp参数必须是一致的,不然会导致插入堆失败,最后一个元素还是在最后位置。

  • pop_heap()是在堆的基础上,弹出堆顶元素
//默认为大顶堆
template <class RandomAccessIterator>
  void pop_heap (RandomAccessIterator first, RandomAccessIterator last);
//comp是greater时为小顶堆,less时为大顶堆
template <class RandomAccessIterator, class Compare>
  void pop_heap (RandomAccessIterator first, RandomAccessIterator last,
                 Compare comp);

这里需要注意的是,pop_heap()并没有删除元素,而是将堆顶元素和容器最后一个元素进行了替换,如果要删除这个元素,还需要对容器进行pop_back()操作

示例

# include <iostream>
# include <functional>
# include <vector>
# include <algorithm>

using namespace std;

void printVec(vector<int> nums)
{
    for (int i = 0; i < nums.size(); ++i)
        cout << nums[i] << " ";
    cout << endl;
}
int main(void)
{
    int nums_temp[] = {8, 3, 4, 8, 9, 2, 3, 4, 10};
    vector<int> nums(nums_temp, nums_temp + 9);
    cout << "make_heap之前: ";
    printVec(nums);

    cout << "(默认(less))make_heap: ";
    make_heap(nums.begin(), nums.end());
    printVec(nums);

    cout << "(less)make_heap: ";
    make_heap(nums.begin(), nums.end(), less<int> ());
    printVec(nums);

    cout << "(greater)make_heap: ";
    make_heap(nums.begin(), nums.end(), greater<int> ());
    printVec(nums);

    //先向容器插入一个数,之后在调用push_heap调整容器
    cout << "此时,nums为小顶堆 greater" << endl;
    cout << "push_back(3)" << endl;
    nums.push_back(3);
    
    cout << "默认(less)push_heap 此时push_heap失败: ";
    push_heap(nums.begin(), nums.end());
    printVec(nums);
    
    cout << "push_heap为greater 和make_heap一致,此时push_heap成功: ";
    push_heap(nums.begin(), nums.end(), greater<int>());
    printVec(nums);
    
    //先调用pop_heap将堆顶元素交换到容器末尾,然后调用容器的pop_back才能删除元素
    cout << "(greater)pop_heap: ";
    pop_heap(nums.begin(), nums.end(),greater<int>());
    printVec(nums);
    
    cout << "pop_back(): ";
    nums.pop_back();
    printVec(nums);

    system("pause");
    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值