堆:
堆数据结构是一种数组对象,它可以被视为一颗完全二叉树结构,何为完全二叉树,简单来讲就是一棵二叉树除了从左到右除了最后一层的节点不需要是满,其余层的都必须是满的,而且最后一层的节点必须是连续的,中间不可以有空,具体的请看我的二叉树文章(https://blog.csdn.net/angry_youth/article/details/76037302),有详细介绍;
通常情况下堆一般用数组存储,如图:
堆结构的二叉存储:
**最大堆:**所有的父节点都比左右孩子大;
**最小堆:**所有的父节点都比左右孩子小;
注意:这里需要和二叉搜索树有所区分,二叉搜索树任意父节点的值大于左子树的任意节点,父节点的值小于右子树的任意节点,所以二叉搜索树的中序遍历是一个从小到大的排序,但是堆不可以,堆的排序是发生在不同层级的节点中的,从根节点到叶子节点的每一条路径是有序的。
堆的优缺点:
缺点:
堆不支持遍历去排序;
堆在查找节点上比较困难;
优点:
堆在移除最大(最小)值非常方便;
堆在插入上也很方便;
接下里主要针对堆的几个操作进行讲解:
移除:删除最大(最小)值,就是将堆的根节点移除掉,那么需要填补一个新的根节点,我们使用最后一个叶子节点移动到根节点的位置,然后向下调整,一直向下筛选移动,直到把它放在一个比它大(小)节点后面,以及一个比它小(大)的节点前面,形成一个新的最大(小)堆。
如图:
**向下调整:**需要关心父节点比当前子节点小的问题;(使用于任何情况,可以用来创建大堆、小堆,或者插入结点后调整)
插入一个节点,我们采用向上调整的方式来进行,首先将这个节点插入到尾部,然后开始向上移动筛选,直到把它放在一个比它大(小)节点后面,以及一个比它小(大)的节点前面,形成一个新的最大(小)堆。
如图:
**向上调整:**不用关心父节点比当前子节点小的问题;(适用于已经把堆建好,插入新节点时调整使用)
代码示例:
//堆结构其实就是一个数组结构
#include<vector>
//创建仿函数来提高代码的复用性
//仿函数其实就是一个类,然而这个类只有一个运算符重载函数
template <class T>
struct Less
{
bool operator()(const T& left, const T& right) const
{
return left < right;
}
};
template <class T>
struct Great
{
bool operator()(const T& left, const T& right)const
{
return left > right;
}
};
template <class T,class Compare = Great<T>>
class Heap
{
protected:
vector<T> _arr;
public:
Heap(T* arr,const size_t n)
{
_arr.reserve(n);//先开空间再建堆
_CreatHeap(arr,n);
}
//建堆的过程------建大堆
void _CreatHeap(T* arr,const size_t n)
{
for(size_t i = 0; i < n; i++)
{
_arr.push_back(arr[i]);
}
//调整建堆---向下调整,找到最后一个叶子节点的父亲,然后从他开始,一个一个父节点开始向下调整
for(int i = (n-2)/2; i >= 0 ; --i)
{
AdjustDown(i);
}
}
//向上调整-----不需要关心父节点比子节点小的问题---建大堆
void AdjustUp(int x)//每次传进来的下标是孩子的下标
{
Compare compare;
assert(x < _arr.size());
int parent = (x-1)/2;
while(parent >= 0)
{
if(compare(_arr[x],_arr[parent]))
{
swap(_arr[parent],_arr[x]);
x = parent;
parent = (x-1)/2;
}
else
break;
}
}
//向下调整-----需要关心当前父节点比子节点小的问题---建大堆
void AdjustDown(int x)
{
Compare compare;
int parent = x;
int leftchild = 2*x+1;//求出左孩子
while(leftchild < _arr.size())
{
size_t rightchild = leftchild + 1;//求出右孩子
if(rightchild < _arr.size() && compare(_arr[rightchild],_arr[leftchild]))
{
leftchild = rightchild;
}
if(compare(_arr[leftchild],_arr[parent]))
{
swap(_arr[leftchild],_arr[parent]);
parent = leftchild;
leftchild = 2*leftchild+1;//求出左孩子
}
else
break;
}
}
void showHeap()
{
cout<<"输出堆结果:"<<endl;
size_t i = 0;
while(i < _arr.size())
{
cout<<_arr[i]<<" ";
++i;
}
cout<<endl;
}
void Push(const T& data)//尾插入结点
{
_arr.push_back(data);
AdjustUp(_arr.size()-1);//传入孩子的下标
}
void Pop()//取出根节点,取出最大/最小值
{
//对于vector来讲pop()是取出尾结点
swap(_arr[0],_arr[_arr.size()-1]);
_arr.pop_back();
AdjustDown(0);//重新从根开始向下调整
}
//判断是不是一个大堆----大堆的每个父节点都比子节点要大
//判断是不是一个小堆----小堆的每个父节点都比子节点要小
bool IsHeap()
{
Compare compare;
for(size_t i = 0; i < (_arr.size()-2)/2; i++)
{
if(compare(_arr[2*i+1],_arr[i]) ||
(2*i+2 < _arr.size() && compare(_arr[2*i+2],_arr[i])))
{
return false;
}
}
return true;
}
//递归法判断是不是一个大小堆----分割成子问题
bool IsHeapR(int root)
{
Compare compare;
if(root >= _arr.size())
{
return true;
}
int left = root*2+1;
int right = root*2+2;
if(left < _arr.size())
{
if(right < _arr.size())
{
if(compare(_arr[right],_arr[root]))
{
return false;
}
}
if(compare(_arr[left],_arr[root]))
{
return false;
}
}
return IsHeapR(left) && IsHeapR(right);
}
};
用堆实现的优先级队列:
队列中主要的特点是:先进先出---------pushBak()和popFront()
但是优先级队列靠的是堆来实现--------push()和pop()
堆的push()和pop()是建立在vector的基础上的(vector是pushBack()和popBack()),然后通过堆的转化模拟实现变成了pushBack()和popFront();
队列是用链表实现的,二者名字相近但不可混淆;
//优先级队列-----堆实现----不是队列的适配器
//队列的适配器是stack
template <class T,class Compare=Great<T>>
class PriorityQueue
{
protected:
Heap<T,Compare> _pq;
public:
void push(const T& data)//O(lgN)
{
_pq.Push(data);
}
void pop()//O(lgN)
{
_pq.Pop();
}
T& top()
{
return _pq.Top();
}
};
TopK问题:给定一组海量的数据,在里面找到最大的前k个,首先用前k个数建立一个小堆,然后从第k+1个数开始遍历,分别和小堆的对顶比较,如果比它大就替换掉它,知道遍历结束,这个小堆留下来的数就是我们最大的前k个数,代码如下:
//面试题:N个数中找出最大的前K个?------建小堆----时间复杂度NlgK
void AdjustDown(int* arr, size_t n, int x)
{
assert(arr);
int left = 2*x+1;
while(left < n)
{
if(left+1 < n && arr[left+1] < arr[left])
{
++left;
}
if(arr[left] < arr[x])
{
swap(arr[left],arr[x]);
x = left;
left = 2*x+1;
}
else
break;
}
}
void show(int* arr,int n)
{
for(int i = 0; i < n; i++)
{
cout<<arr[i]<<endl;
}
}
const int N = 10000;
const int K = 10;
void TopK()
{
int arr[N] = {0};
int heap[K] = {0};
for(size_t i = 0; i < N; i++)
{
arr[i] = rand()%N;
}
for(size_t i = 0; i < K; i++)
{
heap[i] = arr[i];
}
//建小堆
for(int i = (K-2)/2; i >= 0; --i)
{
AdjustDown(heap,K,i);
}
for(size_t i = K; i < N; i++)
{
if(arr[i] > heap[0])
{
heap[0] = arr[i];
AdjustDown(heap,K,0);
}
}
show(heap,K);
}
void TestHeapK()
{
TopK();
}