优先队列html
许多应用程序都须要处理有序的元素,但不必定要求它们所有有序,或是不必定要一次就将它们排序。不少状况下是收集一些元素,处理当前键值最大的元素,而后再收集更多的元素,再处理当前键值最大的元素。这种状况下,须要的数据结构支持两种操做:删除最大的元素和插入元素。这种数据结构类型叫优先队列。算法
这里,优先队列基于二叉堆数据结构实现,用数组保存元素并按照必定条件排序,以实现对数级别的删除和插入操做。数组
1.API缓存
优先队列是一种抽象数据类型,它表示了一组值和对这些值的操做,抽象层使应用和实现隔离开来。数据结构
2.初级实现spa
1.无序数组实现3d
优先队列的 insert 方法和下压栈的 push 方法同样。删除最大元素时,遍历数组找出最大元素,和边界元素交换。指针
2.有序数组实现code
插入元素时,将较大的元素向右移一格(和插入排序同样)。这样删除时,就能够直接 pop。htm
使用连接也是同样的逻辑。
这些实现总有一种操做须要线性级别的时间复杂度。使用二叉堆能够保证操做在对数级别的时间完成。
3.堆的定义
数据结构二叉堆能够很好地实现优先队列地基本操做。在二叉堆数组中,每一个元素都要保证大于等于另两个特定位置地元素。一样,这两个位置地元素又至少要大于等于数组中另外两个元素,以此类推。用二叉树表示:
当一棵二叉树的每一个结点都大于等于它的两个子节点时,它被成为堆有序。从任意结点向上,都能获得一列非递减的元素;从任意结点向下,都能获得一列非递增的元素。根结点是堆有序的二叉树中最大的结点。
二叉堆表示法
这里使用彻底二叉树表示:将二叉树的结点按照层级顺序(从上到下,从左往右)放入数组中,不使用数组的第一个位置(为了方便计算),根结点在位置 1 ,它的子结点在位置 2 和 3,子结点的子结点分别在位置 4,5,6,7,一次类推。
在一个二叉堆中,位置 k 的结点的父节点位置在 k/2,而它的两个子结点在 2k 和 2k + 1。能够经过计算数组的索引而不是指针就能够在树中上下移动。
一棵大小为 N 的彻底二叉树的高度为 lgN。
4.堆的算法
用长度为 N+1 的私有数组 pq[ ] 表示一个大小为 N 的堆。
堆在进行插入或删除操做时,会打破堆的状态,须要遍历堆并按照要求将堆的状态恢复。这个过程称为 堆的有序化。
堆的有序化分为两种状况:当某个结点的优先级上升(或在堆底加入一个新的元素)时,须要由下至上恢复堆的顺序;当某个结点的优先级降低(例如将根节点替换为一个较小的元素),须要由上至下恢复堆的顺序。
上浮(由下至上的堆的有序化)
当某个结点比它的父结点更大时,交换它和它的父节点,这个结点交换到它父节点的位置。但有可能比它如今的父节点大,须要继续上浮,直到遇到比它大的父节点。(这里不须要比较这个子结点和同级的另外一个子结点,由于另外一个子结点比它们的父结点小)
//上浮 private void Swim(int n) { while (n > 1 && Less(n / 2, n)) { Exch(n/2,n); n = n / 2; } }
下沉(由上至下的堆的有序化)
当某个结点 k 变得比它的两个子结点(2k 和 2k+1)更小时,能够经过将它和它的两个子结点较大者交换来恢复堆有序。交换后在子结点处可能继续打破堆有序,须要继续重复下沉,直到它的子结点都比它小或到达底部。
//下沉 private void Sink(int k) { while (2 * k <= N) { int j = 2 * k; //取最大的子节点 if (j < N && Less(j, j + 1)) j++; //若是父节点不小子节点,退出循环 if (!Less(k,j)) break; //不然交换,继续下沉 Exch(j,k); k = j; } }
知道了上浮和下沉的逻辑,就能够很好理解在二叉堆中插入和删除元素的逻辑。
插入元素:将新元素加到数组末尾,增长堆的大小并让这个新元素上浮到合适的位置。
删除最大元素:从数组顶端(即 pq[1])删除最大元素,并将数组最后一个元素放到顶端,减小数组大小并让这个元素下沉到合适位置。
public class MaxPriorityQueue { private IComparable[] pq; public int N; public MaxPriorityQueue(int maxN) { pq = new IComparable[maxN+1]; } public bool IsEmpty() { return N == 0; } public void Insert(IComparable value) { pq[++N] = value; Swim(N); } public IComparable DeleteMax() { IComparable max = pq[1]; Exch(1,N--); pq[N + 1] = null; Sink(1); return max; } //下沉 private void Sink(int k) { while (2 * k <= N) { int j = 2 * k; //取最大的子节点 if (j < N && Less(j, j + 1)) j++; //若是父节点不小子节点,退出循环 if (!Less(k,j)) break; //不然交换,继续下沉 Exch(j,k); k = j; } } //上浮 private void Swim(int n) { while (n > 1 && Less(n / 2, n)) { Exch(n/2,n); n = n / 2; } } private void Exch(int i, int j) { IComparable temp = pq[i]; pq[i] = pq[j]; pq[j] = temp; } private bool Less(int i, int j) { return pq[i].CompareTo(pq[j]) < 0; } }
上述算法对优先队列的实现可以保证插入和删除最大元素这两个操做的用时和队列的大小成对数关系。这里省略了动态调整数组大小的代码,能够参考下压栈。
对于一个含有 N 个元素的基于堆的优先队列,插入元素操做只须要不超过(lgN + 1)次比较,由于 N 可能不是 2 的幂。删除最大元素的操做须要不超过 2lgN次比较(两个子结点的比较和父结点与较大子节点的比较)。
对于须要大量混杂插入和删除最大元素的操做,优先队列很适合。
改进
1. 多叉堆
基于数组表示的彻底三叉树:对于数组 1 至 N 的 N 个元素,位置 k 的结点大于等于位于 3k-1, 3k ,3k +1 的结点,小于等于位于 (k+1)/ 3 的结点。
2.调整数组大小
使用动态数组,能够构造一个无需关注队列大小的优先队列。能够参考下压栈。
3.索引优先队列
在许多应用程序中,容许客户端引用优先级队列中已经存在的项目是有意义的。一种简单的方法是将惟一的整数索引与每一个项目相关联。
堆排序
咱们能够把任意优先队列变成一种排序方法:先将全部元素插入一个查找最小元素的优先队列,再重复调用删除操做删除最小元素来将它们按顺序删除。这种排序成为堆排序。
堆排序的第一步是堆的构造,第二步是下沉排序阶段。
1.堆的构造
简单的方法是利用前面优先队列插入元素的方法,从左到右遍历数组调用 Swim 方法(由上算法所需时间和 N logN 成正比)。一个更聪明高效的方法是,从右(中间位置)到左调用 Sink 方法,只需遍历一半数组,由于另外一半是大小为 1 的堆。这种方法只需少于 2N 次比较和 少于 N 次交换。(堆的构造过程当中处理的堆都比较小。例如,要构造一个 127 个元素的数组,须要处理 32 个大小为 3 的堆, 16 个大小为 7 的堆,8 个大小为 15 的堆, 4 个大小为 31 的堆, 2 个大小为 63 的堆和 1 个大小为127的堆,所以在最坏状况下,须要 32*1 + 16*2 + 8*3 + 4*4 + 2*5 + 1*6 = 120 次交换,以及两倍的比较)。
2.下沉排序
堆排序的主要工做在第二阶段。将堆中最大元素和堆底元素交换,并下沉至 N--。至关于删除最大元素并将堆底元素放至堆顶(优先队列删除操做),将删除的最大元素放入空出的数组位置。
public class MaxPriorityQueueSort { public static void Sort(IComparable[] pq) { int n = pq.Length; for (var k = n / 2; k >= 1; k--) { Sink(pq, k, n); } //上浮须要遍历所有 //for (var k = n; k >= 1; k--) //{ // Swim(pq, k); //} while (n > 1) { Exch(pq,1,n--); Sink(pq,1,n); } } private static void Swim(IComparable[] pq, int n) { while (n > 1 && Less(pq,n / 2, n)) { Exch(pq,n / 2, n); n = n / 2; } } //下沉 private static void Sink(IComparable[] pq,int k, int N) { while (2 * k <= N) { int j = 2 * k; //取最大的子节点 if (j < N && Less(pq,j, j + 1)) j++; //若是父节点不小子节点,退出循环 if (!Less(pq, k,j)) break; //不然交换,继续下沉 Exch(pq, j,k); k = j; } } private static void Exch(IComparable[] pq, int i, int j) { IComparable temp = pq[i-1]; pq[i - 1] = pq[j - 1]; pq[j - 1] = temp; } private static bool Less(IComparable[] pq, int i, int j) { return pq[i - 1].CompareTo(pq[j - 1]) < 0; } public static void Show(IComparable[] a) { for (var i = 0; i < a.Length; i++) Console.WriteLine(a[i]); } }
堆排序的轨迹
将 N 个元素排序,堆排序只需少于 (2N lgN + 2N)次比较以及一半次数的交换。2N 来自堆的构造,2N lgN 是每次下沉操做最多须要 2lgN 次比较。
先下沉后上浮
在排序过程当中,大多数从新插入堆中的项目都会一直到达底部。所以,经过避免检查元素是否已到达其位置,能够简单地提高两个子结点中的较大者直到到达底部,而后上浮到适当位置,从而节省时间。这个方法将比较数减小了2倍,但须要额外的簿空间。只有当比较操做代价较高时可使用这种方法。(例如将字符串或其余键值较长类型的元素排序)。
堆排序是可以同时最优利用空间和时间的方法,在最坏状况下也能保证 ~2N lgN 次比较和恒定的额外空间。当空间紧张时,可使用堆排序。但堆排序没法利用缓存。由于它的数组元素不多喝相邻的其余元素比较,所以缓存未命中的次数要远高于大多数比较都在相邻元素之间进行的算法。