劍指offer—醜數


題目

把只包含質因子2、3和5的數稱作醜數(Ugly Number)。例如6、8都是醜數,但14不是,因爲它包含質因子7。 習慣上我們把1當做是
第一個醜數。求按從小到大的順序的第N個醜數。

題中醜數的定義是值包含質因子2、3、5的數,求解第N個數。
拿到該題第一想法是一次羅列出1、2、3……N個醜數,最後一個數即爲要求的數,大致思路如下:

1. 初始版

比如判斷某個數是否是醜數

/**
 * 判斷某個數是否是醜數
 **/
public boolean isUgly(int number){
    while(number % 2 == 0 ){
        number = number / 2;
    }
    while(number % 3 == 0 ){
        number = number / 3;
    }
    while(number % 5 == 0 ){
        number = number / 5;
    }
    return number == 1 ? true : false;
}

然後一個一個數判斷:

public class Sword031GetUglyNumber {
    public int GetUglyNumber_Solution(int index) {
        int total = 0;
        int number = 1;
        boolean result = false;
        while(total <= index){
            result = isUgly(number);
            if(result){
                total++;
            }
            number++;
        }
        return number;
    }
}

該方法優點是比較簡單直接,依次判斷每個數,做累加,但是時間複雜度較高,相當於把每個數都需要遍歷一次。初步估計時間複雜度是O(N2)O(N^2),而且實際運行也確實會超時。

2. 改進版

上一個初始版,最費勁的是每次到下一個數都需要判斷一下是否是醜數,之前計算的數據完全沒有用上,但是實際上某個醜數肯定是另一個醜數乘以2(或者乘以3、或者乘以5)。
第二點:題目中要求的是求解第N個醜數,如果求解某個醜數的時候想要利用上之前的醜數,需要將之前的醜數存儲起來,並且按照順序存儲,如果是無序,怎麼知道在求解某個數是否是醜數時候應該乘誰呢?
第三點:如果求解下一個醜數呢,比如目前有三個醜數 {1, 2 , 3},按照上一步思想,下一個醜數,肯定需要乘以2或者3或者5。

  1. 如果乘以2,則 待選序列變成 {2,4, 6 }
  2. 如果乘以3,則待選序列 變成 {3,6, 9 }
  3. 如果乘以5,則待選序列變成 {5,10,15}

理論上需要從這麼多數中選擇最小的數,如果直接這麼比較,篩選出來的數爲2,但是2已經在原來排好序的醜數數組中,肯定會重複的。
因此需要繼續考慮將已在醜數數組中的數排除掉,最好是比現在最大的醜數(不妨定義爲:uglyNow)大那麼一點點。。
找出第k個醜數,使得(k-1)個醜數比uglyNow小,但是第k個醜數比uglyNow大。(這一步是關鍵,理論需要一個簡單的while循環篩選)

方法總結:根據以上的思想,現在需要考慮幾點

  • 必須建一個數組
  • 必須尋找下一個剛好等於現在最大丑數的下標
  • 從第二步中尋找最小的醜數。



基於以上分析寫出如下代碼:

public int GetUglyNumber_Solution(int index) {
    
    // 快速返回,俗稱防禦性編程
    if(index <= 0){
        return 0;
    }
    
    // 保存現在的醜數,有序的
    int[] num = new int[index];
    
    // 根據現有的定義設置第一個醜數
    num[0] = 1;
    int nextIndex = 1;
    
    // 分別用三個數值表示需要待乘數下標
    int index2 = 0;
    int index3 = 0;
    int index5 = 0;
    while(nextIndex < index){
        int min = min(num[index2]*2,num[index3]*3,num[index5]*5);
        num[nextIndex] = min;
        //  接下來需要尋找下一個x 2|3|5的下標
        while(num[index2]*2 <= num[nextIndex] ){
            ++index2;
        }
        while(num[index3]*3 <= num[nextIndex] ){
            ++index3;
        }
        while(num[index5]*5 <= num[nextIndex] ){
            ++index5;
        }
        ++nextIndex;
    }
    int ugly = num[nextIndex - 1];
    return ugly;
}


public int min(int a,int b, int c){
    int min = Math.min(a,b);
    return Math.min(min,c);
}

根據以上的分析,對於該方法是不是似曾相識呢,建一個空間,尋找下一個醜數(遞推公式)。

根據以上兩個特點,感覺和動態規劃很相似吧,所以嘗試利用動態規劃解題。

public int GetUglyNumber_Solution(int index) {
    if(index <= 0){
        return 0;
    }
    int[] nums = new int[index];
    nums[0] = 1;
    int index2 = 0;
    int index3 = 0;
    int index5 = 0;
    for(int i = 1;i < index;i++){
        nums[i] = min(nums[index2]*2,nums[index3]*3,nums[index5]*5);
        // 看代碼的時候,可以思考一下爲什麼這裏可以利用if判斷?
        if(nums[i] == nums[index2]*2){
            index2++;
        }
        if(nums[i] == nums[index3]*3){
            index3++;
        }
        if(nums[i] == nums[index5]*5){
            index5++;
        }
    }
    return nums[index-1];
}

代碼中利用if判斷,而不是利用while判斷,是因爲只要是醜數,乘以(2、3、5)中的任意一個數,肯定會等於剛剛計算的新的醜數。只要> 越過該值就可以避免下一次計算的醜數小於現在醜數的最大值。

總結

到目前爲止,該題從我的角度看,已經解決了,主要是利用空間換時間,需要仔細思考其中的規律。
同樣需要從有限的數據中尋找規律。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章