寫在前面 整個項目都托管在了 Github 上:https://github.com/ikesnowy/Algorithms-4th-Edition-in-Csharp 這一節內容可能會用到的庫文件有 Sort和 SortData,同樣在 Github 上可以找到。 善用 Ctrl + F 查找題目 ...
寫在前面
整個項目都托管在了 Github 上:https://github.com/ikesnowy/Algorithms-4th-Edition-in-Csharp
這一節內容可能會用到的庫文件有 Sort和 SortData,同樣在 Github 上可以找到。
善用 Ctrl + F 查找題目。
習題&題解
2.1.1
題目
按照演算法 2.1 所示軌跡的格式給出選擇排序是如何將數組 E A S Y Q U E S T I O N 排序的。
解答
2.1.2
題目
在選擇排序中,一個元素最多可能會被交換多少次?平均可能會被交換多少次?
解答
最多會被交換 n 次,只要將一個有序數列迴圈右移一位就可以構造這樣的情況。
例如:
平均每個元素被交換了 N/N=1 次。(總共 N 個元素,總共發生了 N 次交換)
2.1.3
題目
構造一個含有 N 個元素的數組,使選擇排序(演算法 2.1)運行過程中 a[j] < a[min] (由此 min 會不斷更新)成功的次數最大。
解答
你需要一個逆序的數組。
例如:9 8 7 6 5 4 3 2 1
i=0 條件滿足 8 次,1 和 9 交換,1 8 7 6 5 4 3 2 9。
i=1 條件滿足 6 次,2 和 8 交換,1 2 7 6 5 4 3 8 9。
i=2 條件滿足 4 次,3 和 7 交換,1 2 3 6 5 4 7 8 9。
i=3 條件滿足 2 次,4 和 6 交換。1 2 3 4 5 6 7 8 9。
一共滿足了 8+6+4+2=20 次
2.1.4
題目
按照演算法 2.2 所示軌跡的格式給出插入排序是如何將數組 E A S Y Q U E S T I O N 排序的。
解答
2.1.5
題目
構造一個含有 N 個元素的數組,使插入排序(演算法 2.2)運行過程中內迴圈(for)的兩個判斷結果總是假。
解答
條件是:
j > 0 && less(a[j], a[j - 1])
第一個條件屬於迴圈計數用的條件,與數組元素無關;
第二個條件當 a[j] 和 a[j - 1] 是一組逆序對時滿足,因此這個條件總是為假 = 數組沒有逆序對 = 數組有序。
因此只要輸入已經排好序的數組即可。
逆序對:指序列中順序相反的兩個數,例如 1 2 3 4 5 7 6 8 9 中的 7 6。
2.1.6
題目
在所有主鍵都相同時,選擇排序和插入排序誰更快?
解答
插入排序更快。
選擇排序無論如何都需要 n + (n-1) + (n-2) + … + 1 = n^2/2 次比較。
插入排序在這種情況下只需要 n 次比較。(所有主鍵相同 = 數組已排序)
2.1.7
題目
對於逆序數組,選擇排序和插入排序誰更快?
解答
假設比較的開銷小於等於交換的開銷,此時選擇排序更快,具體比較見下表。
比較次數 | 交換次數 | |
插入排序 | ~N^2/2 | ~N^2/2 |
選擇排序 | ~N^2/2 | N |
2.1.8
題目
假設元素只可能有三種值,使用插入排序處理這樣一個隨機數組的運行時間是線性的還是平方級別的?或是介於兩者之間?
解答
平方級別。
如果數組中元素各不相同,那麼這個結論很容易證明(一般的插入排序)。
接下來我們證明有重覆元素的情況下,這個結論仍然成立:
首先對於插入排序過程中的某一時刻,我們有下圖這樣的一般情況:
其中,1,2,3 分別代表三種不同的取值以及其先後順序。
假設這是第 i 次插入前,如果第 i 次插入的是 1,我們需要交換 b+c 次,插入 2 則需要交換 c 次,插入 3 則不需要交換。
根據題意,這是一個隨機數組,我們假設其為均勻分佈,那麼三種取值的出現幾率相等。
第 i 次插入所需要的平均交換次數即為:
第 i 次插入後,b + 2c 視插入的元素不同會出現不同的變化:
如果插入的是 1,那麼 b+2c 的值不會變化。
如果插入的是 2,那麼 b+2c 的值增加 1。
如果插入的是 3,那麼 b+2c 的值增加 2。
同樣由於三種取值的概率相等,我們得出第 i + 1 次插入平均需要交換的次數為:
也就是說,平均每次插入都會使下一次插入的交換次數增加 1/3。
令 i=0,此時交換次數為 0,i+1 的交換次數即為 1/3,i+2 的交換次數即為 2/3,以此類推。
我們可以得出總交換次數:
由此證明,在元素取值為 3 種且出現概率相等時,插入排序的交換開銷時平方級別的。
比較開銷和交換開銷類似,一般情況下比較次數=交換次數+1,除非插入的數是已知最小的數(移動到最左側),這個時候比較次數和交換次數相等。
因此比較次數=交換次數+N-e,e 是一個不大於 N 的數,代表插入的數是已知最小的數這種情況發生的次數。
根據上式可以得出結論:在元素取值為 3 種且出現概率相等時,插入排序的比較開銷也是平方級別的。
綜合兩個結論即可證明插入排序的開銷在題目描述的情況下是平方級別的。
證明完畢。
2.1.9
題目
按照演算法 2.3 所示軌跡的格式給出希爾排序是如何將數組 E A S Y S H E L L S O R T Q U E S T I O N 排序的。
解答
2.1.10
題目
在希爾排序中為什麼在實現 h 有序時不使用選擇排序?
解答
對於部分有序的數組,插入排序比選擇排序快。
這個結論可以在中文版 P158, 英文版 P252 找到。
2.1.11
題目
將希爾排序中實時計算遞增序列改為預先計算並存儲在一個數組中。
解答
希爾排序的官方實現:https://algs4.cs.princeton.edu/21elementary/Shell.java.html
只要稍作修改即可,詳情見代碼。
代碼
/// <summary> /// 利用希爾排序將數組按升序排序。 /// </summary> /// <param name="a">需要排序的數組。</param> public override void Sort<T>(T[] a) { int n = a.Length; int[] h = new int[2]; // 預先準備好的 h 值數組 int hTemp = 1; int sequenceSize = 0; for (sequenceSize = 0; hTemp < n; sequenceSize++) { if (sequenceSize >= h.Length) // 如果數組不夠大則雙倍擴容 { int[] expand = new int[h.Length * 2]; for (int j = 0; j < h.Length; j++) { expand[j] = h[j]; } h = expand; } h[sequenceSize] = hTemp; hTemp = hTemp * 3 + 1; } for (int t = sequenceSize - 1; t >= 0; t--) { for (int i = h[t]; i < n; i++) { for (int j = i; j >= h[t] && Less(a[j], a[j - h[t]]); j -= h[t]) { Exch(a, j, j - h[t]); } } Debug.Assert(IsHSorted(a, h[t])); } Debug.Assert(IsSorted(a)); }
2.1.12
題目
令希爾排序列印出遞增序列的每個元素所帶來的比較次數和數組大小的比值。
編寫一個測試用例對隨機 Double 數組進行希爾排序,驗證該值是一個小常數,數組大小按照 10 的冪次遞增,不小於 100。
解答
結果截圖如下,同一個 h 值對應的比值在數組大小不同時保持為一個小常數:
代碼
class Program { // 查看最後結果 // 可以發現相同的 h 在數組大小不同時所產生的比值十分接近。 static void Main(string[] args) { Random random = new Random(); ShellSort sort = new ShellSort(); int size = 100; for (int i = 0; i < 5; i++) { double[] a = new double[size]; for (int j = 0; j < size; j++) { a[j] = random.NextDouble() * 100; } Console.WriteLine("ArraySize:" + size); sort.Sort(a); size *= 10; } } }
2.1.13
題目
紙牌排序。
說說你會如何將一副撲克牌按花色排序(花色排序是黑桃、紅桃、梅花和方片),
限制條件是所有牌都是背面朝上排成一列,而你一次只能翻看兩張牌或者交換兩張牌(保持背面朝上)。
解答
可以用冒泡排序做,具體方法如下:
翻一二兩張,是逆序對就交換,否則什麼也不做
翻二三兩張,是逆序對就交換,否則什麼也不做
一直到最後,可以保證最右側的是最大花色的牌
然後不斷重覆上述過程,就可以完全排序
2.1.14
題目
出列順序。
說說你會如何將一副撲克牌排序,
限制條件是只能查看最上面的兩張牌,交換最上面的兩張牌,或是將最上面的一張牌放到這摞牌的最下麵。
解答
用一種類似於冒泡的方法做,具體步驟為:
重覆以下步驟,直到全部完成一遍之後沒有發生交換 重覆以下步驟 n-1 次 如果頂端兩張牌逆序,那麼交換它們。 將第一張牌放到牌堆底部。
具體步驟圖:
我們將牌排成一個環,用一支筆隔開,這裡我們標記筆的左側是牌堆頂部,右側是牌堆底部。
那麼我們能做的三個操作在這裡即為:
查看最上面兩張牌 = 從筆的位置開始,逆時針查看兩張牌。
交換最上面兩張牌 = 從筆的位置開始,逆時針選擇兩張牌並交換。
將最上面的一張牌放到最下麵 = 將筆的位置逆時針移動一位。
下麵我們開始執行開始說過的操作,目標順序是自頂向下從小到大排列。
初始情況如圖所示:
梅花7 和 紅桃4 不是逆序對,直接將筆逆時針移動一位。
紅桃4 和 黑桃6 不是逆序對,我們將筆逆時針移動一位。
再看 黑桃6 和 方片A,是逆序對,我們交換並將筆逆時針移動一位。
再看 黑桃6 和 紅桃J,是逆序對,我們交換並將筆逆時針移動一位。
現在我們已經操作了 4 次,內部迴圈結束,我們將筆放回初始位置。
這樣一次迴圈之後,我們就把最大的牌放在了最下麵,依次類推即可完全排序。
2.1.15
題目
昂貴的交換。
一家貨運公司的一位職工得到了一項任務,需要將若幹大貨箱按照發貨時間擺放。
比較發貨時間很容易(對照標簽即可),但將兩個貨箱交換位置則很困難(移動麻煩)。
倉庫已經快滿了,只有一個空閑的倉位。這位職員應該使用哪種排序演算法呢?
解答
選擇排序
交換(也就是 Exch() 方法)需要一個額外空間,這裡的條件滿足。
現在我們應該使交換次數最少,選擇排序只需要 N 次交換,比插入排序平均 N^2/4 少(N > 2)。
2.1.16
題目
驗證。
編寫一個 check() 方法,調用 sort() 對任意數組排序。
如果排序成功而且數組中的所有對象均沒有被修改則返回 true,否則返回 false。
不要假設 sort() 只能通過 exch() 來移動數據,可以信任並使用 Array.sort()。
解答
如果移動數據時新建了對象,那麼雖然值沒有改變,但是數組中的對象被修改了。
代碼
插入排序中的 Exch() 換成瞭如下方式:
string temp = new string(s[i].ToCharArray()); s[i] = s[min]; s[min] = temp;
全部程式代碼如下:
using System; namespace _2._1._16 { /* * 2.1.16 * * 驗證。 * 編寫一個 check() 方法, * 調用 sort() 對任意數組排序。 * 如果排序成功而且數組中的所有對象均沒有被修改則返回 true, * 否則返回 false。 * 不要假設 sort() 只能通過 exch() 來移動數據, * 可以信任並使用 Array.sort()。 * */ public class Program { static void Main(string[] args) { string[] test = new string[5] { "a", "b", "d", "c", "e" }; Console.WriteLine(CheckArraySort(test)); Console.WriteLine(CheckSelectionSort(test)); } /// <summary> /// 測試 Array.Sort() 方法。 /// </summary> /// <param name="a">用於測試的數組。</param> /// <returns>如果數組對象沒有改變,返回 true,否則返回 false。</returns> static bool CheckArraySort(string[] a) { string[] backup = new string[a.Length]; a.CopyTo(backup, 0); Array.Sort(a); foreach (string n in a) { bool isFind = false; for (int i = 0; i < a.Length; i++) { if (ReferenceEquals(n, backup[i])) { isFind = true; break; } } if (!isFind) { return false; } } return true; } /// <summary> /// 測試選擇排序。 /// </summary> /// <param name="a">用於測試的數組。</param> /// <returns>如果數組對象沒有改變,返回 true,否則返回 false。</returns> static bool CheckSelectionSort(string[] a) { string[] backup = new string[a.Length]; a.CopyTo(backup, 0); SelectionSort(a); foreach (string n in a) { bool isFind = false; for (int i = 0; i < a.Length; i++) { if (ReferenceEquals(n, backup[i])) { isFind = true; break; } } if (!isFind) { return false; } } return true; } /// <summary> /// 選擇排序,其中的交換部分使用新建對象並複製的方法。 /// </summary> /// <param name="s">用於排序的數組。</param> public static void SelectionSort(string[] s) { for (int i = 0; i < s.Length; i++) { int min = i; for (int j = i + 1; j < s.Length; j++) { if (s[j].CompareTo(s[min]) < 0) { min = j; } } string temp = new string(s[i].ToCharArray()); s[i] = s[min]; s[min] = temp; } } } }
2.1.17
題目
動畫。
修改插入排序和選擇排序的代碼,使之將數組內容繪製成正文中所示的棒狀圖。
在每一輪排序後重繪圖片來產生動畫效果,並以一張“有序”的圖片作為結束,即所有的圓棒均已按照高度有序排列。
提示:使用類似於正文中的用例來隨機生成 Double 值,在排序代碼的適當位置調用 show() 方法,併在 show() 方法中清理畫布並繪製棒狀圖。
解答
選擇排序:
插入排序:
代碼
使用一個 timer 按一定時間重繪數組,排序演算法裡面一次迴圈後等待一段時間再進行下一次迴圈。
這裡排序演算法是另開線程運行的,防止 Sleep 的時候讓程式無響應。
選擇排序:
using System; using System.Drawing; using System.Windows.Forms; using System.Threading; namespace _2._1._17 { public partial class Form2 : Form { double[] randomDoubles; public Form2(int N) { InitializeComponent(); this.randomDoubles = new double[N]; Random random = new Random(); for (int i = 0; i < N; i++) { this.randomDoubles[i] = random.NextDouble() * 0.8 + 0.2; } drawPanel(); this.timer1.Interval = 60; this.timer1.Start(); Thread thread = new Thread(new ThreadStart(this.SelectionSort)); thread.IsBackground = true; thread.Start(); } /// <summary> /// 選擇排序。 /// </summary> private void SelectionSort() { for (int i = 0; i < this.randomDoubles.Length; i++) { int min = i; for (int j = i; j < this.randomDoubles.Length; j++) { if (this.randomDoubles[min] > this.randomDoubles[j]) { min = j; } } double temp = this.randomDoubles[i]; this.randomDoubles[i] = this.randomDoubles[min]; this.randomDoubles[min] = temp; Thread.Sleep(1000); } } /// <summary> /// 在屏幕上用柱形圖繪製數組。 /// </summary> private void drawPanel() { Graphics graphics = this.CreateGraphics(); graphics.Clear(this.BackColor); graphics.TranslateTransform(0, this.Height); graphics.ScaleTransform(1, -1); Rectangle clientRect = this.ClientRectangle; Rectangle drawRect = new Rectangle(clientRect.X + 10, clientRect.Y + 10, clientRect.Width - 10, clientRect.Height - 10); PointF[] barX = new PointF[this.randomDoubles.Length]; float unitX = (float)drawRect.Width / this.randomDoubles.Length; unitX -= 4; barX[0] = new PointF(4, drawRect.Top); for (int i = 1; i < this.randomDoubles.Length; i++) { barX[i] = new PointF(2 + unitX + barX[i - 1].X, drawRect.Top); } RectangleF[] bars = new RectangleF[this.randomDoubles.Length]; for (int i = 0; i < this.randomDoubles.Length; i++) { SizeF size = new SizeF(unitX, (float)this.randomDoubles[i] * drawRect.Height); bars[i] = new RectangleF(barX[i], size); } graphics.FillRectangles(Brushes.Black, bars); graphics.Dispose(); } private void timer1_Tick(object sender, EventArgs e) { drawPanel(); } } }
插入排序:
using System; using System.Drawing; using System.Windows.Forms; using System.Threading; namespace _2._1._17 { public partial class Form3 : Form { double[] randomDoubles; public Form3(int N) { InitializeComponent(); this.randomDoubles = new double[N]; Random random = new Random(); for (int i = 0; i < N; i++) { this.randomDoubles[i] = random.NextDouble() * 0.8 + 0.2; } drawPanel(); this.timer1.Interval = 60; this.timer1.Start(); Thread thread = new Thread(new ThreadStart(this.InsertionSort)); thread.IsBackground = true; thread.Start(); } /// <summary> /// 插入排序。 /// </summary> private void InsertionSort() { for (int i = 0; i < this.randomDoubles.Length; i++) { for (int j = i; j > 0 && this.randomDoubles[j] < this.randomDoubles[j - 1]; j--) { double temp = this.randomDoubles[j]; this.randomDoubles[j] = this.randomDoubles[j - 1]; this.randomDoubles[j - 1] = temp; Thread.Sleep(500); } } } /// <summary> /// 在屏幕上用柱形圖繪製數組。 /// </summary> private void drawPanel() { Graphics graphics = this.CreateGraphics(); graphics.Clear(this.BackColor); graphics.TranslateTransform(0, this.Height); graphics.ScaleTransform(1, -1); Rectangle clientRect = this.ClientRectangle; Rectangle drawRect = new Rectangle(clientRect.X + 10, clientRect.Y + 10, clientRect.Width - 10, clientRect.Height - 10); PointF[] barX = new PointF[this.randomDoubles.Length]; float unitX = (float)drawRect.Width / this.randomDoubles.Length; unitX -= 4; barX[0] = new PointF(4, drawRect.Top); for (int i = 1; i < this.randomDoubles.Length; i++) { barX[i] = new PointF(2 + unitX + barX[i - 1].X, drawRect.Top); } RectangleF[] bars = new RectangleF[this.randomDoubles.Length]; for (int i = 0; i < this.randomDoubles.Length; i++) { SizeF size = new SizeF(unitX, (float)this.randomDoubles[i] * drawRect.Height); bars[i] = new RectangleF(barX[i], size); } graphics.FillRectangles(Brushes.Black, bars); graphics.Dispose(); } private void timer1_Tick(object sender, EventArgs e) { drawPanel(); } } }
2.1.18
題目
可視軌跡。
修改你為上一題給出的解答,為插入排序和選擇排序生成和正文中類似的可視軌跡。
提示:使用 setYscale() 函數是一個明智的選擇。
附加題:添加必要的代碼,與正文中的圖片一樣用紅色和灰色強調不同角色的元素。
解答
選擇排序
插入排序
代碼
與上題類似,但要特別標出移動的元素。
選擇排序:
using System; using System.Drawing; using System.Windows.Forms; using System.Threading; namespace _2._1._18 { public partial class Form2 : Form { double[] randomDoubles; int sortI; int sortJ; int sortMin; public Form2(int N) { InitializeComponent(); this.randomDoubles = new double[N]; Random random = new Random(); for (int i = 0; i < N; i++) { this.randomDoubles[i] = random.NextDouble() * 0.8 + 0.2; } } /// <summary> /// 選擇排序。 /// </summary> private void SelectionSort() { for (this.sortI = 0; this.sortI < this.randomDoubles.Length; this.sortI++) { this.sortMin = this.sortI; for (this.sortJ = this.sortI; this.sortJ < this.randomDoubles.Length; this.sortJ++) { if (this.randomDoubles[this.sortMin] > this.randomDoubles[this.sortJ]) { this.sortMin = this.sortJ; } } drawPanel(); double temp = this.randomDoubles[this.sortI]; this.randomDoubles[this.sortI] = this.randomDoubles[this.sortMin]; this.randomDoubles[this.sortMin] = temp; Thread.Sleep(1000); } } /// <summary> /// 繪製柱形圖。 /// </summary> private void drawPanel() { Graphics graphics = this.CreateGraphics(); graphics.Clear(this.BackColor); graphics.TranslateTransform(0, this.Height); graphics.ScaleTransform(1, -1); Rectangle clientRect = this.ClientRectangle; Rectangle drawRect = new Rectangle(clientRect.X + 10, clientRect.Y + 10, clientRect.Width - 10, clientRect.Height - 10); PointF[] barX = new PointF[this.randomDoubles.Length]; float unitX = (float)drawRect.Width / this.randomDoubles.Length; unitX -= 4; barX[0] = new PointF(4, drawRect.Top); for (int i = 1; i < this.randomDoubles.Length; i++) { barX[i] = new PointF(2 + unitX + barX[i - 1].X, drawRect.Top); } RectangleF[] bars = new RectangleF[this.randomDoubles.Length]; for (int i = 0; i < this.randomDoubles.Length; i++) { SizeF size = new SizeF(unitX, (float)this.randomDoubles[i] * drawRect.Height); bars[i] = new RectangleF(barX[i], size); } for (int i = 0; i < bars.Length; i++) { if (i == this.sortMin) { graphics.FillRectangle(Brushes.Red, bars[i]); } else if (i < this.sortI) { graphics.FillRectangle(Brushes.Gray, bars[i]); } else { graphics.FillRectangle(Brushes.Black, bars[i]); } } graphics.Dispose(); } private void Form2_Shown(object sender, EventArgs e) { SelectionSort(); } } }
插入排序
using System; using System.Drawing; using System.Windows.Forms; using System.Threading; namespace _2._1._18 { public partial class Form3 : Form { double[] randomDoubles; int sortI; int sortJ; int n = 0; public Form3(int N) { InitializeComponent(); this.randomDoubles = new double[N]; Random random = new Random(); for (int i = 0; i < N; i++) { this.randomDoubles[i] = random.NextDouble() * 0.8 + 0.2; } } /// <summary> /// 插入排序。 /// </summary> private void InsertionSort() { for (this.sortI = 0; this.sortI < this.randomDoubles.Length; this.sortI++) { for (this.sortJ = this.sortI; this.sortJ > 0 && this.randomDoubles[this.sortJ] < this.randomDoubles[this.sortJ - 1]; this.sortJ--) { double temp = this.randomDoubles[this.sortJ]; this.randomDoubles[this.sortJ] = this