堆数据结构是一种数组对象,它可以被视为一颗完全二叉树结构
最大堆:每个父节点都大于孩子结点
最小堆:每个父节点都小于孩子结点
堆的优势:效率高 增加删除的时间复杂度均为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 {};要区分清楚,优先级队列不是用适配器适配的,是用堆实现的写死了,不能适配