Dotnet C# PriorityQueue 學習並使用 .Net6 提供的 PriorityQueue

Published on Tuesday, March 7, 2023

.Net PriorityQueue

今天來學習一下在 .Net6 才終於提供的 PriorityQueue,先到 Github上面查看一下原始碼

/// <typeparam name="TElement">Specifies the type of elements in the queue.</typeparam>
/// <typeparam name="TPriority">Specifies the type of priority associated with enqueued elements.</typeparam>
public class PriorityQueue<TElement, TPriority>

使用方法也與 Queue 相同,加入 Queue 使用 Enqueue 方法,移出 Queue 使用 Dequeue 方法,查看第一個元素使用 Peek 方法
不過 PriorityQueue 使用前需要先指定元素的類型(TElement)以及優先度(TPriority)

舉例來說有一組使用者 User1User2User3 優先度分別是 123

User1 1
User2 2
User3 3

首先我們建立一個 PriorityQueue 類型為 String 順序使用 Int

var pQueue = new PriorityQueue<string, int>();
	
pQueue.Enqueue("User1", 1);
pQueue.Enqueue("User2", 2);
pQueue.Enqueue("User3", 3);

這時我們可以使用 Peek方法來看一下目前誰位於首要元素

pQueue.Peek();

User1

這邊回傳的結果是 User1,那為什麼是是回傳 User1 呢?
我們可以先看看原始碼 這邊 PriorityQueue 提供了許多的 Constructor ,我們今天使用的是 Default Constructor

public PriorityQueue()
{
    _nodes = Array.Empty<(TElement, TPriority)>();
    _comparer = InitializeComparer(null);
}

這邊除了賦值了一個空的 Array ,下方還呼叫了一個 InitializeComparer 的方法,原始碼

private static IComparer<TPriority>? InitializeComparer(IComparer<TPriority>? comparer)
{
    if (typeof(TPriority).IsValueType)
    {
        if (comparer == Comparer<TPriority>.Default)
        {
            // if the user manually specifies the default comparer,
            // revert to using the optimized path.
            return null;
  
        return comparer;
    }
    else
    {
        // Currently the JIT doesn't optimize direct Comparer<T>.Default.Compare
        // calls for reference types, so we want to cache the comparer instance instead.
        // TODO https://github.com/dotnet/runtime/issues/10050: Update if this changes in the future.
        return comparer ?? Comparer<TPriority>.Default;
    }
}

到這邊就很清楚了,此方法會檢查你有沒有要使用自己寫的 Comparer,如果沒有的的話就使用類型預設的 Comparer
以我們的例子來看我們 TPriority 使用的類型是 Int32 所以最後使用的是 Int32 裡面的 CompareTo

Console.WriteLine(0.CompareTo(1));
Console.WriteLine(1.CompareTo(1));
Console.WriteLine(2.CompareTo(1));

-1
0
1

所以呼叫 PriorityQueue 的 Enqueue 方法時就會比較 int 的大小,如果數字比較小的話比較結果會回傳 -1
所以這時 Enqueue 方法就會知道這個新插入的元素是比目前第一個元素優先度更高,這就是為什麼一開始的範例回傳的是 User1

知道原理後我們也可以試試其他類型,例如前幾天使用的字串比較方法 StringComparer.Ordinal 也剛好是一個 Comparer

var spQueue = new PriorityQueue<string, string>(StringComparer.Ordinal);
	
spQueue.Enqueue("User1", "aGroup");
spQueue.Enqueue("User2", "bGroup");
spQueue.Enqueue("User3", "AGroup");

這次我們在 Constructor 直接指定我們要使用 StringComparer.Ordinal, 並且我們知道此比較方法是使用 ASCII 進行比較 結果也如設想回傳的是 user3

pQueue.Peek();

user3

還有一點需要注意順序的調整會在 EnqueueDequeue 方法中進行,並且只會跟第一節點進行比較 例如在新增一個 "User4" "BGroup",正常來說 BGroup 會比 aGroup 和 bGroup 優先度高,不過它目前的位置並不會在第二優先位置反而會在最後一位

var spQueue = new PriorityQueue<string, string>(StringComparer.Ordinal);
	
spQueue.Enqueue("User1", "aGroup");
spQueue.Enqueue("User2", "bGroup");
spQueue.Enqueue("User3", "AGroup");
spQueue.Enqueue("User4", "BGroup");

Console.WriteLine(spQueue);

IReadOnlyCollection<ValueTuple<String,String>> (4 items)•••
Item1	Item2
User3	AGroup
User2	bGroup
User1	aGroup
User4	BGroup

這時我們先呼叫一次 Dequeue 方法把 User3 移除,最後在檢查一次 這時 User4 又會回到第一優先位置了,畢竟 Queue 的特性也確實沒必要完全保持 Collection 順序的正確性

spQueue.Dequeue();
Console.WriteLine(spQueue);

IReadOnlyCollection<ValueTuple<String,String>> (3 items)•••
Item1	Item2
User4	BGroup
User2	bGroup
User1	aGroup

Summary

今天學習了 PriorityQueue 的使用方法,跟 Queue 相比之下多了一個優先度的概念
有了這個特性我們可以拿來做一些需要優先執行的相關功能,類似VIP等相關功能需要優先執行的群體的任務
就可以使用到PriorityQueue