CodeGym /課程 /C# SELF /集合排序

集合排序

C# SELF
等級 29 , 課堂 2
開放

1. 前言

如果你曾經有個襪子抽屜,想找一雙配對的襪子,那你肯定遇過排序的問題。東西亂七八糟的時候,找東西就像在闖關一樣。寫程式處理集合時,其實也是同一回事啦。

排序,就是把集合裡的元素按照某個規則(像是字母順序、數值、日期等等)排好。這很重要,因為:

  • 給使用者看資料時(沒人喜歡亂七八糟的畫面)。
  • 方便搜尋(像 binary search 只能在已排序的集合用)。
  • 比較、報表、匯出資料等等操作。

百萬大哉問:「如果我只有五個元素,是不是不用排序?」——理論上可以,但五個變五百、五千時,不自動化就 GG 了。

在 .NET 裡,排序主要有兩種方式:

  • 直接改原本的集合(像 List<T>Sort 方法)。
  • 先複製一份,再排序(像複製陣列再排)。

.Sort() 排 List

Sort()List<T> 的方法,因為這個 class 有實作 IList<T> 介面,可以用 index 存取,也能改變元素順序。

範例——數字遞增排序:


using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var numbers = new List<int> { 5, 2, 9, 1, 5, 6 };
        numbers.Sort();
        Console.WriteLine("遞增排序:");
        foreach(var number in numbers)
        {
            Console.Write($"{number} "); // 1 2 5 5 6 9
        }
    }
}

這裡是「原地排序」:原本的 numbers 會被改掉,元素順序直接變了。

字串排序

字串也可以直接排序:


var words = new List<string> { "柳橙", "蘋果", "香蕉", "梨子" };
words.Sort();
Console.WriteLine(string.Join(", ", words)); // 柳橙, 香蕉, 梨子, 蘋果

有趣的是:預設字串排序是照 Unicode 排,不是「人類」的字母順序(多語言應用要注意)。

2. 自訂排序規則

有時候預設排序不夠用。比如你想照年齡或註冊日期排使用者,而不是名字。

用 lambda 表達式排序(Sort(Comparison<T>) 方法)

Sort 方法可以傳一個自訂的比較規則——也就是比較兩個元素的 function。

照年齡排序使用者:


class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// ...

var users = new List<User>
{
    new User { Name = "艾莉絲", Age = 30 },
    new User { Name = "鮑伯", Age = 25 },
    new User { Name = "伊娃", Age = 35 }
};

users.Sort((u1, u2) => u1.Age.CompareTo(u2.Age));

foreach (var user in users)
{
    Console.WriteLine($"{user.Name}: {user.Age}");
}
// 輸出:
// 鮑伯: 25
// 艾莉絲: 30
// 伊娃: 35

這招怎麼運作?lambda (u1, u2) => u1.Age.CompareTo(u2.Age) 會回傳負數代表 u1 比較年輕,正數代表比較老,0 則是年齡一樣。

IComparer<T> 介面

有時候你想把排序規則寫成一個 class,像是有很多集合或很複雜的邏輯時。


class UserAgeComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        return x.Age.CompareTo(y.Age);
    }
}

// ...

var users = new List<User>{ /* ... */ };
users.Sort(new UserAgeComparer()); // 現在照年齡排

這樣很方便,尤其是你在不同地方都要用到同一種排序。

手動多欄位排序

如果有些使用者年齡一樣,但你想在同年齡裡再照名字排:


users.Sort((u1, u2) => {
    int ageCompare = u1.Age.CompareTo(u2.Age);
    if (ageCompare != 0)
        return ageCompare;
    else
        return u1.Name.CompareTo(u2.Name);
});

這樣就會先照年齡,再照名字排序。

3. 排序集合的複本(不動原本的)

如果你不想動到原本的 list,排序前先複製一份:


var copy = new List<int>(numbers);
copy.Sort();

自訂物件也是一樣:


var usersCopy = new List<User>(users);
usersCopy.Sort((a, b) => a.Age.CompareTo(b.Age));

4. 陣列排序

陣列(T[])也很簡單:


int[] numbers = { 4, 2, 9, 7 };
Array.Sort(numbers); // 會改到原本的陣列

要自訂排序規則也可以:


Array.Sort(numbers, (a, b) => b.CompareTo(a)); // 遞減排序

