二叉堆
二叉堆是一棵完全二叉树,树上的每一个节点对应数组的每个元素。除了最底层外,该树是完全充满的,而且是从左向右填充。
满足两个性质:
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(N∗logN)
额外空间复杂度: 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)
- 先利用heapInsert原地建立大顶堆,根节点即为max
- 然后将根节点(max)与最后一个元素交换,同时将堆的尾部前移(因为第一次的max已经放到正确位置了),
- 然后比较新的根节点与左右孩子节点中的最大值,新根节点小于左右节点最大值,则交换;若交换发生,就一直下潜到最下层;(这个方法用于删除头节点也可以)
这里有个值得初学者注意的点:
一个数组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;
}