优先队列
优先队列(priority queue):
可以在O(1)时间获得最大值,在O(log n)时间内取出或者插入最大值。常用堆(heap)来实现。
1.实现
实现队列和栈与实现优先队列的不同在于性能的要求。队列和栈实现在常数时间完成所有操作。使用基础结构来实现优先队列,在最坏情况下插入元素和删除最大元素需要用线性时间来完成。基于堆来实现优先队列可以保证两个操作在logN的时间完成。
-
堆
一个完全二叉树,每个节点的值总是大于等于子节点的值。通常使用一个数组来建立一个树。堆的两个核心操作是上浮和下沉。如果一个节点比父节点大,那么需要交换两个节点,一直不断比较和交换:这个操作为上浮。下沉同理,如果有两个子节点比父节点大,则选择较大的子节点。节点i,父节点i/2,子节点是2i与2i+1。
1.1 C++代码
vector<int> heap;
//获得最大值
void top(){
return heap[0];
}
//插入任意值:把新的数字放在最后一位,然后上浮
void push(int k){
heap.push_back(k);
swim(heap.size()-1);
}
//删除最大值:把最后一个数字挪到开头然后下沉
void pop(){
heap[0] = heap.back();
heap.pop_back();
sink(0);
}
//上浮
void swim(int k){
while(k>1 && heap[k/2] < heap[k]){
swap(heap[k/2] , heap[k]);
k = k/2;
}
}
//下沉
void sink(int k){
while(2*k <=N){
int j = 2*k;
if(j < N && heap[j]<heap[j+1])
j++;
if(!heap[k]<heap[j])
break;
swap(heap[k], heap[j]);
k = j;
}
}
1.2 Java代码
pubilc class MaxPQ<Key extends Comparable<Key>>{
private Key[] pq;//基于堆的完全二叉树
private int N = 0;//存储与pq[1..N]中,pq[0]没有使用
public MaxPQ(int maxN){
pq = (Key[]) new Comparable[maxN+1];
}
public boolean isEmpty(){
return N == 0;
}
public int size(){
return N;
}
public void insert(Key v){
pq[++N] = v;
swim(N);
}
public Key delMax(){
Key max = pq[1];//从根节点得到最大元素
exch(1, N--);//将其和最后一个节点交换
pd[N+1] = null;//防止对象游离
sink(1);//恢复堆的有序性
return max;
}
//辅助方法
private boolean less(int i, int j){
return pq[i].compareTo(pq[j])<0;
}
//交换
private void exch(int i, int j){
Key t = pq[i];
pq[i] = pq[j];
pq[j] = t;
}
//上浮
private void swim(int k){
while(k>1 && less(k/2,k)){
exch(k/2, k);
k = k/2;
}
}
//下沉
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
break;
exch(k, j);
k = j;
}
}
}
2.题目
2.1合并k个增序的链表(LeetCode 23 Hard)
除了利用归并排序进行两两合并,还可以使用优先队列(最小堆)来快速解决。
最小堆思路:用优先队列存储k个链表的首元素,然后每次提取最小的元素加入最终的结果链表dummy。然后把取出元素的下一个元素再加入到堆中,以此类推。直到所有链表被提取结束。
C++版本
//比较函数来维持递减的关系
struct Comp{
bool operator()(ListNode* l1, ListNode* l2){
return l1->val > l2->val;
}
};
//
ListNode* mergeKLists(vector<ListNode*>& lists){
if(lists.empty())
return nullptr;
priority_queue<ListNode*, vector<ListNode*>, Comp> q;
for(ListNode* list: lists){
if(list)
q.push(list);
}
ListNode* dummy = new ListNode(0);
ListNode* *cur = dummy;
while(!q.empty()){
cur->next = q.top();
q.pop();
cur=cur->next;
if(cur->next)
q.push(cur->next);
}
return dummy->next;
}
java版本:
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
PriorityQueue<ListNode> queue = new PriorityQueue<>(lists.length, new Comparator<ListNode>() {
@Override
public int compare(ListNode o1, ListNode o2) {
if (o1.val < o2.val) return -1;
else if (o1.val == o2.val) return 0;
else return 1;
}
});
ListNode dummy = new ListNode(0);
ListNode p = dummy;
for (ListNode node : lists) {
if (node != null) queue.add(node);
}
while (!queue.isEmpty()) {
p.next = queue.poll();
p = p.next;
if (p.next != null) queue.add(p.next);
}
return dummy.next;
}
2.2 丑数
1)丑数I:依次整除,最后判断是否为1。
2)丑数II:从下往上寻找丑数。
int nthUglyNumber(int n) {
vector<int> nums;
nums.push_back(1);
int i2 = 0, i3 = 0, i5 = 0;
for(int i= 1; i < n; i++){//注意这里从1开始
int ugly = min(nums[i2]*2, min(nums[i3]*3,nums[i5]*5));//从小到大算出丑数
nums.push_back(ugly);
//依次*2,*3,*5,
if(nums[i]==nums[i2]*2) ++i2;
if(nums[i]==nums[i3]*3) ++i3;
if(nums[i]==nums[i5]*5) ++i5;
}
return nums[n-1];
}
3)丑数III:此题的丑数定义与263. 丑数和264. 丑数 II并不相同。是困难的 878. 第N个神奇数字 的升级版。寻找小于等于x的a的倍数、b的倍数、c的倍数组成的集合分别称为A、B、C。由容斥原理
可得:
则小于等于x的丑数个数为
对于f(x)进行二分查找即可。
int nthUglyNumber(int n, int a, int b, int c){
long ab = lcm(a, b); // a,b的最大公倍数
long ac = lcm(a, c); // a,c的最大公倍数
long bc = lcm(b, c); // b,c的最大公倍数
long t = lcm(ab, c); // a,b,c三个数的最大公倍数
long floor = min(a, min(b, c));
long ceil = 2e9;
while(floor < ceil){
long mid = (ceil - floor) / 2 + floor;
// num为[1...mid]中的丑数个数(容斥原理)
long num = mid / a + mid / b + mid / c - mid / ab - mid / ac - mid / bc + mid / t;
if(num >= n){
ceil = mid;
}
else{
floor = mid +1;
}
}
return floor;
}
//最大公因数
long gcd(long x, long y){
return !y?x:gcd(y, x%y);
}
//最小公倍数
long lcm(long x, long y){
return x*y / gcd(x,y);
}
4)超级丑数:
超级丑数II题目的扩展版本
依然维持一个数组nums
维持一个primes对应的序号
class Solution {
public:
int nthSuperUglyNumber(int n, vector<int>& primes) {
vector<int> nums;
nums.push_back(1);
int np = primes.size();
// primes的对应nums的序号
vector<int> plist(np,0);
for (int i = 1; i < n; ++i)
{
int currMin = INT_MAX;
for (int j = 0; j < np; ++j)
{
currMin = min(currMin, nums[plist[j]]*primes[j]);
}
nums.push_back(currMin);
for (int j = 0; j < np; ++j)
{
if (currMin == nums[plist[j]]*primes[j])
{
++plist[j];
}
}
}
return nums[n-1];
}
};
2.3 taxicab number
想象一个二维矩阵m [i] [j] = i ^ 3 + j ^ 3。我们不必在内存中创建此矩阵。行的项目按升序排列,列的项目也按升序排列。我们可以使用minHeap(minPQ)首先存储对角线。然后执行以下步骤,直到minHeap为空:
- 从minHeap获取当前的min(minCur),将其与preMin进行比较,如果它们相等,我们将找到一对和(a ^ 3 + b ^ 3 = c ^ 3 + d ^ 3)。
- 将项目放置在minCur右侧的minHeap矩阵中。
该算法起作用是因为它保证矩阵中的所有项目都按顺序添加并从minHeap中取出。
class Taxicab implements Comparable<Taxicab>{
int n1;
int n2;
int cube;
Taxicab(int n1, int n2) {
this.n1 = n1;
this.n2 = n2;
this.cube = n1 * n1 * n1 + n2 * n2 * n2;
}
@Override
public int compareTo(Taxicab that) {
if (that.cube > this.cube) return -1;
if (that.cube < this.cube) return 1;
return 0;
}
@Override
public boolean equals(Object o) {
if (o instanceof Taxicab) {
if (((Taxicab)o).compareTo(this) == 0)
return true;
}
return false;
}
@Override
public String toString() {
return "number: " + cube + " (" + n1 + ", " + n2 + ")";
}
}
public void findTaxinumber(int N) {
MinPQ<Taxicab> candidates = new MinPQ<Taxicab>();
for (int i = 1; i <= N; i++) {
for (int j = i + 1; j <= N; j++) {
Taxicab t = new Taxicab(i, j);
if (candidates.size() < N) {
candidates.insert(t);
}
else {
Queue<Taxicab> temp = new Queue<Taxicab>();
Taxicab min = candidates.delMin();
while (candidates.min().equals(min)) {
temp.enqueue(candidates.delMin());
}
if (!t.equals(min)) {
candidates.insert(t);
}
else {
temp.enqueue(t);
}
if (!temp.isEmpty()) {
for (Taxicab taxi: temp) {
System.out.println(taxi);
}
System.out.println(min);
}
}
}
}
}