注意 Array.Sort 會直接改原本的陣列,不會回傳新陣列。如果想保留原本的,記得先複製:


int[] oldNumbers = { 3, 2, 1 };
int[] copy = (int[])oldNumbers.Clone();
Array.Sort(copy);

5. 「排序」Dictionary(Dictionary<TKey, TValue>

Dictionary<TKey, TValue> 本質上是無序的(也就是列舉時順序不保證)。但有時候你還是想要「排序過的 key-value 對」。

要拿到排序過的 key 或 value,先把 pair 複製到 list,再照你要的方式排:


var dict = new Dictionary<string, int>
{
    { "蘋果", 2 },
    { "柳橙", 5 },
    { "梨子", 3 }
};

// 照 key 排序:
var keyValueList = new List<KeyValuePair<string, int>>(dict);
keyValueList.Sort((a, b) => a.Key.CompareTo(b.Key));
foreach (var kv in keyValueList)
{
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}

// 照 value 排序:
keyValueList.Sort((a, b) => a.Value.CompareTo(b.Value));
foreach (var kv in keyValueList)
{
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}

如果你只想要排序過的 key 或 value:


var sortedKeys = new List<string>(dict.Keys);
sortedKeys.Sort();

var sortedValues = new List<int>(dict.Values);
sortedValues.Sort();

但記住,Dictionary 結構本身不會變——你只是拿到排序過的列舉而已。到 .NET 9 會有 OrderedDictionary<TKey, TValue>,它會記住元素順序。雖然不常用,但有時候真的會需要。

6. 有用的小細節

一定要實作比較介面嗎?

很簡單:如果你想讓你的物件「知道」怎麼比較自己(像是照日期或名字),就實作 IComparable<T> 介面。


class Product : IComparable<Product>
{
    public string Name { get; set; }
    public decimal Price { get; set; }

    public int CompareTo(Product other)
    {
        return Price.CompareTo(other.Price);
    }
}

這樣你就可以:


var products = new List<Product> { /* ... */ };
products.Sort(); // 會照價格排

如果預設比較不合用——就用 IComparer<T> 或 lambda,像上面那樣。

照字母排序使用者清單

假設你的 app 有個使用者清單:


List<string> users = new List<string> { "維克多", "安娜", "葉卡捷琳娜", "鮑里斯" };
users.Sort(); // 現在 users 排好了:安娜, 鮑里斯, 維克多, 葉卡捷琳娜

依照緊急程度分配任務


class Task
{
    public string Title { get; set; }
    public int Priority { get; set; } // 1 - 緊急, 2 - 重要, 3 - 可以等等
}

var todo = new List<Task>
{
    new Task { Title = "寫作業", Priority = 2 },
    new Task { Title = "買麵包", Priority = 1 },
    new Task { Title = "追劇", Priority = 3 }
};

todo.Sort((a, b) => a.Priority.CompareTo(b.Priority));

foreach (var task in todo)
    Console.WriteLine($"{task.Priority}: {task.Title}");

排序方法比較

集合 會原地改變嗎? 排序方法 能自訂規則嗎?
List<T>
Sort()
可以:lambda 或 IComparer
T[]
Array.Sort()
可以:lambda 或 IComparer
Dictionary<TKey, TValue>
不會 —(先做 list 再排) 可以:lambda 或 IComparer

7. 常見錯誤和排序小陷阱

原地排序——會直接改原本的集合!如果原始資料不能動,記得先複製。

想排不能改的集合(像 ReadOnlyCollection<T> 會直接噴 runtime error。

不同文化下字串比較會不一樣:字串排序(尤其有西里爾字母、變音符號等)在不同地區設定下會不一樣。要正確排序,請用 Comparer.Create(...) 並指定文化。

Dictionary 排序不會改變結構——你拿到的永遠是新序列(或新 list)。

複雜排序請用 IComparer<T> 加你自己的邏輯。

這是錯誤排序的範例(實務上常踩雷):


var numbers = new List<int> { 1, 2, 3 };
var sorted = numbers.Sort(); // 錯誤:Sort() 回傳 void!

正確寫法:


numbers.Sort(); // 直接改 numbers

// 如果要新集合:
var sorted = new List<int>(numbers);
sorted.Sort();
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION