一、題目:丑數
題目:我們把只包含因子2、3和5的數稱作丑數(Ugly Number)。求按從小到大的順序的第1500個丑數。例如6、8都是丑數,但14不是,因為它包含因子7。習慣上我們把1當做第一個丑數。
二、兩種解決方案
2.1 一一遍歷法:時間效率低下
使用遍歷法求第k個丑數,從1開始遍歷,如果是丑數則count++,直到count=k為止。那么如何判斷丑數呢?根據丑數的定義,丑數只有2,3,5這三個因子,那么我們就拿數字除以這三個因子。具體算法如下:
Step1.如果一個數能夠被2整除,那么讓他繼續除以2;
Step2.如果一個數能夠被3整除,那么讓他繼續除以3;
Step3.如果一個數能夠被5整除,那么讓他繼續除以5;
Step4.如果最后這個數變為1,那么這個數就是丑數,否則不是。
根據以上算法實現代碼如下:
public int GetUglyNumber(int index) { if (index <= 0) { return 0; } int number = 0; int uglyCount = 0; while (uglyCount < index) { number++; if (IsUgly(number)) { uglyCount++; } } return number; } private bool IsUgly(int number) { while (number % 2 == 0) { number /= 2; } while (number % 3 == 0) { number /= 3; } while (number % 5 == 0) { number /= 5; } return number == 1 ? true : false; }
該算法非常直觀,代碼也非常簡潔,但最大的問題就在於每個整數都需要計算。即使一個數字不是丑數,我們還是需要對它做求余數和除法操作。因此該算法的時間效率不是很高,
2.2 空間換時間法:時間效率較高
根據丑數的定義,我們可以知道丑數可以由另外一個丑數乘以2,3或者5得到。因此我們可以創建一個數組,里面的數字是排好序的丑數,每一個丑數都是前面的丑數乘以2,3或者5得到的。
我們把得到的第一個丑數乘以2以后得到的大於M的結果記為M2。同樣,我們把已有的每一個丑數乘以3和5,能得到第一個大於M的結果M3和M5。那么M后面的那一個丑數應該是M2,M3和M5當中的最小值:Min(M2,M3,M5)。比如將丑數數組中的數字按從小到大乘以2,直到得到第一個大於M的數為止,那么應該是2*2=4<M,3*2=6>M,所以M2=6。同理,M3=6,M5=10。所以下一個丑數應該是6。
根據以上思路實現代碼如下:
public int GetUglyNumber(int index) { if (index <= 0) { return 0; } int[] uglyNumbers = new int[index]; uglyNumbers[0] = 1; int nextUglyIndex = 1; int multiply2 = 0; int multiply3 = 0; int multiply5 = 0; int min = 0; while (nextUglyIndex < index) { min = Min(uglyNumbers[multiply2] * 2, uglyNumbers[multiply3] * 3, uglyNumbers[multiply5] * 5); uglyNumbers[nextUglyIndex] = min; while (uglyNumbers[multiply2] * 2 <= uglyNumbers[nextUglyIndex]) { multiply2++; } while (uglyNumbers[multiply3] * 3 <= uglyNumbers[nextUglyIndex]) { multiply3++; } while (uglyNumbers[multiply5] * 5 <= uglyNumbers[nextUglyIndex]) { multiply5++; } nextUglyIndex++; } int result = uglyNumbers[index - 1]; uglyNumbers = null; return result; } private int Min(int num1, int num2, int num3) { int min = num1 < num2 ? num1 : num2; min = min < num3 ? min : num3; return min; }
和第一種方案相比,第二種方案不需要在非丑數的整數上做任何計算,因此時間效率有明顯提升。但也需要指出,第二種算法由於需要保存已經生成的丑數,因此需要一個數組,從而增加了空間消耗。如果是求第1500個丑數,將創建一個能容納1500個丑數的數組,這個數組占內存6KB。
三、單元測試與性能對比
3.1 單元測試
(1)測試用例

public static void Main(string[] args) { Program p = new Program(); p.Test(1); p.Test(2); p.Test(3); p.Test(4); p.Test(5); p.Test(6); p.Test(7); p.Test(8); p.Test(9); p.Test(10); p.Test(11); p.Test(1500); p.Test(0); p.Test(1500); Console.ReadKey(); } public void Test(int index) { int result = GetUglyNumberWithSpace(index); Console.WriteLine("Test result is {0}", result); Console.WriteLine("-------------End-------------"); }
(2)測試結果
3.2 性能對比
這里我們使用兩種解決方案來求第1500個丑數,通過下面的圖片可以清楚地看到兩種方案的響應時間。(這里借助老趙的CodeTimer類來進行時間效率的監測)
(1)一一遍歷法:65秒,我等得花兒都謝了
(2)空間換時間法:4毫秒,迅雷不及掩耳
由對比可以看出,一個簡單的優化,再通過6KB的空間換取了巨大的時間效率,在實際開發中是一個值得實踐的解決思路(當然,事先得權衡一下利弊)。