or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Syncing
xxxxxxxxxx
Crust of Rust: Sorting Algorithms
直播錄影
Introduction
0:00:00
In this Crust of Rust episode, we implement some common sorting algorithms in Rust. This episode doesn't aim to explain any single concept, but rather showcase what writing "normal" Rust code is like, and explaining various "odd bits" we come across along the way. The thinking here is that sorting algorithms are both familiar and easy to compare across languages, so this might serve as a good bridge into Rust if you are familiar with other languages.
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →排序在電腦科學是相當常見的,雖然你幾乎不需要在編寫程式的時候自己實作排序演算法,事實上由於它常見的特性,所以它很好作為與其他程式語言的比較點。本次直播的真正目的不是教你如何排序,而是讓你看到這些排序演算法是如何在 Rust 使用慣用的手法(idiomatic way) 去實作的,這樣你就可以跟你熟悉的程式語言做比較。
之前直播的教學方式是專注於特定的 Rust 功能,本次直播更關注更廣泛的問題,讓我們對程式碼進行排序,然後看看我們一路上學到了什麼。
Ord and sort in std
0:02:42
Trait std::cmp::Ord
Rust 中的 Ord trait 是一種表達可以相互比較的類型的方式。
本次實作使用 Ord 而不是 PartialOrd,因為如果該集合中某些東西的順序無關緊要,那麼真的很難排序某個東西。不過 Ord 有一些奇怪的屬性,因為 Rust 中的某些類型您可能期望是可排序的,但實際上卻不是,特別是浮點數類型 (f32, f64)。它們實作了 PartialOrd,但沒有實作 Ord,因為浮點數有特別的值叫做 NaN,而 NaN 與 NaN 之間是無法比較的。浮點數類型沒有實作 Eq 的原因也是因為 NaN 的關係,兩個無法比較的東西卻說它們是相等的也是有點奇怪。
搜尋標準函式庫的排序
Current Implementation : The current algorithm is an adaptive, iterative merge sort inspired by timsort.
等等可能會稍微提一下 timsort,如果沒有的話請自行查閱。
而 sort_by 是客製化的比較函式,例如你可以指定比較結構的某欄位,又或是可以反序排列,後者的應用較為常見 :
sort_by_key 的話你要傳入函式,而該函式的回傳值必須有實作 Ord :
Sorting algorithms
0:10:04
我們接下來將要做的事是定義 Sort trait,接著為這個 trait 實作很多不同的排序機制。詳細的排序資訊請參考 : Sorting algorithm。
Bubble sort
0:12:02
首先先看到 Bubble sort,雖然它的 worst case 是 \(n^2\),但它相當的直覺。
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →實作完所有的排序演算法後,我們會再實作非常簡易的 benchmark 用來計數我們實作的每個演算法的比較次數,並將輸入長度和該輸入長度的比較次數印出來。
開始建置 Rust 專案 :
先定義排序演算法的 API :
測試目前 API 的正確性 :
Q : How much harder would it be implement these for Iter (or IterMut) instead of slices?
A : 比較難為 Iter 實作,因為 Iter 的話你不知道你將會得到幾個輸入值,例如 stream,除非你用 Vector 來收集這些 stream。所以為 slice 實作排序的情況比為 Iter 實作排序的情況要為常見。
Q : Random access is very useful in sorting
A : 你基本上都需要 random access。
Q : Could we use ExactSizeIterator?
A : 如果你使用 ExactSizeIterator 並知道總共有多少元素,但最終你必須等到全部的值才能做排序。
開始實作 bubble sort 之前,先 uncomment src/lib.rs 的其中一行 :
接著開啟 src/bubblesort.rs 檔案開始實作原型 :
Q : why does vim say [object Object] in the status bar?
A : 可能 Jon 的 vim 某些設定壞了吧,Jon 也不確定真正原因。
Q : coc should keep rust-analyzer up to date. Does for me
A : Jon 用它的 coc 集手動編譯 rust-analyzer,因為他想要讓他 rust-analyzer 支援最新的 Nightly Rust。
繼續為 BubbleSort 實作 Sorter :
也可以省掉考慮邊界條件判斷 (省 branch),只要調整 for 終止條件即可 :
接著新增 test case :
通過了測試 :
再次更改範圍,程式可能看起來更漂亮 ? :
一般的 swap 需要暫存變數 :
標準函式庫的 slice::swap 函式簽章 :
Q : make sort's generic <S, T> to not require the underscore?
A :
目前程式碼
src/lib.rs :
src/bubblesort.rs :
Insertion sort
0:27:42
Insertion sort 也很直覺。
新增一個模組到 src/lib.rs :
接著開啟 src/insertionsort.rs 檔案開始實作原型 :
繼續為 InsertionSort 實作 Sorter :
Q : why not do Sorter::sort(..) directly without free-standing function? am I missing something?
A : 可以。這樣做可以讓你的程式看起來更漂亮,不過有一個缺點是你要讓 Sorter trait 在 scope 內 :
Q : Wouldn't be more lose if
T: PartialEq
?A : 不行,PartialEq 不能讓你排列。也不能是 PartialOrd,因為你必須在排列的時候必須知道確切的排序規則。
繼續為 InsertionSort 實作 Sorter :
Q : That just sounds like bubble sort going through an identity crisis
A : insertion sort 和 bubble sort 不太相同,而且通常 bubble sort 的表現會更差,因為 bubble sort 每次迭代範圍最多只少掉一個元素,而 insertion sort 只要找到插入點,該次迭代就結束了,所以理論上 bubble sort 的 swap 次數 > insertion sort 的 swap 次數。
如果現在的資料結構是鏈結串列的話,insertion sort 美次迭代實際上就只要找到插入點插入,不需要跟 slice 一樣,將插入點後的元素右移;但如果是 bubble sort 的話每次迭代仍然要慢慢 swap 鏈結串列的節點。所以在鏈結串列上更可以看到 insertion sort 的表現一定會比 bubble sort 還要好。
圖解 insertion sort :

我們的實作是採用 Method 1 一直 swap 到插入點為止。Method 2 是找到插入點之後,將所有元素右移。這兩個方法的執行時間主要取決於輸入資料的特徵,但它們的複雜度是相同的。
通過測試 :
目前程式碼
src/lib.rs :
src/bubblesort.rs :
src/insertionsort.rs :
Q : Would it be cheating to use binary_search + insert?
A : 這麼做可以減少比較次數,不過 swap 元素仍無法避免 :
現在 smart 無論設為
True
還是False
都可以通過測試了 :為了讓程式碼更彈性,首先為 InsertionSort 結構新增成員 :
回頭更改 API,src/lib.rs :
為了一致性,BubbleSort 也要更改,src/bubblesorc.rs :
讓 InsertionSort 使用 smart 成員,並新增一個 test case :
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →::
vs..
你可以將
::
等價於其他程式語言的 classmethod,它是該型別的 associated method;而.
則是該型別的方法,所以我們必須建構出 BubbleSort 才能呼叫它的 sort 方法。Q : what happens if you binary_search() on an unsorted slice?
A : 你只會得到隨機的索引值。
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →兩種都可以,Jon 偏好 match,因為 match 更明確。
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →現在支援 or pattern 的只有在 match 裡面 :
Q : isnt static function when you dont need an instance of the "object" to call that method, sry im noob
A : 是的,你可以將它想為 static 方法。
目前程式碼
src/lib.rs :
src/bubblesort.rs :
src/insertionsort.rs :
Selection sort
0:52:18
Selection sort 比 insertion sort 還要糟糕,但一樣很直覺而且不需要在排序過程中使用到輔助的記憶體。
新增一個模組到 src/lib.rs :
接著開啟 src/selectionsort.rs 檔案開始實作原型 :
繼續為 SelectionSort 實作 Sorter :
目前程式碼
Q : There's a min() on iterators
A : slice 是 iterator,min 會僅回傳最小值,而不是最小值所在的位置 :
使用 enumerate 函式來解決 min 沒辦法回傳最小值位置的問題 :
雖然兩種版本的效能經過編譯器的程式碼最佳化之後效能是差不多的,但迭代器函式版本可以清楚知道每一步在做什麼,所以我們會保留迭代器函式版本。
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →為何 Line 2 正確,Line 錯誤 ? 待解決 !
.min_by_key(|(_, v)| v)
編譯失敗 :min_by_key 的回傳型態是
<Option<Self::Item>>
,而其中的Self::Item
為參考型態,從 min_by_key 的a
需要參考可以得知 :Self::Item
之所以是回傳型態是因為在尋找最小元素的過程中會走訪所有的元素,最後在回傳最小元素的參考。所以在走訪的時候不能將元素的所有權給 closure。v
本身是 slice 的參考,而 tuple 的參考只存活在迭代期間,這個 tuple 的參考活這麼短是因為它是由迭代器產生出來的。我們想要 min_by_key 回傳 slice 的參考,而現在的寫法是會回傳 slice 的參考的參考,當迭代結束時,迭代器的值 (slice 的參考的參考) 早已消失,所以編譯器才會報錯。&
告訴 closure,解參考這個 tuple ((_, v)
) ,並抓到v
本身,而不是v
的參考。使用 map 使實作更加精簡,同時只保留迭代器函式版本 :
目前程式碼
src/selectionsort.rs
Quicksort
1:07:27
Quicksort : 並沒那麼直覺,但也沒那麼複雜。
Q : does the compiler take advantage of assert! statements? like if you do assert!(i < slice.len()); let _ = slice[i]; is it gonna skip the slice range check?
A : Jon 覺得可能會。因為 assert 變成了條件語句,並在條件不成立的 branch 上退出。因此編譯器可以假設以下程式碼中的條件成立。Jon 不確定它在多大程度上利用了這一點。
新增一個模組到 src/lib.rs :
接著開啟 src/quicksort.rs 檔案開始實作原型 :
我們首先實作配置記憶體版本,因為比較好理解,再來實作原地版本。
開始實作配置記憶體版本 quicksort :
我們試圖移出 slice,但我們不被允許移出 slice。因此,我們必須做一些類似的技巧來移動參考,或者像
left
和right
只保存索引值。這樣就開始變得很麻煩,雖然配置記憶體版本並沒有實作完整,但至少現在你有一個 high level 的想法知道 quicksort 在做什麼。Q : Whats the difference between
<T: Ord>
and<T> ... Where T: Ord
?A : 沒有區別。
開始實作原地版本 quicksort,這次我們
left
和right
只用在 slice,而不用像配置記憶體版本用在 vector :這次嘗試將
left
與right
指向 slice 的兩端 :不像前面 for 迴圈的版本在 swap 之後無法查看同一位置,現在的 while 迴圈讓我們可以在 swap 之後繼續查看同一位置 :

邊界條件示意圖 :

Q : you also need to leave a hole for your pivot :s
A : 我們不需要為 pivot 留位置,因為 pivot 已在正確位置上。
Q : the swap line is wrong, you are swapping beyond the end of the slice
A : 沒有錯,
right
的值是slice.len() - 1
。剛剛的實作並沒有排序完,我們必須遞迴的去呼叫 quicksort 函式 :
Q : Could you add an extra ifelse branch to just move the right counter without swap if it's already on the right side?
A : 如下 :
若不使用新增條件會造成不必要的交換,如下面動畫的

5
就多做了 1 次沒必要的交換 :Q : why are we splitting again if we already sorted rest?
A : 我們還沒到
rest
做排序,只是將分成大的一堆,小的一堆而已。Q : Instead of while+<=, loop+if left == right { break; } can also be used
A : 我們現在要的是 left 與 right 相等的情況下,處理完再退出;如果用後者的寫法會再 left 與 right 相等的情況下就直接退出了,是可以這樣寫,但條件式要稍微修改,寫起來會比較不好看一點。
Q : Basically we just want to swap elements that are on the wrong side. So we can just move the counters if they are already on the correct side.
A : 對,如下 :
目前程式碼
src/quicksort.rs :
並沒有通過測試 :
pivot 應該最終會位於陣列中最終排序的位置。但我們現在的作法是,我們選擇 pivot 作為第一個元素,然而我們最後沒有移動 pivot。因此 pivot 仍包含在兩個 slice 中。因此,我們的方法是將 pivot 明確放置在最終排序的位置 :
Q : to avoid this you can choose the pivot at the middle of the slice, then you about some moves.
A : Jon 認為並非事實。如果你選擇中間的 pivot,您可能會遇到必須移動 pivot 位置的情形,這聽起來更煩人。
Q : partition_at_index used instead of split_at_mut will nicely exclude the pivot
A : 這個函式還幫你排序,這樣就作弊了 :
目前程式碼
src/quicksort.rs :
通過測試 :
Benchmarking
1:46:22
創建 benckmark 目錄以及檔案 :
本次要評估的是比較次數,而不是複雜度。
新增程式碼到 src/lib.rs 使得當匯入 orst 函式庫時,會自動匯入我們實作的排序演算法 :
開始實作 src/bin/main.rs :
Q : will we also count swaps?
A : 因為當兩個值交換的時候,並沒有方法被呼叫。所以我們事實上沒有辦法可以計算交換次數。我們需要實作與我們合作。我們可以有一個實作應該呼叫的觸發器,但這似乎有點煩人。
Q : was this faster than just using criterion?
A : Jon 這裡不想使用 criterion 是因為它帶來了更多的複雜性,在這種情況下並不那麼有趣,而且因為 criterion 不會衡量比較次數而是給我們是執行時間的估計值。我們可以在這裡測量執行時間,但我們現在想看看複雜度,這與執行時間有關,但並不完全相同。
Q : Can't the increment ordering be Relaxed, since you're not locking anything?
A : 可以,因為我們的效能評估程式是單執行緒的。雖然 ArcRel 的使用會帶來額外的開銷,但我們現在要測量的是比較次數,所以執行時間比較長沒關係。
ArcRel
都換成Relaxed
:實作 main 函式 :
Q : is there a performance difference between using
atomicusize
andcell<usize>
if u only use 1 thread?A : 有一點差異。我們的實作可以這麼做,因為我們現在不需要 thread safe :
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →Q : why is it not object safe tho
A : Sorter 不是 object safe 的原因是因為我們有一個 generic method。
Q : Can’t you Box the Sorter?
A : 因為 Sorter 有 generic method,所以我們不能 box 它。
解決編譯不過的問題,改用呼叫函式的方法。另外寫一個 bench 函式 :
回頭修改 main 函式,使用函式呼叫,而不是用到 closure :
Q : can't you simply sort
SortEvaluator<T>
instead ofT
?A :修改 bench 的函式簽章 :
目前程式碼
現在問題變成是,我們如何建構我們實際要測試的東西? 先匯入 rand crate,修改 orst/Cargo.toml :
src/bin/main.rs 開頭匯入 rand crate :
接著修改 benches/main.rs 的 main 函式 : 2:14:55~~~
目前程式碼
src/bin/main.rs :
Quicksort underflow
2:06:20
測試發現我們的 quick sort 有 underflow 的問題 :
(\(2^{64}=18446744073709551616\))
Q : why not work in debug mode?
src/quicksort.rs :
A : 因為在 DEBUG 模式下,Rust 會因為 overflow 和 underflow 的發生而 panic。
在解決 quicksort underflow 之前,先改一下 src/bin/main.rs 產生隨機亂數資料的方法 :
開始解決
right
遇到 underflow 的問題 (不是最佳解),修改 src/quicksort.rs 的 quicksort 函式 :目前程式碼
src/bin/main.rs :
src/quicksort.rs :
Back to benchmarking
2:13:46
測試發現 bubble, insertion-dumb, 以及 quick 的比較次數為 0 :
因為它們都是使用 partial_cmp。Rust 中的
<
運算子和>
運算子都會呼叫 partial_cmp,而不是 cmp。有兩種方法可以解決這個問題。一種是不使用這些運算子,直接使用呼叫 cmp。另一個是將其移至 partial_cmp,我們採用第二種方法,修改 src/bin/main.sr 檔案 :Q : Is there a reason the loop body needs to run if left==right? Can't you just replace the loop condition with left < right?
A : 有,因為
left==right
的時候還沒結束,因為還有一個元素我們沒查看到。Q : Is incrementing left correct in the else statement too? Don't we need to check it again if we've swapped it?
A : 增加
left
值是 ok,雖然理論上left
有可能 overflow,但實際上 overflow 並不太可能發生,因為要 overflow 的話數字要很大,而 underflow 的話數字不是很大,只要是小陣列就有可能觸發 underflow。Q : can you add a check that the list is sorted after bench? just to be sure
A : 可以。
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →Q : can't you increment on both Ords, since the Ord implementation will never call ord on the wrapper but only on T?
A : 我們可以這麼做,因為這最終會重定向,或者最終會直接呼叫 T。修改 src/bin/main.rs :
Equality 也可以做相同的事。修改 src/bin/main.rs :
Q : wont checking if array is sorted also increment the counter?
A : 對,修改 src/bin/main.rs :
修正完之後再次測試 :
目前程式碼 (Tag)
src/bin/main.rs :
src/lib.rs :
將評估結果導到 values.dat :
Q : how bad is using nightly for an binary?
A : 如果不需要用就不要用。
Evaluating the results
2:18:56
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →執行以下程式沒辦法得到我們想要的結果,必須要用比較複雜的語法才可以,原本是希望第 1 欄當作 title,第 2 以及第 3 欄當作橫軸以及縱軸的資料 :
產出的圖如下 :

