写在前面

整个项目都托管在了 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

代码
  1. namespace Measurement
  2. {
  3. /// <summary>
  4. /// 用暴力方法寻找数组中和为零的三元组。
  5. /// </summary>
  6. public static class ThreeSum
  7. {
  8. /// <summary>
  9. /// 输出所有和为零的三元组。
  10. /// </summary>
  11. /// <param name="a">输入数组。</param>
  12. public static void PrintAll(int[] a)
  13. {
  14. int n = a.Length;
  15. for (int i = ; i < n; ++i)
  16. {
  17. for (int j = i + ; j < n; ++j)
  18. {
  19. for (int k = j + ; k < n; ++k)
  20. {
  21. if ((long)a[i] + a[j] + a[k] == )
  22. {
  23. Console.WriteLine($"{a[i]} + {a[j]} + {a[k]}");
  24. }
  25. }
  26. }
  27. }
  28. }
  29.  
  30. /// <summary>
  31. /// 计算和为零的三元组的数量。
  32. /// </summary>
  33. /// <param name="a">输入数组。</param>
  34. /// <returns></returns>
  35. public static int Count(int[] a)
  36. {
  37. int n = a.Length;
  38. int count = ;
  39. for (int i = ; i < n; ++i)
  40. {
  41. for (int j = i + ; j < n; ++j)
  42. {
  43. for (int k = j + ; k < n; ++k)
  44. {
  45. if ((long)a[i] + a[j] + a[k] == )
  46. {
  47. count++;
  48. }
  49. }
  50. }
  51. }
  52. return count;
  53. }
  54. }
  55. }

1.4.3

解答

见代码,这里贴出绘图函数,窗体只是在得到测试结果之后简单调用以下这两个函数。

代码
  1. public static void PaintLinear(double[] testResult)
  2. {
  3. //新建一个绘图窗口
  4. Form2 linear = new Form2();
  5. linear.Show();
  6. //新建画布
  7. Graphics canvas = linear.CreateGraphics();
  8. //获取窗口区域
  9. Rectangle rect = linear.ClientRectangle;
  10. //计算单位长度(十等分)
  11. int unitY = rect.Height / ;
  12. int unitX = rect.Width / ;
  13. //获取中心区域(上下左右增加 10% 的内补)
  14. Rectangle center = new Rectangle(rect.X + unitX, rect.Y + unitY, unitX * , unitY * );
  15. //绘制坐标系
  16. canvas.DrawLine(Pens.Black, center.X, center.Y, center.X, center.Y + center.Height);
  17. canvas.DrawLine(Pens.Black, center.X, center.Y + center.Height, center.X + center.Width, center.Y + center.Height);
  18. //对 X 轴 10 等分,对 Y 轴 10 等分
  19. int xaxisUnit = center.Width / ;
  20. int yaxisUnit = center.Height / ;
  21. //标记 X 轴坐标值
  22. for (int i = ; i <= ; i += i)
  23. {
  24. canvas.DrawString(i + "N", linear.Font, Brushes.Black, center.X + i * xaxisUnit, center.Y + center.Height);
  25. }
  26. //反转坐标系
  27. canvas.TranslateTransform(, linear.ClientRectangle.Height);
  28. canvas.ScaleTransform(, -);
  29. //计算单位长度
  30. double Unit = center.Height / testResult[];
  31. //标记
  32. PointF[] result = new PointF[];
  33. for (int i = , j = ; i < && j <= ; ++i, j += j)
  34. {
  35. result[i] = new PointF(center.X + j * xaxisUnit, (float)(center.Y + Unit * testResult[i]));
  36. }
  37. //链接
  38. canvas.DrawLines(Pens.Black, result);
  39.  
  40. canvas.Dispose();
  41. }
  42.  
  43. public static void PaintLogarithm(double[] testResult)
  44. {
  45. //新建一个绘图窗口
  46. Form2 log = new Form2();
  47. log.Show();
  48. //新建画布
  49. Graphics canvas = log.CreateGraphics();
  50. //获取窗口区域
  51. Rectangle rect = log.ClientRectangle;
  52. //计算单位长度(十等分)
  53. int unitY = rect.Height / ;
  54. int unitX = rect.Width / ;
  55. //获取中心区域(上下左右增加 10% 的内补)
  56. Rectangle center = new Rectangle(rect.X + unitX, rect.Y + unitY, unitX * , unitY * );
  57. //绘制坐标系
  58. canvas.DrawLine(Pens.Black, center.X, center.Y, center.X, center.Y + center.Height);
  59. canvas.DrawLine(Pens.Black, center.X, center.Y + center.Height, center.X + center.Width, center.Y + center.Height);
  60. //对 X 轴 10 等分,对 Y 轴 10 等分
  61. int xaxisUnit = center.Width / ;
  62. int yaxisUnit = center.Height / ;
  63. //标记 X 轴坐标值
  64. for (int i = ; i <= ; i += i)
  65. {
  66. canvas.DrawString(i + "N", log.Font, Brushes.Black, center.X + i * xaxisUnit, center.Y + center.Height);
  67. }
  68. //反转坐标系
  69. canvas.TranslateTransform(, log.ClientRectangle.Height);
  70. canvas.ScaleTransform(, -);
  71. //计算单位长度
  72. double Unit = center.Height / testResult[];
  73. //标记
  74. PointF[] result = new PointF[];
  75. for (int i = , j = ; i < && j <= ; ++i, j += j)
  76. {
  77. result[i] = new PointF(center.X + j * xaxisUnit, (float)(center.Y + Unit * testResult[i]));
  78. }
  79. //链接
  80. canvas.DrawLines(Pens.Black, result);
  81. canvas.Dispose();
  82. }

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

解答

平方级别:直接二层循环遍历一遍。

线性对数:只遍历一遍数组,在遍历过程中用二分查找确认在剩余数组中是否有相等的整数。

代码
  1. /// <summary>
  2. /// 暴力查找数组中相等的整数对。
  3. /// </summary>
  4. /// <param name="a">需要查找的数组。</param>
  5. /// <returns></returns>
  6. static int CountEqual(int[] a)
  7. {
  8. int n = a.Length;
  9. int count = ;
  10. for (int i = ; i < n; i++)
  11. {
  12. for (int j = i + ; j < n; j++)
  13. {
  14. if (a[i] == a[j])
  15. count++;
  16. }
  17. }
  18.  
  19. return count;
  20. }

暴力算法↑

二分查找算法↓

  1. /// <summary>
  2. /// 利用 Array.Sort 进行优化的查找相等整数对算法。
  3. /// </summary>
  4. /// <param name="a">需要查找的数组。</param>
  5. /// <returns></returns>
  6. static int CountEqualLog(int[] a)
  7. {
  8. int n = a.Length;
  9. int count = ;
  10. Array.Sort(a);
  11. int dup = ; // dup = 重复元素数量-1
  12. for (int i = ; i < n; i++)
  13. {
  14. while (a[i - ] == a[i])
  15. {
  16. dup++;
  17. i++;
  18. }
  19. count += dup * (dup + ) / ;
  20. dup = ;
  21. }
  22. return count;
  23. }

1.4.9

解答

1.4.10

解答

修改二分查找的结束条件,找到后仍然向左侧寻找,如果还能找到更小的,则返回较小的下标;否则返回当前下标。

代码
  1. namespace _1._4._10
  2. {
  3. /// <summary>
  4. /// 二分查找。
  5. /// </summary>
  6. public class BinarySearch
  7. {
  8. /// <summary>
  9. /// 用递归方法进行二分查找。
  10. /// </summary>
  11. /// <param name="key">关键字。</param>
  12. /// <param name="a">查找范围。</param>
  13. /// <param name="lo">查找的起始下标。</param>
  14. /// <param name="hi">查找的结束下标。</param>
  15. /// <returns>返回下标,如果没有找到则返回 -1。</returns>
  16. public static int Rank(int key, int[] a, int lo, int hi)
  17. {
  18. if (hi < lo)
  19. return -;
  20. int mid = (hi - lo) / + lo;
  21. if (a[mid] == key)
  22. {
  23. int mini = Rank(key, a, lo, mid - );
  24. if (mini != -)
  25. return mini;
  26. return mid;
  27. }
  28. else if (a[mid] < key)
  29. {
  30. return Rank(key, a, mid + , hi);
  31. }
  32. else
  33. {
  34. return Rank(key, a, lo, mid - );
  35. }
  36. }
  37. }
  38. }

1.4.11

解答

这里给出官网上的 Java 实现:StaticSETofInts.java

howMany() 可以用二分查找实现,在找到一个值后继续向两侧查找,最后返回找到的次数。

代码
  1. using System;
  2.  
  3. namespace Measurement
  4. {
  5. /// <summary>
  6. /// 有序数组,能够快速查找并自动维护其中的顺序。
  7. /// </summary>
  8. public class StaticSETofInts
  9. {
  10. private int[] a;
  11.  
  12. /// <summary>
  13. /// 用一个数组初始化有序数组。
  14. /// </summary>
  15. /// <param name="keys">源数组。</param>
  16. public StaticSETofInts(int[] keys)
  17. {
  18. this.a = new int[keys.Length];
  19. for (int i = ; i < keys.Length; ++i)
  20. {
  21. this.a[i] = keys[i];
  22. }
  23. Array.Sort(this.a);
  24. }
  25.  
  26. /// <summary>
  27. /// 检查数组中是否存在指定元素。
  28. /// </summary>
  29. /// <param name="key">要查找的值。</param>
  30. /// <returns>存在则返回 true,否则返回 false。</returns>
  31. public bool Contains(int key)
  32. {
  33. return Rank(key, , this.a.Length - ) != -;
  34. }
  35.  
  36. /// <summary>
  37. /// 返回某个元素在数组中存在的数量。
  38. /// </summary>
  39. /// <param name="key">关键值。</param>
  40. /// <returns>返回某个元素在数组中存在的数量。</returns>
  41. public int HowMany(int key)
  42. {
  43. int hi = this.a.Length - ;
  44. int lo = ;
  45.  
  46. return HowMany(key, lo, hi);
  47. }
  48.  
  49. /// <summary>
  50. /// 返回某个元素在数组中存在的数量。
  51. /// </summary>
  52. /// <param name="key">关键值。</param>
  53. /// <param name="lo">查找起始下标。</param>
  54. /// <param name="hi">查找结束下标。</param>
  55. /// <returns>返回某个元素在数组中存在的数量。</returns>
  56. private int HowMany(int key, int lo, int hi)
  57. {
  58. int mid = Rank(key, lo, hi);
  59. if (mid == -)
  60. return ;
  61. else
  62. {
  63. return + HowMany(key, lo, mid - ) + HowMany(key, mid + , hi);
  64. }
  65. }
  66.  
  67. /// <summary>
  68. /// 二分查找。
  69. /// </summary>
  70. /// <param name="key">关键值。</param>
  71. /// <param name="lo">查找的起始下标。</param>
  72. /// <param name="hi">查找的结束下标。</param>
  73. /// <returns>返回关键值的下标,如果不存在则返回 -1。</returns>
  74. public int Rank(int key, int lo, int hi)
  75. {
  76. while (lo <= hi)
  77. {
  78. int mid = (hi - lo) / + lo;
  79. if (key < this.a[mid])
  80. hi = mid - ;
  81. else if (key > this.a[mid])
  82. lo = mid + ;
  83. else
  84. return mid;
  85. }
  86. return -;
  87. }
  88. }
  89. }

1.4.12

解答

由于两个数组都是有序的,可以同时进行比较。

设 i, j 分别为两个数组的下标。
如果 a[i] == a[j],i 和 j 都向后移动一位。
如果 a[i] != a[j],比较小的那个向后移动一位。
循环直到某个数组遍历完毕。

这样最后的时间复杂度 ~2N

