算法(第四版)C# 習題題解——1.4


寫在前面

整個項目都托管在了 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

解答

image

代碼分塊↑

時間分析↓

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

解答

image

 

 

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

解答

和二分查找的方式類似,先確認中間的值是否是局部最小,如果不是,則向較小的一側二分查找。

在三個數中比較得到最小值需要兩次比較,因此最壞情況下為 ~2lgN 次比較。

代碼
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

解答

image

 

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)。

rect9035

代碼

首先是 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

解答

數據量比較大時才會有比較明顯的差距。

image

代碼

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 中調用的函數稍作修改即可。

image

代碼

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 次后取平均即可。

image

代碼

修改后的 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

image

代碼
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

解答

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

解答

image

代碼

修改后的 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

解答

每生成一個隨機數都和之前生成過的隨機數相比較。

image

代碼
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。

image

代碼
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;
        }
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM