Algorithm——优先队列(九)

优先队列与堆
本文介绍如何使用堆数据结构实现优先队列,并提供最大优先队列的Java实现代码示例,包括插入、提取最大值、增加关键字等操作。

Algorithm——优先队列


堆除了可以实现前面所述的堆排序算法,还可以用来实现优先队列。优先队列是一种用来维护一组元素构成的集合S的数据结构,其中每一个元素都有一个相关的值,称为“关键字”。一个最大优先队列支持以下操作:

  1. INSERT(S, x):把元素x插入集合S中
  2. MAXIMUM(S):返回S中具有最大关键字的元素
  3. EXTRACT-MAX(S):去掉并返回S中具有最大关键字的元素
  4. INCREASE-KEY(S, x, k):将元素x的关键字值增加到k,这里假设k的值不小于x的圆关键字值

最大优先队列的应用有很多,其中一个就是在共享计算机系统的作业调度。最大优先队列记录将要执行的各个作业以及它们之间的相对优先级。当一个作业完成或者被中断后,调度器调用EXTRACT-MAX从所有等待的作业中,选出具有最高优先级的作业来执行。在任何时候,调度器可以调用INSERT把一个新作业加入到队列中来。

根据《算法导论》对最大优先队列的描述,用Java实现的测试代码如下所示:

/**
	 * 
	 * 建立初始最大堆
	 * 
	 * 由完全二叉树的性质可以推出:如果要将数组A[1...n]转换成最大堆,则子数组元素A[n/2 +
	 * 1...n]都是树的叶子节点,且叶子节点可以看做是只有一个元素的堆
	 * 
	 * @param A
	 */
	public void buildMaxHeap(int[] A, int heap_size) {
		for (int i = A.length / 2 - 1; i >= 0; i--) {
			maxHeapify(A, heap_size, i);
		}
	}

	/**
	 * 堆排序中使用最大堆性质
	 * 
	 * 维护最大堆性质:以iMax节点为根节点,维护此子二叉树中的最大堆性质
	 * 
	 * @param A
	 *            排序的数组
	 * @param idex
	 *            操作的子树的根节点对应的下标值
	 */
	public void maxHeapify(int[] A, int heap_size, int iMax) {

		int left = getLeftIndex(iMax);// 该根节点的左节点元素下标
		int right = getRightIndex(iMax);// 该根节点的右节点元素下标

		int largest = iMax;

		if (heap_size >= left && A[left] > A[iMax])// 判断根节点下标的元素与左子节点下标的元素哪个大;largest存放较大的元素的下标
			largest = left;
		else
			largest = iMax;

		if (heap_size >= right && A[right] > A[largest])// 判断largest下标的代表元素与右节点下标的元素哪个大;largest存放较大的元素的下标
			largest = right;

		if (iMax != largest) {// 如果largest与iMax不等,说明根节点的元素并不是最大的;随即进行元素调换,保证根节点的元素值是根节点/左右子节点中最大的那个
			exchange(A, iMax, largest);
			maxHeapify(A, heap_size, largest);// 在递归调用maxHeapify()函,使以largest下标元素为根节点的子树也保证堆最大性质
		}
	}

	public void exchange(int[] A, int idex, int idex2) {
		int temp = A[idex];
		A[idex] = A[idex2];
		A[idex2] = temp;

	}

	/**
	 * 
	 * 获取下标i对应元素的父元素下标
	 * 
	 * @param i
	 * @return
	 */
	public int getParentIndex(int iMax) {
		return (iMax == 0) ? 0 : (iMax % 2 == 0 ? (iMax - 1 / 2) : (iMax / 2));
	}

	/**
	 * 
	 * 获取下标i代表的父元素的左子元素的下标
	 * 
	 * @param i
	 * @return
	 */
	public int getLeftIndex(int iMax) {
		return 2 * iMax + 1;
	}

	/**
	 * 获取下标i代表的父元素的右子元素的下标
	 * 
	 * @param i
	 * @return
	 */
	public int getRightIndex(int iMax) {
		return 2 * (iMax + 1);
	}

	/**
	 * 
	 * 返回最大堆A中关键字最大的元素(这里也就是依据数组A先创建最大堆,然后返回此最大堆中关键字最大的元素;在此场景中,关键字就是二叉堆中每个节点的元素值)
	 * 
	 * @param A
	 * @return
	 */
	public int heapMaximum(int[] A) {
		return A[0];
	}

	/**
	 * 去掉并返回最大二叉堆中具有最大关键字的元素;也即是去掉heap_size范围内,元素值最大的那个节点(最大堆中,此节点一定是根节点)
	 * 
	 * @param A
	 * @param heap_size
	 * @return
	 */
	public int heapExtractMax(int[] A, int heap_size) {
		if (heap_size < 1)
			throw new IllegalArgumentException("heap underflow");
		int max = A[0];
		A[0] = A[heap_size];
		heap_size--;
		maxHeapify(A, heap_size, 0);
		return max;
	}

	/**
	 * 将二叉堆中下标i指定的元素的关键字值增加到key;在一个最大堆中,如果替换了某个节点的关键字值后,为了保证最大堆性质;只需将此次被替换节点的值循环与其父节点、爷爷节点(依次类推)的值比较即可
	 * 
	 * @param A
	 * @param i
	 *            指定要替换关键字元素的下标
	 * @param key
	 *            指定元素要被替换的key值
	 */
	public void heapIncreaseKey(int[] A, int i, int key) {
		if (key < A[i])
			throw new IllegalArgumentException("new key is smaller than current key");

		A[i] = key;
		while (i > 0 && A[getParentIndex(i)] < A[i]) {
			exchange(A, i, getParentIndex(i));
			i = getParentIndex(i);
		}
	}

	/**
	 * 在heap_size范围内,将指定的元素key插入到堆A中
	 * 
	 * @param A
	 * @param heap_size
	 *            指定的heap_size范围;在实际情况中,实现中还应该考虑数组拓展的问题
	 * @param key
	 *            要插入的元素
	 */
	public void maxHeapInsert(int[] A, int heap_size, int key) {
		heap_size++;// 考虑数组拓展
		A[heap_size] = Integer.MIN_VALUE;
		heapIncreaseKey(A, heap_size, key);
	}
相应地,最小优先队列支持以下操作:
  1. INSERT
  2. MINIMUM
  3. EXTRACT-MIN
  4. DECREASE-KEY

如果要得出最小优先队列的伪代码实现,最需要关注的点就是保持最大堆性质maxHeapify();根据最小堆性质调整该函数的实现,就基本可以得出最小优先队列的实现思路了。

文中的最大优先队列代码并不能直接使用,因为实际情况中的场景远比本文讲述的要复杂。本文主要是了解优先队列的实现思路和原理,如果是使用Java语言,JDK的util包中已经提供了优先队列的实现类了。







洛谷平台上有多个与优先队列相关的题目及对应代码示例: - 洛谷P5019:分组【unordered_map,优先队列】。该题通过优先队列解决分组问题,代码中使用`unordered_map`和优先队列处理元素分组。最后遍历每个优先队列的`top`记录最小值。以下是相关代码: ```cpp #include<iostream> #include<algorithm> #include<vector> #include<queue> #include<unordered_map> using namespace std; const int N=100010; vector<int> list; int n; int ans=1e9; unordered_map<int,priority_queue<int,vector<int>,greater<int>>> ha; int main(){ cin>>n; for(int i=1;i<=n;i++){ int x; cin>>x; list.push_back(x); } sort(list.begin(),list.end()); for(int i=0;i<n;i++){ if(ha[list[i]].size()){ int t=ha[list[i]].top(); ha[list[i]].pop(); ha[list[i]+1].push(t+1); }else{ ha[list[i]+1].push(1); } } for(auto t:ha){ if(t.second.size()) ans=min(t.second.top(),ans); } cout<<ans; } ``` - 洛谷P1090——优先队列。这道题利用优先队列解决问题,代码中定义了一个小顶堆优先队列,通过不断取出堆顶元素合并计算答案。以下是代码: ```cpp #include<iostream> #include<queue> using namespace std; priority_queue<int,vector<int>,greater<int> > a; int main() { int n; int x; int ans=0; int t1,t2; int i,j; cin>>n; for (i=1;i<=n;i++) { cin>>x; a.push(x); } while (a.size()>=2) { t1=a.top(),a.pop(); t2=a.top(),a.pop(); ans+=t1+t2; a.push(t1+t2); } cout<<ans; return 0; } ``` - 洛谷题目p3887题解优先队列。该题使用多个优先队列处理不同的数据输入,根据输入的条件计算结果。代码如下: ```cpp #include<bits/stdc++.h> using namespace std; priority_queue<int> q1,q2,q3,q4; int k,d,m,f,q,a,b,c,dd; int jjj,jjjj,jjjjj; double sum,ans; int main() { cin>>k>>d>>m>>f; for(int i=1;i<=k;i++) { cin>>a; q1.push(a); } for(int i=1;i<=d;i++) { cin>>b; q2.push(b); } for(int i=1;i<=m;i++) { cin>>c; q3.push(c); } for(int i=1;i<=f;i++) { cin>>dd; q4.push(dd); } cin>>q; for(int i=1;i<=q;i++) { cin>>jjj>>jjjj>>jjjjj; ans=jjj+jjjj+jjjjj+1; sum+=q1.top(); q1.pop(); for(int j=1;j<=jjj;j++) { sum+=q2.top(); q2.pop(); } for(int j=1;j<=jjjj;j++) { sum+=q3.top(); q3.pop(); } for(int j=1;j<=jjjjj;j++) { sum+=q4.top(); q4.pop(); } double cnt; cnt=sum/ans; cout<<fixed<<setprecision(2)<<cnt<<endl; sum=0; cnt=0; ans=0; } return 0; } ``` - 洛谷P4053 ← 优先队列。题目描述为小刚玩“建筑抢修”游戏,T部落基地有N个受损建筑,只有一个修理工人,每个建筑修复需要一定时间且有报废期限,需要合理安排修理顺序以抢修尽可能多的建筑 [^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值