堆数据结构是一种数组对象,它可以被视为一颗完全二叉树结构
最大堆:每个父节点都大于孩子结点
最小堆:每个父节点都小于孩子结点
堆的优势:效率高 增加删除的时间复杂度均为O( lg N),不用堆vector插入删除总有一个为O(1),一个为O(N)
堆的基本实现:
我们先来建个
最大堆,首先堆的成员是个数组,而且这个数组不能是固定的大小,因为我们还要实现堆的插入删除,因此我们可以用库中的vector来创建数组。
最大堆:
#include<iostream>
#include<vector>
#include<assert.h>
using namespace std;
template <class T>
class Heap
{
public:
Heap()//无参构造,缺省构造 并非什么都不做,初始化列表写不写都会初始化,整形初始化为随机值,一般为0. 此处去掉vector的缺省构造函数
{};
Heap(T* a, size_t n)
{
_a.reserve(n);//开空间 用_resize()也可以就要配合赋值来用,因为_resize是已经初始化了的
for (size_t i = 0; i < n; ++i)
{
_a.push_back(a[i]);
}
//建堆 找最后一个非叶子结点,依次向下调整
for (int i = (_a.size() - 2) / 2; i >= 0; i--)//如何找最后一个非叶子结点 即(最后一个结点下标-1)/2,即(_a.size()-2)/2
{ //注意此处不能用size_t会导致死循环
Adjustdown(i);
}
}
void Adjustdown(int root)
{
int parent = root;
int child = parent * 2 + 1;//默认左孩子
while (child<_a.size())//结束条件有两个 1.左孩子不存在(当然右孩子不存在)完全二叉树2.父亲大于大的那个孩子
{
if (child + 1 < _a.size() && _a[child + 1] > _a[child])
{
child = child + 1;
}
//此时默认child指向大的那个孩子 这里不关心左孩子大还是右孩子大,只关心child指向的是最大的孩子
if (_a[child] > _a[parent])//如果孩子大于父亲则交换,否则跳出循环
{
swap(_a[child], _a[parent]);
parent = child;
child = 2 * parent - 1;
}
else//父亲大于大的那个孩子
{
break;
}
}
}
void Adjustup(int Child)//向上调整
{
int child = Child;
int parent = (child - 1) / 2;
while (parent>=0)//或者child>0都可以
{
if (_a[child] > _a[parent])
{
swap(_a[child],_a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
bool IsHeap()//递归判断大堆
{
int _root = 0;
return _IsHeap(_root);
}
bool _IsHeap(int root)
{
if (root >= _a.size())//不存在 是大堆
return true;//不存在是大堆 否则叶子结点怎样判断都不是堆
int left = root * 2 + 1;
int right = root * 2 + 2;
if (left < _a.size())
{
if (_a[left]>_a[root])
{
return false;
}
if (right < _a.size())
{
if (_a[right]>_a[root])
{
return false;
}
}
}
return _IsHeap(left) && _IsHeap(right);
}
bool IsHeapNor()//非递归判断是否为大堆
{
for (size_t i = 0; i < (_a.size() - 2) / 2; i++)
{
if (_a[i] < _a[i * 2 + 1] || _a[i] < _a[i * 2 + 2])
{
return false;
}
}
return true;
}
void Push(const T& x)//向上调整,会影响本结点到根节点路径上上的所有结点,不会影响其他
{
_a.push_back(x);
Adjustup(_a.size() - 1);//注意此处一定要减1,否则报错
}
void Pop()
{
swap(_a[_a.size() - 1], _a[0]);
_a.pop_back();
Adjustdown(_a[0]);
}
size_t Size()
{
return _a.size();
}
bool Empty()
{
return _a.pop();
}
T& Top()
{
assert(!_a.empty());
return _a[0];
}
private:
vector<int> _a;
};
void test()
{
int a[] = { 10, 11, 13, 12, 16, 18, 15, 17, 14, 19 };
Heap<int>hp(a, sizeof(a) / sizeof(a[0]));
cout << hp.IsHeap() << endl;
}
int main()
{
test();
system("pause");
return 0;
}
建堆时间复杂度:O(NlgN)
Push 时间复杂度:O(lgN) Pop时间复杂度:O(lg N)
用仿函数建立大堆或者小堆 (实现复用)
#include<iostream>
#include<vector>
#include<assert.h>
using namespace std;
template <class T>
struct Less
{
bool operator() (T&left, T&right)
{
return left < right;
}
};
template <class T>
struct Greater
{
bool operator() (T&left, T&right)
{
return left >right;
}
};
template <class T,class Compare=Greater<T>>
class Heap
{
public:
Heap()//无参构造,缺省构造 并非什么都不做,初始化列表写不写都会初始化,整形初始化为随机值,一般为0. 此处去掉vector的缺省构造函数
{};
Heap(T* a, size_t n)
{
_a.reserve(n);//开空间 用_resize()也可以就要配合赋值来用,因为_resize是已经初始化了的
for (size_t i = 0; i < n; ++i)
{
_a.push_back(a[i]);
}
//建堆 找最后一个非叶子结点,依次向下调整
for (int i = (_a.size() - 2) / 2; i >= 0; i--)//如何找最后一个非叶子结点 即(最后一个结点下标-1)/2,即(_a.size()-2)/2
{ //注意此处不能用size_t会导致死循环
Adjustdown(i);
}
}
void Adjustdown(int root)
{
Compare com;
int parent = root;
int child = parent * 2 + 1;//默认左孩子
while (child<_a.size())//结束条件有两个 1.左孩子不存在(当然右孩子不存在)完全二叉树2.父亲大于大的那个孩子
{
if (child + 1 < _a.size() && com(_a[child + 1], _a[child]))
{
child = child + 1;
}
//此时默认child指向大的那个孩子 这里不关心左孩子大还是右孩子大,只关心child指向的是最大的孩子
//if (_a[child] > _a[parent])//如果孩子大于父亲则交换,否则跳出循环
if (com(_a[child], _a[parent]))
{
swap(_a[child], _a[parent]);
parent = child;
child = 2 * parent - 1;
}
else//父亲大于大的那个孩子
{
break;
}
}
}
void Adjustup(int Child)//向上调整
{
Compare com;
int child = Child;
int parent = (child - 1) / 2;
while (parent>=0)//或者child>0都可以
{
//if (_a[child] > _a[parent])
if (com(_a[child], _a[parent]))
{
swap(_a[child],_a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
bool IsHeap()//递归判断大堆
{
int _root = 0;
return _IsHeap(_root);
}
bool _IsHeap(int root)
{
Compare com;
if (root >= _a.size())//不存在 是大堆
return true;//不存在是大堆 否则叶子结点怎样判断都不是堆
int left = root * 2 + 1;
int right = root * 2 + 2;
if (left < _a.size())
{
//if (_a[left]>_a[root])
if (com(_a[left], _a[root]))
{
return false;
}
if (right<_a.size())
{
//if (_a[right]>_a[root])
if (com(_a[right],_a[root]))
{
return false;
}
}
}
return _IsHeap(left) && _IsHeap(right);
}
bool IsHeapNor()//非递归判断是否为大堆
{
Compare com;
for (size_t i = 0; i < (_a.size() - 2) / 2; i++)
{
//if (_a[i] < _a[i * 2 + 1] || _a[i] < _a[i * 2 + 2])
if (com(_a[i * 2 + 1], _a[i]) || com(_a[i * 2 + 2], _a[i]))
{
return false;
}
}
return true;
}
void Push(const T& x)//向上调整,会影响本结点到根节点路径上上的所有结点,不会影响其他
{
_a.push_back(x);
Adjustup(_a.size() - 1);//注意此处一定要减1,否则报错
}
void Pop()
{
swap(_a[_a.size() - 1], _a[0]);
_a.pop_back();
Adjustdown(_a[0]);
}
size_t Size()
{
return _a.size();
}
bool Empty()
{
return _a.pop();
}
T& Top()
{
assert(!_a.empty());
return _a[0];
}
private:
vector<int> _a;
};
void test()
{
int a[] = { 10, 11, 13, 12, 16, 18, 15, 17, 14, 19 };
/*Heap<int>hp(a, sizeof(a) / sizeof(a[0]));
cout << hp.IsHeap() << endl;*/
Heap<int, Less<int>>hp(a, sizeof(a) / sizeof(a[0]));
cout << hp.IsHeap() << endl;
}
int main()
{
test();
system("pause");
return 0;
}
堆的应用:
1.TopK问题 N个数,找出最大的前K个。
思想:
如果我们的N非常大,内存是放不下的
建立一个K大小的堆,如果建立最大堆,那么只能找到最大的一个,我们要建立小堆
方法:
用前N个数建立一个小堆,从第N+1个开始,如果比堆顶的数据大,替代堆顶数据(因为堆顶是K个元素中最小的),并且向下
调整,最后堆里面就是最大的前K个元素。
这种问题就是要用堆来解决,有的人肯定会说排序就可以解决,
我们来看下堆处理和排序处理的优缺点:
目前最快的排序:时间复杂度 O(N*lgN) 空间复杂度O(N)
堆: 时间复杂度 建堆K*lgK +(N-K)*lgK=O(N*lg K) 空间复杂度 O(1)
其实仅仅看时间复杂度排序和堆是差不多的,但是空间复杂度就相差很多了。试想,如果有10亿个元素,用排序内存根本放不下
具体实现代码如下:
#include<iostream>
#include<assert.h>
using namespace std;
const size_t M = 1000;
const size_t K = 5;
void Adjustdown(int* Heap, int n, int pos)//n代表堆的大小,pos代表位置,表示调整哪个
{
assert(Heap);
int parent = pos;
int child = parent * 2 + 1;
while (child < n)
{
if ((child + 1 < n) && (Heap[child + 1] < Heap[child]))
{
child = child + 1;
}
//此处child表示最大的孩子
if (Heap[child] < Heap[parent])
{
swap(Heap[child], Heap[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void TopK()
{
int arr[M] = { 0 };
for (int i = 0; i < M; i++)
{
arr[i] = rand() % M;//这些数都小于M
}
//待会打印的前K个元素,就应该是下面这5个。
arr[0] = 1111;
arr[99] = 1911;
arr[987] = 1023;
arr[100] = 9999;
arr[235] = 5678;
//建一个K个元素的堆
int Heap[K] = { 0 };
for (size_t i = 0; i < K; i++)//用前K个元素建堆
{
Heap[i] = arr[i];
}
//建堆
for (int i = (K-2)/2; i >=0; i--)//用int 否则死循环
{
Adjustdown(Heap, (K - 2) / 2,i);
}
for (size_t i = K; i < M; i++)//用K+1个元素之后的元素和堆顶比较,如果大于堆顶,交换然后调整
{
if (arr[i]>Heap[0])
{
Heap[0] = arr[i];
Adjustdown(Heap, K, 0);
}
}
for (size_t i = 0; i < K; i++)
{
cout << Heap[i] << " ";
}
}
int main()
{
TopK();
system("pause");
return 0;
}
2.堆排序(升序)
升序:建立最大堆,然后最大的和最后一个结点进行交换,然后size--,然后再用次大的和/size-1交换,依次进行
升序:建立最小堆,将最小的和0位置结点进行交换,但是根节点变了,又要重新建堆,之前建的堆用不上,因此只能排好一个 元素,所以升序就要建大堆。
void Adjustdown(int* Heap, int n, int pos)//n代表堆的大小,pos代表位置,表示调整哪个
{
assert(Heap);
int parent = pos;
int child = parent * 2 + 1;
while (child < n)
{
if ((child + 1 < n) && (Heap[child + 1] > Heap[child]))
{
child = child + 1;
}
//此处child表示最大的孩子
if (Heap[child]>Heap[parent])//建立大堆
{
swap(Heap[child], Heap[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int *a, size_t n)
{
//建堆,升序建大堆
for (int i = (n - 2) / 2; i >= 0; i--)
{
Adjustdown(a, n, i);
}
int end = n - 1;
while (end >= 0)
{
swap(a[0], a[end]);
Adjustdown(a, end, 0);
end=end-1;
}
}
int main()
{
int a[] = { 10, 11, 13, 12, 16, 18, 15, 17, 14, 19 };
HeapSort(a, sizeof(a) / sizeof(a[0]));
for (int i = 0; i <sizeof(a) / sizeof(a[0]); i++)
{
cout << a[i] << " ";
}
system("pause");
return 0;
}
堆排序时间复杂度:建堆NlgN +调整NlgN =O(lgN).
3.优先级队列
(谁的优先级高,谁先出)用堆实现
template<class T,class Compare=Greater<int>>//注意栈是用vector适配的,优先级队列是用堆算法完成 不是适配器 库里面没有叫堆的容器,但是有叫堆的算法
class PriorityQueue
{
void Push(const T& x)
{
_hp.Push(x);
}
void Pop()
{
_hp.Pop();
}
T& Top()
{
return _hp.Top();
}
private:
Heap<T, Compare> _hp;
};
template<class T, class Container>//这里container默认vector但是给list也可以适配 这时适配器适配的
class Stack
{};
要区分清楚,优先级队列不是用适配器适配的,是用堆实现的写死了,不能适配