寫在前面
整個項目都托管在了 Github 上:https://github.com/ikesnowy/Algorithms-4th-Edition-in-Csharp
這一節內容可能會用到的庫文件有 Sort 和 SortData,同樣在 Github 上可以找到。
善用 Ctrl + F 查找題目。
習題&題解
2.1.1
解答
2.1.2
解答
最多會被交換 n 次,只要將一個有序數列循環右移一位就可以構造這樣的情況。
例如:
平均每個元素被交換了 N/N=1 次。(總共 N 個元素,總共發生了 N 次交換)
2.1.3
解答
你需要一個逆序的數組。
例如: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.1.5
解答
條件是:
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.1.10
解答
對於部分有序的數組,插入排序比選擇排序快。
這個結論可以在中文版 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
解答
結果截圖如下,同一個 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
解答
如果移動數據時新建了對象,那么雖然值沒有改變,但是數組中的對象被修改了。
代碼
插入排序中的 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
解答
選擇排序:
插入排序:
代碼
使用一個 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
解答
選擇排序
插入排序
代碼
與上題類似,但要特別標出移動的元素。
選擇排序:
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.randomDoubles[this.sortJ - 1]; this.randomDoubles[this.sortJ - 1] = temp; } drawPanel(); 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.sortJ) { graphics.FillRectangle(Brushes.Red, bars[i]); } else if (i <= this.sortI && i > this.sortJ) { graphics.FillRectangle(Brushes.Black, bars[i]); } else { graphics.FillRectangle(Brushes.Gray, bars[i]); } } graphics.Dispose(); } private void Form3_Shown(object sender, EventArgs e) { InsertionSort(); } } }
2.1.19
解答
不得不說這道題意外的難。
放上論文鏈接:Shellsort and Sorting Networks (Outstanding Dissertations in the Computer Sciences)
這篇論文的第二章給出了一種構造最壞序列的方法,當然理想最壞(n^(3/2))是達不到的了。
最后結果是 793 次。
代碼
構造最壞情況的類
namespace _2._1._19 { class ShellSortWorstCase { /// <summary> /// 獲得最壞情況的數組。 /// </summary> /// <param name="n">數組大小。</param> /// <returns>希爾排序最壞情況的數組。</returns> public static int[] GetWorst(int n) { int l = 0; int?[] a = new int?[n + 1]; for (int i = 0; i < a.Length; i++) { a[i] = null; } int P = 40; int PAddition = P; for (int i = 0; l < 100; i++) { for (int j = 1; j <= n; j++) { if (a[j] == null && IsVisible(j, P)) { l++; a[j] = l; } } P += PAddition; } int[] b = new int[n]; for (int i = 0; i < n; i++) { b[i] = (int)a[i + 1]; } return b; } /// <summary> /// 確認 j - i 是不是在排序樣板(Sorting Template)上。 /// </summary> /// <param name="i"></param> /// <param name="j"></param> /// <returns></returns> public static bool IsVisible(int i, int j) { int k = 0; while (k <= 100) { if (j - i >= k * 40 && j - i <= k * 41) return true; k++; } return false; } } }
會顯示比較次數的 ShellSort 類
using System; using System.Diagnostics; using Sort; namespace _2._1._19 { public class ShellSort : BaseSort { /// <summary> /// 默認構造函數。 /// </summary> public ShellSort() { } /// <summary> /// 利用希爾排序將數組按升序排序。 /// </summary> /// <param name="a">需要排序的數組。</param> public override void Sort<T>(T[] a) { int n = a.Length; int compareTime = 0; int h = 1; while (h < n / 3) { h = 3 * h + 1; } while (h >= 1) { for (int i = h; i < n; i++) { for (int j = i; j >= h && Less(a[j], a[j - h]); j -= h) { Exch(a, j, j - h); compareTime++; } compareTime++; } Debug.Assert(IsHSorted(a, h)); h /= 3; } Console.WriteLine("CompareTime:" + compareTime); Debug.Assert(IsSorted(a)); } /// <summary> /// 檢查一次希爾排序后的子數組是否有序。 /// </summary> /// <param name="a">排序后的數組。</param> /// <param name="h">子數組間隔。</param> /// <returns>是否有序。</returns> private bool IsHSorted<T>(T[] a, int h) where T : IComparable<T> { for (int i = h; i < a.Length; i++) { if (Less(a[i], a[i - h])) { return false; } } return true; } } }
Main 方法
using System; namespace _2._1._19 { /* * 2.1.19 * * 希爾排序的最壞情況。 * 用 1 到 100 構造一個含有 100 個元素的數組並用希爾排序和 * 遞增序列 1 4 13 40 對其排序, * 使比較次數盡可能多。 * */ class Program { // 開放題,沒有標准答案 // 共參考的最差情況為 n^(3/2) // 本例共 793 次 static void Main(string[] args) { int[] b; ShellSort sort = new ShellSort(); b = ShellSortWorstCase.GetWorst(100); for (int i = 0; i < b.Length; i++) { Console.Write(b[i] + " "); } Console.WriteLine(); sort.Sort(b); } } }
2.1.20
解答
由於每次 h 排序都是插入排序,希爾排序最好情況就是插入排序的最好情況,也就是已排序的數組。
2.1.21
解答
事實上官方給出來的 Date 類以及 Transaction 類都已經實現了這些接口。
Date 類:Date.java
Transaction 類:Transaction.java
代碼
using System; using Sort; namespace _2._1._21 { /* * 2.1.21 * * 可比較的交易。 * 用我們的 Date 類(請見 2.1.1.4 節) * 作為模板擴展你的 Transaction 類(請見練習 1.2.13), * 實現 Comparable 接口,使交易能夠按照金額排序。 * */ class Program { static void Main(string[] args) { Transaction[] a = new Transaction[4]; a[0] = new Transaction("Turing 6/17/1990 644.08"); a[1] = new Transaction("Tarjan 3/26/2002 4121.85"); a[2] = new Transaction("Knuth 6/14/1999 288.34"); a[3] = new Transaction("Dijkstra 8/22/2007 2678.40"); Console.WriteLine("Unsorted"); for (int i = 0; i < a.Length; i++) { Console.WriteLine(a[i]); } Console.WriteLine(); Console.WriteLine("Sort by amount"); InsertionSort insertionSort = new InsertionSort(); insertionSort.Sort(a, new Transaction.HowMuchOrder()); for (int i = 0; i < a.Length; i++) Console.WriteLine(a[i]); Console.WriteLine(); } } }
2.1.22
解答
和上題類似,只要傳入事先寫好的比較器就可以了。
代碼
using System; using Sort; namespace _2._1._22 { /* * 2.1.22 * * 交易排序測試用例。 * 編寫一個 SortTransaction 類, * 在靜態方法 main() 中從標准輸入讀取一系列交易, * 將它們排序並在標准輸出中打印結果。 * */ class Program { static void Main(string[] args) { Transaction[] a = new Transaction[4]; // 樣例輸入 // Turing 6/17/1990 644.08 // Tarjan 3/26/2002 4121.85 // Knuth 6/14/1999 288.34 // Dijkstra 8/22/2007 2678.40 for (int i = 0; i < a.Length; i++) { string input = Console.ReadLine(); a[i] = new Transaction(input); } InsertionSort insertionSort = new InsertionSort(); Console.WriteLine("Unsorted"); for (int i = 0; i < a.Length; i++) { Console.WriteLine(a[i]); } Console.WriteLine(); Console.WriteLine("Sort by date"); insertionSort.Sort(a, new Transaction.WhenOrder()); for (int i = 0; i < a.Length; i++) Console.WriteLine(a[i]); Console.WriteLine(); Console.WriteLine("Sort by customer"); insertionSort.Sort(a, new Transaction.WhoOrder()); for (int i = 0; i < a.Length; i++) Console.WriteLine(a[i]); Console.WriteLine(); Console.WriteLine("Sort by amount"); insertionSort.Sort(a, new Transaction.HowMuchOrder()); for (int i = 0; i < a.Length; i++) Console.WriteLine(a[i]); Console.WriteLine(); } } }
2.1.23
解答
方法多種多樣。
首先是冒泡,見習題 2.1.13
插入排序也可以,如下:
從前往后不斷翻牌,
對於翻到的每張牌,一直和之前的牌交換,
直至前面的牌比它小或者它已經是第一張了。
也可以用基數排序
從前向后依次翻開牌,
按照花色分成四堆,
然后按花色從大到小重新排列。
比較符合直覺的是選擇排序
尋找最小的牌並放到第一位,
尋找范圍向右縮減一位,重復上一步,直到最后一張。
還有其他方法,這里不再贅述。
2.1.24
解答
如果使用官方的實現(InsertionX.java),最后結果可能會比一般插入排序慢,因為它是用冒泡的方法找最小值的。
一般做法是在待排序數組的最前端插入一個很小的值(比如 int.MinValue),然后對 a[1]~a[n] 排序。
代碼
參考官方實現的插入排序:
using System.Collections.Generic; using System.Diagnostics; using Sort; namespace _2._1._24 { /// <summary> /// 插入排序類。 /// </summary> public class InsertionSort : BaseSort { /// <summary> /// 默認構造函數。 /// </summary> public InsertionSort() { } /// <summary> /// 利用插入排序將數組按升序排序。 /// </summary> /// <param name="a">需要排序的數組。</param> public override void Sort<T>(T[] a) { int n = a.Length; int exchanges = 0; for (int i = n - 1; i > 0; i--) { if (Less(a[i], a[i - 1])) { Exch(a, i, i - 1); exchanges++; } } if (exchanges == 0) return; for (int i = 1; i < n; i++) { for (int j = i; Less(a[j], a[j - 1]); --j) { Exch(a, j, j - 1); } Debug.Assert(IsSorted(a, 0, i)); } Debug.Assert(IsSorted(a)); } /// <summary> /// 利用插入排序將數組排序。(使用指定比較器) /// </summary> /// <typeparam name="T">數組元素類型。</typeparam> /// <param name="a">需要排序的數組。</param> /// <param name="c">比較器。</param> public void Sort<T>(T[] a, IComparer<T> c) { int n = a.Length; int exchanges = 0; for (int i = n - 1; i > 0; i--) { if (Less(a[i], a[i - 1], c)) { Exch(a, i, i - 1); exchanges++; } } if (exchanges == 0) return; for (int i = 1; i < n; i++) { for (int j = i; Less(a[j], a[j - 1], c); --j) { Exch(a, j, j - 1); } Debug.Assert(IsSorted(a, 0, i, c)); } Debug.Assert(IsSorted(a, c)); } } }
2.1.25
解答
使用依次賦值的方式騰出空間,到達指定位置之后再把元素插入。
看代碼會方便理解一點。
官方實現:InsertionX.java。
代碼
using System.Collections.Generic; using System.Diagnostics; using Sort; namespace _2._1._25 { /// <summary> /// 插入排序類。 /// </summary> public class InsertionSort : BaseSort { /// <summary> /// 默認構造函數。 /// </summary> public InsertionSort() { } /// <summary> /// 利用插入排序將數組按升序排序。 /// </summary> /// <param name="a">需要排序的數組。</param> public override void Sort<T>(T[] a) { int n = a.Length; int exchanges = 0; for (int i = n - 1; i > 0 ; i--) { if (Less(a[i], a[i - 1])) { Exch(a, i, i - 1); exchanges++; } } if (exchanges == 0) return; for (int i = 2; i < n; i++) { int j = i; T v = a[i]; while (Less(v, a[j - 1])) { a[j] = a[j - 1]; j--; } a[j] = v; Debug.Assert(IsSorted(a, 0, i)); } Debug.Assert(IsSorted(a)); } /// <summary> /// 利用插入排序將數組排序。(使用指定比較器) /// </summary> /// <typeparam name="T">數組元素類型。</typeparam> /// <param name="a">需要排序的數組。</param> /// <param name="c">比較器。</param> public void Sort<T>(T[] a, IComparer<T> c) { int n = a.Length; int exchanges = 0; for (int i = n - 1; i > 0; i--) { if (Less(a[i], a[i - 1], c)) { Exch(a, i, i - 1); exchanges++; } } if (exchanges == 0) return; for (int i = 2; i < n; i++) { int j = i; T v = a[i]; while (Less(v, a[j - 1], c)) { a[j] = a[j - 1]; j--; } a[j] = v; Debug.Assert(IsSorted(a, 0, i, c)); } Debug.Assert(IsSorted(a, c)); } } }
2.1.26
解答
直接針對特殊值的話顯然會快很多。
代碼
直接把泛型改成 int 即可。
namespace _2._1._26 { /// <summary> /// 插入排序類。 /// </summary> public class InsertionSort { /// <summary> /// 默認構造函數。 /// </summary> public InsertionSort() { } /// <summary> /// 利用插入排序將數組按升序排序。 /// </summary> /// <param name="a">需要排序的數組。</param> public void Sort(int[] a) { int n = a.Length; for (int i = 0; i < n; i++) { for (int j = i; j > 0 && a[j] < a[j - 1]; --j) { int t = a[j]; a[j] = a[j - 1]; a[j - 1] = t; } } } } }
2.1.27
解答
數據比較大的時候會比較明顯。
代碼
using System; using Sort; namespace _2._1._27 { /* * 2.1.27 * * 希爾排序的用時是次平方級的。 * 在你的計算機上用 SortCompare 比較希爾排序和插入排序以及選擇排序。 * 測試數組的大小按照 2 的冪次遞增,從 128 開始。 * */ class Program { static void Main(string[] args) { int n = 128; Random random = new Random(); double shellPrev = 1; double insertionPrev = 1; double selectionPrev = 1; while (n < 65538) { int[] testShell = new int[n]; int[] testInsertion = new int[n]; int[] testSelection = new int[n]; for (int i = 0; i < n; i++) { testShell[i] = random.Next(); testInsertion[i] = testShell[i]; testSelection[i] = testShell[i]; } Console.WriteLine("數組大小:" + n); Console.Write("Shell Sort:"); double shellNow = SortCompare.Time(new ShellSort(), testShell); Console.WriteLine(shellNow + "\t\tNow/Prev=" + shellNow / shellPrev); Console.Write("Insertion Sort:"); double insertionNow = SortCompare.Time(new InsertionSort(), testInsertion); Console.WriteLine(insertionNow + "\tNow/Prev=" + insertionNow / insertionPrev); Console.Write("Selection Sort:"); double selectionNow = SortCompare.Time(new SelectionSort(), testSelection); Console.WriteLine(selectionNow + "\tNow/Prev=" + selectionNow / selectionPrev); Console.WriteLine(); shellPrev = shellNow; insertionPrev = insertionNow; selectionPrev = selectionNow; n *= 2; } } } }
2.1.28
解答
插入排序會比選擇排序快上許多,當然增長級別不變。
代碼
using System; using Sort; namespace _2._1._28 { /* * 2.1.28 * * 相等的主鍵。 * 對於主鍵僅可能取兩種值的數組, * 評估和驗證插入排序和選擇排序的性能, * 假設兩種主鍵值出現的概率相同。 * */ class Program { static void Main(string[] args) { int n = 1024; Random random = new Random(); double insertionPrev = 1; double selectionPrev = 1; while (n < 65538) { int[] testInsertion = new int[n]; int[] testSelection = new int[n]; for (int i = 0; i < n; i++) { testInsertion[i] = random.Next(2); testSelection[i] = testInsertion[i]; } Console.WriteLine("數組大小:" + n); Console.Write("Insertion Sort:"); double insertionNow = SortCompare.Time(new InsertionSort(), testInsertion); Console.WriteLine(insertionNow + "\tNow/Prev=" + insertionNow / insertionPrev); Console.Write("Selection Sort:"); double selectionNow = SortCompare.Time(new SelectionSort(), testSelection); Console.WriteLine(selectionNow + "\tNow/Prev=" + selectionNow / selectionPrev); Console.WriteLine(); insertionPrev = insertionNow; selectionPrev = selectionNow; n *= 2; } } } }
2.1.29
解答
當然是題目給出的遞增序列更快啦,因為這個序列就是作者提出來的嘛。
(論文鏈接: http://linkinghub.elsevier.com/retrieve/pii/0196677486900015)
代碼
修改了一下 shellsort,讓它按照給定的 h 序列排序。
using System; using System.Diagnostics; using Sort; namespace _2._1._29 { public class ShellSort : BaseSort { /// <summary> /// 默認構造函數。 /// </summary> public ShellSort() { } /// <summary> /// 利用希爾排序將數組按升序排序。 /// </summary> /// <typeparam name="T">待排序的元素類型。</typeparam> /// <param name="a">待排序的數組。</param> /// <param name="h">需要使用的遞增序列。</param> public void Sort<T>(T[] a, int[] h) where T : IComparable<T> { int n = a.Length; int t = 0; while (h[t] < a.Length) { t++; if (t >= h.Length) break; } t--; for ( ; 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)); } /// <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)); } /// <summary> /// 檢查一次希爾排序后的子數組是否有序。 /// </summary> /// <param name="a">排序后的數組。</param> /// <param name="h">子數組間隔。</param> /// <returns>是否有序。</returns> private bool IsHSorted<T>(T[] a, int h) where T : IComparable<T> { for (int i = h; i < a.Length; i++) { if (Less(a[i], a[i - h])) { return false; } } return true; } } }
2.1.30
解答
2,3,4
t 越大的話,按照這個遞增序列,10^6 次能夠滿足的 h 也就越少。
代碼
using System; using Sort; using System.Diagnostics; namespace _2._1._30 { /* * 2.1.30 * * 幾何級數遞增序列。 * 通過實驗找到一個 t,使得對於大小為 N=10^6 的任意隨機數組, * 使用遞增序列 1, [t], [t^2], [t^3], [t^4], ... 的希爾排序的運行時間最短。 * 給出你能找到的三個最佳 t 值以及相應的遞增序列。 * 以下練習描述的是各種用於評估排序算法的測試用例。 * 它們的作用是用隨機數據幫助你增進對性能特性的理解。 * 隨着命令行指定的實驗測試的增大, * 可以和 SortCompare 一樣在它們中使用 time() 函數來得到更精確的結果。 * 在以后的幾節中我們會使用這些練習來評估更為復雜的算法。 * */ class Program { // t = 2, 3, 4 // t 大於 10 之后,由於每次排序 h 縮減的太快, // 時間會越來越近似於直接插入排序。 static void Main(string[] args) { int[] array = SortCompare.GetRandomArrayInt(1000000); int[] array2 = new int[array.Length]; array.CopyTo(array2, 0); Stopwatch timer = new Stopwatch(); long[] bestTimes = new long[3]; long[] bestTs = new long[3]; for (int i = 0; i < bestTimes.Length; i++) { bestTimes[i] = long.MaxValue; bestTs[i] = int.MaxValue; } long nowTime = 0; ShellSort shellSort = new ShellSort(); for (int t = 2; t <= 1000000; t++) { Console.WriteLine(t); timer.Restart(); shellSort.Sort(array, t); nowTime = timer.ElapsedMilliseconds; timer.Stop(); Console.WriteLine("Elapsed Time:" + nowTime); for (int i = 0; i < bestTimes.Length; i++) { Console.Write("t:" + bestTs[i]); Console.WriteLine("\tTime:" + bestTimes[i]); } if (bestTimes[2] > nowTime) { bestTimes[2] = nowTime; bestTs[2] = t; Array.Sort(bestTimes, bestTs); } array2.CopyTo(array, 0); } for (int i = 0; i < bestTimes.Length; i++) { Console.Write("t:" + bestTs[i]); Console.Write("\tTime:" + bestTimes[i]); } } } }
2.1.31
解答
這里截取數據量比較大的時候的數據。
插入排序和選擇排序顯然都是平方級別的。
希爾排序猜測是線性的,實際上要比線性大一點(次平方級)。
代碼
using System; using Sort; namespace _2._1._31 { /* * 2.1.31 * * 雙倍測試。 * 編寫一個能夠對排序算法進行雙倍測試的用例。 * 數組規模 N 的起始值為 1000, * 排序后打印 N、估計排序用時、實際排序用時以及在 N 倍增之后兩次用時的比例。 * 用這段程序驗證在隨機輸入模型下插入排序和選擇排序的運行時間都是平方級別的。 * 對希爾排序的性能做出猜想並驗證你的猜想。 * */ class Program { static void Main(string[] args) { int N = 1000; InsertionSort insertion = new InsertionSort(); SelectionSort selection = new SelectionSort(); ShellSort shell = new ShellSort(); double prevInsertion = 0; double prevSelection = 0; double prevShell = 0; for (int i = 0; i < 10; i++) { Console.WriteLine("N:" + N); int[] array = SortCompare.GetRandomArrayInt(N); int[] arrayBak = new int[N]; array.CopyTo(arrayBak, 0); Console.WriteLine("\tInsertion Sort"); double now = SortCompare.Time(insertion, array); Console.WriteLine("\t\tActual Time(ms):" + now); if (i != 0) { Console.WriteLine("\t\tEstimate Time(ms):" + prevInsertion * 4); Console.WriteLine("\t\tRatio:" + now / prevInsertion); } prevInsertion = now; arrayBak.CopyTo(array, 0); Console.WriteLine("\tSelection Sort"); now = SortCompare.Time(selection, array); Console.WriteLine("\t\tActual Time(ms):" + now); if (i != 0) { Console.WriteLine("\t\tEstimate Time(ms):" + prevSelection * 4); Console.WriteLine("\t\tRatio:" + now / prevSelection); } prevSelection = now; arrayBak.CopyTo(array, 0); Console.WriteLine("\tShell Sort"); now = SortCompare.Time(shell, array); Console.WriteLine("\t\tActual Time(ms):" + now); if (i != 0) { Console.WriteLine("\t\tEstimate Time(ms):" + prevShell * 2); Console.WriteLine("\t\tRatio:" + now / prevShell); } prevShell = now; N *= 2; } } } }
2.1.32
解答
基本上都是這么個樣子:
代碼
using System; using System.Drawing; using System.Linq; using System.Windows.Forms; using Sort; namespace _2._1._32 { public partial class Form2 : Form { BaseSort sort; int n; double[] result; /// <summary> /// 構造一個繪圖結果窗口。 /// </summary> /// <param name="sort">用於做測試的排序算法。</param> /// <param name="n">用於測試的初始數據量。</param> public Form2(BaseSort sort, int n) { InitializeComponent(); this.sort = sort; this.n = n; this.result = Test(n); this.timer1.Interval = 1000; this.timer1.Start(); } /// <summary> /// 執行八次耗時測試,每次數據量翻倍。 /// </summary> /// <param name="n">初始數據量。</param> /// <returns>測試結果數據。</returns> public double[] Test(int n) { double[] result = new double[8]; for (int i = 0; i < result.Length; i++) { result[i] = SortCompare.TimeRandomInput(this.sort, n, 3); n *= 2; } return result; } /// <summary> /// 繪制曲線圖。 /// </summary> /// <param name="result">結果數組。</param> public void DrawPanel(double[] result) { Graphics graphics = this.CreateGraphics(); graphics.TranslateTransform(0, this.ClientRectangle.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[] dataPoints = new PointF[result.Length]; float unitX = (float)drawRect.Width / result.Length; float unitY = (float)(drawRect.Height / result.Max()); SizeF pointSize = new SizeF(8, 8); for (int i = 0; i < result.Length; i++) { dataPoints[i] = new PointF(drawRect.Left + unitX * i, (float)(unitY * result[i])); graphics.FillEllipse(Brushes.Black, new RectangleF(dataPoints[i], pointSize)); } } private void timer1_Tick(object sender, EventArgs e) { DrawPanel(this.result); this.timer1.Stop(); } } }
2.1.33
解答
這里每次結果的 Y 軸位置都是隨機生成的,這樣圖像會好看點。
X 軸代表消耗的時間。
選擇排序:
插入排序:
希爾排序:
代碼
using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Windows.Forms; using Sort; namespace _2._1._33 { public partial class Form2 : Form { List<double> resultList; List<float> resultYList; Rectangle clientRect; Rectangle drawRect; BaseSort sort; int n; /// <summary> /// 構造一個繪制結果窗口。 /// </summary> /// <param name="sort">用於測試的排序算法。</param> /// <param name="n">測試算法是生成的數據量。</param> public Form2(BaseSort sort, int n) { InitializeComponent(); this.resultList = new List<double>(); this.resultYList = new List<float>(); this.clientRect = this.ClientRectangle; this.drawRect = new Rectangle(this.clientRect.X + 10, this.clientRect.Y + 10, this.clientRect.Width - 10, this.clientRect.Height - 10); this.sort = sort; this.n = n; this.timer1.Interval = 500; this.timer1.Start(); } /// <summary> /// 執行一次測試並繪制圖像。 /// </summary> public void Test() { Random random = new Random(); double[] array = SortCompare.GetRandomArrayDouble(this.n); double time = SortCompare.Time(this.sort, array); this.resultList.Add(time); this.resultYList.Add((float)(random.NextDouble() * this.drawRect.Height)); DrawPanel(this.resultList.ToArray(), this.resultYList.ToArray()); } /// <summary> /// 根據已有的數據繪制圖像。 /// </summary> /// <param name="result">耗時數據(X 軸)</param> /// <param name="resultY">Y 軸數據</param> public void DrawPanel(double[] result, float[] resultY) { Graphics graphics = this.CreateGraphics(); graphics.Clear(this.BackColor); graphics.TranslateTransform(0, this.ClientRectangle.Height); graphics.ScaleTransform(1, -1); PointF[] dataPoints = new PointF[result.Length]; float unitX = (float)(this.drawRect.Width / (result.Max() - result.Min())); double min = result.Min(); SizeF pointSize = new SizeF(8, 8); for (int i = 0; i < result.Length; i++) { dataPoints[i] = new PointF((float)(unitX * (result[i] - min)), resultY[i]); graphics.FillEllipse(Brushes.Black, new RectangleF(dataPoints[i], pointSize)); } } private void timer1_Tick(object sender, EventArgs e) { Test(); } } }
2.1.34
解答
代碼
using System; using Sort; namespace _2._1._34 { /* * 2.1.34 * * 罕見情況。 * 編寫一個測試用例, * 調用 sort() 方法對實際應用中可能出現困難或極端情況的數組進行排序。 * 比如,數組可能已經是有序的, * 或是逆序的, * 數組中的所有主鍵相同, * 數組的主鍵只有兩種值, * 大小是 0 或 1 的數組。 * */ class Program { static void Main(string[] args) { InsertionSort insertionSort = new InsertionSort(); SelectionSort selectionSort = new SelectionSort(); ShellSort shellSort = new ShellSort(); // 逆序 Console.WriteLine("逆序"); Console.WriteLine("Insertion Sort Time: " + ReverseSortTest(insertionSort)); Console.WriteLine("Selection Sort Time: " + ReverseSortTest(selectionSort)); Console.WriteLine("Shell Sort Time: " + ReverseSortTest(shellSort)); // 順序 Console.WriteLine("順序"); Console.WriteLine("Insertion Sort Time: " + SortedSortTest(insertionSort)); Console.WriteLine("Selection Sort Time: " + SortedSortTest(selectionSort)); Console.WriteLine("Shell Sort Time: " + SortedSortTest(shellSort)); // 主鍵相同 Console.WriteLine("主鍵相同"); Console.WriteLine("Insertion Sort Time: " + EqualSortTest(insertionSort)); Console.WriteLine("Selection Sort Time: " + EqualSortTest(selectionSort)); Console.WriteLine("Shell Sort Time: " + EqualSortTest(shellSort)); // 二元數組 Console.WriteLine("二元數組"); Console.WriteLine("Insertion Sort Time: " + BinarySortTest(insertionSort)); Console.WriteLine("Selection Sort Time: " + BinarySortTest(selectionSort)); Console.WriteLine("Shell Sort Time: " + BinarySortTest(shellSort)); // 空數組 Console.WriteLine("空數組"); Console.WriteLine("Insertion Sort Time: " + ZeroArraySizeSort(insertionSort)); Console.WriteLine("Selection Sort Time: " + ZeroArraySizeSort(selectionSort)); Console.WriteLine("Shell Sort Time: " + ZeroArraySizeSort(shellSort)); // 只有一個元素的數組 Console.WriteLine("只有一個元素的數組"); Console.WriteLine("Insertion Sort Time: " + OneArraySizeSort(insertionSort)); Console.WriteLine("Selection Sort Time: " + OneArraySizeSort(selectionSort)); Console.WriteLine("Shell Sort Time: " + OneArraySizeSort(shellSort)); } /// <summary> /// 構造逆序數組並用其對指定輸入算法進行測試。 /// </summary> /// <param name="sort">需要做測試的算法。</param> /// <returns>算法耗時。</returns> static double ReverseSortTest(BaseSort sort) { int[] array = new int[10000]; for (int i = 0; i < array.Length; i++) { array[i] = array.Length - i; } return SortCompare.Time(sort, array); } /// <summary> /// 構造已排序的數組並用其對指定排序算法測試。 /// </summary> /// <param name="sort">需要做測試的排序算法。</param> /// <returns>算法的耗時。</returns> static double SortedSortTest(BaseSort sort) { return SortCompare.TimeSortedInput(sort, 10000, 1); } /// <summary> /// 構造只有一個值的數組並用其對指定排序算法做測試。 /// </summary> /// <param name="sort">需要做測試的排序算法。</param> /// <returns>算法的耗時。</returns> static double EqualSortTest(BaseSort sort) { int[] array = new int[10000]; Random random = new Random(); int num = random.Next(); for (int i = 0; i < array.Length; i++) { array[i] = num; } return SortCompare.Time(sort, array); } /// <summary> /// 構造只有兩種取值的數組並用其對指定排序算法做測試。 /// </summary> /// <param name="sort">需要做測試的排序算法。</param> /// <returns>排序算法的耗時。</returns> static double BinarySortTest(BaseSort sort) { int[] array = new int[10000]; Random random = new Random(); for (int i = 0; i < array.Length; i++) { array[i] = random.Next(2); } return SortCompare.Time(sort, array); } /// <summary> /// 構造空數組並用其對指定排序算法做測試。 /// </summary> /// <param name="sort">需要做測試的排序算法。</param> /// <returns>排序算法的耗時。</returns> static double ZeroArraySizeSort(BaseSort sort) { int[] array = new int[0]; return SortCompare.Time(sort, array); } /// <summary> /// 構造只有一個元素的數組並用其對指定排序算法做測試。 /// </summary> /// <param name="sort">需要做測試的排序算法。</param> /// <returns>排序算法的耗時。</returns> static double OneArraySizeSort(BaseSort sort) { int[] array = new int[1]; Random random = new Random(); array[0] = random.Next(); return SortCompare.Time(sort, array); } } }
2.1.35
解答
難點是如何生成符合這些分布的隨機數。
Java 的話官方給的 stdRandom 里面都有相應的實現。
結果:
代碼
幾種隨機數的實現:
using System; namespace Sort { /// <summary> /// 靜態類,包含用於生成排序算法測試數據的方法。 /// </summary> public static class SortUtil { public static Random UniformGenerator = new Random(); /// <summary> /// 產生符合正態分布的隨機數。 /// </summary> /// <param name="average">正態分布的期望值 μ。</param> /// <param name="standardDeviation">正態分布的標准差 σ。</param> /// <returns>符合正態分布的隨機數。</returns> public static double Normal(double average, double standardDeviation) { double u1 = UniformGenerator.NextDouble(); double u2 = UniformGenerator.NextDouble(); double z0 = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Cos(Math.PI * 2 * u2); return z0 * standardDeviation + average; } /// <summary> /// 生成符合泊松分布的隨機數。 /// </summary> /// <param name="average">泊松分布的期望值 λ。</param> /// <returns>一個符合泊松分布的隨機數。</returns> public static double Poission(double average) { double x = 0; double p = Math.Pow(Math.E, -average); double s = p; double u = UniformGenerator.NextDouble(); do { x++; p *= average / x; s += p; } while (u > s); return x; } /// <summary> /// 生成符合幾何分布的隨機數。 /// </summary> /// <param name="p">幾何分布的概率 p,這應該是一個小於 1 的非負數。</param> /// <exception cref="ArgumentOutOfRangeException">概率不能大於 1.</exception> /// <returns>符合幾何分布的隨機數。</returns> public static double Geometry(double p) { if (p > 1) { throw new ArgumentOutOfRangeException("p", "概率不能大於 1"); } double result; result = Math.Ceiling(Math.Log(1 - UniformGenerator.NextDouble()) / Math.Log(1 - p)); return result; } /// <summary> /// 根據指定的幾率數組產生符合離散分布的隨機數。 /// </summary> /// <param name="probabilities">各取值的可能性。</param> /// <returns>符合隨機分布的隨機整數。</returns> public static double Discrete(double[] probabilities) { if (probabilities == null) { throw new ArgumentNullException("Argument array is null"); } double EPSION = 1E-14; double sum = 0; for (int i = 0; i < probabilities.Length; i++) { if (probabilities[i] <= 0) { throw new ArgumentException("array entry " + i + " must be nonnegative:" + probabilities[i]); } sum += probabilities[i]; } if (sum > 1.0 + EPSION || sum < 1.0 - EPSION) { throw new ArgumentException("sum of array entries does not equal 1.0:" + sum); } while (true) { double r = UniformGenerator.NextDouble(); sum = 0.0; for (int i = 0; i < probabilities.Length; i++) { sum += probabilities[i]; if (sum > r) { return i; } } } } } }
Main 方法:
using System; using Sort; namespace _2._1._35 { /* * 2.1.35 * * 不均勻的概率分布。編寫一個測試用例,使用非均勻分布的概率來生成隨機排列的數據,包括: * 高斯分布 * 泊松分布 * 幾何分布 * 離散分布(一種特殊情況見練習 2.1.28)。 * 評估並驗證這些輸入數據對本節討論的算法的影響。 * */ class Program { static void Main(string[] args) { InsertionSort insertionSort = new InsertionSort(); SelectionSort selectionSort = new SelectionSort(); ShellSort shellSort = new ShellSort(); int n = 10000; // 高斯分布(正態分布) double[] arrayInsertion = SortCompare.GetNormalDistributionArray(n); double[] arraySelection = new double[n]; double[] arrayShell = new double[n]; arrayInsertion.CopyTo(arraySelection, 0); arrayInsertion.CopyTo(arrayShell, 0); Console.WriteLine("Normal Distribution:"); Console.WriteLine("Insertion: " + SortCompare.Time(insertionSort, arrayInsertion)); Console.WriteLine("Selection: " + SortCompare.Time(selectionSort, arraySelection)); Console.WriteLine("Shell: " + SortCompare.Time(shellSort, arrayShell)); // 泊松分布 arrayInsertion = SortCompare.GetPossionDistributionArray(n); arrayInsertion.CopyTo(arraySelection, 0); arrayInsertion.CopyTo(arrayShell, 0); Console.WriteLine("Poission Distribution:"); Console.WriteLine("Insertion: " + SortCompare.Time(insertionSort, arrayInsertion)); Console.WriteLine("Selection: " + SortCompare.Time(selectionSort, arraySelection)); Console.WriteLine("Shell: " + SortCompare.Time(shellSort, arrayShell)); // 幾何分布 arrayInsertion = SortCompare.GetGeometricDistributionArray(n, 0.3); arrayInsertion.CopyTo(arraySelection, 0); arrayInsertion.CopyTo(arrayShell, 0); Console.WriteLine("Geometric Distribution:"); Console.WriteLine("Insertion: " + SortCompare.Time(insertionSort, arrayInsertion)); Console.WriteLine("Selection: " + SortCompare.Time(selectionSort, arraySelection)); Console.WriteLine("Shell: " + SortCompare.Time(shellSort, arrayShell)); // 離散分布 arrayInsertion = SortCompare.GetDiscretDistributionArray(n, new double[] { 0.1, 0.2, 0.3, 0.1, 0.1, 0.1, 0.1 }); arrayInsertion.CopyTo(arraySelection, 0); arrayInsertion.CopyTo(arrayShell, 0); Console.WriteLine("Discret Distribution:"); Console.WriteLine("Insertion: " + SortCompare.Time(insertionSort, arrayInsertion)); Console.WriteLine("Selection: " + SortCompare.Time(selectionSort, arraySelection)); Console.WriteLine("Shell: " + SortCompare.Time(shellSort, arrayShell)); } } }
2.1.36
解答
最后結果:
代碼
using System; using Sort; namespace _2._1._36 { /* * 2.1.36 * * 不均勻的數據。 * 編寫一個測試用例, * 生成不均勻的測試數據,包括: * 一半數據是 0,一半數據是 1 * 一半數據是 0,1/4 是 1,1/4 是 2,以此類推 * 一半數據是 0,一半是隨機 int 值。 * 評估並驗證這些輸入數據對本節討論的算法的性能的影響。 * */ class Program { // 選擇排序的耗時與輸入值的內容無關,不受影響。 // 對於插入排序,以上幾種情況都是重復值較多的情況,插入排序的速度會加快。 // 希爾排序本質上也是插入排序,因此也會更快一些。 static void Main(string[] args) { int n = 10000; InsertionSort insertionSort = new InsertionSort(); SelectionSort selectionSort = new SelectionSort(); ShellSort shellSort = new ShellSort(); int[] arrayInsertion = new int[n]; int[] arraySelection = new int[n]; int[] arrayShell = new int[n]; // 對照,完全隨機 arrayInsertion = HalfZeroHalfOne(n); arrayInsertion.CopyTo(arraySelection, 0); arrayInsertion.CopyTo(arrayShell, 0); Console.WriteLine("totally random"); Console.WriteLine("Insertion Sort:" + SortCompare.TimeRandomInput(insertionSort, n, 1)); Console.WriteLine("Selection Sort:" + SortCompare.TimeRandomInput(selectionSort, n, 1)); Console.WriteLine("Shell Sort:" + SortCompare.TimeRandomInput(shellSort, n, 1)); Console.WriteLine(); // 一半是 0 一半是 1 arrayInsertion = HalfZeroHalfOne(n); arrayInsertion.CopyTo(arraySelection, 0); arrayInsertion.CopyTo(arrayShell, 0); Console.WriteLine("half 0 and half 1"); Console.WriteLine("Insertion Sort:" + SortCompare.Time(insertionSort, arrayInsertion)); Console.WriteLine("Selection Sort:" + SortCompare.Time(selectionSort, arraySelection)); Console.WriteLine("Shell Sort:" + SortCompare.Time(shellSort, arrayShell)); Console.WriteLine(); // 一半是 0, 1/4 是 1, 1/8 是 2…… arrayInsertion = HalfAndHalf(n); arrayInsertion.CopyTo(arraySelection, 0); arrayShell.CopyTo(arrayShell, 0); Console.WriteLine("half and half and half ..."); Console.WriteLine("Insertion Sort:" + SortCompare.Time(insertionSort, arrayInsertion)); Console.WriteLine("Selection Sort:" + SortCompare.Time(selectionSort, arraySelection)); Console.WriteLine("Shell Sort:" + SortCompare.Time(shellSort, arrayShell)); Console.WriteLine(); // 一半是 0,一半是隨機 int 值 arrayInsertion = HalfZeroHalfRandom(n); arrayInsertion.CopyTo(arraySelection, 0); arrayShell.CopyTo(arrayShell, 0); Console.WriteLine("half 0 half random"); Console.WriteLine("Insertion Sort:" + SortCompare.Time(insertionSort, arrayInsertion)); Console.WriteLine("Selection Sort:" + SortCompare.Time(selectionSort, arraySelection)); Console.WriteLine("Shell Sort:" + SortCompare.Time(shellSort, arrayShell)); } /// <summary> /// 獲取一半是 0 一半是 1 的隨機 <see cref="int"/> 數組。 /// </summary> /// <param name="n">數組大小。</param> /// <returns>一半是 0 一半是 1 的 <see cref="int"/>數組。</returns> static int[] HalfZeroHalfOne(int n) { int[] result = new int[n]; Random random = new Random(); for (int i = 0; i < n; i++) { if (random.NextDouble() >= 0.5) { result[i] = 0; } else { result[i] = 1; } } return result; } /// <summary> /// 生成 1/2 為 0, 1/4 為 1, 1/8 為 2 …… 的 <see cref="int"/> 數組。 /// </summary> /// <param name="n">數組長度。</param> /// <returns>1/2 為 0, 1/4 為 1, 1/8 為 2 …… 的 <see cref="int"/> 數組。</returns> static int[] HalfAndHalf(int n) { int[] array = new int[n]; HalfIt(0, 0, n / 2, array); Shuffle(array); return array; } /// <summary> /// 遞歸生成 1/2 為 0, 1/4 為 1, 1/8 為 2 …… 的 <see cref="int"/> 數組。 /// </summary> /// <param name="start">填充起點。</param> /// <param name="number">起始編號。</param> /// <param name="length">填充長度</param> /// <param name="array">用於填充的數組。</param> /// <returns>一個 <see cref="int"/> 數組。</returns> static int[] HalfIt(int start, int number, int length, int[] array) { if (length == 0) return array; for (int i = 0; i < length; i++) { array[start + i] = number; } return HalfIt(start + length, number + 1, length / 2, array); } /// <summary> /// 生成一半是 0 一半是隨機整數的 <see cref="int"/> 數組。 /// </summary> /// <param name="n">數組大小。</param> /// <returns>生成一半是 0 一半是隨機整數的 <see cref="int"/> 數組。</returns> static int[] HalfZeroHalfRandom(int n) { int[] array = new int[n]; Random random = new Random(); for (int i = 0; i < n / 2; i++) { array[i] = 0; } for (int i = n / 2; i < n; i++) { array[i] = random.Next(); } Shuffle(array); return array; } /// <summary> /// 打亂數組。 /// </summary> /// <param name="a">需要打亂的數組。</param> static void Shuffle(int[] a) { int N = a.Length; Random random = new Random(); for (int i = 0; i < N; i++) { int r = i + random.Next(N - i);// 等於StdRandom.uniform(N-i) int temp = a[i]; a[i] = a[r]; a[r] = temp; } } } }
2.1.37
解答
主要說一下第二個的實現,把一個數組循環左移/右移幾位即可。
代碼
using System; using System.Collections.Generic; using Sort; namespace _2._1._37 { /* * 2.1.37 * * 部分有序。 * 編寫一個測試用例,生成部分有序數組,包括: * 95% 有序,其余部分為隨機值。 * 所有元素和它們的正確位置的距離都不超過 10。 * 5% 的元素隨機分布在整個數組中,剩下的數據都是有序的。 * 評估並驗證這些輸入數據對本節討論的算法的性能的影響。 * */ class Program { // 選擇排序的性能只與數組大小有關,以上三種情況耗時都是近似的。 // 插入排序的性能與逆序對數量有關,部分有序的情況下耗時會小於完全隨機。 // 希爾排序與插入排序類似。 static void Main(string[] args) { InsertionSort insertionSort = new InsertionSort(); SelectionSort selectionSort = new SelectionSort(); ShellSort shellSort = new ShellSort(); int n = 10000; int[] selectionArray = new int[n]; int[] insertionArray = new int[n]; int[] shellArray = new int[n]; // 完全隨機的對照 Console.WriteLine("totally random"); Console.WriteLine("Selection Sort:" + SortCompare.TimeRandomInput(selectionSort, n, 1)); Console.WriteLine("Insertion Sort:" + SortCompare.TimeRandomInput(insertionSort, n, 1)); Console.WriteLine("Shell Sort:" + SortCompare.TimeRandomInput(shellSort, n, 1)); // 95% 有序,其余部分為隨機值。 selectionArray = Sorted95Random5(n); selectionArray.CopyTo(insertionArray, 0); selectionArray.CopyTo(shellArray, 0); Console.WriteLine("95% sorted + 5% random"); Console.WriteLine("Selection Sort:" + SortCompare.Time(selectionSort, selectionArray)); Console.WriteLine("Insertion Sort:" + SortCompare.Time(insertionSort, insertionArray)); Console.WriteLine("Shell Sort:" + SortCompare.Time(shellSort, shellArray)); // 所有元素和它們的正確位置的距離都不超過 10。 selectionArray = RandomIn10(n); selectionArray.CopyTo(insertionArray, 0); selectionArray.CopyTo(shellArray, 0); Console.WriteLine("a sorted array that left shift 6 times"); Console.WriteLine("Selection Sort:" + SortCompare.Time(selectionSort, selectionArray)); Console.WriteLine("Insertion Sort:" + SortCompare.Time(insertionSort, insertionArray)); Console.WriteLine("Shell Sort:" + SortCompare.Time(shellSort, shellArray)); // 5% 的元素隨機分布在整個數組中,剩下的數據都是有序的。 selectionArray = RandomIn10(n); selectionArray.CopyTo(insertionArray, 0); selectionArray.CopyTo(shellArray, 0); Console.WriteLine("95% elements is sorted while 5% elements are placed randomly"); Console.WriteLine("Selection Sort:" + SortCompare.Time(selectionSort, selectionArray)); Console.WriteLine("Insertion Sort:" + SortCompare.Time(insertionSort, insertionArray)); Console.WriteLine("Shell Sort:" + SortCompare.Time(shellSort, shellArray)); } /// <summary> /// 生成 95% 有序,最后 5% 隨機的 <see cref="int"/> 數組。 /// </summary> /// <param name="n">數組的大小。</param> /// <returns>95% 有序,最后 5% 隨機的 <see cref="int"/> 數組。</returns> static int[] Sorted95Random5(int n) { int[] array = new int[n]; int randomStart = (int)(n * 0.05); Random random = new Random(); for (int i = 0; i < n - randomStart; i++) { array[i] = i; } for (int i = n - randomStart; i < n; i++) { array[i] = random.Next(); } return array; } /// <summary> /// 返回一個 <see cref="int"/> 數組,其中的每個元素和它的正確位置的距離都不超過 10。 /// </summary> /// <param name="n">數組大小。</param> /// <returns>一個 <see cref="int"/> 數組,其中的每個元素和它的正確位置的距離都不超過 10。</returns> static int[] RandomIn10(int n) { Queue<int> array = new Queue<int>(); Random random = new Random(); for (int i = 0; i < n; i++) { array.Enqueue(i); } for (int i = 0; i < 6; i++) { array.Enqueue(array.Dequeue()); } return array.ToArray(); } /// <summary> /// 生成 5% 元素隨機分布,剩余有序的 <see cref="int"/> 數組。 /// </summary> /// <param name="n">需要生成的數組大小。</param> /// <returns>5% 元素隨機分布,剩余有序的 <see cref="int"/> 數組。</returns> static int[] Shuffle5Percent(int n) { Random random = new Random(); int percent5 = (int)(n * 0.05); int[] randomIndex = new int[percent5]; for (int i = 0; i < percent5; i++) { randomIndex[i] = random.Next(percent5); } int[] randomValue = new int[percent5]; for (int i = 0; i < percent5; i++) { randomValue[i] = randomIndex[i]; } Shuffle(randomValue); int[] array = new int[n]; for (int i = 0; i < n; i++) { array[i] = i; } for (int i = 0; i < percent5; i++) { array[randomIndex[i]] = randomValue[i]; } return array; } /// <summary> /// 打亂數組。 /// </summary> /// <param name="a">需要打亂的數組。</param> static void Shuffle(int[] a) { int N = a.Length; Random random = new Random(); for (int i = 0; i < N; i++) { int r = i + random.Next(N - i);// 等於StdRandom.uniform(N-i) int temp = a[i]; a[i] = a[r]; a[r] = temp; } } } }
2.1.38
解答
這里實現了一個 Pair 類,用來排序。
每一個元素都有相應的 key 值和 value 值,排序時只使用 key 值進行排序。
代碼
using System; using Sort; namespace _2._1._38 { /* * 2.1.38 * * 不同類型的元素。 * 編寫一個測試用例,生成由多種數據類型元素組成的數組,元素的主鍵值隨機,包括: * 每個元素的主鍵均為 String 類型(至少長 10 個字符),並含有一個 double 值。 * 每個元素的主鍵均為 double 類型,並含有 10 個 String 值(每個都至少長 10 個字符)。 * 每個元素的主鍵均為 int 類型,並含有一個 int[20] 值。 * 評估並驗證這些輸入數據對本節討論的算法的性能的影響。 * */ class Program { static void Main(string[] args) { int n = 10000; double[] results = TestA(n); Console.WriteLine("string + double"); Console.WriteLine("Insertion Sort:" + results[0]); Console.WriteLine("Selection Sort:" + results[1]); Console.WriteLine("Shell Sort:" + results[2]); results = TestB(n); Console.WriteLine("double + 10 string"); Console.WriteLine("Insertion Sort:" + results[0]); Console.WriteLine("Selection Sort:" + results[1]); Console.WriteLine("Shell Sort:" + results[2]); results = TestC(n); Console.WriteLine("int + int[]"); Console.WriteLine("Insertion Sort:" + results[0]); Console.WriteLine("Selection Sort:" + results[1]); Console.WriteLine("Shell Sort:" + results[2]); } /// <summary> /// 第一個測試,測試結果按照 Insertion, Selection, Shell 排序。 /// </summary> /// <param name="n">測試的數組長度。</param> /// <returns>測試結果。</returns> static double[] TestA(int n) { InsertionSort insertionSort = new InsertionSort(); SelectionSort selectionSort = new SelectionSort(); ShellSort shellSort = new ShellSort(); Random random = new Random(); // 每個元素的主鍵均為 String 類型(至少長 10 個字符),並含有一個 double 值。 Pair<string, double>[] array = new Pair<string, double>[n]; Pair<string, double>[] arrayBak = new Pair<string, double>[n]; for (int i = 0; i < n; i++) { array[i] = new Pair<string, double>(RandomString(20, random), random.NextDouble()); } array.CopyTo(arrayBak, 0); double[] results = new double[3]; results[0] = SortCompare.Time(insertionSort, array); arrayBak.CopyTo(array, 0); results[1] = SortCompare.Time(selectionSort, array); arrayBak.CopyTo(array, 0); results[2] = SortCompare.Time(shellSort, array); return results; } /// <summary> /// 第二個測試,測試結果按照 Insertion, Selection, Shell 排序。 /// </summary> /// <param name="n">測試的數組長度。</param> /// <returns>測試結果。</returns> static double[] TestB(int n) { InsertionSort insertionSort = new InsertionSort(); SelectionSort selectionSort = new SelectionSort(); ShellSort shellSort = new ShellSort(); Random random = new Random(); // 每個元素的主鍵均為 double 類型,並含有 10 個 String 值(每個都至少長 10 個字符)。 Pair<double, string[]>[] array = new Pair<double, string[]>[n]; Pair<double, string[]>[] arrayBak = new Pair<double, string[]>[n]; for (int i = 0; i < n; i++) { string[] temp = new string[10]; for (int j = 0; j < 10; j++) { temp[j] = RandomString(12, random); } array[i] = new Pair<double, string[]>(random.NextDouble(), temp); } array.CopyTo(arrayBak, 0); double[] results = new double[3]; results[0] = SortCompare.Time(insertionSort, array); arrayBak.CopyTo(array, 0); results[1] = SortCompare.Time(selectionSort, array); arrayBak.CopyTo(array, 0); results[2] = SortCompare.Time(shellSort, array); return results; } /// <summary> /// 第三個測試,測試結果按照 Insertion, Selection, Shell 排序。 /// </summary> /// <param name="n">測試的數組長度。</param> /// <returns>測試結果。</returns> static double[] TestC(int n) { InsertionSort insertionSort = new InsertionSort(); SelectionSort selectionSort = new SelectionSort(); ShellSort shellSort = new ShellSort(); Random random = new Random(); // 每個元素的主鍵均為 int 類型,並含有一個 int[20] 值。 Pair<int, int[]>[] array = new Pair<int, int[]>[n]; Pair<int, int[]>[] arrayBak = new Pair<int, int[]>[n]; for (int i = 0; i < n; i++) { int[] temp = new int[20]; for (int j = 0; j < 20; j++) { temp[j] = random.Next(); } array[i] = new Pair<int, int[]>(random.Next(), temp); } array.CopyTo(arrayBak, 0); double[] results = new double[3]; results[0] = SortCompare.Time(insertionSort, array); arrayBak.CopyTo(array, 0); results[1] = SortCompare.Time(selectionSort, array); arrayBak.CopyTo(array, 0); results[2] = SortCompare.Time(shellSort, array); return results; } /// <summary> /// 獲取一個隨機 <see cref="string"/>。 /// </summary> /// <param name="n"><see cref="string"/> 的長度。</param> /// <param name="random">隨機數生成器。</param> /// <returns>獲取一個隨機 <see cref="string"/>。</returns> static string RandomString(int n, Random random) { char[] value = new char[n]; for (int i = 0; i < n; i++) { value[i] = (char)random.Next(char.MinValue + 10, char.MaxValue - 10); } return new string(value); } } }