优先级队列,也叫二叉堆、堆(不要和内存中的堆区搞混了,不是一个东西,一个是内存区域,一个是数据结构)。算法
堆的本质上是一种彻底二叉树,分为:api
最小堆(小根堆):树中每一个非叶子结点都不大于其左右孩子结点的值,也就是根节点最小的堆,图(a)。数组
最大堆(大根堆):树中每一个非叶子结点都不小于其左右孩子结点的值,也就是根节点最大的堆,图(b)。数据结构
均以大根堆为例spa
堆本质上是一颗彻底二叉树,使用数组进行存储,从\(a[1]\)开始存储,这样对于下标为\(k\)的结点\(a[k]\)来讲,其左孩子的下标为\(2*k\),右孩子的下标为\(2*k+1\)。,且不论 \(k\) 是奇数仍是偶数,其父亲结点(若是有的话)就是 $\left \lfloor k/2 \right \rfloor $。code
假如咱们向一个堆中插入一个元素,要使其仍然保持堆的结构。应该怎么办呢?blog
能够把想要添加的元素放在数组的最后,也就是彻底二叉树的最后一个结点的后面,而后进行向上调整(heapinsert)。向上调整老是把欲调整结点与父亲结点比较,若是权值比父亲结点大,那么就交换其与父亲结点,反复比较,直到到达堆顶或父亲结点的值较大为止。向上调整示意图以下:排序
代码以下,时间复杂度为\(O(logn)\):队列
void heapinsert(int* arr, int n) { int k = n; //若是 K 结点有父节点,且比父节点的权值大 while (k > 1 && arr[k] > arr[k / 2]) { //交换 K 与父节点的值 swap(arr[k / 2], arr[k]); k >>= 1; } }
这样添加元素就很简单了内存
void insert(int* arr, int n, int x) { arr[++n] = x;//将x置于数组末尾 heapinsert(arr, n);//向上调整x }
假如咱们要删除一个堆中的堆顶元素,要使其仍然保持堆的结构。应该怎么办呢?
移除堆顶元素后,将最后一个元素移动到堆顶,而后对这个元素进行向下调整(heapify),向下调整老是把欲调整结点 \(K\) 与其左右孩子结点比较,若是孩子中存在权值比当前结点 \(K\) 大的,那么就将其中权值最大的那个孩子结点与结点 \(K\),反复比较,直到到结点 \(K\) 为叶子结点或结点 \(K\) 的值比孩子结点都大为止。向下调整示意图以下:
代码以下,时间复杂度也是\(O(logn)\):
void heapify(int* arr, int k, int n) { //若是结点 K 存在左孩子 while (k * 2 <= n) { int left = k * 2; //若是存在右孩子,而且右孩子的权值大于左孩子 if (left + 1 <= n && arr[left] < arr[left + 1]) left++; //就选中右孩子 //若是节点 K 的权值已经大于左右孩子中较大的节点 if (arr[k] > arr[left]) break; swap(arr[left], arr[k]); k = left; } }
这样删除堆顶元素也就变得很简单了
void deleteTop(int* arr, int n) { arr[1] = arr[n--];//用最后一个元素覆盖第一个元素,并让n-1 heapify(arr, 1, n); }
自顶向下建堆的思想是,从第 \(i=1\) 个元素开始,对其进行向上调整,始终使前 \(i\) 个元素保持堆的结构。时间复杂度 \(O(nlogn)\)
void ArrayToHeap(int *a,int n) { for (int i = 1; i <= n; i++) { heapinsert(a, i); } }
自底向上建堆的思想是,从底 $i=\left \lfloor n/2 \right \rfloor $ 个元素开始,对其进行向下调整,始终让后 \(n-i\) 个元素保持堆的结构。
void ArrayToBheap(int *a, int n) { int i = n / 2; for (; i >= 1; i--) { heapify(a, i, n); } }
若是仅从代码上直观观察,会得出构造二叉堆的时间复杂度为\(O(nlogn)\)的结果。固然这个上界是正确的,但却不是渐近界,能够观察到,不一样结点在运行 heapify
的时间与该结点的树高(树高是指该结点到最底层叶子结点的值,不要和深度搞混了)相关,并且大部分结点的高度都很小。利用如下性质能够获得一个更准确的渐近界:
【能够画颗树试一下,具体证实请看算法导论】
在一个高度为 \(h\) 的结点上运行 heapify
的代价为 \(O(h)\),咱们能够将自顶向下建堆的总复杂度表示为
\[ \sum ^{h}_{k=0} \lceil \frac {n}{2^{k+1}} \rceil O(h)= O(n\sum ^{h} _{k=0}\frac {k}{2^{k}}) \]
这个式子
\[ \sum ^{h} _{k=0}\frac {k}{2^{k}} \]
其实就是求前 \(n\) 项和,高中数学的知识
\[ T(k)=\frac{1}{2}+\frac{2}{2^2}+\frac{3}{2^3}+\cdots+\frac{k}{2^k}\\\frac{1}{2}T(k)=\frac{1}{2^2}+\frac{2}{2^3}+\frac{3}{2^4}+\cdots+\frac{k-1}{2^k}+\frac{k}{2^k+1}\\T(k)-\frac{1}{2}T(k)=\frac{1}{2}+\frac{1}{2^2}+\frac{1}{2^3}+\cdots+\frac{1}{2^k}-\frac{k}{2^{k+1}}\\\frac{1}{2}T(k)=\frac{\frac{1}{2}(1-(\frac{1}{2})^k)}{1-\frac{1}{2}}-\frac{k}{2^{k+1}}\\T(k)=2-\frac{1}{2^{k-1}}-\frac{k}{2^{k}} \]
到这儿就须要求极限,高等数学的知识 \(\frac{1}{2^{k-1}}\) 当 \(k\) 趋于无穷大时极限是 \(0\),对 \(\frac{k}{2^{k}}\) 用洛必达法则极限也是 \(0\)
也就是说当 \(h\) 趋向于无穷大时,\(O(n\sum ^{h} _{k=0}\frac {k}{2^{k}})=O(n\cdot 2)\) ,去掉常数项,因此自底向上建堆复杂度为 \(O(n)\)
堆排序的思想:假设一个大根堆有 \(n\) 个元素,每次把第 \(1\) 个元素,与第 \(n\) 个元素交换,对第一个元素进行向下调整(heapify),并使得 \(n=n-1\) ,直到 \(n=1\)
void heapSort(int* arr, int n) { //先自底向上建堆 int i = n / 2; for (; i >= 1; i--) { heapify(arr, i, n); } for (int i = 50; i > 1; i--) { swap(arr[1], arr[i]); heapify(arr, 1, i - 1); } }
首先用数组的前k个元素构建一个小根堆,而后遍历剩余数组和堆顶比较,若是当前元素大于堆顶,则把当前元素放在堆顶位置,并调整堆(heapify)。遍历结束后,堆顶就是数组的最大k个元素中的最小值,也就是第k大元素。
void heapify(int* a, int index, int length) { int left = index * 2 + 1; while (left <= length) { if (left + 1 <= length - 1 && a[left + 1] > a[left])left++; if (a[index] > a[left])break; swap(a[index], a[left]); index = left; } } void ArrayToBheap(int* a, int length) { int i = length / 2 - 1; for (; i >= 0; i--) { heapify(a, i, length); } } void FindKMax(int* a, int k, int length) { ArrayToBheap(a, k); for (int i = k; i < length; i++) { if (a[i] > a[0]) a[0] = a[i]; heapify(a, 0, k); } }
时间复杂度\(O(n)\),只是举个例子。
事实上对于这个问题是有更快的作法的,快速排序的思想,时间复杂度 \(O(logn)\)
int Search_K(int left, int right, int k) { int i = left, j = right; int p = rand() % (right - left + 1) + left; int sign = a[p]; swap(a[p], a[i]); while (i < j) { while (i < j && a[j] >= sign)j--; while (i < j && a[i] <= sign)i++; swap(a[i], a[j]); } swap(a[i], a[left]); if (i - left + 1 == k)return a[i]; if (i - left + 1 < k)return Search_K(i + 1, right, k - (i - left + 1)); else return Search_K(left, i - 1, k); }
堆更多时候,由于它建堆\(O(n)\),调整\(O(logn)\),当须要有序获得某些数据,是要优于排序(\(O(nlogn)\))算法的,并且若是数据规模是动态增长的,那堆就要彻底优于排序算法了,在C++的STL是有堆的实现的,叫作 priority_queue
。