放棄使用 gnuplot,改用 R 語言的 ggplot2 package 來畫圖。
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →開始畫圖,方式有兩種 :
從圖可以看到,bubble sort 表現的最爛,insertion-smart 以及 quick sort 表現的最好 :

目前程式碼
搜尋 "目前程式碼 (Tag)"
Q : we have now pivoted to the crust of R episode
A : XDD
多測一個標準函式庫的排序演算法
src/lib.rs :
修改 src/bin/main.rs 的 main 函式 :
再次輸出並畫圖 :
目前程式碼
src/bin/main.rs :
*src/lib.rs :
plot.r :
將圖取對數,修改 plot.r :
再次執行腳本產生圖 :
改取 log 之後發現,insertion-smart 的比較次數其實比 quick 還要少,感覺很奇怪,不過這裡是衡量比較次數而已,並沒有把交換的次數考慮進來,所以其實還是 quick 會比較快 :

目前程式碼
src/bin/main.rs :
*src/lib.rs :
plot.r :
接著測試執行時間,修改 src/bin/main.rs 的 main 函式以及 bench 函式 :
修改腳本:
再次輸出並畫圖 :
目前程式碼
src/bin/main.rs :
src/lib.rs :
plot.r :
Q : could we try an all decreasing slice, I think quick gets pretty bad with that
A : 確實輸入資料為反序的話 quick sort 表現很差,但我們這次直播主要並不是討論排序演算法本身,而是如何用 Rust 去實作排序法,所以我們才隨機的產生資料。實際上你也會想要用到 Timsort 之類的東西,而不是使用我們實作的其中任何一個。
待整理