代码
  1. using System;
  2.  
  3. namespace _1._4._12
  4. {
  5. /*
  6. * 1.4.12
  7. *
  8. * 编写一个程序,有序打印给定的两个有序数组(含有 N 个 int 值) 中的所有公共元素,
  9. * 程序在最坏情况下所需的运行时间应该和 N 成正比。
  10. *
  11. */
  12. class Program
  13. {
  14. static void Main(string[] args)
  15. {
  16. int[] a = new int[] { , , , };
  17. int[] b = new int[] { , , , , , };
  18.  
  19. //2N 次数组访问,数组 a 和数组 b 各遍历一遍
  20. for (int i = , j = ; i < a.Length && j < b.Length; )
  21. {
  22. if (a[i] < b[j])
  23. {
  24. i++;
  25. }
  26. else if (a[i] > b[j])
  27. {
  28. j++;
  29. }
  30. else
  31. {
  32. Console.WriteLine($"Common Element:{a[i]}, First index: (a[{i}], b[{j}])");
  33. i++;
  34. j++;
  35. }
  36. }
  37.  
  38. }
  39. }
  40. }

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

解答

这里给出暴力方法,将最内侧循环换成二分查找即为优化版本。

代码
  1. using System;
  2.  
  3. namespace Measurement
  4. {
  5. /// <summary>
  6. /// 用暴力方法查找数组中和为零的四元组。
  7. /// </summary>
  8. public static class FourSum
  9. {
  10. /// <summary>
  11. /// 输出数组中所有和为 0 的四元组。
  12. /// </summary>
  13. /// <param name="a">包含所有元素的数组。</param>
  14. public static void PrintAll(long[] a)
  15. {
  16. int N = a.Length;
  17. for (int i = ; i < N; ++i)
  18. {
  19. for (int j = i + ; j < N; ++j)
  20. {
  21. for (int k = j + ; k < N; ++k)
  22. {
  23. for (int l = k + ; l < N; ++l)
  24. {
  25. if (a[i] + a[j] + a[k] + a[l] == )
  26. {
  27. Console.WriteLine($"{a[i]} + {a[j]} + {a[k]} + {a[l]} = 0");
  28. }
  29. }
  30. }
  31. }
  32. }
  33. }
  34.  
  35. /// <summary>
  36. /// 计算和为零的四元组的数量。
  37. /// </summary>
  38. /// <param name="a">包含所有元素的数组。</param>
  39. /// <returns></returns>
  40. public static int Count(long[] a)
  41. {
  42. int N = a.Length;
  43. int cnt = ;
  44.  
  45. for (int i = ; i < N; ++i)
  46. {
  47. for (int j = i + ; j < N; ++j)
  48. {
  49. for (int k = j + ; k < N; ++k)
  50. {
  51. for (int l = k + ; l < N; ++l)
  52. {
  53. if (a[i] + a[j] + a[k] + a[l] == )
  54. {
  55. cnt++;
  56. }
  57. }
  58. }
  59. }
  60. }
  61.  
  62. return cnt;
  63. }
  64. }
  65. }

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 外层再套一个循环即可。

代码
  1. /// <summary>
  2. /// TwoSum 的快速实现。(线性级别)
  3. /// </summary>
  4. /// <param name="a">需要查找的数组范围。</param>
  5. /// <returns>数组中和为零的整数对数量。</returns>
  6. static int TwoSumFaster(int[] a)
  7. {
  8. int lo = ;
  9. int hi = a.Length - ;
  10. int count = ;
  11. while (lo < hi)
  12. {
  13. if (a[lo] + a[hi] == )
  14. {
  15. count++;
  16. lo++;
  17. hi--;
  18. }
  19. else if (a[lo] + a[hi] < )
  20. {
  21. lo++;
  22. }
  23. else
  24. {
  25. hi--;
  26. }
  27. }
  28. return count;
  29. }
  30.  
  31. /// <summary>
  32. /// ThreeSum 的快速实现。(平方级别)
  33. /// </summary>
  34. /// <param name="a">需要查找的数组范围。</param>
  35. /// <returns>数组中和为零的三元组数量。</returns>
  36. static int ThreeSumFaster(int[] a)
  37. {
  38. int count = ;
  39. for (int i = ; i < a.Length; ++i)
  40. {
  41. int lo = i + ;
  42. int hi = a.Length - ;
  43. while (lo <= hi)
  44. {
  45. if (a[lo] + a[hi] + a[i] == )
  46. {
  47. count++;
  48. lo++;
  49. hi--;
  50. }
  51. else if (a[lo] + a[hi] + a[i] < )
  52. {
  53. lo++;
  54. }
  55. else
  56. {
  57. hi--;
  58. }
  59. }
  60. }
  61. return count;
  62. }

1.4.16

解答

先将数组从小到大排序,再遍历一遍即可得到差距最小的两个数。

排序算法需要消耗 NlogN,具体见 MSDN:Array.Sort 方法 (Array)

代码
  1. using System;
  2.  
  3. namespace _1._4._16
  4. {
  5. /*
  6. * 1.4.16
  7. *
  8. * 最接近一对(一维)。
  9. * 编写一个程序,给定一个含有 N 个 double 值的数组 a[],
  10. * 在其中找到一对最接近的值:两者之差(绝对值)最小的两个数。
  11. * 程序在最坏情况下所需的运行时间应该是线性对数级别的。
  12. *
  13. */
  14. class Program
  15. {
  16. //总运行时间: NlogN + N = NlogN
  17. static void Main(string[] args)
  18. {
  19. double[] a = new double[] { 0.1, 0.3, 0.6, 0.8, };
  20. Array.Sort(a);//Nlog(N) 具体见 https://msdn.microsoft.com/zh-cn/library/6tf1f0bc(v=vs.110).aspx 备注部分
  21. double minDiff = double.MaxValue;
  22. double minA = ;
  23. double minB = ;
  24. for (int i = ; i < a.Length - ; ++i)//N
  25. {
  26. if (a[i + ] - a[i] < minDiff)
  27. {
  28. minA = a[i];
  29. minB = a[i + ];
  30. minDiff = a[i + ] - a[i];
  31. }
  32. }
  33. Console.WriteLine($"Min Pair: {minA} {minB}, Min Value: {minDiff}");
  34. }
  35. }
  36. }

1.4.17

解答

遍历找到最小值和最大值即可。

代码
  1. using System;
  2.  
  3. namespace _1._4._17
  4. {
  5. /*
  6. * 1.4.17
  7. *
  8. * 最遥远的一对(一维)。
  9. * 编写一个程序,给定一个含有 N 个 double 值的数组 a[],
  10. * 在其中找到一对最遥远的值:两者之差(绝对值)最大的两个数。
  11. * 程序在最坏情况下所需的运行时间应该是线性级别的。
  12. *
  13. */
  14. class Program
  15. {
  16. static void Main(string[] args)
  17. {
  18. double[] a = new double[] { 0.1, 0.3, 0.6, 0.8, };
  19. double min = int.MaxValue;
  20. double max = int.MinValue;
  21.  
  22. for (int i = ; i < a.Length; ++i)
  23. {
  24. if (a[i] > max)
  25. {
  26. max = a[i];
  27. }
  28. if (a[i] < min)
  29. {
  30. min = a[i];
  31. }
  32. }
  33.  
  34. Console.WriteLine($"MaxDiff Pair: {min} {max}, Max Difference: {Math.Abs(max - min)}");
  35. }
  36. }
  37. }

1.4.18

解答

和二分查找的方式类似,先确认中间的值是否是局部最小,如果不是,则向较小的一侧二分查找。

在三个数中比较得到最小值需要两次比较,因此最坏情况下为 ~2lgN 次比较。

代码
  1. using System;
  2.  
  3. namespace _1._4._18
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. var a = new int[] { , , , , };
  10. Console.WriteLine(LocalMinimum(a));
  11. }
  12.  
  13. /// <summary>
  14. /// 寻找数组的局部最小元素。
  15. /// </summary>
  16. /// <param name="a">寻找范围。</param>
  17. /// <returns>局部最小元素的值。</returns>
  18. static int LocalMinimum(int[] a)
  19. {
  20. int lo = ;
  21. int hi = a.Length - ;
  22. while (lo <= hi)
  23. {
  24. int mid = (hi - lo) / + lo;
  25. int min = mid;
  26.  
  27. // 取左中右最小值的下标
  28. if (mid != hi && a[min] >= a[mid + ])
  29. min = mid + ;
  30. if (mid != lo && a[min] >= a[mid - ])
  31. min = mid - ;
  32.  
  33. if (min == mid)
  34. return mid;
  35. if (min > mid)
  36. lo = min;
  37. else
  38. hi = min;
  39. }
  40. return -;
  41. }
  42. }
  43. }

1.4.19

解答

算法过程类似于 “滑雪”,从数值较高的一侧向周围数值较小的一侧移动,直到到达“山谷”(局部最小)。

首先在中间行搜索最小值,再将最小值与其上下两个元素比较,如果不满足题意,则“滑向”较小的一侧,矩阵被分为了两半(上下两侧)。

在较小的一侧,找到中间列的最小值,再将最小值与其左右两个元素比较,如果不满足题意,类似的移动到较小的一侧(左右两侧)。

现在查找范围缩小到了原来矩阵的四分之一,递归的进行上述操作,最后可以得到答案。

每次查找最小值都是对行/列进行遍历,遍历耗时和 N 成正比。

代码
  1. using System;
  2.  
  3. namespace _1._4._19
  4. {
  5. /*
  6. * 1.4.19
  7. *
  8. * 矩阵的局部最小元素。
  9. * 给定一个含有 N^2 个不同整数的 N×N 数组 a[]。
  10. * 设计一个运行时间和 N 成正比的算法来找出一个局部最小元素:
  11. * 满足 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。
  12. * 程序运行时间在最坏情况下应该和 N 成正比。
  13. *
  14. */
  15. class Program
  16. {
  17. // 先查找 N/2 行中的最小元素,并令其与上下元素比较,
  18. // 如果不满足题意,则向相邻的最小元素靠近再次查找
  19. static void Main(string[] args)
  20. {
  21. int[,] matrix = new int[, ]
  22. {
  23. { , , , , },
  24. { , , , , },
  25. { , , , , },
  26. { , , , , },
  27. { , , , , }
  28. };
  29. Console.WriteLine(MinimumRow(matrix, , , , ));
  30. }
  31.  
  32. /// <summary>
  33. /// 在矩阵中间行查找局部最小。
  34. /// </summary>
  35. /// <param name="matrix">矩阵。</param>
  36. /// <param name="rowStart">实际查找范围的行起始。</param>
  37. /// <param name="rowLength">实际查找范围的行结尾。</param>
  38. /// <param name="colStart">实际查找范围的列起始。</param>
  39. /// <param name="colLength">实际查找范围的列结尾。</param>
  40. /// <returns>矩阵中的局部最小元素。</returns>
  41. static int MinimumRow(int[,] matrix, int rowStart, int rowLength, int colStart, int colLength)
  42. {
  43. int min = int.MaxValue;
  44. if (rowLength < )
  45. return int.MaxValue;
  46. int mid = rowStart + rowLength / ;
  47. int minCol = ;
  48. // 获取矩阵中间行的最小值
  49. for (int i = ; i < colLength; ++i)
  50. {
  51. if (min > matrix[mid, colStart + i])
  52. {
  53. min = matrix[mid, colStart + i];
  54. minCol = i;
  55. }
  56. }
  57. // 检查是否满足条件
  58. if (matrix[mid, minCol] < matrix[mid - , minCol] && matrix[mid, minCol] < matrix[mid + , minCol])
  59. {
  60. return matrix[mid, minCol];
  61. }
  62. // 如果不满足则向较小一侧移动
  63. if (matrix[mid - , minCol] > matrix[mid + , minCol])
  64. {
  65. return MinimumCol(matrix, rowStart, rowLength, mid + , colLength / + );
  66. }
  67. else
  68. {
  69. return MinimumCol(matrix, rowStart, rowLength, colStart, colLength / + );
  70. }
  71. }
  72.  
  73. /// <summary>
  74. /// 在矩阵中间列查找局部最小。
  75. /// </summary>
  76. /// <param name="matrix">矩阵。</param>
  77. /// <param name="rowStart">实际查找范围的行起始。</param>
  78. /// <param name="rowLength">实际查找范围的行结尾。</param>
  79. /// <param name="colStart">实际查找范围的列起始。</param>
  80. /// <param name="colLength">实际查找范围的列结尾。</param>
  81. /// <returns>矩阵中的局部最小元素。</returns>
  82. static int MinimumCol(int[,] matrix, int rowStart, int rowLength, int colStart, int colLength)
  83. {
  84. int min = int.MaxValue;
  85. int n = matrix.GetLength();
  86. int mid = n / ;
  87. int minRow = ;
  88.  
  89. // 获取矩阵中间列最小值
  90. for (int i = ; i < n; ++i)
  91. {
  92. if (min > matrix[i, mid])
  93. {
  94. min = matrix[i, mid];
  95. minRow = i;
  96. }
  97. }
  98. // 检查是否满足条件
  99. if (matrix[minRow, mid] < matrix[minRow, mid - ] && matrix[minRow, mid] < matrix[minRow, mid + ])
  100. {
  101. return matrix[minRow, mid];
  102. }
  103. // 如果不满足则向较小一侧移动
  104. if (matrix[minRow, mid - ] > matrix[minRow, mid + ])
  105. {
  106. return MinimumRow(matrix, mid + , rowLength / + , colStart, colLength);
  107. }
  108. else
  109. {
  110. return MinimumRow(matrix, rowStart, rowLength / + , colStart, colLength);
  111. }
  112. }
  113. }
  114. }

1.4.20

解答

首先给出 BitMax 类的官方 Java 实现:BitonicMax.java

我们使用这个类生成双调数组,并使用其中的 Max() 方法找到双调数组的最大值。

找到最大值之后分别对左右两侧进行二分查找,注意对于升序和降序的数组二分查找的实现有所不同。

代码

BitonicMax 类

  1. using System;
  2.  
  3. namespace _1._4._20
  4. {
  5. /// <summary>
  6. /// 双调查找类。
  7. /// </summary>
  8. public class BitonicMax
  9. {
  10. /// <summary>
  11. /// 生成双调数组。
  12. /// </summary>
  13. /// <param name="N">数组的大小。</param>
  14. /// <returns></returns>
  15. public static int[] Bitonic(int N)
  16. {
  17. Random random = new Random();
  18. int mid = random.Next(N);
  19. int[] a = new int[N];
  20. for (int i = ; i < mid; ++i)
  21. {
  22. a[i] = a[i - ] + + random.Next();
  23. }
  24.  
  25. if (mid > )
  26. {
  27. a[mid] = a[mid - ] + random.Next() - ;
  28. }
  29.  
  30. for (int i = mid + ; i < N; ++i)
  31. {
  32. a[i] = a[i - ] - - random.Next();
  33. }
  34.  
  35. return a;
  36. }
  37.  
  38. /// <summary>
  39. /// 寻找数组中的最大值。
  40. /// </summary>
  41. /// <param name="a">查找范围。</param>
  42. /// <param name="lo">查找起始下标。</param>
  43. /// <param name="hi">查找结束下标。</param>
  44. /// <returns>返回数组中最大值的下标。</returns>
  45. public static int Max(int[] a, int lo, int hi)
  46. {
  47. if (lo == hi)
  48. {
  49. return hi;
  50. }
  51. int mid = lo + (hi - lo) / ;
  52. if (a[mid] < a[mid + ])
  53. {
  54. return Max(a, mid + , hi);
  55. }
  56. if (a[mid] > a[mid + ])
  57. {
  58. return Max(a, lo, mid);
  59. }
  60. return mid;
  61. }
  62. }
  63. }

主程序

  1. using System;
  2.  
  3. namespace _1._4._20
  4. {
  5. /*
  6. * 1.4.20
  7. *
  8. * 双调查找。
  9. * 如果一个数组中的所有元素是先递增后递减的,则称这个数组为双调的。
  10. * 编写一个程序,给定一个含有 N 个不同 int 值的双调数组,判断它是否含有给定的整数。
  11. * 程序在最坏情况下所需的比较次数为 ~3lgN
  12. *
  13. */
  14. class Program
  15. {
  16. static void Main(string[] args)
  17. {
  18. int[] a = BitonicMax.Bitonic();
  19. int max = BitonicMax.Max(a, , a.Length - );
  20. int key = a[];
  21. int leftside = BinarySearchAscending(a, key, , max);
  22. int rightside = BinarySearchDescending(a, key, max, a.Length - );
  23.  
  24. if (leftside != -)
  25. {
  26. Console.WriteLine(leftside);
  27. }
  28. else if (rightside != -)
  29. {
  30. Console.WriteLine(rightside);
  31. }
  32. else
  33. {
  34. Console.WriteLine("No Result");
  35. }
  36. }
  37.  
  38. /// <summary>
  39. /// 对升序数组的二分查找。
  40. /// </summary>
  41. /// <param name="a">升序数组。</param>
  42. /// <param name="key">关键值。</param>
  43. /// <param name="lo">查找的左边界。</param>
  44. /// <param name="hi">查找的右边界。</param>
  45. /// <returns>返回找到关键值的下标,如果没有找到则返回 -1。</returns>
  46. static int BinarySearchAscending(int[] a, int key, int lo, int hi)
  47. {
  48. while (lo <= hi)
  49. {
  50. int mid = lo + (hi - lo) / ;
  51.  
  52. if (a[mid] < key)
  53. {
  54. lo = mid + ;
  55. }
  56. else if (a[mid] > key)
  57. {
  58. hi = mid - ;
  59. }
  60. else
  61. {
  62. return mid;
  63. }
  64. }
  65.  
  66. return -;
  67. }
  68.  
  69. /// <summary>
  70. /// 对降序数组的二分查找。
  71. /// </summary>
  72. /// <param name="a">升序数组。</param>
  73. /// <param name="key">关键值。</param>
  74. /// <param name="lo">查找的左边界。</param>
  75. /// <param name="hi">查找的右边界。</param>
  76. /// <returns>返回找到关键值的下标,如果没有找到则返回 -1。</returns>
  77. static int BinarySearchDescending(int[] a, int key, int lo, int hi)
  78. {
  79. while (lo < hi)
  80. {
  81. int mid = lo + (hi - lo) / ;
  82.  
  83. if (a[mid] > key)
  84. {
  85. lo = mid + ;
  86. }
  87. else if (a[mid] < key)
  88. {
  89. hi = mid - ;
  90. }
  91. else
  92. {
  93. return mid;
  94. }
  95. }
  96.  
  97. return -;
  98. }
  99. }
  100. }

1.4.21

解答

直接将 Contains() 实现为二分查找即可。

代码
  1. /// <summary>
  2. /// 检查数组中是否存在指定元素。
  3. /// </summary>
  4. /// <param name="key">要查找的值。</param>
  5. /// <returns>存在则返回 true,否则返回 false。</returns>
  6. public bool Contains(int key)
  7. {
  8. return Rank(key, , this.a.Length - ) != -;
  9. }
  10.  
  11. /// <summary>
  12. /// 二分查找。
  13. /// </summary>
  14. /// <param name="key">关键值。</param>
  15. /// <param name="lo">查找的起始下标。</param>
  16. /// <param name="hi">查找的结束下标。</param>
  17. /// <returns>返回关键值的下标,如果不存在则返回 -1。</returns>
  18. public int Rank(int key, int lo, int hi)
  19. {
  20. while (lo <= hi)
  21. {
  22. int mid = (hi - lo) / + lo;
  23. if (key < this.a[mid])
  24. hi = mid - ;
  25. else if (key > this.a[mid])
  26. lo = mid + ;
  27. else
  28. return mid;
  29. }
  30. return -;
  31. }

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。

递归上述过程即可完成查找。

代码
  1. /// <summary>
  2. /// 使用斐波那契数列进行的查找。
  3. /// </summary>
  4. /// <param name="a">查找范围。</param>
  5. /// <param name="key">关键字。</param>
  6. /// <returns>返回查找到的关键值下标,没有结果则返回 -1。</returns>
  7. static int rank(int[] a, int key)
  8. {
  9. // 使用斐波那契数列作为缩减范围的依据
  10. int Fk = ;
  11. int Fk_1 = ;
  12. int Fk_2 = ;
  13.  
  14. // 获得 Fk,Fk需要大于等于数组的大小,复杂度 lgN
  15. while (Fk < a.Length)
  16. {
  17. Fk = Fk + Fk_1;
  18. Fk_1 = Fk_1 + Fk_2;
  19. Fk_2 = Fk - Fk_1;
  20. }
  21.  
  22. int lo = ;
  23.  
  24. // 按照斐波那契数列缩减查找范围,复杂度 lgN
  25. while (Fk_2 >= )
  26. {
  27. int i = lo + Fk_2 > a.Length - ? a.Length - : lo + Fk_2;
  28. if (a[i] < key)
  29. {
  30. lo = lo + Fk_2;
  31. }
  32. else if (a[i] == key)
  33. {
  34. return i;
  35. }
  36. Fk = Fk_1;
  37. Fk_1 = Fk_2;
  38. Fk_2 = Fk - Fk_1;
  39. }
  40.  
  41. return -;
  42. }

1.4.23

解答

根据书中的提示,将二分查找中判断相等的条件改为两个数的差小于等于 1/N2

代码
  1. // 将二分查找中的相等判定条件修改为差值小于 x,其中 x = 1/N^2。
  2. /// <summary>
  3. /// 二分查找。
  4. /// </summary>
  5. /// <param name="a">查找范围。</param>
  6. /// <param name="key">关键字。</param>
  7. /// <returns>结果的下标,没有结果时返回 -1。</returns>
  8. static int BinarySearch(double[] a, double key)
  9. {
  10. int lo = ;
  11. int hi = a.Length - ;
  12. double threshold = 1.0 / (a.Length * a.Length);
  13.  
  14. while (lo <= hi)
  15. {
  16. int mid = lo + (hi - lo) / ;
  17. if (Math.Abs(a[mid] - key) <= threshold)
  18. {
  19. return mid;
  20. }
  21. else if (a[mid] < key)
  22. {
  23. lo = mid + ;
  24. }
  25. else
  26. {
  27. hi = mid - ;
  28. }
  29. }
  30. return -;
  31. }

1.4.24

解答

第一问:二分查找即可。

第二问:

按照第 1, 2, 4, 8,..., 2^k 层顺序查找,一直到 2^k > F,
随后在 [2^(k - 1), 2^k] 范围中二分查找。

代码

这里建立了一个结构体用于返回测试结果:

  1. struct testResult
  2. {
  3. public int F;// 找到的 F 值。
  4. public int BrokenEggs;// 打碎的鸡蛋数。
  5. }

用于测试的方法:

  1. /// <summary>
  2. /// 扔鸡蛋,没碎返回 true,碎了返回 false。
  3. /// </summary>
  4. /// <param name="floor">扔鸡蛋的高度。</param>
  5. /// <returns></returns>
  6. static bool ThrowEgg(int floor)
  7. {
  8. return floor <= F;
  9. }
  10.  
  11. /// <summary>
  12. /// 第一种方案。
  13. /// </summary>
  14. /// <param name="a">大楼。</param>
  15. /// <returns></returns>
  16. static testResult PlanA(int[] a)
  17. {
  18. int lo = ;
  19. int hi = a.Length - ;
  20. int mid = ;
  21. int eggs = ;
  22. testResult result = new testResult();
  23.  
  24. while (lo <= hi)
  25. {
  26. mid = lo + (hi - lo) / ;
  27. if (ThrowEgg(mid))
  28. {
  29. lo = mid + ;
  30. }
  31. else
  32. {
  33. eggs++;
  34. hi = mid - ;
  35. }
  36. }
  37.  
  38. result.BrokenEggs = eggs;
  39. result.F = hi;
  40. return result;
  41. }
  42.  
  43. /// <summary>
  44. /// 第二种方案。
  45. /// </summary>
  46. /// <param name="a">大楼。</param>
  47. /// <returns></returns>
  48. static testResult PlanB(int[] a)
  49. {
  50. int lo = ;
  51. int hi = ;
  52. int mid = ;
  53. int eggs = ;
  54. testResult result = new testResult();
  55.  
  56. while (ThrowEgg(hi))
  57. {
  58. lo = hi;
  59. hi *= ;
  60. }
  61. eggs++;
  62.  
  63. if (hi > a.Length - )
  64. {
  65. hi = a.Length - ;
  66. }
  67.  
  68. while (lo <= hi)
  69. {
  70. mid = lo + (hi - lo) / ;
  71. if (ThrowEgg(mid))
  72. {
  73. lo = mid + ;
  74. }
  75. else
  76. {
  77. eggs++;
  78. hi = mid - ;
  79. }
  80. }
  81.  
  82. result.BrokenEggs = eggs;
  83. result.F = hi;
  84. return result;
  85. }

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] 范围中顺序查找。

代码

这里我们同样定义了一个结构体:

  1. struct testResult
  2. {
  3. public int F;// 测试得出的 F 值
  4. public int BrokenEggs;// 碎掉的鸡蛋数。
  5. public int ThrowTimes;// 扔鸡蛋的次数。
  6. }

之后是测试用的方法:

  1. /// <summary>
  2. /// 扔鸡蛋,没碎返回 true,碎了返回 false。
  3. /// </summary>
  4. /// <param name="floor">扔鸡蛋的高度。</param>
  5. /// <returns></returns>
  6. static bool ThrowEgg(int floor)
  7. {
  8. return floor <= F;
  9. }
  10.  
  11. /// <summary>
  12. /// 第一种方案。
  13. /// </summary>
  14. /// <param name="a">大楼。</param>
  15. /// <returns></returns>
  16. static testResult PlanA(int[] a)
  17. {
  18. int lo = ;
  19. int hi = ;
  20. int eggs = ;
  21. int throwTimes = ;
  22. testResult result = new testResult();
  23.  
  24. while (ThrowEgg(hi))
  25. {
  26. throwTimes++;
  27. lo = hi;
  28. hi += (int)Math.Sqrt(a.Length);
  29. }
  30. eggs++;
  31.  
  32. if (hi > a.Length - )
  33. {
  34. hi = a.Length - ;
  35. }
  36.  
  37. while (lo <= hi)
  38. {
  39. if (!ThrowEgg(lo))
  40. {
  41. eggs++;
  42. break;
  43. }
  44. throwTimes++;
  45. lo++;
  46. }
  47.  
  48. result.BrokenEggs = eggs;
  49. result.F = lo - ;
  50. result.ThrowTimes = throwTimes;
  51. return result;
  52. }
  53.  
  54. /// <summary>
  55. /// 第二种方案。
  56. /// </summary>
  57. /// <param name="a">大楼。</param>
  58. /// <returns></returns>
  59. static testResult PlanB(int[] a)
  60. {
  61. int lo = ;
  62. int hi = ;
  63. int eggs = ;
  64. int throwTimes = ;
  65. testResult result = new testResult();
  66.  
  67. for (int i = ; ThrowEgg(hi); ++i)
  68. {
  69. throwTimes++;
  70. lo = hi;
  71. hi += i;
  72. }
  73. eggs++;
  74.  
  75. if (hi > a.Length - )
  76. {
  77. hi = a.Length - ;
  78. }
  79.  
  80. while (lo <= hi)
  81. {
  82. if (!ThrowEgg(lo))
  83. {
  84. eggs++;
  85. break;
  86. }
  87. lo++;
  88. throwTimes++;
  89. }
  90.  
  91. result.BrokenEggs = eggs;
  92. result.F = lo - ;
  93. result.ThrowTimes = throwTimes;
  94. return result;
  95. }

1.4.26

解答

1.4.27

解答

实现比较简单,想象两个栈背靠背接在一起,左侧栈负责出队,右侧栈负责入队。

当左侧栈为空时就把右侧栈中的元素倒到左侧栈,这个过程是 O(n) 的。

但在这个过程之前必然有 n 个元素入栈,均摊后即为 O(1)。

代码
  1. namespace _1._4._27
  2. {
  3. /// <summary>
  4. /// 用两个栈模拟的队列。
  5. /// </summary>
  6. /// <typeparam name="Item">队列中的元素。</typeparam>
  7. class StackQueue<Item>
  8. {
  9. Stack<Item> H;//用于保存出队元素
  10. Stack<Item> T;//用于保存入队元素
  11.  
  12. /// <summary>
  13. /// 构造一个队列。
  14. /// </summary>
  15. public StackQueue()
  16. {
  17. this.H = new Stack<Item>();
  18. this.T = new Stack<Item>();
  19. }
  20.  
  21. /// <summary>
  22. /// 将栈 T 中的元素依次弹出并压入栈 H 中。
  23. /// </summary>
  24. private void Reverse()
  25. {
  26. while (!this.T.IsEmpty())
  27. {
  28. this.H.Push(this.T.Pop());
  29. }
  30. }
  31.  
  32. /// <summary>
  33. /// 将一个元素出队。
  34. /// </summary>
  35. /// <returns></returns>
  36. public Item Dequeue()
  37. {
  38. //如果没有足够的出队元素,则将 T 中的元素移动过来
  39. if (this.H.IsEmpty())
  40. {
  41. Reverse();
  42. }
  43.  
  44. return this.H.Pop();
  45. }
  46.  
  47. /// <summary>
  48. /// 将一个元素入队。
  49. /// </summary>
  50. /// <param name="item">要入队的元素。</param>
  51. public void Enqueue(Item item)
  52. {
  53. this.T.Push(item);
  54. }
  55. }
  56. }

1.4.28

解答

每次入队的时候将队列倒转,这样入队的元素就是第一个了。

代码
  1. namespace _1._4._28
  2. {
  3. /// <summary>
  4. /// 用一条队列模拟的栈。
  5. /// </summary>
  6. /// <typeparam name="Item">栈中保存的元素。</typeparam>
  7. class QueueStack<Item>
  8. {
  9. Queue<Item> queue;
  10.  
  11. /// <summary>
  12. /// 初始化一个栈。
  13. /// </summary>
  14. public QueueStack()
  15. {
  16. this.queue = new Queue<Item>();
  17. }
  18.  
  19. /// <summary>
  20. /// 向栈中添加一个元素。
  21. /// </summary>
  22. /// <param name="item"></param>
  23. public void Push(Item item)
  24. {
  25. this.queue.Enqueue(item);
  26. int size = this.queue.Size();
  27. // 倒转队列
  28. for (int i = ; i < size - ; ++i)
  29. {
  30. this.queue.Enqueue(this.queue.Dequeue());
  31. }
  32. }
  33.  
  34. /// <summary>
  35. /// 从栈中弹出一个元素。
  36. /// </summary>
  37. /// <returns></returns>
  38. public Item Pop()
  39. {
  40. return this.queue.Dequeue();
  41. }
  42.  
  43. /// <summary>
  44. /// 确定栈是否为空。
  45. /// </summary>
  46. /// <returns></returns>
  47. public bool IsEmpty()
  48. {
  49. return this.queue.IsEmpty();
  50. }
  51. }
  52. }

1.4.29

解答

和用两个栈实现队列的方法类似。

push 的时候把右侧栈内容倒到左侧栈,之后再入栈。

pop 的时候也做相同操作,右侧栈内容进左侧栈,之后再出栈。

enqueue 的时候则将左侧栈内容倒到右侧栈,之后再入队。

代码
  1. namespace _1._4._29
  2. {
  3. /// <summary>
  4. /// 用两个栈模拟的 Steque。
  5. /// </summary>
  6. /// <typeparam name="Item">Steque 中的元素类型。</typeparam>
  7. class StackSteque<Item>
  8. {
  9. Stack<Item> H;
  10. Stack<Item> T;
  11.  
  12. /// <summary>
  13. /// 初始化一个 Steque
  14. /// </summary>
  15. public StackSteque()
  16. {
  17. this.H = new Stack<Item>();
  18. this.T = new Stack<Item>();
  19. }
  20.  
  21. /// <summary>
  22. /// 向栈中添加一个元素。
  23. /// </summary>
  24. /// <param name="item"></param>
  25. public void Push(Item item)
  26. {
  27. ReverseT();
  28. this.H.Push(item);
  29. }
  30.  
  31. /// <summary>
  32. /// 将 T 中的元素弹出并压入到 H 中。
  33. /// </summary>
  34. private void ReverseT()
  35. {
  36. while (!this.T.IsEmpty())
  37. {
  38. this.H.Push(this.T.Pop());
  39. }
  40. }
  41.  
  42. /// <summary>
  43. /// 将 H 中的元素弹出并压入到 T 中。
  44. /// </summary>
  45. private void ReverseH()
  46. {
  47. while (!this.H.IsEmpty())
  48. {
  49. this.T.Push(this.H.Pop());
  50. }
  51. }
  52.  
  53. /// <summary>
  54. /// 从 Steque 中弹出一个元素。
  55. /// </summary>
  56. /// <returns></returns>
  57. public Item Pop()
  58. {
  59. ReverseT();
  60. return this.H.Pop();
  61. }
  62.  
  63. /// <summary>
  64. /// 在 Steque 尾部添加一个元素。
  65. /// </summary>
  66. /// <param name="item"></param>
  67. public void Enqueue(Item item)
  68. {
  69. ReverseH();
  70. this.T.Push(item);
  71. }
  72.  
  73. /// <summary>
  74. /// 检查 Steque 是否为空。
  75. /// </summary>
  76. /// <returns></returns>
  77. public bool IsEmpty()
  78. {
  79. return this.H.IsEmpty() && this.T.IsEmpty();
  80. }
  81. }
  82. }

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。

代码
  1. namespace _1._4._30
  2. {
  3. /// <summary>
  4. /// 用一个栈和一个 Steque 模拟的双向队列。
  5. /// </summary>
  6. /// <typeparam name="Item">双向队列中保存的元素类型。</typeparam>
  7. class Deque<Item>
  8. {
  9. Stack<Item> stack;//代表队列尾部
  10. Steque<Item> steque;//代表队列头部
  11.  
  12. /// <summary>
  13. /// 创建一条空的双向队列。
  14. /// </summary>
  15. public Deque()
  16. {
  17. this.stack = new Stack<Item>();
  18. this.steque = new Steque<Item>();
  19. }
  20.  
  21. /// <summary>
  22. /// 在左侧插入一个新元素。
  23. /// </summary>
  24. /// <param name="item">要插入的元素。</param>
  25. public void PushLeft(Item item)
  26. {
  27. this.steque.Push(item);
  28. }
  29.  
  30. /// <summary>
  31. /// 将栈中的内容移动到 Steque 中。
  32. /// </summary>
  33. private void StackToSteque()
  34. {
  35. while (!this.stack.IsEmpty())
  36. {
  37. this.steque.Push(this.stack.Pop());
  38. }
  39. }
  40.  
  41. /// <summary>
  42. /// 将 Steque 中的内容移动到栈中。
  43. /// </summary>
  44. private void StequeToStack()
  45. {
  46. while (!this.steque.IsEmpty())
  47. {
  48. this.stack.Push(this.steque.Pop());
  49. }
  50. }
  51.  
  52. /// <summary>
  53. /// 从双向队列左侧弹出一个元素。
  54. /// </summary>
  55. /// <returns></returns>
  56. public Item PopLeft()
  57. {
  58. if (this.steque.IsEmpty())
  59. {
  60. StackToSteque();
  61. }
  62. return this.steque.Pop();
  63. }
  64.  
  65. /// <summary>
  66. /// 向双向队列右侧添加一个元素。
  67. /// </summary>
  68. /// <param name="item">要插入的元素。</param>
  69. public void PushRight(Item item)
  70. {
  71. if (this.stack.IsEmpty())
  72. {
  73. this.steque.Enqueue(item);
  74. }
  75. else
  76. {
  77. this.stack.Push(item);
  78. }
  79. }
  80.  
  81. /// <summary>
  82. /// 从双向队列右侧弹出一个元素。
  83. /// </summary>
  84. /// <returns></returns>
  85. public Item PopRight()
  86. {
  87. if (this.stack.IsEmpty())
  88. {
  89. StequeToStack();
  90. }
  91. return this.stack.Pop();
  92. }
  93.  
  94. /// <summary>
  95. /// 判断队列是否为空。
  96. /// </summary>
  97. /// <returns></returns>
  98. public bool IsEmpty()
  99. {
  100. return this.stack.IsEmpty() && this.steque.IsEmpty();
  101. }
  102.  
  103. /// <summary>
  104. /// 返回队列中元素的数量。
  105. /// </summary>
  106. /// <returns></returns>
  107. public int Size()
  108. {
  109. return this.stack.Size() + this.steque.Size();
  110. }
  111. }
  112. }

1.4.31

解答

三个栈分别命名为左中右。

左侧栈和右侧栈负责模拟队列,和用两个栈模拟队列的方法类似。

由于是双向队列,左栈和右栈会频繁的倒来倒去,因此每次都只倒一半的元素可以有效减少开销。

有一侧栈为空时,另一侧栈中上半部分先移动到中间栈中,下半部分倒到另一侧栈里,再从中间栈拿回上半部分元素。

这样可以确保接下来的 pop 操作一定是常数级别的。

代码
  1. namespace _1._4._31
  2. {
  3. /// <summary>
  4. /// 用三个栈模拟的双向队列。
  5. /// </summary>
  6. /// <typeparam name="Item">双向队列中的元素。</typeparam>
  7. class Deque<Item>
  8. {
  9. Stack<Item> left;
  10. Stack<Item> middle;
  11. Stack<Item> right;
  12.  
  13. /// <summary>
  14. /// 构造一条新的双向队列。
  15. /// </summary>
  16. public Deque()
  17. {
  18. this.left = new Stack<Item>();
  19. this.middle = new Stack<Item>();
  20. this.right = new Stack<Item>();
  21. }
  22.  
  23. /// <summary>
  24. /// 向双向队列左侧插入一个元素。
  25. /// </summary>
  26. /// <param name="item">要插入的元素。</param>
  27. public void PushLeft(Item item)
  28. {
  29. this.left.Push(item);
  30. }
  31.  
  32. /// <summary>
  33. /// 向双向队列右侧插入一个元素。
  34. /// </summary>
  35. /// <param name="item">要插入的元素。</param>
  36. public void PushRight(Item item)
  37. {
  38. this.right.Push(item);
  39. }
  40.  
  41. /// <summary>
  42. /// 当一侧栈为空时,将另一侧的下半部分元素移动过来。
  43. /// </summary>
  44. /// <param name="source">不为空的栈。</param>
  45. /// <param name="destination">空栈。</param>
  46. private void Move(Stack<Item> source, Stack<Item> destination)
  47. {
  48. int n = source.Size();
  49. // 将上半部分元素移动到临时栈 middle
  50. for (int i = ; i < n / ; ++i)
  51. {
  52. this.middle.Push(source.Pop());
  53. }
  54. // 将下半部分移动到另一侧栈中
  55. while (!source.IsEmpty())
  56. {
  57. destination.Push(source.Pop());
  58. }
  59. // 从 middle 取回上半部分元素
  60. while (!this.middle.IsEmpty())
  61. {
  62. source.Push(this.middle.Pop());
  63. }
  64. }
  65.  
  66. /// <summary>
  67. /// 检查双端队列是否为空。
  68. /// </summary>
  69. /// <returns></returns>
  70. public bool IsEmpty()
  71. {
  72. return this.right.IsEmpty() && this.middle.IsEmpty() && this.left.IsEmpty();
  73. }
  74.  
  75. /// <summary>
  76. /// 从右侧弹出一个元素。
  77. /// </summary>
  78. /// <returns></returns>
  79. public Item PopRight()
  80. {
  81. if (this.right.IsEmpty())
  82. {
  83. Move(this.left, this.right);
  84. }
  85.  
  86. return this.right.Pop();
  87. }
  88.  
  89. /// <summary>
  90. /// 从左侧弹出一个元素。
  91. /// </summary>
  92. /// <returns></returns>
  93. public Item PopLeft()
  94. {
  95. if (this.left.IsEmpty())
  96. {
  97. Move(this.right, this.left);
  98. }
  99.  
  100. return this.left.Pop();
  101. }
  102.  
  103. /// <summary>
  104. /// 返回双端队列的大小。
  105. /// </summary>
  106. /// <returns></returns>
  107. public int Size()
  108. {
  109. return this.left.Size() + this.middle.Size() + this.right.Size();
  110. }
  111. }
  112. }

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 类

  1. using System;
  2.  
  3. namespace _1._4._34
  4. {
  5. /// <summary>
  6. /// 某次猜测的结果。
  7. /// </summary>
  8. enum GuessResult
  9. {
  10. Hot = , // 比上次猜测更接近目标。
  11. Equal = , // 猜中目标。
  12. Cold = -, // 比上次猜测更远离目标。
  13. FirstGuess = - // 第一次猜测。
  14. }
  15.  
  16. /// <summary>
  17. /// 游戏类。
  18. /// </summary>
  19. class Game
  20. {
  21. public int N { get; } // 目标值的最大范围。
  22. public int SecretNumber { get; } // 目标值。
  23. public int LastGuess { get; private set; } // 上次猜测的值
  24.  
  25. /// <summary>
  26. /// 构造函数,新开一局游戏。
  27. /// </summary>
  28. /// <param name="N">目标值的最大范围。</param>
  29. public Game(int N)
  30. {
  31. Random random = new Random();
  32. this.N = N;
  33. this.SecretNumber = random.Next(N - ) + ;
  34. this.LastGuess = -;
  35. }
  36.  
  37. /// <summary>
  38. /// 猜测,根据与上次相比更为接近还是远离目标值返回结果。
  39. /// </summary>
  40. /// <param name="guess">本次的猜测值</param>
  41. /// <returns>接近或不变返回 Hot,远离则返回 Cold,猜中返回 Equal。</returns>
  42. public GuessResult Guess(int guess)
  43. {
  44. if (guess == this.SecretNumber)
  45. {
  46. return GuessResult.Equal;
  47. }
  48. if (this.LastGuess == -)
  49. {
  50. this.LastGuess = guess;
  51. return GuessResult.FirstGuess;
  52. }
  53.  
  54. int lastDiff = Math.Abs(this.LastGuess - this.SecretNumber);
  55. this.LastGuess = guess;
  56. int nowDiff = Math.Abs(guess - this.SecretNumber);
  57. if (nowDiff > lastDiff)
  58. {
  59. return GuessResult.Cold;
  60. }
  61. else
  62. {
  63. return GuessResult.Hot;
  64. }
  65. }
  66.  
  67. /// <summary>
  68. /// 重置游戏,清空上次猜测的记录。目标值和最大值都不变。
  69. /// </summary>
  70. public void Restart()
  71. {
  72. this.LastGuess = -;
  73. }
  74. }
  75. }

之后是实际测试的方法:

  1. using System;
  2.  
  3. namespace _1._4._34
  4. {
  5. /*
  6. * 1.4.34
  7. *
  8. * 热还是冷。
  9. * 你的目标是猜出 1 到 N 之间的一个秘密的整数。
  10. * 每次猜完一个整数后,你会直到你的猜测距离该秘密整数是否相等(如果是则游戏结束)。
  11. * 如果不相等,你会知道你的猜测相比上一次猜测距离秘密整数是比较热(接近),还是比较冷(远离)。
  12. * 设计一个算法在 ~2lgN 之内找到这个秘密整数,然后设计一个算法在 ~1lgN 之内找到这个秘密整数。
  13. *
  14. */
  15. class Program
  16. {
  17. /// <summary>
  18. /// 某种方案的测试结果,包含猜测结果和尝试次数。
  19. /// </summary>
  20. struct TestResult
  21. {
  22. public int SecretNumber;// 猜测到的数字。
  23. public int TryTimes;// 尝试次数。
  24. }
  25.  
  26. static void Main(string[] args)
  27. {
  28. Game game = new Game();
  29. TestResult A = PlayGameA(game);
  30. game.Restart();
  31. TestResult B = PlayGameB(game);
  32.  
  33. Console.WriteLine($"SecretNumber:{game.SecretNumber}");
  34. Console.WriteLine("TestResultA:");
  35. Console.WriteLine($"SecretNumber:{A.SecretNumber}, TryTimes:{A.TryTimes}");
  36. Console.WriteLine();
  37. Console.WriteLine("TestResultB:");
  38. Console.WriteLine($"SecretNumber:{B.SecretNumber}, TryTimes:{B.TryTimes}");
  39. }
  40.  
  41. /// <summary>
  42. /// 方案一,用二分查找实现,需要猜测 2lgN 次。
  43. /// </summary>
  44. /// <param name="game">用于猜测的游戏对象。</param>
  45. /// <returns>返回测试结果,包含猜测结果和尝试次数。</returns>
  46. static TestResult PlayGameA(Game game)
  47. {
  48. TestResult result;
  49. result.TryTimes = ;
  50. result.SecretNumber = ;
  51. GuessResult guessResult;
  52.  
  53. int hi = game.N;
  54. int lo = ;
  55.  
  56. // 利用二分查找猜测,2lgN
  57. while (lo <= hi)
  58. {
  59. int mid = lo + (hi - lo) / ;
  60.  
  61. guessResult = game.Guess(lo);
  62. result.TryTimes++;
  63. if (guessResult == GuessResult.Equal)
  64. {
  65. result.SecretNumber = lo;
  66. return result;
  67. }
  68.  
  69. guessResult = game.Guess(hi);
  70. result.TryTimes++;
  71. if (guessResult == GuessResult.Equal)
  72. {
  73. result.SecretNumber = hi;
  74. return result;
  75. }
  76. else if (guessResult == GuessResult.Hot)
  77. {
  78. lo = mid;
  79. }
  80. else
  81. {
  82. hi = mid;
  83. }
  84. }
  85.  
  86. return result;
  87. }
  88.  
  89. /// <summary>
  90. /// 方案二,根据 (lastGuess + nowGuess)/2 = (lo + hi) / 2 确定每次猜测的值。
  91. /// </summary>
  92. /// <param name="game">用于猜测的游戏对象。</param>
  93. /// <returns>返回测试结果,包含猜测结果和尝试次数。</returns>
  94. static TestResult PlayGameB(Game game)
  95. {
  96. TestResult result;
  97. result.TryTimes = ;
  98. result.SecretNumber = ;
  99. GuessResult guessResult;
  100.  
  101. int hi = game.N;
  102. int lo = ;
  103. bool isRightSide = true;
  104.  
  105. // 第一次猜测
  106. guessResult = game.Guess();
  107. result.TryTimes++;
  108. if (guessResult == GuessResult.Equal)
  109. {
  110. result.SecretNumber = ;
  111. return result;
  112. }
  113.  
  114. while (lo < hi)
  115. {
  116. int mid = lo + (hi - lo) / ;
  117. int nowGuess = (lo + hi) - game.LastGuess;
  118.  
  119. guessResult = game.Guess(nowGuess);
  120. result.TryTimes++;
  121. if (guessResult == GuessResult.Equal)
  122. {
  123. result.SecretNumber = nowGuess;
  124. break;
  125. }
  126. else if (guessResult == GuessResult.Hot)
  127. {
  128. if (isRightSide)
  129. {
  130. lo = mid;
  131. }
  132. else
  133. {
  134. hi = mid;
  135. }
  136. }
  137. else
  138. {
  139. if (isRightSide)
  140. {
  141. hi = mid;
  142. }
  143. else
  144. {
  145. lo = mid;
  146. }
  147. }
  148. isRightSide = !isRightSide;
  149. if (hi - lo <= )
  150. {
  151. break;
  152. }
  153. }
  154. if (game.Guess(lo) == GuessResult.Equal)
  155. {
  156. result.TryTimes++;
  157. result.SecretNumber = lo;
  158. }
  159. else if (game.Guess(hi) == GuessResult.Equal)
  160. {
  161. result.TryTimes++;
  162. result.SecretNumber = hi;
  163. }
  164.  
  165. return result;
  166. }
  167. }
  168. }

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 修改而来:

  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4.  
  5. namespace _1._4._37
  6. {
  7. /// <summary>
  8. /// 固定大小的整型数据栈。
  9. /// </summary>
  10. class FixedCapacityStackOfInts : IEnumerable<int>
  11. {
  12. private int[] a;
  13. private int N;
  14.  
  15. /// <summary>
  16. /// 默认构造函数。
  17. /// </summary>
  18. /// <param name="capacity">栈的大小。</param>
  19. public FixedCapacityStackOfInts(int capacity)
  20. {
  21. this.a = new int[capacity];
  22. this.N = ;
  23. }
  24.  
  25. /// <summary>
  26. /// 检查栈是否为空。
  27. /// </summary>
  28. /// <returns></returns>
  29. public bool IsEmpty()
  30. {
  31. return this.N == ;
  32. }
  33.  
  34. /// <summary>
  35. /// 检查栈是否已满。
  36. /// </summary>
  37. /// <returns></returns>
  38. public bool IsFull()
  39. {
  40. return this.N == this.a.Length;
  41. }
  42.  
  43. /// <summary>
  44. /// 将一个元素压入栈中。
  45. /// </summary>
  46. /// <param name="item">要压入栈中的元素。</param>
  47. public void Push(int item)
  48. {
  49. this.a[this.N] = item;
  50. this.N++;
  51. }
  52.  
  53. /// <summary>
  54. /// 从栈中弹出一个元素,返回被弹出的元素。
  55. /// </summary>
  56. /// <returns></returns>
  57. public int Pop()
  58. {
  59. this.N--;
  60. return this.a[this.N];
  61. }
  62.  
  63. /// <summary>
  64. /// 返回栈顶元素(但不弹出它)。
  65. /// </summary>
  66. /// <returns></returns>
  67. public int Peek()
  68. {
  69. return this.a[this.N - ];
  70. }
  71.  
  72. public IEnumerator<int> GetEnumerator()
  73. {
  74. return new ReverseEnmerator(this.a);
  75. }
  76.  
  77. IEnumerator IEnumerable.GetEnumerator()
  78. {
  79. return GetEnumerator();
  80. }
  81.  
  82. private class ReverseEnmerator : IEnumerator<int>
  83. {
  84. private int current;
  85. private int[] a;
  86.  
  87. public ReverseEnmerator(int[] a)
  88. {
  89. this.current = a.Length;
  90. this.a = a;
  91. }
  92.  
  93. int IEnumerator<int>.Current => this.a[this.current];
  94.  
  95. object IEnumerator.Current => this.a[this.current];
  96.  
  97. void IDisposable.Dispose()
  98. {
  99. this.current = -;
  100. this.a = null;
  101. }
  102.  
  103. bool IEnumerator.MoveNext()
  104. {
  105. if (this.current == )
  106. return false;
  107. this.current--;
  108. return true;
  109. }
  110.  
  111. void IEnumerator.Reset()
  112. {
  113. this.current = this.a.Length;
  114. }
  115. }
  116. }
  117. }

FixedCapacityStack<Item>

  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4.  
  5. namespace _1._4._37
  6. {
  7. /// <summary>
  8. /// 固定大小的栈。
  9. /// </summary>
  10. class FixedCapacityStack<Item> : IEnumerable<Item>
  11. {
  12. private Item[] a;
  13. private int N;
  14.  
  15. /// <summary>
  16. /// 默认构造函数。
  17. /// </summary>
  18. /// <param name="capacity">栈的大小。</param>
  19. public FixedCapacityStack(int capacity)
  20. {
  21. this.a = new Item[capacity];
  22. this.N = ;
  23. }
  24.  
  25. /// <summary>
  26. /// 检查栈是否为空。
  27. /// </summary>
  28. /// <returns></returns>
  29. public bool IsEmpty()
  30. {
  31. return this.N == ;
  32. }
  33.  
  34. /// <summary>
  35. /// 检查栈是否已满。
  36. /// </summary>
  37. /// <returns></returns>
  38. public bool IsFull()
  39. {
  40. return this.N == this.a.Length;
  41. }
  42.  
  43. /// <summary>
  44. /// 将一个元素压入栈中。
  45. /// </summary>
  46. /// <param name="item">要压入栈中的元素。</param>
  47. public void Push(Item item)
  48. {
  49. this.a[this.N] = item;
  50. this.N++;
  51. }
  52.  
  53. /// <summary>
  54. /// 从栈中弹出一个元素,返回被弹出的元素。
  55. /// </summary>
  56. /// <returns></returns>
  57. public Item Pop()
  58. {
  59. this.N--;
  60. return this.a[this.N];
  61. }
  62.  
  63. /// <summary>
  64. /// 返回栈顶元素(但不弹出它)。
  65. /// </summary>
  66. /// <returns></returns>
  67. public Item Peek()
  68. {
  69. return this.a[this.N - ];
  70. }
  71.  
  72. public IEnumerator<Item> GetEnumerator()
  73. {
  74. return new ReverseEnmerator(this.a);
  75. }
  76.  
  77. IEnumerator IEnumerable.GetEnumerator()
  78. {
  79. return GetEnumerator();
  80. }
  81.  
  82. private class ReverseEnmerator : IEnumerator<Item>
  83. {
  84. private int current;
  85. private Item[] a;
  86.  
  87. public ReverseEnmerator(Item[] a)
  88. {
  89. this.current = a.Length;
  90. this.a = a;
  91. }
  92.  
  93. Item IEnumerator<Item>.Current => this.a[this.current];
  94.  
  95. object IEnumerator.Current => this.a[this.current];
  96.  
  97. void IDisposable.Dispose()
  98. {
  99. this.current = -;
  100. this.a = null;
  101. }
  102.  
  103. bool IEnumerator.MoveNext()
  104. {
  105. if (this.current == )
  106. return false;
  107. this.current--;
  108. return true;
  109. }
  110.  
  111. void IEnumerator.Reset()
  112. {
  113. this.current = this.a.Length;
  114. }
  115. }
  116. }
  117. }

测试函数:

  1. using System;
  2. using Measurement;
  3.  
  4. namespace _1._4._37
  5. {
  6. /// <summary>
  7. /// FixedCapacityStackOfInts 测试类。
  8. /// </summary>
  9. public static class DoubleTest
  10. {
  11. private static readonly int MAXIMUM_INTEGER = ;
  12.  
  13. /// <summary>
  14. /// 返回对 n 个随机整数的栈进行 n 次 push 和 n 次 pop 所需的时间。
  15. /// </summary>
  16. /// <param name="n">随机数组的长度。</param>
  17. /// <returns></returns>
  18. public static double TimeTrial(int n)
  19. {
  20. int[] a = new int[n];
  21. FixedCapacityStackOfInts stack = new FixedCapacityStackOfInts(n);
  22. Random random = new Random(DateTime.Now.Millisecond);
  23. for (int i = ; i < n; ++i)
  24. {
  25. a[i] = random.Next(-MAXIMUM_INTEGER, MAXIMUM_INTEGER);
  26. }
  27. Stopwatch timer = new Stopwatch();
  28. for (int i = ; i < n; ++i)
  29. {
  30. stack.Push(a[i]);
  31. }
  32. for (int i = ; i < n; ++i)
  33. {
  34. stack.Pop();
  35. }
  36. return timer.ElapsedTimeMillionSeconds();
  37. }
  38.  
  39. /// <summary>
  40. /// 返回对 n 个随机整数的栈进行 n 次 push 和 n 次 pop 所需的时间。
  41. /// </summary>
  42. /// <param name="n">随机数组的长度。</param>
  43. /// <returns></returns>
  44. public static double TimeTrialGeneric(int n)
  45. {
  46. int[] a = new int[n];
  47. FixedCapacityStack<int> stack = new FixedCapacityStack<int>(n);
  48. Random random = new Random(DateTime.Now.Millisecond);
  49. for (int i = ; i < n; ++i)
  50. {
  51. a[i] = random.Next(-MAXIMUM_INTEGER, MAXIMUM_INTEGER);
  52. }
  53. Stopwatch timer = new Stopwatch();
  54. for (int i = ; i < n; ++i)
  55. {
  56. stack.Push(a[i]);
  57. }
  58. for (int i = ; i < n; ++i)
  59. {
  60. stack.Pop();
  61. }
  62. return timer.ElapsedTimeMillionSeconds();
  63. }
  64. }
  65. }

主函数:

  1. using System;
  2.  
  3. namespace _1._4._37
  4. {
  5. /*
  6. * 1.4.37
  7. *
  8. * 自动装箱的性能代价。
  9. * 通过实验在你的计算机上计算使用自动装箱所付出的性能代价。
  10. * 实现一个 FixedCapacityStackOfInts,
  11. * 并使用类似 DoublingRatio 的用例比较它和泛型 FixedCapacityStack 在进行大量 push() 和 pop() 时的性能。
  12. *
  13. */
  14. class Program
  15. {
  16. static void Main(string[] args)
  17. {
  18. Console.WriteLine("测试量\t非泛型耗时(毫秒)\t泛型耗时(毫秒)\t差值");
  19. for (int n = ; true; n += n)
  20. {
  21. double time = DoubleTest.TimeTrial(n);
  22. double timeGeneric = DoubleTest.TimeTrialGeneric(n);
  23. Console.WriteLine($"{n}\t{time}\t{timeGeneric}\t{Math.Abs(time - timeGeneric)}");
  24. }
  25. }
  26. }
  27. }

1.4.38

解答

把 DoublingTest 中调用的函数稍作修改即可。

代码

ThreeSum 测试类

  1. using System;
  2.  
  3. namespace _1._4._38
  4. {
  5. /// <summary>
  6. /// ThreeSum 测试类。
  7. /// </summary>
  8. public static class DoubleTest
  9. {
  10. private static readonly int MAXIMUM_INTEGER = ;
  11.  
  12. /// <summary>
  13. /// 返回对 n 个随机整数的数组进行一次 ThreeSum 所需的时间。
  14. /// </summary>
  15. /// <param name="n">随机数组的长度。</param>
  16. /// <returns></returns>
  17. public static double TimeTrial(int n)
  18. {
  19. int[] a = new int[n];
  20. Random random = new Random(DateTime.Now.Millisecond);
  21. for (int i = ; i < n; ++i)
  22. {
  23. a[i] = random.Next(-MAXIMUM_INTEGER, MAXIMUM_INTEGER);
  24. }
  25. Measurement.Stopwatch timer = new Measurement.Stopwatch();
  26. ThreeSum.Count(a);
  27. return timer.ElapsedTime();
  28. }
  29. }
  30. }

ThreeSum

  1. using System;
  2.  
  3. namespace _1._4._38
  4. {
  5. /// <summary>
  6. /// 用暴力方法寻找数组中和为零的三元组。
  7. /// </summary>
  8. public static class ThreeSum
  9. {
  10. /// <summary>
  11. /// 输出所有和为零的三元组。
  12. /// </summary>
  13. /// <param name="a">输入数组。</param>
  14. public static void PrintAll(int[] a)
  15. {
  16. int n = a.Length;
  17. for (int i = ; i < n; ++i)
  18. {
  19. for (int j = ; j < n; ++j)
  20. {
  21. for (int k = ; k < n; ++k)
  22. {
  23. if (i < j && j < k)
  24. {
  25. if ((long)a[i] + a[j] + a[k] == )
  26. {
  27. Console.WriteLine($"{a[i]} + {a[j]} + {a[k]}");
  28. }
  29. }
  30. }
  31. }
  32. }
  33. }
  34.  
  35. /// <summary>
  36. /// 计算和为零的三元组的数量。
  37. /// </summary>
  38. /// <param name="a">输入数组。</param>
  39. /// <returns></returns>
  40. public static int Count(int[] a)
  41. {
  42. int n = a.Length;
  43. int count = ;
  44. for (int i = ; i < n; ++i)
  45. {
  46. for (int j = ; j < n; ++j)
  47. {
  48. for (int k = ; k < n; ++k)
  49. {
  50. if (i < j && j < k)
  51. {
  52. if ((long)a[i] + a[j] + a[k] == )
  53. {
  54. count++;
  55. }
  56. }
  57. }
  58. }
  59. }
  60. return count;
  61. }
  62. }
  63. }

1.4.39

解答

执行 N 次后取平均即可。

代码

修改后的 DoublingTest:

  1. using System;
  2. using Measurement;
  3.  
  4. namespace _1._4._39
  5. {
  6. /// <summary>
  7. /// ThreeSum 测试类。
  8. /// </summary>
  9. public static class DoubleTest
  10. {
  11. private static readonly int MAXIMUM_INTEGER = ;
  12.  
  13. /// <summary>
  14. /// 返回对 n 个随机整数的数组进行一次 ThreeSum 所需的时间。
  15. /// </summary>
  16. /// <param name="n">随机数组的长度。</param>
  17. /// <param name="repeatTimes">重复测试的次数。</param>
  18. /// <returns></returns>
  19. public static double TimeTrial(int n, int repeatTimes)
  20. {
  21. int[] a = new int[n];
  22. double sum = ;
  23. Random random = new Random(DateTime.Now.Millisecond);
  24. for (int i = ; i < n; ++i)
  25. {
  26. a[i] = random.Next(-MAXIMUM_INTEGER, MAXIMUM_INTEGER);
  27. }
  28. for (int i = ; i < repeatTimes; ++i)
  29. {
  30. Stopwatch timer = new Stopwatch();
  31. ThreeSum.Count(a);
  32. sum += timer.ElapsedTime();
  33. }
  34. return sum / repeatTimes;
  35. }
  36. }
  37. }

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

代码
  1. using System;
  2.  
  3. namespace _1._4._40
  4. {
  5. /*
  6. * 1.4.40
  7. *
  8. * 随机输入下的 3-sum 问题。
  9. * 猜测找出 N 个随机 int 值中和为 0 的整数三元组的数量所需的时间并验证你的猜想。
  10. * 如果你擅长数学分析,请为此问题给出一个合适的数学模型,
  11. * 其中所有值均匀的分布在 -M 和 M 之间,且 M 不能是一个小整数。
  12. *
  13. */
  14. class Program
  15. {
  16. // 数学模型
  17. //
  18. // N 个数可组成的三元组的总数为:
  19. // C(N, 3) = N(N-1)(N-2)/3! = ~ (N^3)/6 (组合数公式)
  20. // [-M, M]中随机 N 次,有 (2M+1)^N 种随机序列(每次随机都有 2M+1 种可能)
  21. // 按照分步计数方法,将随机序列分为和为零的三元组和其余 N-3 个数
  22. // 这些序列中,和为零的三元组有 3M^2 + 3M + 1 种可能。
  23. // 其他不为零的 N-3 个位置有 (2M+1)^(N-3) 种可能。
  24. // 总共有 ((N^3)/6) * (3M^2 + 3M + 1) * (2M+1)^(N-3) 种可能性
  25. // 平均值为:
  26. // [((N^3)/6) * (3M^2 + 3M + 1) * (2M+1)^(N-3)] / (2M+1)^N
  27. // (N^3) * (3M^2 + 3M + 1) / 6 * (2M+1)^3
  28. // ~ N^3 * 3M^2 / 6 * 8M^3
  29. // N^3/16M
  30. static void Main(string[] args)
  31. {
  32. int M = ;
  33.  
  34. for (int n = ; n < ; n += n)
  35. {
  36. Random random = new Random();
  37. int[] a = new int[n];
  38. for (int i = ; i < n; ++i)
  39. {
  40. a[i] = random.Next( * M) - M;
  41. }
  42. Console.WriteLine($"N={n}, 计算值={Math.Pow(n, 3) / (16 * M)}, 实际值={ThreeSum.Count(a)}");
  43. }
  44. }
  45. }
  46. }

1.4.41

解答

代码

这里使用了委托来简化代码。

DoublingRatio

  1. using System;
  2. using Measurement;
  3.  
  4. namespace _1._4._41
  5. {
  6. public delegate int Count(int[] a);
  7. static class DoublingRatio
  8. {
  9. /// <summary>
  10. /// 从指定字符串中读入按行分割的整型数据。
  11. /// </summary>
  12. /// <param name="inputString">源字符串。</param>
  13. /// <returns>读入的整型数组</returns>
  14. private static int[] ReadAllInts(string inputString)
  15. {
  16. char[] split = new char[] { '\n' };
  17. string[] input = inputString.Split(split, StringSplitOptions.RemoveEmptyEntries);
  18. int[] a = new int[input.Length];
  19. for (int i = ; i < a.Length; ++i)
  20. {
  21. a[i] = int.Parse(input[i]);
  22. }
  23. return a;
  24. }
  25.  
  26. /// <summary>
  27. /// 使用给定的数组进行一次测试,返回耗时(毫秒)。
  28. /// </summary>
  29. /// <param name="Count">要测试的方法。</param>
  30. /// <param name="a">测试用的数组。</param>
  31. /// <returns>耗时(秒)。</returns>
  32. public static double TimeTrial(Count Count, int[] a)
  33. {
  34. Stopwatch timer = new Stopwatch();
  35. Count(a);
  36. return timer.ElapsedTimeMillionSeconds();
  37. }
  38.  
  39. /// <summary>
  40. /// 对 TwoSum、TwoSumFast、ThreeSum 或 ThreeSumFast 的 Count 方法做测试。
  41. /// </summary>
  42. /// <param name="Count">相应类的 Count 方法</param>
  43. /// <returns>随着数据量倍增,方法耗时增加的比率。</returns>
  44. public static double Test(Count Count)
  45. {
  46. double ratio = ;
  47. double times = ;
  48.  
  49. // 1K
  50. int[] a = ReadAllInts(TestCase.Properties.Resources._1Kints);
  51. double prevTime = TimeTrial(Count, a);
  52. Console.WriteLine("数据量\t耗时\t比值");
  53. Console.WriteLine($"1000\t{prevTime / 1000}\t");
  54.  
  55. // 2K
  56. a = ReadAllInts(TestCase.Properties.Resources._2Kints);
  57. double time = TimeTrial(Count, a);
  58. Console.WriteLine($"2000\t{time / 1000}\t{time / prevTime}");
  59. if (prevTime != )
  60. {
  61. ratio += time / prevTime;
  62. }
  63. else
  64. {
  65. times--;
  66. }
  67. prevTime = time;
  68.  
  69. // 4K
  70. a = ReadAllInts(TestCase.Properties.Resources._4Kints);
  71. time = TimeTrial(Count, a);
  72. Console.WriteLine($"4000\t{time / 1000}\t{time / prevTime}");
  73. if (prevTime != )
  74. {
  75. ratio += time / prevTime;
  76. }
  77. else
  78. {
  79. times--;
  80. }
  81. prevTime = time;
  82.  
  83. // 8K
  84. a = ReadAllInts(TestCase.Properties.Resources._8Kints);
  85. time = TimeTrial(Count, a);
  86. Console.WriteLine($"8000\t{time / 1000}\t{time / prevTime}");
  87. if (prevTime != )
  88. {
  89. ratio += time / prevTime;
  90. }
  91. else
  92. {
  93. times--;
  94. }
  95. prevTime = time;
  96.  
  97. return ratio / times;
  98. }
  99.  
  100. public static double TestTwoSumFast(Count Count)
  101. {
  102. double ratio = ;
  103. double times = ;
  104.  
  105. // 8K
  106. int[] a = ReadAllInts(TestCase.Properties.Resources._8Kints);
  107. double prevTime = TimeTrial(Count, a);
  108. Console.WriteLine("数据量\t耗时\t比值");
  109. Console.WriteLine($"8000\t{prevTime / 1000}\t");
  110.  
  111. // 16K
  112. a = ReadAllInts(TestCase.Properties.Resources._16Kints);
  113. double time = TimeTrial(Count, a);
  114. Console.WriteLine($"16000\t{time / 1000}\t{time / prevTime}");
  115. if (prevTime != )
  116. {
  117. ratio += time / prevTime;
  118. }
  119. else
  120. {
  121. times--;
  122. }
  123. prevTime = time;
  124.  
  125. // 32K
  126. a = ReadAllInts(TestCase.Properties.Resources._32Kints);
  127. time = TimeTrial(Count, a);
  128. Console.WriteLine($"32000\t{time / 1000}\t{time / prevTime}");
  129. if (prevTime != )
  130. {
  131. ratio += time / prevTime;
  132. }
  133. else
  134. {
  135. times--;
  136. }
  137. prevTime = time;
  138.  
  139. return ratio / times;
  140. }
  141. }
  142. }

主方法:

  1. using System;
  2. using Measurement;
  3.  
  4. namespace _1._4._41
  5. {
  6. /*
  7. * 1.4.41
  8. *
  9. * 运行时间。
  10. * 使用 DoublingRatio 估计在你的计算机上用 TwoSumFast、TwoSum、ThreeSumFast 以及 ThreeSum 处理一个含有 100 万个整数的文件所需的时间。
  11. *
  12. */
  13. class Program
  14. {
  15. static void Main(string[] args)
  16. {
  17. int[] a = new int[];
  18. Random random = new Random();
  19. for (int i = ; i < ; ++i)
  20. {
  21. a[i] = random.Next() - ;
  22. }
  23.  
  24. // ThreeSum
  25. Console.WriteLine("ThreeSum");
  26. double time = DoublingRatio.TimeTrial(ThreeSum.Count, a);
  27. Console.WriteLine($"数据量:977 耗时:{time / 1000}");
  28. double doubleRatio = DoublingRatio.Test(ThreeSum.Count);
  29. Console.WriteLine($"数据量:1000000 估计耗时:{time * doubleRatio * 1024 / 1000}");
  30. Console.WriteLine();
  31.  
  32. //// ThreeSumFast
  33. Console.WriteLine("ThreeSumFast");
  34. time = DoublingRatio.TimeTrial(ThreeSumFast.Count, a);
  35. doubleRatio = DoublingRatio.Test(ThreeSumFast.Count);
  36. Console.WriteLine($"数据量:977 耗时:{time / 1000}");
  37. Console.WriteLine($"数据量:1000000 估计耗时:{time * doubleRatio * 1024 / 1000}");
  38. Console.WriteLine();
  39.  
  40. //// TwoSum
  41. Console.WriteLine("TwoSum");
  42. time = DoublingRatio.TimeTrial(TwoSum.Count, a);
  43. doubleRatio = DoublingRatio.Test(TwoSum.Count);
  44. Console.WriteLine($"数据量:977 耗时:{time / 1000}");
  45. Console.WriteLine($"数据量:1000000 估计耗时:{time * doubleRatio * 1024 / 1000}");
  46. Console.WriteLine();
  47.  
  48. // TwoSumFast
  49. // 速度太快,加大数据量
  50. a = new int[];
  51. for (int i = ; i < ; ++i)
  52. {
  53. a[i] = random.Next() - ;
  54. }
  55. Console.WriteLine("TwoSumFast");
  56. time = DoublingRatio.TimeTrial(TwoSumFast.Count, a);
  57. doubleRatio = DoublingRatio.TestTwoSumFast(TwoSumFast.Count);
  58. Console.WriteLine($"数据量:62500 耗时:{time / 1000}");
  59. Console.WriteLine($"数据量:1000000 估计耗时:{time * doubleRatio * 16 / 1000}");
  60. Console.WriteLine();
  61. }
  62. }
  63. }

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

  1. using System;
  2. using Measurement;
  3.  
  4. namespace _1._4._43
  5. {
  6. static class DoublingRatio
  7. {
  8. /// <summary>
  9. /// 从指定字符串中读入按行分割的整型数据。
  10. /// </summary>
  11. /// <param name="inputString">源字符串。</param>
  12. /// <returns>读入的整型数组</returns>
  13. private static int[] ReadAllInts(string inputString)
  14. {
  15. char[] split = new char[] { '\n' };
  16. string[] input = inputString.Split(split, StringSplitOptions.RemoveEmptyEntries);
  17.  
  18. int[] a = new int[input.Length];
  19. for (int i = ; i < a.Length; ++i)
  20. {
  21. a[i] = int.Parse(input[i]);
  22. }
  23. return a;
  24. }
  25.  
  26. /// <summary>
  27. /// 使用给定的数组对链栈进行一次测试,返回耗时(毫秒)。
  28. /// </summary>
  29. /// <param name="a">测试用的数组。</param>
  30. /// <returns>耗时(毫秒)。</returns>
  31. public static double TimeTrialLinkedStack(int[] a)
  32. {
  33. LinkedStack<int> stack = new LinkedStack<int>();
  34. int n = a.Length;
  35. Stopwatch timer = new Stopwatch();
  36. for (int i = ; i < n; ++i)
  37. {
  38. stack.Push(a[i]);
  39. }
  40. for (int i = ; i < n; ++i)
  41. {
  42. stack.Pop();
  43. }
  44. return timer.ElapsedTimeMillionSeconds();
  45. }
  46.  
  47. /// <summary>
  48. /// 使用给定的数组对数组栈进行一次测试,返回耗时(毫秒)。
  49. /// </summary>
  50. /// <param name="a">测试用的数组。</param>
  51. /// <returns>耗时(毫秒)。</returns>
  52. public static double TimeTrialDoublingStack(int[] a)
  53. {
  54. DoublingStack<int> stack = new DoublingStack<int>();
  55. int n = a.Length;
  56. Stopwatch timer = new Stopwatch();
  57. for (int i = ; i < n; ++i)
  58. {
  59. stack.Push(a[i]);
  60. }
  61. for (int i = ; i < n; ++i)
  62. {
  63. stack.Pop();
  64. }
  65. return timer.ElapsedTimeMillionSeconds();
  66. }
  67.  
  68. /// <summary>
  69. /// 对链栈和基于大小可变的数组栈做测试。
  70. /// </summary>
  71. public static void Test()
  72. {
  73. double linkedTime = ;
  74. double arrayTime = ;
  75. Console.WriteLine("数据量\t链栈\t数组\t比值\t单位:毫秒");
  76. // 16K
  77. int[] a = ReadAllInts(TestCase.Properties.Resources._16Kints);
  78. linkedTime = TimeTrialLinkedStack(a);
  79. arrayTime = TimeTrialDoublingStack(a);
  80. Console.WriteLine($"16000\t{linkedTime}\t{arrayTime}\t{linkedTime / arrayTime}");
  81.  
  82. // 32K
  83. a = ReadAllInts(TestCase.Properties.Resources._32Kints);
  84. linkedTime = TimeTrialLinkedStack(a);
  85. arrayTime = TimeTrialDoublingStack(a);
  86. Console.WriteLine($"32000\t{linkedTime}\t{arrayTime}\t{linkedTime / arrayTime}");
  87.  
  88. // 1M
  89. a = ReadAllInts(TestCase.Properties.Resources._1Mints);
  90. linkedTime = TimeTrialLinkedStack(a);
  91. arrayTime = TimeTrialDoublingStack(a);
  92. Console.WriteLine($"1000000\t{linkedTime}\t{arrayTime}\t{linkedTime / arrayTime}");
  93. }
  94. }
  95. }

1.4.44

解答

每生成一个随机数都和之前生成过的随机数相比较。

代码
  1. using System;
  2.  
  3. namespace _1._4._44
  4. {
  5. /*
  6. * 1.4.44
  7. *
  8. * 生日问题。
  9. * 编写一个程序,
  10. * 从命令行接受一个整数 N 作为参数并使用 StdRandom.uniform() 生成一系列 0 到 N-1 之间的随机整数。
  11. * 通过实验验证产生第一个重复的随机数之前生成的整数数量为 ~√(πN/2)。
  12. *
  13. */
  14. class Program
  15. {
  16. static void Main(string[] args)
  17. {
  18. Random random = new Random();
  19. int N = ;
  20. int[] a = new int[N];
  21. int dupNum = ;
  22. int times = ;
  23. for (times = ; times < ; ++times)
  24. {
  25. for (int i = ; i < N; ++i)
  26. {
  27. a[i] = random.Next(N);
  28. if (IsDuplicated(a, i))
  29. {
  30. dupNum += i;
  31. Console.WriteLine($"生成{i + 1}个数字后发生重复");
  32. break;
  33. }
  34. }
  35. }
  36. Console.WriteLine($"√(πN/2)={Math.Sqrt(Math.PI * N / 2.0)},平均生成{dupNum / times}个数字后出现重复");
  37. }
  38.  
  39. /// <summary>
  40. /// 检查是否有重复的数字出现。
  41. /// </summary>
  42. /// <param name="a">需要检查的数组。</param>
  43. /// <param name="i">当前加入数组元素的下标。</param>
  44. /// <returns>有重复则返回 true,否则返回 false。</returns>
  45. static bool IsDuplicated(int[] a, int i)
  46. {
  47. for (int j = ; j < i; ++j)
  48. {
  49. if (a[j] == a[i])
  50. {
  51. return true;
  52. }
  53. }
  54. return false;
  55. }
  56.  
  57. }
  58. }

1.4.45

解答

建立一个布尔数组,将每次随机出来的数作为下标,将相应位置的布尔值改为 true,每次随机都检查一遍这个数组是否都是 true。

代码
  1. using System;
  2.  
  3. namespace _1._4._45
  4. {
  5. /*
  6. * 1.4.45
  7. *
  8. * 优惠券收集问题。
  9. * 用和上一题相同的方式生成随机整数。
  10. * 通过实验验证生成所有可能的整数值所需生成的随机数总量为 ~NHN。
  11. * (这里的 HN 中 N 是下标)
  12. *
  13. */
  14. class Program
  15. {
  16. // HN 指的是调和级数
  17. static void Main(string[] args)
  18. {
  19. Random random = new Random();
  20. int N = ;
  21. bool[] a = new bool[N];
  22. int randomSize = ;
  23. int times = ;
  24. for (times = ; times < ; ++times)
  25. {
  26. for (int i = ; i < N; ++i)
  27. {
  28. a[i] = false;
  29. }
  30. for(int i = ; true; ++i)
  31. {
  32. int now = random.Next(N);
  33. a[now] = true;
  34. if (IsAllGenerated(a))
  35. {
  36. randomSize += i;
  37. Console.WriteLine($"生成{i}次后所有可能均出现过了");
  38. break;
  39. }
  40. }
  41. }
  42. Console.WriteLine($"\nNHN={N * HarmonicSum(N)},平均生成{randomSize / times}个数字后所有可能都出现");
  43. }
  44.  
  45. /// <summary>
  46. /// 计算 N 阶调和级数的和。
  47. /// </summary>
  48. /// <param name="N">调和级数的 N 值</param>
  49. /// <returns>N 阶调和级数的和。</returns>
  50. static double HarmonicSum(int N)
  51. {
  52. double sum = ;
  53. for (int i = ; i <= N; ++i)
  54. {
  55. sum += 1.0 / i;
  56. }
  57. return sum;
  58. }
  59.  
  60. /// <summary>
  61. /// 检查所有数字是否都生成过了。
  62. /// </summary>
  63. /// <param name="a">布尔数组。</param>
  64. /// <returns>全都生成则返回 true,否则返回 false。</returns>
  65. static bool IsAllGenerated(bool[] a)
  66. {
  67. foreach (bool i in a)
  68. {
  69. if (!i)
  70. return false;
  71. }
  72. return true;
  73. }
  74. }
  75. }

算法(第四版)C# 习题题解——1.4的更多相关文章

  1. 算法(第四版)C#题解——2.1

    算法(第四版)C#题解——2.1   写在前面 整个项目都托管在了 Github 上:https://github.com/ikesnowy/Algorithms-4th-Edition-in-Csh ...

  2. 算法第四版 在Eclipse中调用Algs4库

    首先下载Eclipse,我选择的是Eclipse IDE for Java Developers64位版本,下载下来之后解压缩到喜欢的位置然后双击Eclipse.exe启动 然后开始新建项目,File ...

  3. 算法第四版jar包下载地址

    算法第四版jar包下载地址:https://algs4.cs.princeton.edu/code/

  4. 算法第四版-文字版-下载地址-Robert Sedgewick

    下载地址:https://download.csdn.net/download/moshenglv/10777447 算法第四版,文字版,可复制,方便copy代码 目录: 第1章 基 础 ...... ...

  5. 二项分布。计算binomial(100,50,0.25)将会产生的递归调用次数(算法第四版1.1.27)

    算法第四版35页问题1.1.27,估计用一下代码计算binomial(100,50,0.25)将会产生的递归调用次数: public static double binomial(int n,int ...

  6. 算法第四版学习笔记之优先队列--Priority Queues

    软件:DrJava 参考书:算法(第四版) 章节:2.4优先队列(以下截图是算法配套视频所讲内容截图) 1:API 与初级实现 2:堆得定义 3:堆排序 4:事件驱动的仿真 优先队列最重要的操作就是删 ...

  7. 算法第四版学习笔记之快速排序 QuickSort

    软件:DrJava 参考书:算法(第四版) 章节:2.3快速排序(以下截图是算法配套视频所讲内容截图) 1:快速排序 2:

  8. C程序设计(第四版)课后习题完整版 谭浩强编著

    //复习过程中,纯手打,持续更新,觉得好就点个赞吧. 第一章:程序设计和C语言 习题 1.什么是程序?什么是程序设计? 答:程序就是一组计算机能识别和执行的指令.程序设计是指从确定任务到得到结果,写出 ...

  9. 算法第四版 coursera公开课 普林斯顿算法 ⅠⅡ部分 Robert Sedgewick主讲《Algorithms》

    这是我在网上找到的资源,下载之后上传到我的百度网盘了. 包含两部分:1:算法视频的种子 2:字幕 下载之后,请用迅雷播放器打开,因为迅雷可以直接在线搜索字幕. 如果以下链接失效,请在下边留言,我再更新 ...

  10. 相似度分析,循环读入文件(加入了HanLP,算法第四版的库)

    相似度分析的,其中的分词可以采用HanLP即可: http://www.open-open.com/lib/view/open1421978002609.htm /****************** ...

随机推荐

  1. linux开通端口允许其他机器访问

    命令开通8080端口允许其他机器对linux的访问: iptables -I INPUT -p tcp --dport 8080 -j ACCEPT

  2. jQuery中的$.getJSON、$.ajax、$.get、$.post的区别

    jQuery中的$.getJSON.$.ajax.$.get.$.post的区别 使用见Flask(python)异步(ajax)返回json格式数据 ①.$.getJSON $.getJSON()是 ...

  3. 如何让模拟的json数据接口能够正常的在手机上有效果

    1. 确保手机与PC在同一个ip网下 这里我是通过------------360随身WIFI,20块钱淘宝上卖的,外观像U盘一样的,直接插在电脑的USB上就能在PC上创建一个WiFi,手机连接上就可以 ...

  4. Python 多进程和进程池

    一,前言 进程:是程序,资源集合,进程控制块组成,是最小的资源单位 特点:就对Python而言,可以实现真正的并行效果 缺点:进程切换很容易消耗cpu资源,进程之间的通信相对线程来说比较麻烦 线程:是 ...

  5. MATLAB绘制函数图

    序言 Matlab可以根据用户给出的数据绘制相应的函数图.对于单个2D函数图,需要给出一个行向量x作为函数图上离散点集的横坐标,以及一个与x列数一样的横坐标y作为函数图上点集的纵坐标. 向量x和y的取 ...

  6. git删除和提交

    //删除git分支git branch -D BranchNamegit branch -r -D origin/BranchNamegit push origin -d BranchName//提交 ...

  7. Xamarin.Forms 未能找到路径“x:\platforms”的一部分

    https://stackoverflow.com/questions/45500269/xamarin-android-common-targets-error-could-not-find-a-p ...

  8. 关于mysql分组查询

    在mysql查询中,用到GROUP BY 根据某一字段分组之后,每组显示的结果都只有第一条,这样的结果通常不是我们想要的. GROUP_CONCAT('字段')   可以将每一组下面的这个字段所有的数 ...

  9. React-typescript-antd 常见问题

    一.The key 'Accept' is not sorted alphabetically //tslint.json { "extends": ["tslint:r ...

  10. CLR 无法从 COM 上下文 0x208f68 转换为 COM 上下文 0x2090d8,这种状态已持续 60 秒

    问题: CLR 无法从 COM 上下文 0x208f68 转换为 COM 上下文 0x2090d8,这种状态已持续 60 秒.拥有目标上下文/单元的线程很有可能执行的是非泵式等待或者在不发送 Wind ...