排序這個動作在軟體開發中常常會使用到, 從使用者期望所見的順序到資料處理的效能議題都與排序息息相關, 因此掌握程式語言的排序功能是非常重要的一個環節, 而我們在閱讀他人的Go專案程式碼時也會看到排序的方式有些許不同, 那究竟有何差異呢? 就讓我們繼續看下去吧…
其實在進入今天的主題之前, 我們心中只要記得排序方式不同的主因主要有兩點不同, 「快速簡易」與「高度客製化」這兩個需求的差異之下衍生了不同的排序方式, 其實軟體寫久了大家會覺得程式語言與真實需求的許多場景息息相關, 畢竟需求是「人」衍生出來的, 而我們「人」所設計的程式語言也是圍繞著「需求」而生。
假設我們定義了一個User的struct, 將儲存姓名與年齡。
type User struct {
Name string
Age int
}
那我們建立幾個user如下:
func main() {
// 創建包含 User 結構的切片
users := []User{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
}
Q: 我們今天收到一個需求, 請對於「年齡(Age)」進行升冪(由低到高)的排序, 請問應該怎麼做?
首先我們可以先看看Slice函數, 它提供了自訂比較函式的功能, 因此我們可以基於這樣的功能來設計我們想要怎麼排, 由於內建並沒有很直接的對struct資料結構進行排序的功能, 因此我們可以利用「sort.Slice」的特性來進行, 函式的使用大致如下:
// x 欲排序的結構目標
// less 自訂比較函式
func Slice(x any, less func(i, j int) bool) {
...
}
理解了使用方式之後我們就可以這樣排:
// 使用 sort.Slice 函數對切片進行排序
sort.Slice(users, func(i, j int) bool {
return users[i].Age < users[j].Age
})
接著印出結果:
// 打印排序後的切片
fmt.Println("按年齡排序:")
for _, user := range users {
fmt.Printf("Name: %s, Age: %d\\n", user.Name, user.Age)
}
但這邊要特別注意的地方是官方的說明文件(https://pkg.go.dev/sort#Slice)中指出「這種排序不保證穩定性, 也就是兩個元素等值時, 每次執行後的結果不一定都相同」, 對於穩定的排序結果請使用「SliceStable」。
// 使用 sort.Slice 函數對切片進行排序
sort.SliceStable(users, func(i, j int) bool {
return users[i].Age < users[j].Age
})
我們都知道, 排序演算法裡面最重要的三個部份就是:
以上三點我們可以將原本的struct進行三種功能的擴充, 而以下的程式碼是我們標準的排序方式。
type Users []User
// Len 方法返回切片的長度
func (u Users) Len() int {
return len(u)
}
// Less 方法定義了排序的規則,這裡按照 Age 升序排序
func (u Users) Less(i, j int) bool {
return u[i].Age < u[j].Age
}
// Swap 方法交換切片中兩個元素的位置
func (u Users) Swap(i, j int) {
u[i], u[j] = u[j], u[i]
}
設計完排序的長度、規則、換法之後, 實際上就可以很容易的進行Sort….
func main() {
...
// 使用 sort.Sort 函數來排序,使用自定義排序方式
sort.Sort(users)
fmt.Println("按年齡排序:")
for _, user := range users {
fmt.Printf("Name: %s, Age: %d\\n", user.Name, user.Age)
}
}
看到這邊您可能會疑惑, 為什麼要多此一舉, 不用前面所介紹的「sort.SliceStable」或者「sort.Slice」就好了呢? 因為我們總是看到事情的某個面位, 事實上面對於複雜的案例, 為了可讀性與不同的排序法則時我們就會需要這樣設計了。
假設你正在開發一個電子商務網站,你有一個商品結構體和一個購物車結構體,你希望能夠根據不同條件對購物車中的商品進行排序,例如按照價格、按照銷量、按照名稱等。
package main
import (
"fmt"
"sort"
)
type Product struct {
Name string
Price float64
Quantity int
}
type ShoppingCart struct {
Items []Product
}
// 替購物車定義一個排序的介面
type Sorter interface {
Len() int
Swap(i, j int)
Less(i, j int) bool
}
// 設計一個按照價格排序的類別
type ByPrice ShoppingCart
func (s ByPrice) Len() int { return len(s.Items) }
func (s ByPrice) Swap(i, j int) { s.Items[i], s.Items[j] = s.Items[j], s.Items[i] }
func (s ByPrice) Less(i, j int) bool { return s.Items[i].Price < s.Items[j].Price }
// 設計一個按銷量排序的類別
type ByQuantity ShoppingCart
func (s ByQuantity) Len() int { return len(s.Items) }
func (s ByQuantity) Swap(i, j int) { s.Items[i], s.Items[j] = s.Items[j], s.Items[i] }
func (s ByQuantity) Less(i, j int) bool { return s.Items[i].Quantity < s.Items[j].Quantity }
func main() {
cart := ShoppingCart{
Items: []Product{
{"Laptop", 1000.0, 50},
{"Phone", 500.0, 100},
{"Tablet", 300.0, 30},
},
}
// 按照價格排序
sort.Sort(ByPrice(cart))
fmt.Println("按價格排序:", cart.Items)
// 按照銷量排序
sort.Sort(ByQuantity(cart))
fmt.Println("按銷量排序:", cart.Items)
}
在上面的範例中,我們為購物車定義了兩種不同的排序方式,以價格(ByPrice)和依照銷售(ByQuantity),這兩種排序方式都實作了Sorter介面的方法,然後,我們可以使用sort.Sort函數來對購物車中的商品進行排序。
軟體開發的過程中常常會遇到的就是排序問題,基本上目前程式語言大部分都已經將排序功能封裝的非常易用了,我們不需要從頭到尾實作演算法,雖然如此,但我們還是得學會基本的原理以及這些API的使用方式,而每個語言的排序API又有些差異,但相信我們只要掌握核心就能夠得心應手。