寫在前面
整個項目都托管在了 Github 上:https://github.com/ikesnowy/Algorithms-4th-Edition-in-Csharp
這一節內容可能會用到的庫文件有 Measurement 和 TestCase,同樣在 Github 上可以找到。
善用 Ctrl + F 查找題目。
習題&題解
1.4.1
解答
即為證明組合計算公式:
C(N, 3)
= N! / [(N - 3)! × 3!]
= [(N - 2) * (N - 1) * N] / 3!
= N(N - 1)(N - 2) / 6
顯然 N 必須大於等於 3。
N = 3 時公式正確,只有一種組合。
N = 4 時公式正確,只有四種組合。
擴展到 N+1 個數,將 N = N + 1 代入,可得:
(N + 1)N(N - 1) / 6
N + 1 個數能組成的三位數組合可以這樣理解
前 N 個數中取三個數的所有組合 +多出的一個數和前 N 個數中的任意取兩個數的所有組合
即為 N(N-1)(N - 2) / 6 + C(N, 2)
變形后即為(N + 1)N(N - 1) / 6
得證。
1.4.2
解答
將 a[i] + a[j] + a[k] 改為 (long)a[i] + a[j] + a[k] 即可。
此時整個式子將按照精度最高(也就是 long)的標准計算。
long.MaxValue = 9223372036854775807 > int.MaxValue * 3 = 6442450941
代碼
namespace Measurement { /// <summary> /// 用暴力方法尋找數組中和為零的三元組。 /// </summary> public static class ThreeSum { /// <summary> /// 輸出所有和為零的三元組。 /// </summary> /// <param name="a">輸入數組。</param> public static void PrintAll(int[] a) { int n = a.Length; for (int i = 0; i < n; ++i) { for (int j = i + 1; j < n; ++j) { for (int k = j + 1; k < n; ++k) { if ((long)a[i] + a[j] + a[k] == 0) { Console.WriteLine($"{a[i]} + {a[j]} + {a[k]}"); } } } } } /// <summary> /// 計算和為零的三元組的數量。 /// </summary> /// <param name="a">輸入數組。</param> /// <returns></returns> public static int Count(int[] a) { int n = a.Length; int count = 0; for (int i = 0; i < n; ++i) { for (int j = i + 1; j < n; ++j) { for (int k = j + 1; k < n; ++k) { if ((long)a[i] + a[j] + a[k] == 0) { count++; } } } } return count; } } }
1.4.3
解答
見代碼,這里貼出繪圖函數,窗體只是在得到測試結果之后簡單調用以下這兩個函數。
代碼
public static void PaintLinear(double[] testResult) { //新建一個繪圖窗口 Form2 linear = new Form2(); linear.Show(); //新建畫布 Graphics canvas = linear.CreateGraphics(); //獲取窗口區域 Rectangle rect = linear.ClientRectangle; //計算單位長度(十等分) int unitY = rect.Height / 10; int unitX = rect.Width / 10; //獲取中心區域(上下左右增加 10% 的內補) Rectangle center = new Rectangle(rect.X + unitX, rect.Y + unitY, unitX * 8, unitY * 8); //繪制坐標系 canvas.DrawLine(Pens.Black, center.X, center.Y, center.X, center.Y + center.Height); canvas.DrawLine(Pens.Black, center.X, center.Y + center.Height, center.X + center.Width, center.Y + center.Height); //對 X 軸 10 等分,對 Y 軸 10 等分 int xaxisUnit = center.Width / 10; int yaxisUnit = center.Height / 10; //標記 X 軸坐標值 for (int i = 1; i <= 8; i += i) { canvas.DrawString(i + "N", linear.Font, Brushes.Black, center.X + i * xaxisUnit, center.Y + center.Height); } //反轉坐標系 canvas.TranslateTransform(0, linear.ClientRectangle.Height); canvas.ScaleTransform(1, -1); //計算單位長度 double Unit = center.Height / testResult[3]; //標記 PointF[] result = new PointF[4]; for (int i = 0, j = 1; i < 4 && j <= 8; ++i, j += j) { result[i] = new PointF(center.X + j * xaxisUnit, (float)(center.Y + Unit * testResult[i])); } //鏈接 canvas.DrawLines(Pens.Black, result); canvas.Dispose(); } public static void PaintLogarithm(double[] testResult) { //新建一個繪圖窗口 Form2 log = new Form2(); log.Show(); //新建畫布 Graphics canvas = log.CreateGraphics(); //獲取窗口區域 Rectangle rect = log.ClientRectangle; //計算單位長度(十等分) int unitY = rect.Height / 10; int unitX = rect.Width / 10; //獲取中心區域(上下左右增加 10% 的內補) Rectangle center = new Rectangle(rect.X + unitX, rect.Y + unitY, unitX * 8, unitY * 8); //繪制坐標系 canvas.DrawLine(Pens.Black, center.X, center.Y, center.X, center.Y + center.Height); canvas.DrawLine(Pens.Black, center.X, center.Y + center.Height, center.X + center.Width, center.Y + center.Height); //對 X 軸 10 等分,對 Y 軸 10 等分 int xaxisUnit = center.Width / 10; int yaxisUnit = center.Height / 10; //標記 X 軸坐標值 for (int i = 1; i <= 8; i += i) { canvas.DrawString(i + "N", log.Font, Brushes.Black, center.X + i * xaxisUnit, center.Y + center.Height); } //反轉坐標系 canvas.TranslateTransform(0, log.ClientRectangle.Height); canvas.ScaleTransform(1, -1); //計算單位長度 double Unit = center.Height / testResult[3]; //標記 PointF[] result = new PointF[4]; for (int i = 0, j = 1; i < 4 && j <= 8; ++i, j += j) { result[i] = new PointF(center.X + j * xaxisUnit, (float)(center.Y + Unit * testResult[i])); } //鏈接 canvas.DrawLines(Pens.Black, result); canvas.Dispose(); }
1.4.4
解答
代碼分塊↑
時間分析↓
1.4.5
解答
類似於取極限的做法。
a. N
b. 1
c. 1
d. 2N3
e. 1
f. 2
g. N100
1.4.6
解答
a. N + N/2 + N/4 + … = ~2N,線性。
b. 1 + 2 + 4 + … = ~2N,線性。
c. logN * N,線性對數。
1.4.7
解答
最外層循環進行了 N 次比較。
次外層循環進行了 N^2 次比較。
最里層循環進行了 N^3 次比較。
內部 if 語句進行了 N^3 次比較。
if 內部進行了 N(N-1) 次加法。
加起來,~2N^3。
1.4.8
解答
平方級別:直接二層循環遍歷一遍。
線性對數:只遍歷一遍數組,在遍歷過程中用二分查找確認在剩余數組中是否有相等的整數。
代碼
/// <summary> /// 暴力查找數組中相等的整數對。 /// </summary> /// <param name="a">需要查找的數組。</param> /// <returns></returns> static int CountEqual(int[] a) { int n = a.Length; int count = 0; for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (a[i] == a[j]) count++; } } return count; }
暴力算法↑
二分查找算法↓
/// <summary> /// 利用 Array.Sort 進行優化的查找相等整數對算法。 /// </summary> /// <param name="a">需要查找的數組。</param> /// <returns></returns> static int CountEqualLog(int[] a) { int n = a.Length; int count = 0; Array.Sort(a); int dup = 0; // dup = 重復元素數量-1 for (int i = 1; i < n; i++) { while (a[i - 1] == a[i]) { dup++; i++; } count += dup * (dup + 1) / 2; dup = 0; } return count; }
1.4.9
解答
1.4.10
解答
修改二分查找的結束條件,找到后仍然向左側尋找,如果還能找到更小的,則返回較小的下標;否則返回當前下標。
代碼
namespace _1._4._10 { /// <summary> /// 二分查找。 /// </summary> public class BinarySearch { /// <summary> /// 用遞歸方法進行二分查找。 /// </summary> /// <param name="key">關鍵字。</param> /// <param name="a">查找范圍。</param> /// <param name="lo">查找的起始下標。</param> /// <param name="hi">查找的結束下標。</param> /// <returns>返回下標,如果沒有找到則返回 -1。</returns> public static int Rank(int key, int[] a, int lo, int hi) { if (hi < lo) return -1; int mid = (hi - lo) / 2 + lo; if (a[mid] == key) { int mini = Rank(key, a, lo, mid - 1); if (mini != -1) return mini; return mid; } else if (a[mid] < key) { return Rank(key, a, mid + 1, hi); } else { return Rank(key, a, lo, mid - 1); } } } }
1.4.11
解答
這里給出官網上的 Java 實現:StaticSETofInts.java。
howMany() 可以用二分查找實現,在找到一個值后繼續向兩側查找,最后返回找到的次數。
代碼
using System; namespace Measurement { /// <summary> /// 有序數組,能夠快速查找並自動維護其中的順序。 /// </summary> public class StaticSETofInts { private int[] a; /// <summary> /// 用一個數組初始化有序數組。 /// </summary> /// <param name="keys">源數組。</param> public StaticSETofInts(int[] keys) { this.a = new int[keys.Length]; for (int i = 0; i < keys.Length; ++i) { this.a[i] = keys[i]; } Array.Sort(this.a); } /// <summary> /// 檢查數組中是否存在指定元素。 /// </summary> /// <param name="key">要查找的值。</param> /// <returns>存在則返回 true,否則返回 false。</returns> public bool Contains(int key) { return Rank(key, 0, this.a.Length - 1) != -1; } /// <summary> /// 返回某個元素在數組中存在的數量。 /// </summary> /// <param name="key">關鍵值。</param> /// <returns>返回某個元素在數組中存在的數量。</returns> public int HowMany(int key) { int hi = this.a.Length - 1; int lo = 0; return HowMany(key, lo, hi); } /// <summary> /// 返回某個元素在數組中存在的數量。 /// </summary> /// <param name="key">關鍵值。</param> /// <param name="lo">查找起始下標。</param> /// <param name="hi">查找結束下標。</param> /// <returns>返回某個元素在數組中存在的數量。</returns> private int HowMany(int key, int lo, int hi) { int mid = Rank(key, lo, hi); if (mid == -1) return 0; else { return 1 + HowMany(key, lo, mid - 1) + HowMany(key, mid + 1, hi); } } /// <summary> /// 二分查找。 /// </summary> /// <param name="key">關鍵值。</param> /// <param name="lo">查找的起始下標。</param> /// <param name="hi">查找的結束下標。</param> /// <returns>返回關鍵值的下標,如果不存在則返回 -1。</returns> public int Rank(int key, int lo, int hi) { while (lo <= hi) { int mid = (hi - lo) / 2 + lo; if (key < this.a[mid]) hi = mid - 1; else if (key > this.a[mid]) lo = mid + 1; else return mid; } return -1; } } }
1.4.12
解答
由於兩個數組都是有序的,可以同時進行比較。
設 i, j 分別為兩個數組的下標。
如果 a[i] == a[j],i 和 j 都向后移動一位。
如果 a[i] != a[j],比較小的那個向后移動一位。
循環直到某個數組遍歷完畢。
這樣最后的時間復雜度 ~2N
代碼
using System; namespace _1._4._12 { /* * 1.4.12 * * 編寫一個程序,有序打印給定的兩個有序數組(含有 N 個 int 值) 中的所有公共元素, * 程序在最壞情況下所需的運行時間應該和 N 成正比。 * */ class Program { static void Main(string[] args) { int[] a = new int[4] { 2, 3, 4, 10 }; int[] b = new int[6] { 1, 3, 3, 5, 10, 11 }; //2N 次數組訪問,數組 a 和數組 b 各遍歷一遍 for (int i = 0, j = 0; i < a.Length && j < b.Length; ) { if (a[i] < b[j]) { i++; } else if (a[i] > b[j]) { j++; } else { Console.WriteLine($"Common Element:{a[i]}, First index: (a[{i}], b[{j}])"); i++; j++; } } } } }
1.4.13
解答
對象的固定開銷用 Object 表示。
a. Accumulator
使用 1.2.4.3 節給出的實現。
= int * 1 + double + Object * 1
= 4 * 1 + 8 + 16 * 1 = 32
b. Transaction
= string * 1 + Date * 1 + double * 1 + Object * 1
= (40 + 16 + 4 + 4 + 2N) * 1 + (8 + 32) * 1 + 8 * 1 + 16 * 1
= 128 + 2N
c. FixedCapacityStackOfStrings
= string[] * 1 + string * N + int * 1 + Object * 1
= 24 * 1 + N * (64 + 2C) + 4 * 1 + 16 * 1
= N * (64 + 2C) + 44
= N * (64 + 2C) + 48(填充)
d.Point2D
= double * 2 + Object * 1
= 8 * 2 + 16 * 1
= 32
e.Interval1D
= double * 2 + Object * 1
= 8 * 2 + 16 * 1
= 32
f.Interval2D
= Interval1D * 2 + Object * 1
= (8 + 24) * 2 + 16 * 1
= 80
g.Double
= double * 1 + Object * 1
= 8 * 1 + 16 * 1
= 24
1.4.14
解答
這里給出暴力方法,將最內側循環換成二分查找即為優化版本。
代碼
using System; namespace Measurement { /// <summary> /// 用暴力方法查找數組中和為零的四元組。 /// </summary> public static class FourSum { /// <summary> /// 輸出數組中所有和為 0 的四元組。 /// </summary> /// <param name="a">包含所有元素的數組。</param> public static void PrintAll(long[] a) { int N = a.Length; for (int i = 0; i < N; ++i) { for (int j = i + 1; j < N; ++j) { for (int k = j + 1; k < N; ++k) { for (int l = k + 1; l < N; ++l) { if (a[i] + a[j] + a[k] + a[l] == 0) { Console.WriteLine($"{a[i]} + {a[j]} + {a[k]} + {a[l]} = 0"); } } } } } } /// <summary> /// 計算和為零的四元組的數量。 /// </summary> /// <param name="a">包含所有元素的數組。</param> /// <returns></returns> public static int Count(long[] a) { int N = a.Length; int cnt = 0; for (int i = 0; i < N; ++i) { for (int j = i + 1; j < N; ++j) { for (int k = j + 1; k < N; ++k) { for (int l = k + 1; l < N; ++l) { if (a[i] + a[j] + a[k] + a[l] == 0) { cnt++; } } } } } return cnt; } } }
1.4.15
解答
由於數組已經排序(從小到大),負數在左側,正數在右側。
TwoSumFaster
設最左側下標為 lo,最右側下標為 hi。
如果 a[lo] + a[hi] > 0, 說明正數太大,hi--。
如果 a[lo] + a[hi] < 0,說明負數太小,lo++。
否則就找到了一對和為零的整數對,lo++, hi--。
ThreeSumFaster
對於數組中的每一個數 a,ThreeSum 問題就等於求剩余數組中所有和為 -a 的 TwoSum 問題。
只要在 TwoSumFaster 外層再套一個循環即可。
代碼
/// <summary> /// TwoSum 的快速實現。(線性級別) /// </summary> /// <param name="a">需要查找的數組范圍。</param> /// <returns>數組中和為零的整數對數量。</returns> static int TwoSumFaster(int[] a) { int lo = 0; int hi = a.Length - 1; int count = 0; while (lo < hi) { if (a[lo] + a[hi] == 0) { count++; lo++; hi--; } else if (a[lo] + a[hi] < 0) { lo++; } else { hi--; } } return count; } /// <summary> /// ThreeSum 的快速實現。(平方級別) /// </summary> /// <param name="a">需要查找的數組范圍。</param> /// <returns>數組中和為零的三元組數量。</returns> static int ThreeSumFaster(int[] a) { int count = 0; for (int i = 0; i < a.Length; ++i) { int lo = i + 1; int hi = a.Length - 1; while (lo <= hi) { if (a[lo] + a[hi] + a[i] == 0) { count++; lo++; hi--; } else if (a[lo] + a[hi] + a[i] < 0) { lo++; } else { hi--; } } } return count; }
1.4.16
解答
先將數組從小到大排序,再遍歷一遍即可得到差距最小的兩個數。
排序算法需要消耗 NlogN,具體見 MSDN:Array.Sort 方法 (Array)。
代碼
using System; namespace _1._4._16 { /* * 1.4.16 * * 最接近一對(一維)。 * 編寫一個程序,給定一個含有 N 個 double 值的數組 a[], * 在其中找到一對最接近的值:兩者之差(絕對值)最小的兩個數。 * 程序在最壞情況下所需的運行時間應該是線性對數級別的。 * */ class Program { //總運行時間: NlogN + N = NlogN static void Main(string[] args) { double[] a = new double[5] { 0.1, 0.3, 0.6, 0.8, 0 }; Array.Sort(a);//Nlog(N) 具體見 https://msdn.microsoft.com/zh-cn/library/6tf1f0bc(v=vs.110).aspx 備注部分 double minDiff = double.MaxValue; double minA = 0; double minB = 0; for (int i = 0; i < a.Length - 1; ++i)//N { if (a[i + 1] - a[i] < minDiff) { minA = a[i]; minB = a[i + 1]; minDiff = a[i + 1] - a[i]; } } Console.WriteLine($"Min Pair: {minA} {minB}, Min Value: {minDiff}"); } } }
1.4.17
解答
遍歷找到最小值和最大值即可。
代碼
using System; namespace _1._4._17 { /* * 1.4.17 * * 最遙遠的一對(一維)。 * 編寫一個程序,給定一個含有 N 個 double 值的數組 a[], * 在其中找到一對最遙遠的值:兩者之差(絕對值)最大的兩個數。 * 程序在最壞情況下所需的運行時間應該是線性級別的。 * */ class Program { static void Main(string[] args) { double[] a = new double[5] { 0.1, 0.3, 0.6, 0.8, 0 }; double min = int.MaxValue; double max = int.MinValue; for (int i = 0; i < a.Length; ++i) { if (a[i] > max) { max = a[i]; } if (a[i] < min) { min = a[i]; } } Console.WriteLine($"MaxDiff Pair: {min} {max}, Max Difference: {Math.Abs(max - min)}"); } } }
1.4.18
解答
和二分查找的方式類似,先確認中間的值是否是局部最小,如果不是,則向較小的一側二分查找。
代碼
using System; namespace _1._4._18 { class Program { static void Main(string[] args) { var a = new int[5] { 1, 2, 5, 3, 5 }; Console.WriteLine(LocalMinimum(a)); } /// <summary> /// 尋找數組的局部最小元素。 /// </summary> /// <param name="a">尋找范圍。</param> /// <returns>局部最小元素的值。</returns> static int LocalMinimum(int[] a) { int lo = 0; int hi = a.Length - 1; while (lo <= hi) { int mid = (hi - lo) / 2 + lo; int min = mid; // 取左中右最小值的下標 if (mid != hi && a[min] >= a[mid + 1]) min = mid + 1; if (mid != lo && a[min] >= a[mid - 1]) min = mid - 1; if (min == mid) return mid; if (min > mid) lo = min; else hi = min; } return -1; } } }
1.4.19
解答
算法過程類似於 “滑雪”,從數值較高的一側向周圍數值較小的一側移動,直到到達“山谷”(局部最小)。
首先在中間行搜索最小值,再將最小值與其上下兩個元素比較,如果不滿足題意,則“滑向”較小的一側,矩陣被分為了兩半(上下兩側)。
在較小的一側,找到中間列的最小值,再將最小值與其左右兩個元素比較,如果不滿足題意,類似的移動到較小的一側(左右兩側)。
現在查找范圍縮小到了原來矩陣的四分之一,遞歸的進行上述操作,最后可以得到答案。
每次查找最小值都是對行/列進行遍歷,遍歷耗時和 N 成正比。
代碼
using System; namespace _1._4._19 { /* * 1.4.19 * * 矩陣的局部最小元素。 * 給定一個含有 N^2 個不同整數的 N×N 數組 a[]。 * 設計一個運行時間和 N 成正比的算法來找出一個局部最小元素: * 滿足 a[i][j] < a[i+1][j]、a[i][j] < a[i][j+1]、a[i][j] < a[i-1][j] 以及 a[i][j] < a[i][j-1] 的索引 i 和 j。 * 程序運行時間在最壞情況下應該和 N 成正比。 * */ class Program { // 先查找 N/2 行中的最小元素,並令其與上下元素比較, // 如果不滿足題意,則向相鄰的最小元素靠近再次查找 static void Main(string[] args) { int[,] matrix = new int[5, 5] { { 26, 3, 4 , 10, 11 }, { 5, 1, 6, 12, 13 }, { 7, 8, 9 , 14, 15 }, { 16, 17, 18, 27, 20 }, { 21, 22, 23, 24, 25 } }; Console.WriteLine(MinimumRow(matrix, 0, 5, 0, 5)); } /// <summary> /// 在矩陣中間行查找局部最小。 /// </summary> /// <param name="matrix">矩陣。</param> /// <param name="rowStart">實際查找范圍的行起始。</param> /// <param name="rowLength">實際查找范圍的行結尾。</param> /// <param name="colStart">實際查找范圍的列起始。</param> /// <param name="colLength">實際查找范圍的列結尾。</param> /// <returns>矩陣中的局部最小元素。</returns> static int MinimumRow(int[,] matrix, int rowStart, int rowLength, int colStart, int colLength) { int min = int.MaxValue; if (rowLength < 3) return int.MaxValue; int mid = rowStart + rowLength / 2; int minCol = 0; // 獲取矩陣中間行的最小值 for (int i = 0; i < colLength; ++i) { if (min > matrix[mid, colStart + i]) { min = matrix[mid, colStart + i]; minCol = i; } } // 檢查是否滿足條件 if (matrix[mid, minCol] < matrix[mid - 1, minCol] && matrix[mid, minCol] < matrix[mid + 1, minCol]) { return matrix[mid, minCol]; } // 如果不滿足則向較小一側移動 if (matrix[mid - 1, minCol] > matrix[mid + 1, minCol]) { return MinimumCol(matrix, rowStart, rowLength, mid + 1, colLength / 2 + 1); } else { return MinimumCol(matrix, rowStart, rowLength, colStart, colLength / 2 + 1); } } /// <summary> /// 在矩陣中間列查找局部最小。 /// </summary> /// <param name="matrix">矩陣。</param> /// <param name="rowStart">實際查找范圍的行起始。</param> /// <param name="rowLength">實際查找范圍的行結尾。</param> /// <param name="colStart">實際查找范圍的列起始。</param> /// <param name="colLength">實際查找范圍的列結尾。</param> /// <returns>矩陣中的局部最小元素。</returns> static int MinimumCol(int[,] matrix, int rowStart, int rowLength, int colStart, int colLength) { int min = int.MaxValue; int n = matrix.GetLength(0); int mid = n / 2; int minRow = 0; // 獲取矩陣中間列最小值 for (int i = 0; i < n; ++i) { if (min > matrix[i, mid]) { min = matrix[i, mid]; minRow = i; } } // 檢查是否滿足條件 if (matrix[minRow, mid] < matrix[minRow, mid - 1] && matrix[minRow, mid] < matrix[minRow, mid + 1]) { return matrix[minRow, mid]; } // 如果不滿足則向較小一側移動 if (matrix[minRow, mid - 1] > matrix[minRow, mid + 1]) { return MinimumRow(matrix, mid + 1, rowLength / 2 + 1, colStart, colLength); } else { return MinimumRow(matrix, rowStart, rowLength / 2 + 1, colStart, colLength); } } } }
1.4.20
解答
首先給出 BitMax 類的官方 Java 實現:BitonicMax.java。
我們使用這個類生成雙調數組,並使用其中的 Max() 方法找到雙調數組的最大值。
找到最大值之后分別對左右兩側進行二分查找,注意對於升序和降序的數組二分查找的實現有所不同。
代碼
BitonicMax 類
using System; namespace _1._4._20 { /// <summary> /// 雙調查找類。 /// </summary> public class BitonicMax { /// <summary> /// 生成雙調數組。 /// </summary> /// <param name="N">數組的大小。</param> /// <returns></returns> public static int[] Bitonic(int N) { Random random = new Random(); int mid = random.Next(N); int[] a = new int[N]; for (int i = 1; i < mid; ++i) { a[i] = a[i - 1] + 1 + random.Next(9); } if (mid > 0) { a[mid] = a[mid - 1] + random.Next(10) - 5; } for (int i = mid + 1; i < N; ++i) { a[i] = a[i - 1] - 1 - random.Next(9); } return a; } /// <summary> /// 尋找數組中的最大值。 /// </summary> /// <param name="a">查找范圍。</param> /// <param name="lo">查找起始下標。</param> /// <param name="hi">查找結束下標。</param> /// <returns>返回數組中最大值的下標。</returns> public static int Max(int[] a, int lo, int hi) { if (lo == hi) { return hi; } int mid = lo + (hi - lo) / 2; if (a[mid] < a[mid + 1]) { return Max(a, mid + 1, hi); } if (a[mid] > a[mid + 1]) { return Max(a, lo, mid); } return mid; } } }
主程序
using System; namespace _1._4._20 { /* * 1.4.20 * * 雙調查找。 * 如果一個數組中的所有元素是先遞增后遞減的,則稱這個數組為雙調的。 * 編寫一個程序,給定一個含有 N 個不同 int 值的雙調數組,判斷它是否含有給定的整數。 * 程序在最壞情況下所需的比較次數為 ~3lgN * */ class Program { static void Main(string[] args) { int[] a = BitonicMax.Bitonic(100); int max = BitonicMax.Max(a, 0, a.Length - 1); int key = a[50]; int leftside = BinarySearchAscending(a, key, 0, max); int rightside = BinarySearchDescending(a, key, max, a.Length - 1); if (leftside != -1) { Console.WriteLine(leftside); } else if (rightside != -1) { Console.WriteLine(rightside); } else { Console.WriteLine("No Result"); } } /// <summary> /// 對升序數組的二分查找。 /// </summary> /// <param name="a">升序數組。</param> /// <param name="key">關鍵值。</param> /// <param name="lo">查找的左邊界。</param> /// <param name="hi">查找的右邊界。</param> /// <returns>返回找到關鍵值的下標,如果沒有找到則返回 -1。</returns> static int BinarySearchAscending(int[] a, int key, int lo, int hi) { while (lo <= hi) { int mid = lo + (hi - lo) / 2; if (a[mid] < key) { lo = mid + 1; } else if (a[mid] > key) { hi = mid - 1; } else { return mid; } } return -1; } /// <summary> /// 對降序數組的二分查找。 /// </summary> /// <param name="a">升序數組。</param> /// <param name="key">關鍵值。</param> /// <param name="lo">查找的左邊界。</param> /// <param name="hi">查找的右邊界。</param> /// <returns>返回找到關鍵值的下標,如果沒有找到則返回 -1。</returns> static int BinarySearchDescending(int[] a, int key, int lo, int hi) { while (lo < hi) { int mid = lo + (hi - lo) / 2; if (a[mid] > key) { lo = mid + 1; } else if (a[mid] < key) { hi = mid - 1; } else { return mid; } } return -1; } } }
1.4.21
解答
直接將 Contains() 實現為二分查找即可。
代碼
/// <summary> /// 檢查數組中是否存在指定元素。 /// </summary> /// <param name="key">要查找的值。</param> /// <returns>存在則返回 true,否則返回 false。</returns> public bool Contains(int key) { return Rank(key, 0, this.a.Length - 1) != -1; } /// <summary> /// 二分查找。 /// </summary> /// <param name="key">關鍵值。</param> /// <param name="lo">查找的起始下標。</param> /// <param name="hi">查找的結束下標。</param> /// <returns>返回關鍵值的下標,如果不存在則返回 -1。</returns> public int Rank(int key, int lo, int hi) { while (lo <= hi) { int mid = (hi - lo) / 2 + lo; if (key < this.a[mid]) hi = mid - 1; else if (key > this.a[mid]) lo = mid + 1; else return mid; } return -1; }
1.4.22
解答
普通二分查找是通過除法不斷減半縮小搜索范圍。
這里我們用斐波那契數列來縮小范圍。
舉個例子,例如數組大小是 100,比它大的最小斐波那契數是 144。
斐波那契數列如下:0 1 1 2 3 5 8 13 21 34 55 89 144
我們記 F(n) = 144,F(n-1) = 89, F(n-2) = 55。
我們先查看第 0 + F(n-2) 個數,如果比關鍵值小則直接將范圍縮小到 [55, 100];否則則在[0, 55]之間查找。
之后我們令 n = n-1。
遞歸上述過程即可完成查找。
代碼
/// <summary> /// 使用斐波那契數列進行的查找。 /// </summary> /// <param name="a">查找范圍。</param> /// <param name="key">關鍵字。</param> /// <returns>返回查找到的關鍵值下標,沒有結果則返回 -1。</returns> static int rank(int[] a, int key) { // 使用斐波那契數列作為縮減范圍的依據 int Fk = 1; int Fk_1 = 1; int Fk_2 = 0; // 獲得 Fk,Fk需要大於等於數組的大小,復雜度 lgN while (Fk < a.Length) { Fk = Fk + Fk_1; Fk_1 = Fk_1 + Fk_2; Fk_2 = Fk - Fk_1; } int lo = 0; // 按照斐波那契數列縮減查找范圍,復雜度 lgN while (Fk_2 >= 0) { int i = lo + Fk_2 > a.Length - 1 ? a.Length - 1 : lo + Fk_2; if (a[i] < key) { lo = lo + Fk_2; } else if (a[i] == key) { return i; } Fk = Fk_1; Fk_1 = Fk_2; Fk_2 = Fk - Fk_1; } return -1; }
1.4.23
解答
根據書中的提示,將二分查找中判斷相等的條件改為兩個數的差小於等於 1/N2。
代碼
// 將二分查找中的相等判定條件修改為差值小於 x,其中 x = 1/N^2。 /// <summary> /// 二分查找。 /// </summary> /// <param name="a">查找范圍。</param> /// <param name="key">關鍵字。</param> /// <returns>結果的下標,沒有結果時返回 -1。</returns> static int BinarySearch(double[] a, double key) { int lo = 0; int hi = a.Length - 1; double threshold = 1.0 / (a.Length * a.Length); while (lo <= hi) { int mid = lo + (hi - lo) / 2; if (Math.Abs(a[mid] - key) <= threshold) { return mid; } else if (a[mid] < key) { lo = mid + 1; } else { hi = mid - 1; } } return -1; }
1.4.24
解答
第一問:二分查找即可。
第二問:
按照第 1, 2, 4, 8,..., 2^k 層順序查找,一直到 2^k > F,
隨后在 [2^(k - 1), 2^k] 范圍中二分查找。
代碼
這里建立了一個結構體用於返回測試結果:
struct testResult { public int F;// 找到的 F 值。 public int BrokenEggs;// 打碎的雞蛋數。 }
用於測試的方法:
/// <summary> /// 扔雞蛋,沒碎返回 true,碎了返回 false。 /// </summary> /// <param name="floor">扔雞蛋的高度。</param> /// <returns></returns> static bool ThrowEgg(int floor) { return floor <= F; } /// <summary> /// 第一種方案。 /// </summary> /// <param name="a">大樓。</param> /// <returns></returns> static testResult PlanA(int[] a) { int lo = 0; int hi = a.Length - 1; int mid = 0; int eggs = 0; testResult result = new testResult(); while (lo <= hi) { mid = lo + (hi - lo) / 2; if (ThrowEgg(mid)) { lo = mid + 1; } else { eggs++; hi = mid - 1; } } result.BrokenEggs = eggs; result.F = hi; return result; } /// <summary> /// 第二種方案。 /// </summary> /// <param name="a">大樓。</param> /// <returns></returns> static testResult PlanB(int[] a) { int lo = 0; int hi = 1; int mid = 0; int eggs = 0; testResult result = new testResult(); while (ThrowEgg(hi)) { lo = hi; hi *= 2; } eggs++; if (hi > a.Length - 1) { hi = a.Length - 1; } while (lo <= hi) { mid = lo + (hi - lo) / 2; if (ThrowEgg(mid)) { lo = mid + 1; } else { eggs++; hi = mid - 1; } } result.BrokenEggs = eggs; result.F = hi; return result; }
1.4.25
解答
第一問:
第一個蛋按照 √(N), 2√(N), 3√(N), 4√(N),..., √(N) * √(N) 順序查找直至碎掉。這里扔了 k 次,k <= √(N)。
k-1√(N) ~ k√(N) 順序查找直至碎掉,F 值就找到了。這里最多扔 √(N) 次。
第二問:
按照第 1, 3, 6, 10,..., 1/2k^2 層順序查找,一直到 1/2k^2 > F,
隨后在 [1/2k^2 - k, 1/2k^2] 范圍中順序查找。
代碼
這里我們同樣定義了一個結構體:
struct testResult { public int F;// 測試得出的 F 值 public int BrokenEggs;// 碎掉的雞蛋數。 public int ThrowTimes;// 扔雞蛋的次數。 }
之后是測試用的方法:
/// <summary> /// 扔雞蛋,沒碎返回 true,碎了返回 false。 /// </summary> /// <param name="floor">扔雞蛋的高度。</param> /// <returns></returns> static bool ThrowEgg(int floor) { return floor <= F; } /// <summary> /// 第一種方案。 /// </summary> /// <param name="a">大樓。</param> /// <returns></returns> static testResult PlanA(int[] a) { int lo = 0; int hi = 0; int eggs = 0; int throwTimes = 0; testResult result = new testResult(); while (ThrowEgg(hi)) { throwTimes++; lo = hi; hi += (int)Math.Sqrt(a.Length); } eggs++; if (hi > a.Length - 1) { hi = a.Length - 1; } while (lo <= hi) { if (!ThrowEgg(lo)) { eggs++; break; } throwTimes++; lo++; } result.BrokenEggs = eggs; result.F = lo - 1; result.ThrowTimes = throwTimes; return result; } /// <summary> /// 第二種方案。 /// </summary> /// <param name="a">大樓。</param> /// <returns></returns> static testResult PlanB(int[] a) { int lo = 0; int hi = 0; int eggs = 0; int throwTimes = 0; testResult result = new testResult(); for (int i = 0; ThrowEgg(hi); ++i) { throwTimes++; lo = hi; hi += i; } eggs++; if (hi > a.Length - 1) { hi = a.Length - 1; } while (lo <= hi) { if (!ThrowEgg(lo)) { eggs++; break; } lo++; throwTimes++; } result.BrokenEggs = eggs; result.F = lo - 1; result.ThrowTimes = throwTimes; return result; }
1.4.26
解答
1.4.27
解答
實現比較簡單,想象兩個棧背靠背接在一起,左側棧負責出隊,右側棧負責入隊。
當左側棧為空時就把右側棧中的元素倒到左側棧,這個過程是 O(n) 的。
但在這個過程之前必然有 n 個元素入棧,均攤后即為 O(1)。
代碼
namespace _1._4._27 { /// <summary> /// 用兩個棧模擬的隊列。 /// </summary> /// <typeparam name="Item">隊列中的元素。</typeparam> class StackQueue<Item> { Stack<Item> H;//用於保存出隊元素 Stack<Item> T;//用於保存入隊元素 /// <summary> /// 構造一個隊列。 /// </summary> public StackQueue() { this.H = new Stack<Item>(); this.T = new Stack<Item>(); } /// <summary> /// 將棧 T 中的元素依次彈出並壓入棧 H 中。 /// </summary> private void Reverse() { while (!this.T.IsEmpty()) { this.H.Push(this.T.Pop()); } } /// <summary> /// 將一個元素出隊。 /// </summary> /// <returns></returns> public Item Dequeue() { //如果沒有足夠的出隊元素,則將 T 中的元素移動過來 if (this.H.IsEmpty()) { Reverse(); } return this.H.Pop(); } /// <summary> /// 將一個元素入隊。 /// </summary> /// <param name="item">要入隊的元素。</param> public void Enqueue(Item item) { this.T.Push(item); } } }
1.4.28
解答
每次入隊的時候將隊列倒轉,這樣入隊的元素就是第一個了。
代碼
namespace _1._4._28 { /// <summary> /// 用一條隊列模擬的棧。 /// </summary> /// <typeparam name="Item">棧中保存的元素。</typeparam> class QueueStack<Item> { Queue<Item> queue; /// <summary> /// 初始化一個棧。 /// </summary> public QueueStack() { this.queue = new Queue<Item>(); } /// <summary> /// 向棧中添加一個元素。 /// </summary> /// <param name="item"></param> public void Push(Item item) { this.queue.Enqueue(item); int size = this.queue.Size(); // 倒轉隊列 for (int i = 0; i < size - 1; ++i) { this.queue.Enqueue(this.queue.Dequeue()); } } /// <summary> /// 從棧中彈出一個元素。 /// </summary> /// <returns></returns> public Item Pop() { return this.queue.Dequeue(); } /// <summary> /// 確定棧是否為空。 /// </summary> /// <returns></returns> public bool IsEmpty() { return this.queue.IsEmpty(); } } }
1.4.29
解答
和用兩個棧實現隊列的方法類似。
push 的時候把右側棧內容倒到左側棧,之后再入棧。
pop 的時候也做相同操作,右側棧內容進左側棧,之后再出棧。
enqueue 的時候則將左側棧內容倒到右側棧,之后再入隊。
代碼
namespace _1._4._29 { /// <summary> /// 用兩個棧模擬的 Steque。 /// </summary> /// <typeparam name="Item">Steque 中的元素類型。</typeparam> class StackSteque<Item> { Stack<Item> H; Stack<Item> T; /// <summary> /// 初始化一個 Steque /// </summary> public StackSteque() { this.H = new Stack<Item>(); this.T = new Stack<Item>(); } /// <summary> /// 向棧中添加一個元素。 /// </summary> /// <param name="item"></param> public void Push(Item item) { ReverseT(); this.H.Push(item); } /// <summary> /// 將 T 中的元素彈出並壓入到 H 中。 /// </summary> private void ReverseT() { while (!this.T.IsEmpty()) { this.H.Push(this.T.Pop()); } } /// <summary> /// 將 H 中的元素彈出並壓入到 T 中。 /// </summary> private void ReverseH() { while (!this.H.IsEmpty()) { this.T.Push(this.H.Pop()); } } /// <summary> /// 從 Steque 中彈出一個元素。 /// </summary> /// <returns></returns> public Item Pop() { ReverseT(); return this.H.Pop(); } /// <summary> /// 在 Steque 尾部添加一個元素。 /// </summary> /// <param name="item"></param> public void Enqueue(Item item) { ReverseH(); this.T.Push(item); } /// <summary> /// 檢查 Steque 是否為空。 /// </summary> /// <returns></returns> public bool IsEmpty() { return this.H.IsEmpty() && this.T.IsEmpty(); } } }
1.4.30
解答
steque 作為隊列的頭部,stack 作為隊列的尾部。
pushLeft:直接 push 到 steque 中即可。
pushRight:如果 stack 為空,則直接 enqueue 到 steque 中,否則就 push 到 stack 中。
popLeft:如果 steque 為空,則將 stack 中的元素倒到 steque 中去(steque.push(stack.pop())),然后再從 steque 中 pop。
popRight:如果 stack 為空,則將 steque 中的元素倒到 stack 中去,然后再從 stack 中 pop。
代碼
namespace _1._4._30 { /// <summary> /// 用一個棧和一個 Steque 模擬的雙向隊列。 /// </summary> /// <typeparam name="Item">雙向隊列中保存的元素類型。</typeparam> class Deque<Item> { Stack<Item> stack;//代表隊列尾部 Steque<Item> steque;//代表隊列頭部 /// <summary> /// 創建一條空的雙向隊列。 /// </summary> public Deque() { this.stack = new Stack<Item>(); this.steque = new Steque<Item>(); } /// <summary> /// 在左側插入一個新元素。 /// </summary> /// <param name="item">要插入的元素。</param> public void PushLeft(Item item) { this.steque.Push(item); } /// <summary> /// 將棧中的內容移動到 Steque 中。 /// </summary> private void StackToSteque() { while (!this.stack.IsEmpty()) { this.steque.Push(this.stack.Pop()); } } /// <summary> /// 將 Steque 中的內容移動到棧中。 /// </summary> private void StequeToStack() { while (!this.steque.IsEmpty()) { this.stack.Push(this.steque.Pop()); } } /// <summary> /// 從雙向隊列左側彈出一個元素。 /// </summary> /// <returns></returns> public Item PopLeft() { if (this.steque.IsEmpty()) { StackToSteque(); } return this.steque.Pop(); } /// <summary> /// 向雙向隊列右側添加一個元素。 /// </summary> /// <param name="item">要插入的元素。</param> public void PushRight(Item item) { if (this.stack.IsEmpty()) { this.steque.Enqueue(item); } else { this.stack.Push(item); } } /// <summary> /// 從雙向隊列右側彈出一個元素。 /// </summary> /// <returns></returns> public Item PopRight() { if (this.stack.IsEmpty()) { StequeToStack(); } return this.stack.Pop(); } /// <summary> /// 判斷隊列是否為空。 /// </summary> /// <returns></returns> public bool IsEmpty() { return this.stack.IsEmpty() && this.steque.IsEmpty(); } /// <summary> /// 返回隊列中元素的數量。 /// </summary> /// <returns></returns> public int Size() { return this.stack.Size() + this.steque.Size(); } } }
1.4.31
解答
三個棧分別命名為左中右。
左側棧和右側棧負責模擬隊列,和用兩個棧模擬隊列的方法類似。
由於是雙向隊列,左棧和右棧會頻繁的倒來倒去,因此每次都只倒一半的元素可以有效減少開銷。
有一側棧為空時,另一側棧中上半部分先移動到中間棧中,下半部分倒到另一側棧里,再從中間棧拿回上半部分元素。
這樣可以確保接下來的 pop 操作一定是常數級別的。
代碼
namespace _1._4._31 { /// <summary> /// 用三個棧模擬的雙向隊列。 /// </summary> /// <typeparam name="Item">雙向隊列中的元素。</typeparam> class Deque<Item> { Stack<Item> left; Stack<Item> middle; Stack<Item> right; /// <summary> /// 構造一條新的雙向隊列。 /// </summary> public Deque() { this.left = new Stack<Item>(); this.middle = new Stack<Item>(); this.right = new Stack<Item>(); } /// <summary> /// 向雙向隊列左側插入一個元素。 /// </summary> /// <param name="item">要插入的元素。</param> public void PushLeft(Item item) { this.left.Push(item); } /// <summary> /// 向雙向隊列右側插入一個元素。 /// </summary> /// <param name="item">要插入的元素。</param> public void PushRight(Item item) { this.right.Push(item); } /// <summary> /// 當一側棧為空時,將另一側的下半部分元素移動過來。 /// </summary> /// <param name="source">不為空的棧。</param> /// <param name="destination">空棧。</param> private void Move(Stack<Item> source, Stack<Item> destination) { int n = source.Size(); // 將上半部分元素移動到臨時棧 middle for (int i = 0; i < n / 2; ++i) { this.middle.Push(source.Pop()); } // 將下半部分移動到另一側棧中 while (!source.IsEmpty()) { destination.Push(source.Pop()); } // 從 middle 取回上半部分元素 while (!this.middle.IsEmpty()) { source.Push(this.middle.Pop()); } } /// <summary> /// 檢查雙端隊列是否為空。 /// </summary> /// <returns></returns> public bool IsEmpty() { return this.right.IsEmpty() && this.middle.IsEmpty() && this.left.IsEmpty(); } /// <summary> /// 從右側彈出一個元素。 /// </summary> /// <returns></returns> public Item PopRight() { if (this.right.IsEmpty()) { Move(this.left, this.right); } return this.right.Pop(); } /// <summary> /// 從左側彈出一個元素。 /// </summary> /// <returns></returns> public Item PopLeft() { if (this.left.IsEmpty()) { Move(this.right, this.left); } return this.left.Pop(); } /// <summary> /// 返回雙端隊列的大小。 /// </summary> /// <returns></returns> public int Size() { return this.left.Size() + this.middle.Size() + this.right.Size(); } } }
1.4.32
解答
首先,不需要擴容數組的的操作都只需訪問數組一次,M 次操作就是 M 次訪問。
隨后我們有性質, M 次棧操作后額外復制訪問數組的次數小於 2M。
這里簡單證明,設 M 次操作之后棧的大小為 n,那么額外訪問數組的次數為:
S = n/2 + n/4 + n/8 +...+ 2 < n
為了能使棧大小達到 n,M 必須大於等於 n/2
因此 2M >= n > S,得證。
因此我們可以得到 M 次操作后訪問數組次數的總和 S' = S + M < 3M
1.4.33
解答
Integer = 4(int) + 8(對象開銷) = 12
Date = 3 * 4(int * 3) + 8(對象開銷) = 20
Counter = 4(String 的引用) + 4(int) + 8(對象開銷) = 16
int[] = 8(對象開銷) + 4(數組長度) + 4N = 12 + 4N
double[] = 8(對象開銷) + 4(數組長度) + 8N = 12 + 8N
double[][] = 8(對象開銷) + 4(數組長度) + 4M(引用) + M(12 + 8N)(M 個一維數組) = 12 + 16M + 8MN
String = 8(對象開銷) + 3*4(int * 3) + 4(字符數組的引用) = 24
Node = 8(對象開銷) + 4*2(引用*2) = 16
Stack = 8(對象開銷) + 4(引用) + 4(int) = 16
1.4.34
解答
1. 第一種方案,類似於二分查找,先猜測左邊界(lo),再猜測右邊界(hi),如果邊界值猜中的話直接返回,否則:
如果右邊界比較熱,那么左邊界向右邊界靠,lo=mid;否則,右邊界向左邊界靠,hi=mid。其中,mid = lo + (hi – lo)/2。
每次二分查找都要猜測兩次,~2lgN。
2. 第二種方案,假設上次猜測值為 lastGuess,本次即將要猜測的值為 nowGuess,通過方程:
(lastGuess + nowGuess)/2 = (lo + hi)/2
可以求得 nowGuess,具體可以查看示意圖:
數字是猜測順序,黑色范圍是猜測值的范圍(lastGuess 和 nowGuess),綠色的是實際查找的范圍(lo 和 hi)。
代碼
首先是 Game 類
using System; namespace _1._4._34 { /// <summary> /// 某次猜測的結果。 /// </summary> enum GuessResult { Hot = 1, // 比上次猜測更接近目標。 Equal = 0, // 猜中目標。 Cold = -1, // 比上次猜測更遠離目標。 FirstGuess = -2 // 第一次猜測。 } /// <summary> /// 游戲類。 /// </summary> class Game { public int N { get; } // 目標值的最大范圍。 public int SecretNumber { get; } // 目標值。 public int LastGuess { get; private set; } // 上次猜測的值 /// <summary> /// 構造函數,新開一局游戲。 /// </summary> /// <param name="N">目標值的最大范圍。</param> public Game(int N) { Random random = new Random(); this.N = N; this.SecretNumber = random.Next(N - 1) + 1; this.LastGuess = -1; } /// <summary> /// 猜測,根據與上次相比更為接近還是遠離目標值返回結果。 /// </summary> /// <param name="guess">本次的猜測值</param> /// <returns>接近或不變返回 Hot,遠離則返回 Cold,猜中返回 Equal。</returns> public GuessResult Guess(int guess) { if (guess == this.SecretNumber) { return GuessResult.Equal; } if (this.LastGuess == -1) { this.LastGuess = guess; return GuessResult.FirstGuess; } int lastDiff = Math.Abs(this.LastGuess - this.SecretNumber); this.LastGuess = guess; int nowDiff = Math.Abs(guess - this.SecretNumber); if (nowDiff > lastDiff) { return GuessResult.Cold; } else { return GuessResult.Hot; } } /// <summary> /// 重置游戲,清空上次猜測的記錄。目標值和最大值都不變。 /// </summary> public void Restart() { this.LastGuess = -1; } } }
之后是實際測試的方法:
using System; namespace _1._4._34 { /* * 1.4.34 * * 熱還是冷。 * 你的目標是猜出 1 到 N 之間的一個秘密的整數。 * 每次猜完一個整數后,你會直到你的猜測距離該秘密整數是否相等(如果是則游戲結束)。 * 如果不相等,你會知道你的猜測相比上一次猜測距離秘密整數是比較熱(接近),還是比較冷(遠離)。 * 設計一個算法在 ~2lgN 之內找到這個秘密整數,然后設計一個算法在 ~1lgN 之內找到這個秘密整數。 * */ class Program { /// <summary> /// 某種方案的測試結果,包含猜測結果和嘗試次數。 /// </summary> struct TestResult { public int SecretNumber;// 猜測到的數字。 public int TryTimes;// 嘗試次數。 } static void Main(string[] args) { Game game = new Game(1000); TestResult A = PlayGameA(game); game.Restart(); TestResult B = PlayGameB(game); Console.WriteLine($"SecretNumber:{game.SecretNumber}"); Console.WriteLine("TestResultA:"); Console.WriteLine($"SecretNumber:{A.SecretNumber}, TryTimes:{A.TryTimes}"); Console.WriteLine(); Console.WriteLine("TestResultB:"); Console.WriteLine($"SecretNumber:{B.SecretNumber}, TryTimes:{B.TryTimes}"); } /// <summary> /// 方案一,用二分查找實現,需要猜測 2lgN 次。 /// </summary> /// <param name="game">用於猜測的游戲對象。</param> /// <returns>返回測試結果,包含猜測結果和嘗試次數。</returns> static TestResult PlayGameA(Game game) { TestResult result; result.TryTimes = 0; result.SecretNumber = 0; GuessResult guessResult; int hi = game.N; int lo = 1; // 利用二分查找猜測,2lgN while (lo <= hi) { int mid = lo + (hi - lo) / 2; guessResult = game.Guess(lo); result.TryTimes++; if (guessResult == GuessResult.Equal) { result.SecretNumber = lo; return result; } guessResult = game.Guess(hi); result.TryTimes++; if (guessResult == GuessResult.Equal) { result.SecretNumber = hi; return result; } else if (guessResult == GuessResult.Hot) { lo = mid; } else { hi = mid; } } return result; } /// <summary> /// 方案二,根據 (lastGuess + nowGuess)/2 = (lo + hi) / 2 確定每次猜測的值。 /// </summary> /// <param name="game">用於猜測的游戲對象。</param> /// <returns>返回測試結果,包含猜測結果和嘗試次數。</returns> static TestResult PlayGameB(Game game) { TestResult result; result.TryTimes = 0; result.SecretNumber = 0; GuessResult guessResult; int hi = game.N; int lo = 1; bool isRightSide = true; // 第一次猜測 guessResult = game.Guess(1); result.TryTimes++; if (guessResult == GuessResult.Equal) { result.SecretNumber = 1; return result; } while (lo < hi) { int mid = lo + (hi - lo) / 2; int nowGuess = (lo + hi) - game.LastGuess; guessResult = game.Guess(nowGuess); result.TryTimes++; if (guessResult == GuessResult.Equal) { result.SecretNumber = nowGuess; break; } else if (guessResult == GuessResult.Hot) { if (isRightSide) { lo = mid; } else { hi = mid; } } else { if (isRightSide) { hi = mid; } else { lo = mid; } } isRightSide = !isRightSide; if (hi - lo <= 1) { break; } } if (game.Guess(lo) == GuessResult.Equal) { result.TryTimes++; result.SecretNumber = lo; } else if (game.Guess(hi) == GuessResult.Equal) { result.TryTimes++; result.SecretNumber = hi; } return result; } } }
1.4.35
解答
1. 一個 Node 對象包含一個 int(泛型 Item) 的引用和下一個 Node 對象的引用。push 操作創建新 Node 對象時會創建一個引用。
因此對於第一種情況,壓入 n 個 int 類型的元素創建了 N 個 Node 對象,創建了 2N 個引用。
2. 比起上一種情況,每個 Node 對象多包含了一個指向 Integer 的引用。
因此對於第二中情況,壓入 n 個 int 類型的元素創建了 N 個 Node 對象和 N 個 Integer 對象,比起第一種情況多創建了 N 個引用。
3. 對於數組來說,創建對象只有擴容時重新創建數組對象一種情況,對於 N 次 push 操作只需要 lgN 次擴容,因此創建的對象為 lgN 個。
每次擴容都需要重新創建引用,(4 + 8 +...+ 2N)(擴容) + N(每次 push 操作) = 5N - 4 = ~5N
4. 創建引用和上題一樣,創建對象則多出了裝箱過程,每次 push 都會新建一個 Integer 對象,N + lgN = ~N。
1.4.36
解答
1. N 個 Node<int> 對象的空間開銷
= N * (16(對象開銷) + 4(int) + 8(下一個 Node 的引用) + 4(填充字節)) = 32N
2. 比起上一題來說,空間開銷變為
= N * (16(Node 對象開銷) + 8(Integer 對象引用) + (16(Integer 對象開銷) + 4(int) + 4(填充字節)) + 8(下一個對象的引用) = 32N + 24N = 56N。
3. 如果不擴容則是 4N,N 個元素最多可以維持 4N 的棧空間(少於四分之一將縮小)。
4. 比起上一題,數組元素變成了引用每個占用 8 字節,還要額外加上 Integer 對象的每個 24 字節。
= (8 + 24)N ~ (8 * 4 + 24)N
1.4.37
解答
數據量比較大時才會有比較明顯的差距。
代碼
FixedCapacityStackOfInts,根據 FixedCapacityOfString 修改而來:
using System; using System.Collections; using System.Collections.Generic; namespace _1._4._37 { /// <summary> /// 固定大小的整型數據棧。 /// </summary> class FixedCapacityStackOfInts : IEnumerable<int> { private int[] a; private int N; /// <summary> /// 默認構造函數。 /// </summary> /// <param name="capacity">棧的大小。</param> public FixedCapacityStackOfInts(int capacity) { this.a = new int[capacity]; this.N = 0; } /// <summary> /// 檢查棧是否為空。 /// </summary> /// <returns></returns> public bool IsEmpty() { return this.N == 0; } /// <summary> /// 檢查棧是否已滿。 /// </summary> /// <returns></returns> public bool IsFull() { return this.N == this.a.Length; } /// <summary> /// 將一個元素壓入棧中。 /// </summary> /// <param name="item">要壓入棧中的元素。</param> public void Push(int item) { this.a[this.N] = item; this.N++; } /// <summary> /// 從棧中彈出一個元素,返回被彈出的元素。 /// </summary> /// <returns></returns> public int Pop() { this.N--; return this.a[this.N]; } /// <summary> /// 返回棧頂元素(但不彈出它)。 /// </summary> /// <returns></returns> public int Peek() { return this.a[this.N - 1]; } public IEnumerator<int> GetEnumerator() { return new ReverseEnmerator(this.a); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } private class ReverseEnmerator : IEnumerator<int> { private int current; private int[] a; public ReverseEnmerator(int[] a) { this.current = a.Length; this.a = a; } int IEnumerator<int>.Current => this.a[this.current]; object IEnumerator.Current => this.a[this.current]; void IDisposable.Dispose() { this.current = -1; this.a = null; } bool IEnumerator.MoveNext() { if (this.current == 0) return false; this.current--; return true; } void IEnumerator.Reset() { this.current = this.a.Length; } } } }
FixedCapacityStack<Item>
using System; using System.Collections; using System.Collections.Generic; namespace _1._4._37 { /// <summary> /// 固定大小的棧。 /// </summary> class FixedCapacityStack<Item> : IEnumerable<Item> { private Item[] a; private int N; /// <summary> /// 默認構造函數。 /// </summary> /// <param name="capacity">棧的大小。</param> public FixedCapacityStack(int capacity) { this.a = new Item[capacity]; this.N = 0; } /// <summary> /// 檢查棧是否為空。 /// </summary> /// <returns></returns> public bool IsEmpty() { return this.N == 0; } /// <summary> /// 檢查棧是否已滿。 /// </summary> /// <returns></returns> public bool IsFull() { return this.N == this.a.Length; } /// <summary> /// 將一個元素壓入棧中。 /// </summary> /// <param name="item">要壓入棧中的元素。</param> public void Push(Item item) { this.a[this.N] = item; this.N++; } /// <summary> /// 從棧中彈出一個元素,返回被彈出的元素。 /// </summary> /// <returns></returns> public Item Pop() { this.N--; return this.a[this.N]; } /// <summary> /// 返回棧頂元素(但不彈出它)。 /// </summary> /// <returns></returns> public Item Peek() { return this.a[this.N - 1]; } public IEnumerator<Item> GetEnumerator() { return new ReverseEnmerator(this.a); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } private class ReverseEnmerator : IEnumerator<Item> { private int current; private Item[] a; public ReverseEnmerator(Item[] a) { this.current = a.Length; this.a = a; } Item IEnumerator<Item>.Current => this.a[this.current]; object IEnumerator.Current => this.a[this.current]; void IDisposable.Dispose() { this.current = -1; this.a = null; } bool IEnumerator.MoveNext() { if (this.current == 0) return false; this.current--; return true; } void IEnumerator.Reset() { this.current = this.a.Length; } } } }
測試函數:
using System; using Measurement; namespace _1._4._37 { /// <summary> /// FixedCapacityStackOfInts 測試類。 /// </summary> public static class DoubleTest { private static readonly int MAXIMUM_INTEGER = 1000000; /// <summary> /// 返回對 n 個隨機整數的棧進行 n 次 push 和 n 次 pop 所需的時間。 /// </summary> /// <param name="n">隨機數組的長度。</param> /// <returns></returns> public static double TimeTrial(int n) { int[] a = new int[n]; FixedCapacityStackOfInts stack = new FixedCapacityStackOfInts(n); Random random = new Random(DateTime.Now.Millisecond); for (int i = 0; i < n; ++i) { a[i] = random.Next(-MAXIMUM_INTEGER, MAXIMUM_INTEGER); } Stopwatch timer = new Stopwatch(); for (int i = 0; i < n; ++i) { stack.Push(a[i]); } for (int i = 0; i < n; ++i) { stack.Pop(); } return timer.ElapsedTimeMillionSeconds(); } /// <summary> /// 返回對 n 個隨機整數的棧進行 n 次 push 和 n 次 pop 所需的時間。 /// </summary> /// <param name="n">隨機數組的長度。</param> /// <returns></returns> public static double TimeTrialGeneric(int n) { int[] a = new int[n]; FixedCapacityStack<int> stack = new FixedCapacityStack<int>(n); Random random = new Random(DateTime.Now.Millisecond); for (int i = 0; i < n; ++i) { a[i] = random.Next(-MAXIMUM_INTEGER, MAXIMUM_INTEGER); } Stopwatch timer = new Stopwatch(); for (int i = 0; i < n; ++i) { stack.Push(a[i]); } for (int i = 0; i < n; ++i) { stack.Pop(); } return timer.ElapsedTimeMillionSeconds(); } } }
主函數:
using System; namespace _1._4._37 { /* * 1.4.37 * * 自動裝箱的性能代價。 * 通過實驗在你的計算機上計算使用自動裝箱所付出的性能代價。 * 實現一個 FixedCapacityStackOfInts, * 並使用類似 DoublingRatio 的用例比較它和泛型 FixedCapacityStack 在進行大量 push() 和 pop() 時的性能。 * */ class Program { static void Main(string[] args) { Console.WriteLine("測試量\t非泛型耗時(毫秒)\t泛型耗時(毫秒)\t差值"); for (int n = 250; true; n += n) { double time = DoubleTest.TimeTrial(n); double timeGeneric = DoubleTest.TimeTrialGeneric(n); Console.WriteLine($"{n}\t{time}\t{timeGeneric}\t{Math.Abs(time - timeGeneric)}"); } } } }
1.4.38
解答
把 DoublingTest 中調用的函數稍作修改即可。
代碼
ThreeSum 測試類
using System; namespace _1._4._38 { /// <summary> /// ThreeSum 測試類。 /// </summary> public static class DoubleTest { private static readonly int MAXIMUM_INTEGER = 1000000; /// <summary> /// 返回對 n 個隨機整數的數組進行一次 ThreeSum 所需的時間。 /// </summary> /// <param name="n">隨機數組的長度。</param> /// <returns></returns> public static double TimeTrial(int n) { int[] a = new int[n]; Random random = new Random(DateTime.Now.Millisecond); for (int i = 0; i < n; ++i) { a[i] = random.Next(-MAXIMUM_INTEGER, MAXIMUM_INTEGER); } Measurement.Stopwatch timer = new Measurement.Stopwatch(); ThreeSum.Count(a); return timer.ElapsedTime(); } } }
ThreeSum
using System; namespace _1._4._38 { /// <summary> /// 用暴力方法尋找數組中和為零的三元組。 /// </summary> public static class ThreeSum { /// <summary> /// 輸出所有和為零的三元組。 /// </summary> /// <param name="a">輸入數組。</param> public static void PrintAll(int[] a) { int n = a.Length; for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { for (int k = 0; k < n; ++k) { if (i < j && j < k) { if ((long)a[i] + a[j] + a[k] == 0) { Console.WriteLine($"{a[i]} + {a[j]} + {a[k]}"); } } } } } } /// <summary> /// 計算和為零的三元組的數量。 /// </summary> /// <param name="a">輸入數組。</param> /// <returns></returns> public static int Count(int[] a) { int n = a.Length; int count = 0; for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { for (int k = 0; k < n; ++k) { if (i < j && j < k) { if ((long)a[i] + a[j] + a[k] == 0) { count++; } } } } } return count; } } }
1.4.39
解答
執行 N 次后取平均即可。
代碼
修改后的 DoublingTest:
using System; using Measurement; namespace _1._4._39 { /// <summary> /// ThreeSum 測試類。 /// </summary> public static class DoubleTest { private static readonly int MAXIMUM_INTEGER = 1000000; /// <summary> /// 返回對 n 個隨機整數的數組進行一次 ThreeSum 所需的時間。 /// </summary> /// <param name="n">隨機數組的長度。</param> /// <param name="repeatTimes">重復測試的次數。</param> /// <returns></returns> public static double TimeTrial(int n, int repeatTimes) { int[] a = new int[n]; double sum = 0; Random random = new Random(DateTime.Now.Millisecond); for (int i = 0; i < n; ++i) { a[i] = random.Next(-MAXIMUM_INTEGER, MAXIMUM_INTEGER); } for (int i = 0; i < repeatTimes; ++i) { Stopwatch timer = new Stopwatch(); ThreeSum.Count(a); sum += timer.ElapsedTime(); } return sum / repeatTimes; } } }
1.4.40
解答
N 個數可組成的三元組的總數為:
C(N, 3) = N(N-1)(N-2)/3! = ~ (N^3)/6 (組合數公式)
[-M, M]中隨機 N 次,有 (2M+1)^N 種隨機序列(每次隨機都有 2M+1 種可能)
按照分步計數方法,將隨機序列分為和為零的三元組和其余 N-3 個數
這些序列中,和為零的三元組有 3M^2 + 3M + 1 種可能。
其他不為零的 N-3 個位置有 (2M+1)^(N-3) 種可能。
總共有 ((N^3)/6) * (3M^2 + 3M + 1) * (2M+1)^(N-3) 種可能性
平均值為:
[((N^3)/6) * (3M^2 + 3M + 1) * (2M+1)^(N-3)] / (2M+1)^N
N^3/16M
代碼
using System; namespace _1._4._40 { /* * 1.4.40 * * 隨機輸入下的 3-sum 問題。 * 猜測找出 N 個隨機 int 值中和為 0 的整數三元組的數量所需的時間並驗證你的猜想。 * 如果你擅長數學分析,請為此問題給出一個合適的數學模型, * 其中所有值均勻的分布在 -M 和 M 之間,且 M 不能是一個小整數。 * */ class Program { // 數學模型 // // N 個數可組成的三元組的總數為: // C(N, 3) = N(N-1)(N-2)/3! = ~ (N^3)/6 (組合數公式) // [-M, M]中隨機 N 次,有 (2M+1)^N 種隨機序列(每次隨機都有 2M+1 種可能) // 按照分步計數方法,將隨機序列分為和為零的三元組和其余 N-3 個數 // 這些序列中,和為零的三元組有 3M^2 + 3M + 1 種可能。 // 其他不為零的 N-3 個位置有 (2M+1)^(N-3) 種可能。 // 總共有 ((N^3)/6) * (3M^2 + 3M + 1) * (2M+1)^(N-3) 種可能性 // 平均值為: // [((N^3)/6) * (3M^2 + 3M + 1) * (2M+1)^(N-3)] / (2M+1)^N // (N^3) * (3M^2 + 3M + 1) / 6 * (2M+1)^3 // ~ N^3 * 3M^2 / 6 * 8M^3 // N^3/16M static void Main(string[] args) { int M = 10000; for (int n = 125; n < 10000; n += n) { Random random = new Random(); int[] a = new int[n]; for (int i = 0; i < n; ++i) { a[i] = random.Next(2 * M) - M; } Console.WriteLine($"N={n}, 計算值={Math.Pow(n, 3) / (16 * M)}, 實際值={ThreeSum.Count(a)}"); } } } }
1.4.41
解答
代碼
這里使用了委托來簡化代碼。
DoublingRatio
using System; using Measurement; namespace _1._4._41 { public delegate int Count(int[] a); static class DoublingRatio { /// <summary> /// 從指定字符串中讀入按行分割的整型數據。 /// </summary> /// <param name="inputString">源字符串。</param> /// <returns>讀入的整型數組</returns> private static int[] ReadAllInts(string inputString) { char[] split = new char[1] { '\n' }; string[] input = inputString.Split(split, StringSplitOptions.RemoveEmptyEntries); int[] a = new int[input.Length]; for (int i = 0; i < a.Length; ++i) { a[i] = int.Parse(input[i]); } return a; } /// <summary> /// 使用給定的數組進行一次測試,返回耗時(毫秒)。 /// </summary> /// <param name="Count">要測試的方法。</param> /// <param name="a">測試用的數組。</param> /// <returns>耗時(秒)。</returns> public static double TimeTrial(Count Count, int[] a) { Stopwatch timer = new Stopwatch(); Count(a); return timer.ElapsedTimeMillionSeconds(); } /// <summary> /// 對 TwoSum、TwoSumFast、ThreeSum 或 ThreeSumFast 的 Count 方法做測試。 /// </summary> /// <param name="Count">相應類的 Count 方法</param> /// <returns>隨着數據量倍增,方法耗時增加的比率。</returns> public static double Test(Count Count) { double ratio = 0; double times = 3; // 1K int[] a = ReadAllInts(TestCase.Properties.Resources._1Kints); double prevTime = TimeTrial(Count, a); Console.WriteLine("數據量\t耗時\t比值"); Console.WriteLine($"1000\t{prevTime / 1000}\t"); // 2K a = ReadAllInts(TestCase.Properties.Resources._2Kints); double time = TimeTrial(Count, a); Console.WriteLine($"2000\t{time / 1000}\t{time / prevTime}"); if (prevTime != 0) { ratio += time / prevTime; } else { times--; } prevTime = time; // 4K a = ReadAllInts(TestCase.Properties.Resources._4Kints); time = TimeTrial(Count, a); Console.WriteLine($"4000\t{time / 1000}\t{time / prevTime}"); if (prevTime != 0) { ratio += time / prevTime; } else { times--; } prevTime = time; // 8K a = ReadAllInts(TestCase.Properties.Resources._8Kints); time = TimeTrial(Count, a); Console.WriteLine($"8000\t{time / 1000}\t{time / prevTime}"); if (prevTime != 0) { ratio += time / prevTime; } else { times--; } prevTime = time; return ratio / times; } public static double TestTwoSumFast(Count Count) { double ratio = 0; double times = 2; // 8K int[] a = ReadAllInts(TestCase.Properties.Resources._8Kints); double prevTime = TimeTrial(Count, a); Console.WriteLine("數據量\t耗時\t比值"); Console.WriteLine($"8000\t{prevTime / 1000}\t"); // 16K a = ReadAllInts(TestCase.Properties.Resources._16Kints); double time = TimeTrial(Count, a); Console.WriteLine($"16000\t{time / 1000}\t{time / prevTime}"); if (prevTime != 0) { ratio += time / prevTime; } else { times--; } prevTime = time; // 32K a = ReadAllInts(TestCase.Properties.Resources._32Kints); time = TimeTrial(Count, a); Console.WriteLine($"32000\t{time / 1000}\t{time / prevTime}"); if (prevTime != 0) { ratio += time / prevTime; } else { times--; } prevTime = time; return ratio / times; } } }
主方法:
using System; using Measurement; namespace _1._4._41 { /* * 1.4.41 * * 運行時間。 * 使用 DoublingRatio 估計在你的計算機上用 TwoSumFast、TwoSum、ThreeSumFast 以及 ThreeSum 處理一個含有 100 萬個整數的文件所需的時間。 * */ class Program { static void Main(string[] args) { int[] a = new int[977]; Random random = new Random(); for (int i = 0; i < 977; ++i) { a[i] = random.Next(977) - 489; } // ThreeSum Console.WriteLine("ThreeSum"); double time = DoublingRatio.TimeTrial(ThreeSum.Count, a); Console.WriteLine($"數據量:977 耗時:{time / 1000}"); double doubleRatio = DoublingRatio.Test(ThreeSum.Count); Console.WriteLine($"數據量:1000000 估計耗時:{time * doubleRatio * 1024 / 1000}"); Console.WriteLine(); //// ThreeSumFast Console.WriteLine("ThreeSumFast"); time = DoublingRatio.TimeTrial(ThreeSumFast.Count, a); doubleRatio = DoublingRatio.Test(ThreeSumFast.Count); Console.WriteLine($"數據量:977 耗時:{time / 1000}"); Console.WriteLine($"數據量:1000000 估計耗時:{time * doubleRatio * 1024 / 1000}"); Console.WriteLine(); //// TwoSum Console.WriteLine("TwoSum"); time = DoublingRatio.TimeTrial(TwoSum.Count, a); doubleRatio = DoublingRatio.Test(TwoSum.Count); Console.WriteLine($"數據量:977 耗時:{time / 1000}"); Console.WriteLine($"數據量:1000000 估計耗時:{time * doubleRatio * 1024 / 1000}"); Console.WriteLine(); // TwoSumFast // 速度太快,加大數據量 a = new int[62500]; for (int i = 0; i < 977; ++i) { a[i] = random.Next(62500) - 31250; } Console.WriteLine("TwoSumFast"); time = DoublingRatio.TimeTrial(TwoSumFast.Count, a); doubleRatio = DoublingRatio.TestTwoSumFast(TwoSumFast.Count); Console.WriteLine($"數據量:62500 耗時:{time / 1000}"); Console.WriteLine($"數據量:1000000 估計耗時:{time * doubleRatio * 16 / 1000}"); Console.WriteLine(); } } }
1.4.42
解答
這里我們把時限設置為一小時,使用上一題的數據估計。
1.ThreeSum 暴力方法在輸入倍增時耗時增加 2^3 = 8 倍。
1K 數據耗費了 1.15 秒,在一小時內(3600 秒)可以完成 2^3 = 8K 數據。
2.ThreeSumFast 方法在輸入倍增時耗時增加 2^2 = 4 倍。
1K 數據耗費了 0.05 秒,在一小時內(3600 秒)可以完成 2^8 = 256K 數據。
3.TwoSum 暴力方法在輸入倍增時耗時增加 2^2 = 4 倍。
8K 數據耗費了 0.14 秒,在一小時內(3600 秒)可以完成 2^10 = 1024K 數據。
4.TwoSumFast 在輸入倍增時耗時增加 2^1 = 2 倍。
32K 數據耗費了 0.008 秒,在一小時內(3600 秒)可以完成 2^16 = 65536K 數據。
1.4.43
解答
代碼
修改后的 DoublingRatio
using System; using Measurement; namespace _1._4._43 { static class DoublingRatio { /// <summary> /// 從指定字符串中讀入按行分割的整型數據。 /// </summary> /// <param name="inputString">源字符串。</param> /// <returns>讀入的整型數組</returns> private static int[] ReadAllInts(string inputString) { char[] split = new char[1] { '\n' }; string[] input = inputString.Split(split, StringSplitOptions.RemoveEmptyEntries); int[] a = new int[input.Length]; for (int i = 0; i < a.Length; ++i) { a[i] = int.Parse(input[i]); } return a; } /// <summary> /// 使用給定的數組對鏈棧進行一次測試,返回耗時(毫秒)。 /// </summary> /// <param name="a">測試用的數組。</param> /// <returns>耗時(毫秒)。</returns> public static double TimeTrialLinkedStack(int[] a) { LinkedStack<int> stack = new LinkedStack<int>(); int n = a.Length; Stopwatch timer = new Stopwatch(); for (int i = 0; i < n; ++i) { stack.Push(a[i]); } for (int i = 0; i < n; ++i) { stack.Pop(); } return timer.ElapsedTimeMillionSeconds(); } /// <summary> /// 使用給定的數組對數組棧進行一次測試,返回耗時(毫秒)。 /// </summary> /// <param name="a">測試用的數組。</param> /// <returns>耗時(毫秒)。</returns> public static double TimeTrialDoublingStack(int[] a) { DoublingStack<int> stack = new DoublingStack<int>(); int n = a.Length; Stopwatch timer = new Stopwatch(); for (int i = 0; i < n; ++i) { stack.Push(a[i]); } for (int i = 0; i < n; ++i) { stack.Pop(); } return timer.ElapsedTimeMillionSeconds(); } /// <summary> /// 對鏈棧和基於大小可變的數組棧做測試。 /// </summary> public static void Test() { double linkedTime = 0; double arrayTime = 0; Console.WriteLine("數據量\t鏈棧\t數組\t比值\t單位:毫秒"); // 16K int[] a = ReadAllInts(TestCase.Properties.Resources._16Kints); linkedTime = TimeTrialLinkedStack(a); arrayTime = TimeTrialDoublingStack(a); Console.WriteLine($"16000\t{linkedTime}\t{arrayTime}\t{linkedTime / arrayTime}"); // 32K a = ReadAllInts(TestCase.Properties.Resources._32Kints); linkedTime = TimeTrialLinkedStack(a); arrayTime = TimeTrialDoublingStack(a); Console.WriteLine($"32000\t{linkedTime}\t{arrayTime}\t{linkedTime / arrayTime}"); // 1M a = ReadAllInts(TestCase.Properties.Resources._1Mints); linkedTime = TimeTrialLinkedStack(a); arrayTime = TimeTrialDoublingStack(a); Console.WriteLine($"1000000\t{linkedTime}\t{arrayTime}\t{linkedTime / arrayTime}"); } } }
1.4.44
解答
每生成一個隨機數都和之前生成過的隨機數相比較。
代碼
using System; namespace _1._4._44 { /* * 1.4.44 * * 生日問題。 * 編寫一個程序, * 從命令行接受一個整數 N 作為參數並使用 StdRandom.uniform() 生成一系列 0 到 N-1 之間的隨機整數。 * 通過實驗驗證產生第一個重復的隨機數之前生成的整數數量為 ~√(πN/2)。 * */ class Program { static void Main(string[] args) { Random random = new Random(); int N = 10000; int[] a = new int[N]; int dupNum = 0; int times = 0; for (times = 0; times < 500; ++times) { for (int i = 0; i < N; ++i) { a[i] = random.Next(N); if (IsDuplicated(a, i)) { dupNum += i; Console.WriteLine($"生成{i + 1}個數字后發生重復"); break; } } } Console.WriteLine($"√(πN/2)={Math.Sqrt(Math.PI * N / 2.0)},平均生成{dupNum / times}個數字后出現重復"); } /// <summary> /// 檢查是否有重復的數字出現。 /// </summary> /// <param name="a">需要檢查的數組。</param> /// <param name="i">當前加入數組元素的下標。</param> /// <returns>有重復則返回 true,否則返回 false。</returns> static bool IsDuplicated(int[] a, int i) { for (int j = 0; j < i; ++j) { if (a[j] == a[i]) { return true; } } return false; } } }
1.4.45
解答
建立一個布爾數組,將每次隨機出來的數作為下標,將相應位置的布爾值改為 true,每次隨機都檢查一遍這個數組是否都是 true。
代碼
using System; namespace _1._4._45 { /* * 1.4.45 * * 優惠券收集問題。 * 用和上一題相同的方式生成隨機整數。 * 通過實驗驗證生成所有可能的整數值所需生成的隨機數總量為 ~NHN。 * (這里的 HN 中 N 是下標) * */ class Program { // HN 指的是調和級數 static void Main(string[] args) { Random random = new Random(); int N = 10000; bool[] a = new bool[N]; int randomSize = 0; int times = 0; for (times = 0; times < 20; ++times) { for (int i = 0; i < N; ++i) { a[i] = false; } for(int i = 0; true; ++i) { int now = random.Next(N); a[now] = true; if (IsAllGenerated(a)) { randomSize += i; Console.WriteLine($"生成{i}次后所有可能均出現過了"); break; } } } Console.WriteLine($"\nNHN={N * HarmonicSum(N)},平均生成{randomSize / times}個數字后所有可能都出現"); } /// <summary> /// 計算 N 階調和級數的和。 /// </summary> /// <param name="N">調和級數的 N 值</param> /// <returns>N 階調和級數的和。</returns> static double HarmonicSum(int N) { double sum = 0; for (int i = 1; i <= N; ++i) { sum += 1.0 / i; } return sum; } /// <summary> /// 檢查所有數字是否都生成過了。 /// </summary> /// <param name="a">布爾數組。</param> /// <returns>全都生成則返回 true,否則返回 false。</returns> static bool IsAllGenerated(bool[] a) { foreach (bool i in a) { if (!i) return false; } return true; } } }