原文鏈接:https://gopl-zh.github.io/ch7/ch7-06.html
排序操作和字符串格式化一樣是很多程序經(jīng)常使用的操作。盡管一個(gè)最短的快排程序只要15行就可以搞定,但是一個(gè)健壯的實(shí)現(xiàn)需要更多的代碼,并且我們不希望每次我們需要的時(shí)候都重寫(xiě)或者拷貝這些代碼。
幸運(yùn)的是,sort包內(nèi)置的提供了根據(jù)一些排序函數(shù)來(lái)對(duì)任何序列排序的功能。它的設(shè)計(jì)非常獨(dú)到。在很多語(yǔ)言中,排序算法都是和序列數(shù)據(jù)類(lèi)型關(guān)聯(lián),同時(shí)排序函數(shù)和具體類(lèi)型元素關(guān)聯(lián)。相比之下,Go語(yǔ)言的sort.Sort函數(shù)不會(huì)對(duì)具體的序列和它的元素做任何假設(shè)。相反,它使用了一個(gè)接口類(lèi)型sort.Interface來(lái)指定通用的排序算法和可能被排序到的序列類(lèi)型之間的約定。這個(gè)接口的實(shí)現(xiàn)由序列的具體表示和它希望排序的元素決定,序列的表示經(jīng)常是一個(gè)切片。
一個(gè)內(nèi)置的排序算法需要知道三個(gè)東西:序列的長(zhǎng)度,表示兩個(gè)元素比較的結(jié)果,一種交換兩個(gè)元素的方式;這就是sort.Interface的三個(gè)方法:
package sort
type Interface interface {
Len() int
Less(i, j int) bool // i, j are indices of sequence elements
Swap(i, j int)
}
為了對(duì)序列進(jìn)行排序,我們需要定義一個(gè)實(shí)現(xiàn)了這三個(gè)方法的類(lèi)型,然后對(duì)這個(gè)類(lèi)型的一個(gè)實(shí)例應(yīng)用sort.Sort函數(shù)。思考對(duì)一個(gè)字符串切片進(jìn)行排序,這可能是最簡(jiǎn)單的例子了。下面是這個(gè)新的類(lèi)型StringSlice和它的Len,Less和Swap方法
type StringSlice []string
func (p StringSlice) Len() int { return len(p) }
func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
現(xiàn)在我們可以通過(guò)像下面這樣將一個(gè)切片轉(zhuǎn)換為一個(gè)StringSlice類(lèi)型來(lái)進(jìn)行排序:
sort.Sort(StringSlice(names))
這個(gè)轉(zhuǎn)換得到一個(gè)相同長(zhǎng)度,容量,和基于names數(shù)組的切片值;并且這個(gè)切片值的類(lèi)型有三個(gè)排序需要的方法。
對(duì)字符串切片的排序是很常用的需要,所以sort包提供了StringSlice類(lèi)型,也提供了Strings函數(shù)能讓上面這些調(diào)用簡(jiǎn)化成sort.Strings(names)。
這里用到的技術(shù)很容易適用到其它排序序列中,例如我們可以忽略大小寫(xiě)或者含有的特殊字符。(本書(shū)使用Go程序?qū)λ饕~和頁(yè)碼進(jìn)行排序也用到了這個(gè)技術(shù),對(duì)羅馬數(shù)字做了額外邏輯處理。)對(duì)于更復(fù)雜的排序,我們使用相同的方法,但是會(huì)用更復(fù)雜的數(shù)據(jù)結(jié)構(gòu)和更復(fù)雜地實(shí)現(xiàn)sort.Interface的方法。
我們會(huì)運(yùn)行上面的例子來(lái)對(duì)一個(gè)表格中的音樂(lè)播放列表進(jìn)行排序。每個(gè)track都是單獨(dú)的一行,每一列都是這個(gè)track的屬性像藝術(shù)家,標(biāo)題,和運(yùn)行時(shí)間。想象一個(gè)圖形用戶界面來(lái)呈現(xiàn)這個(gè)表格,并且點(diǎn)擊一個(gè)屬性的頂部會(huì)使這個(gè)列表按照這個(gè)屬性進(jìn)行排序;再一次點(diǎn)擊相同屬性的頂部會(huì)進(jìn)行逆向排序。讓我們看下每個(gè)點(diǎn)擊會(huì)發(fā)生什么響應(yīng)。
下面的變量tracks包含了一個(gè)播放列表。(One of the authors apologizes for the other author’s musical tastes.)每個(gè)元素都不是Track本身而是指向它的指針。盡管我們?cè)谙旅娴拇a中直接存儲(chǔ)Tracks也可以工作,sort函數(shù)會(huì)交換很多對(duì)元素,所以如果每個(gè)元素都是指針而不是Track類(lèi)型會(huì)更快,指針是一個(gè)機(jī)器字碼長(zhǎng)度而Track類(lèi)型可能是八個(gè)或更多。
gopl.io/ch7/sorting
type Track struct {
Title string
Artist string
Album string
Year int
Length time.Duration
}
var tracks = []*Track{
{"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")},
{"Go", "Moby", "Moby", 1992, length("3m37s")},
{"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")},
{"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")},
}
func length(s string) time.Duration {
d, err := time.ParseDuration(s)
if err != nil {
panic(s)
}
return d
}
printTracks函數(shù)將播放列表打印成一個(gè)表格。一個(gè)圖形化的展示可能會(huì)更好點(diǎn),但是這個(gè)小程序使用text/tabwriter包來(lái)生成一個(gè)列整齊對(duì)齊和隔開(kāi)的表格,像下面展示的這樣。注意到*tabwriter.Writer
是滿足io.Writer接口的。它會(huì)收集每一片寫(xiě)向它的數(shù)據(jù);它的Flush方法會(huì)格式化整個(gè)表格并且將它寫(xiě)向os.Stdout(標(biāo)準(zhǔn)輸出)。
func printTracks(tracks []*Track) {
const format = "%v\t%v\t%v\t%v\t%v\t\n"
tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0)
fmt.Fprintf(tw, format, "Title", "Artist", "Album", "Year", "Length")
fmt.Fprintf(tw, format, "-----", "------", "-----", "----", "------")
for _, t := range tracks {
fmt.Fprintf(tw, format, t.Title, t.Artist, t.Album, t.Year, t.Length)
}
tw.Flush() // calculate column widths and print table
}
為了能按照Artist字段對(duì)播放列表進(jìn)行排序,我們會(huì)像對(duì)StringSlice那樣定義一個(gè)新的帶有必須的Len,Less和Swap方法的切片類(lèi)型。
type byArtist []*Track
func (x byArtist) Len() int { return len(x) }
func (x byArtist) Less(i, j int) bool { return x[i].Artist < x[j].Artist }
func (x byArtist) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
為了調(diào)用通用的排序程序,我們必須先將tracks轉(zhuǎn)換為新的byArtist類(lèi)型,它定義了具體的排序:
sort.Sort(byArtist(tracks))
在按照artist對(duì)這個(gè)切片進(jìn)行排序后,printTrack的輸出如下
Title Artist Album Year Length
----- ------ ----- ---- ------
Go Ahead Alicia Keys As I Am 2007 4m36s
Go Delilah From the Roots Up 2012 3m38s
Ready 2 Go Martin Solveig Smash 2011 4m24s
Go Moby Moby 1992 3m37s
如果用戶第二次請(qǐng)求“按照artist排序”,我們會(huì)對(duì)tracks進(jìn)行逆向排序。然而我們不需要定義一個(gè)有顛倒Less方法的新類(lèi)型byReverseArtist,因?yàn)閟ort包中提供了Reverse函數(shù)將排序順序轉(zhuǎn)換成逆序。
sort.Sort(sort.Reverse(byArtist(tracks)))
在按照artist對(duì)這個(gè)切片進(jìn)行逆向排序后,printTrack的輸出如下
Title Artist Album Year Length
----- ------ ----- ---- ------
Go Moby Moby 1992 3m37s
Ready 2 Go Martin Solveig Smash 2011 4m24s
Go Delilah From the Roots Up 2012 3m38s
Go Ahead Alicia Keys As I Am 2007 4m36s
sort.Reverse函數(shù)值得進(jìn)行更近一步的學(xué)習(xí),因?yàn)樗褂昧耍ā?.3)章中的組合,這是一個(gè)重要的思路。sort包定義了一個(gè)不公開(kāi)的struct類(lèi)型reverse,它嵌入了一個(gè)sort.Interface。reverse的Less方法調(diào)用了內(nèi)嵌的sort.Interface值的Less方法,但是通過(guò)交換索引的方式使排序結(jié)果變成逆序。
package sort
type reverse struct{ Interface } // that is, sort.Interface
func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
func Reverse(data Interface) Interface { return reverse{data} }
reverse的另外兩個(gè)方法Len和Swap隱式地由原有內(nèi)嵌的sort.Interface提供。因?yàn)閞everse是一個(gè)不公開(kāi)的類(lèi)型,所以導(dǎo)出函數(shù)Reverse返回一個(gè)包含原有sort.Interface值的reverse類(lèi)型實(shí)例。
為了可以按照不同的列進(jìn)行排序,我們必須定義一個(gè)新的類(lèi)型例如byYear:
type byYear []*Track
func (x byYear) Len() int { return len(x) }
func (x byYear) Less(i, j int) bool { return x[i].Year < x[j].Year }
func (x byYear) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
在使用sort.Sort(byYear(tracks))按照年對(duì)tracks進(jìn)行排序后,printTrack展示了一個(gè)按時(shí)間先后順序的列表:
Title Artist Album Year Length
----- ------ ----- ---- ------
Go Moby Moby 1992 3m37s
Go Ahead Alicia Keys As I Am 2007 4m36s
Ready 2 Go Martin Solveig Smash 2011 4m24s
Go Delilah From the Roots Up 2012 3m38s
對(duì)于我們需要的每個(gè)切片元素類(lèi)型和每個(gè)排序函數(shù),我們需要定義一個(gè)新的sort.Interface實(shí)現(xiàn)。如你所見(jiàn),Len和Swap方法對(duì)于所有的切片類(lèi)型都有相同的定義。下個(gè)例子,具體的類(lèi)型customSort會(huì)將一個(gè)切片和函數(shù)結(jié)合,使我們只需要寫(xiě)比較函數(shù)就可以定義一個(gè)新的排序。順便說(shuō)下,實(shí)現(xiàn)了sort.Interface的具體類(lèi)型不一定是切片類(lèi)型;customSort是一個(gè)結(jié)構(gòu)體類(lèi)型。
type customSort struct {
t []*Track
less func(x, y *Track) bool
}
func (x customSort) Len() int { return len(x.t) }
func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) }
func (x customSort) Swap(i, j int) { x.t[i], x.t[j] = x.t[j], x.t[i] }
讓我們定義一個(gè)多層的排序函數(shù),它主要的排序鍵是標(biāo)題,第二個(gè)鍵是年,第三個(gè)鍵是運(yùn)行時(shí)間Length。下面是該排序的調(diào)用,其中這個(gè)排序使用了匿名排序函數(shù):
sort.Sort(customSort{tracks, func(x, y *Track) bool {
if x.Title != y.Title {
return x.Title < y.Title
}
if x.Year != y.Year {
return x.Year < y.Year
}
if x.Length != y.Length {
return x.Length < y.Length
}
return false
}})
這下面是排序的結(jié)果。注意到兩個(gè)標(biāo)題是“Go”的track按照標(biāo)題排序是相同的順序,但是在按照year排序上更久的那個(gè)track優(yōu)先。
Title Artist Album Year Length
----- ------ ----- ---- ------
Go Moby Moby 1992 3m37s
Go Delilah From the Roots Up 2012 3m38s
Go Ahead Alicia Keys As I Am 2007 4m36s
Ready 2 Go Martin Solveig Smash 2011 4m24s
盡管對(duì)長(zhǎng)度為n的序列排序需要 O(n log n)次比較操作,檢查一個(gè)序列是否已經(jīng)有序至少需要n-1次比較。sort包中的IsSorted函數(shù)幫我們做這樣的檢查。像sort.Sort一樣,它也使用sort.Interface對(duì)這個(gè)序列和它的排序函數(shù)進(jìn)行抽象,但是它從不會(huì)調(diào)用Swap方法:這段代碼示范了IntsAreSorted和Ints函數(shù)在IntSlice類(lèi)型上的使用:
values := []int{3, 1, 4, 1}
fmt.Println(sort.IntsAreSorted(values)) // "false"
sort.Ints(values)
fmt.Println(values) // "[1 1 3 4]"
fmt.Println(sort.IntsAreSorted(values)) // "true"
sort.Sort(sort.Reverse(sort.IntSlice(values)))
fmt.Println(values) // "[4 3 1 1]"
fmt.Println(sort.IntsAreSorted(values)) // "false"
為了使用方便,sort包為[]int、[]string和[]float64的正常排序提供了特定版本的函數(shù)和類(lèi)型。對(duì)于其他類(lèi)型,例如[]int64或者[]uint,盡管路徑也很簡(jiǎn)單,還是依賴(lài)我們自己實(shí)現(xiàn)。
練習(xí) 7.8: 很多圖形界面提供了一個(gè)有狀態(tài)的多重排序表格插件:主要的排序鍵是最近一次點(diǎn)擊過(guò)列頭的列,第二個(gè)排序鍵是第二最近點(diǎn)擊過(guò)列頭的列,等等。定義一個(gè)sort.Interface的實(shí)現(xiàn)用在這樣的表格中。比較這個(gè)實(shí)現(xiàn)方式和重復(fù)使用sort.Stable來(lái)排序的方式。
練習(xí) 7.9: 使用html/template包(§4.6)替代printTracks將tracks展示成一個(gè)HTML表格。將這個(gè)解決方案用在前一個(gè)練習(xí)中,讓每次點(diǎn)擊一個(gè)列的頭部產(chǎn)生一個(gè)HTTP請(qǐng)求來(lái)排序這個(gè)表格。
練習(xí) 7.10: sort.Interface類(lèi)型也可以適用在其它地方。編寫(xiě)一個(gè)IsPalindrome(s sort.Interface) bool函數(shù)表明序列s是否是回文序列,換句話說(shuō)反向排序不會(huì)改變這個(gè)序列。假設(shè)如果!s.Less(i, j) && !s.Less(j, i)則索引i和j上的元素相等。
![]() | ![]() |
更多建議: