在上一篇隨筆 Timus 1037. Memory management 的“進一步的討論”小節中,我提到:
這個程式中使用
KeyedPriorityQueue
來儲存已指派的“記憶體塊”,使用
PriorityQueue<T>
來儲存尚未分配的“自由塊”。這兩個優先隊列的演算法是一樣的,可以想辦法合并。這將在下一篇隨筆中討論。
現在,就開始行動吧。
首先,需要一個介面,用來擷取鍵以及擷取和設定值,如下所示:
namespace Skyiv.Util{ interface IKeyValue<T, K, V> { K GetKey(T x); V GetValue(T x); void SetValue(T x, V v); }}
接著,就是我們的帶索引值的優先隊列 KeyedPriorityQueue<T, K, V>
登場了:
using System;using System.Collections.Generic;namespace Skyiv.Util{ class KeyedPriorityQueue<T, K, V> { IComparer<T> comparer; IKeyValue<T, K, V> kver; Dictionary<K, int> keys; bool hasKey; T[] heap; public int Count { get; private set; } public KeyedPriorityQueue(IKeyValue<T, K, V> kv) : this(null, kv) { } public KeyedPriorityQueue(int capacity, IKeyValue<T, K, V> kv) : this(capacity, null, kv) { } public KeyedPriorityQueue(IComparer<T> comparer, IKeyValue<T, K, V> kv) : this(16, comparer, kv) { } public KeyedPriorityQueue(int capacity, IComparer<T> comparer, IKeyValue<T, K, V> kver) { this.keys = new Dictionary<K, int>(); this.comparer = (comparer == null) ? Comparer<T>.Default : comparer; this.kver = kver; this.hasKey = (kver != null); this.heap = new T[capacity]; } public bool ContainsKey(K key) { return keys.ContainsKey(key); } public void Update(T v) { if (!hasKey) throw new NotSupportedException(); if (typeof(T).IsValueType) throw new InvalidOperationException("T 不能是實值型別"); if (!ContainsKey(kver.GetKey(v))) throw new ArgumentOutOfRangeException("v", v, "更新優先隊列時無此健值"); var id = keys[kver.GetKey(v)]; var cmp = comparer.Compare(v, heap[id]); kver.SetValue(heap[id], kver.GetValue(v)); // 注意: 這一句要求 T 是參考型別,不能是實值型別 if (cmp < 0) SiftDown(id); else if (cmp > 0) SiftUp(id); } public void Push(T v) { if (Count >= heap.Length) Array.Resize(ref heap, Count * 2); if (hasKey) keys[kver.GetKey(v)] = Count; heap[Count] = v; SiftUp(Count++); } public T Pop() { var v = Top(); if (hasKey) keys.Remove(kver.GetKey(v)); heap[0] = heap[--Count]; if (Count > 0) SiftDown(hasKey ? (keys[kver.GetKey(heap[0])] = 0) : 0); return v; } public T Top() { if (Count > 0) return heap[0]; throw new InvalidOperationException("優先隊列為空白"); } void SiftUp(int n) { var v = heap[n]; for (var n2 = n / 2; n > 0 && comparer.Compare(v, heap[n2]) > 0; n = n2, n2 /= 2) heap[hasKey ? (keys[kver.GetKey(heap[n2])] = n) : n] = heap[n2]; heap[hasKey ? (keys[kver.GetKey(v)] = n) : n] = v; } void SiftDown(int n) { var v = heap[n]; for (var n2 = n * 2; n2 < Count; n = n2, n2 *= 2) { if (n2 + 1 < Count && comparer.Compare(heap[n2 + 1], heap[n2]) > 0) n2++; if (comparer.Compare(v, heap[n2]) >= 0) break; heap[hasKey ? (keys[kver.GetKey(heap[n2])] = n) : n] = heap[n2]; } heap[hasKey ? (keys[kver.GetKey(v)] = n) : n] = v; } }}
上述 KeyedPriorityQueue<T, K, V>
類中,T
是要儲存在這個帶索引值的優先隊列中的資料的類型,K
是鍵的資料類型,V
是值的資料類型。
Dictionary<K, int> keys
欄位用於儲存鍵(K)在堆(heap)中的索引。
bool ContainsKey(K key)
方法用於尋找指定的鍵是否在該優先隊列中。
Update(T v)
方法用於更新指定項目的值。注意,如果要使用這個方法的話,T
不能是實值型別。之所以在這個方法的第二行:
if (typeof(T).IsValueType) throw new InvalidOperationException("T 不能是實值型別");
進行這個判斷,而不是在將該類聲明為:
class KeyedPriorityQueue<T, K, V> where T : class { ... }
這是因為,如果不需要調用 Update(T v)
方法的話,T
還是允許是實值型別的。
該類的其他方面,除了加入對 keys 欄位的處理以外,就和標準的優先隊列差不多了。
有了這個 KeyedPriorityQueue<T, K, V>
類,就可以從中派生出 PriorityQueue<T>
類來:
class PriorityQueue<T> : KeyedPriorityQueue<T, object, object>{ public PriorityQueue() : base(null) { } public PriorityQueue(int capacity) : base(capacity, null) { } public PriorityQueue(IComparer<T> comparer) : base(comparer, null) { } public PriorityQueue(int capacity, IComparer<T> comparer) : base(capacity, comparer, null) { }}
對於 PriorityQueue<T>
類的執行個體,如果調用 ContainsKey
方法,總是返回 false
。如果調用 Update
方法,總是引發 NotSupportedException
異常。
現在讓我們回到上一篇隨筆 Timus 1037. Memory management 中,需要稍微修改一下我們的記憶體管理器程式。
首先,Block
必須從結構改為類,如下所示:
sealed class Block{ public int Id { get; private set; } public int Time { get; set; } public Block(int id, int time) { Id = id; Time = time; }}
然後,需要一個實現 IKeyValue<T, K, V>
介面的類:
sealed class IdTime : IKeyValue<Block, int, int>{ public int GetKey(Block x) { return x.Id; } public int GetValue(Block x) { return x.Time; } public void SetValue(Block x, int v) { x.Time = v; }}
最後,就剩下最簡單的工作了,將 Main 方法的第二行:
var used = new KeyedPriorityQueue(new TimeComparer());
修改為:
var used = new KeyedPriorityQueue<Block, int, int>(new TimeComparer(), new IdTime());
就可以了。
當然,這也是有代價的,就是已耗用時間和記憶體使用量都增加了:
ID |
Date |
Author |
Problem |
Language |
Judgement result |
Execution time |
Memory used |
2568007 |
21:22:09 18 Apr 2009 |
skyivben |
1037 |
C# |
Accepted |
0.406 |
6 129 KB |
2566773 |
09:24:17 18 Apr 2009 |
skyivben |
1037 |
C# |
Accepted |
0.265 |
4 521 KB |
上表中第二行就是上一篇隨筆中的程式提交的結果,第一行就是按照本篇隨筆修改後的程式提交的結果。
實際上,雖然 KeyedPriorityQueue
類中的代碼與 PriorityQueue<T>
類中代碼有大量的重複,可以用本篇隨筆的方法將 KeyedPriorityQueue
類改為 KeyedPriorityQueue<T, K, V>
泛型類,再從中派生出 PriorityQueue<T>
類來,以消除重複的代碼。但是,這樣必然造成 PriorityQueue<T>
類的運行效率降低。所以,一般情況下,PriorityQueue<T>
類還是使用原來的代碼為好。
當然,如果能夠從 PriorityQueue<T>
類派生出 KeyedPriorityQueue<T, K, V>
類,那就比較完美了。不知各位大俠是否還有更好的方法?
演算法和資料結構目錄