8

這一題要實作atoi(ASCII string TO Integrer),這題有趣的地方在於他一開始就特意說明題目寫得很模糊是故意的,應該是想要考驗解題者考慮各種case的能力。越不一樣的題目對訓練越有用,所以這是一題很棒的練習題。

幸好我現在已經完全忘記atoi的行為,所以可以從最直觀的想法出發,一步步找出答案。首先看到atoi,最直觀會想到的是,他吃進一個String,吐出一個integer,這個關係會是一對一的嗎? 在回答這個問題之前,先停一下,我們平常在解問題的時候,都有一種衝動是,看到一個問題就想跳進去找出答案,但今天想做個新的嘗試,先把相關的疑惑都先列出來,然後我想驗證,是不是只要把這些疑惑都弄清楚了,自然就可以知道該怎麼寫。

  1. string -> atoi -> int 的關係會是一對一嗎? (牽涉到string最大可以多大、int最大可以多大)
  2. string 如果是空的,對應到的int會是什麼? 0嗎?
  3. 長度為1的string,有幾種可能性?是所有ASCII code會涵蓋的範圍嗎?
  4. 同樣長度的string,但ASCII code不同,會怎麼對應到不同的int? (e.g. "a" 跟 "b" 或 "a"跟"A")
  5. 不同長度的string,會怎麼對應到不同的int? (e.g. "a" 跟 "aa", "a" 跟 "ab")

不知不覺就列出了許多問題,理論上,如果我沒有漏問問題,只要回答得出來這些問題我就能實作出來。另一個問題是,如果在手邊沒有資料可以查的時候,要怎麼回答出這些問題?

先不要考慮沒有資料可以查的case,因為這些知識就是我們平常要準備好的。先單純檢驗我們自己提出問題的能力是不是已經足夠縝密。接下來就來一個個回答上面的問題:

  1. 首先,int的範圍是-2,147,483,648 ~ 2,147,483,647,講到+跟-就想到我們要怎麼決定字串是+還是-? 這時我才突然想起來,對吼,我之前在用num=atoi(s)的時候,都是已經可以直接看出s對應的數值該是多少了,我只是需要把它的型態轉換成int而已。所以這邊也順帶回答到第三個問題的答案,如果長度是1的話,就只有0~9這10種可能性而已。所以回到正負號的問題,如果s裡面有-為開頭,那就應該是負的int,反之如果有+或是沒特別寫,那就應該要是正的。

    所以最一般的case應該就是正常的輸入,例如s="-2147483648",轉換起來就很簡單,只要先處理負號,然後看剩下的數字有多長,就從個位數 + 十位數x10 + ...一路算下去得到結果,不過還要處理溢位問題(就假設超過最大值,應該要回傳最大值;低於最小值,回傳最小值)。

  2. string如果是NULL,直觀上想起來對應到的int也應該是NULL。(查了一下應該要回傳的值,在這個case應該是0)

  3. 長度為1的string如果要是合理的int,就只有0~9這10種可能性。這邊倒是延伸出另一個問題,如果傳進來的s長度為1,但是是0~9以外的其他符號,那要怎麼處理?

至於4 & 5這兩題在理解atoi的行為之後,就沒有回答的必要了XD

整合一下目前有的線索,目前我的myAtoi想要cover的input有

  1. s=NULL, 回傳0
  2. s="-2147483648"~"+2147483647",回傳算出來的int
  3. s="-2147483649",回傳-2147483648;s="+2147483648",回傳2147483647

不過如果只是這樣,這題應該就不用出了吧,所以接下來要再考慮到其他例外狀況。例外狀況的本質就是,會有其他的符號出現在某些地方,就像一開始我不知道atoi會有什麼行為的時候,會想要考慮所有ASCII支援符號要怎麼轉成int的case。這些符號能夠介入的地方也只有三種,正常的s的前面、中間跟後面。

舉例來說,s="-11",那可能會有幾種例外情況,順手把我覺得這幾種情況應該怎麼處理寫下來:

  1. s="#-11" => 回傳-11,因為是一路往下trace到負號才開始處理(如果真的在面試的時候碰到,還是要跟面試官確認一下)
  2. s="-1#1" => 回傳-1,因為讀到#就表示目前的數字已經中斷
  3. s="-11#" => 回傳-11,因為讀到#就表示目前的數字已經中斷

我的想像是,不屬於正常數字該有的符號,如果是在最前面讀到,那可以先忽略,因為後面還可能有可以解讀的正常數字;而如果是已經開始讀到正常數字,但是接下來卻讀到奇怪的符號,那就應該停止,回傳目前得到的數字。(Again,這種有自己假設的東西,在面試的時候應該都要跟面試官確認,這邊只需要表達出自己有提出假設的能力即可,要怎麼定義atoi的行為只是知識,詢問無妨)

想像完之後當然還是要查一下,這些答案在C++ atoi的頁面都可以查到,但我們現在是在練習,所以不要去看詳細的描述,只看自己需要的部分就好,之後再來驗證自己是不是還有少問什麼問題。

所以到目前為止,我的myAtoi好像變得比較厲害一點,他可以吃更多種string進來而不會拉肚子:

  1. s=NULL, 回傳0
  2. s="-2147483648"~"+2147483647",回傳算出來的int
  3. s="-2147483649",回傳-2147483648;s="+2147483648",回傳2147483647
  4. s="$@% 456",回傳456
  5. s="-789^%&&",回傳-789
  6. s="-7^%&&89",回傳-7

到這邊為止,應該已經考慮完畢,因為是從最general的想像開始收斂,考慮到各種符號的變化狀況,也考慮到各種符號的排列狀況,接下來要把這些性質(descriptive spec)化成程式的行為(operational spec)。

  1. 如果S=NULL或長度是0,回傳0
  2. 首先假設int是正的,從string最前面一個個往後看,直到看到正負號或數字(第一個數字不能為0)才開始計算要回傳的int。如果都沒有碰到數字,也是回傳0
  3. 開始計算int之後,一個個往後看,每次往後看就把目前計算到的int乘上10,再加上最新一位數字,然後就順便檢查會不會溢位
  4. 碰到字串結束或是非0~9的符號就停止計算,並回傳答案

有了operational spec之後,實作出來的code如下:

class Solution {
public:
    int myAtoi(string str) {
        if(str.length() == 0) 
            return 0;

        int sign = 1;
        int ans = 0, digit;
        bool flag=false;

        for(int i=0; i<str.length(); i++)
        {
            if(flag == false)
            {
                if(str.at(i) == '-' || str.at(i) == '+')
                {
                    sign = (str.at(i) == '+' ? 1 : -1);
                    flag = true;
                }
                else if(str.at(i) > '0' && str.at(i) <='9')
                {
                    ans = str.at(i)-'0';
                    flag = true;
                }
                else
                    continue;
            }
            else
            {
                if(str.at(i) >= '0' && str.at(i) <='9')
                {
                    digit = str.at(i)-'0';

                    //Check if overflow
                    if (INT_MAX/10 < ans || (INT_MAX/10 == ans && INT_MAX % 10 < digit))
                        return sign == 1 ? INT_MAX : INT_MIN;

                    ans = ans*10 + digit;
                }
                else
                    break;
            }
        }

        return sign*ans;
    }
};

不過這個程式碼會掛在s=" b127494"這種case,所以看起來是只要由奇怪符號開頭的話,就直接回傳0。看來這邊是我沒有確認清楚atoi的行為,才會有這個問題,修改之後就可以過了。

class Solution {
public:
    int myAtoi(string str) {
        if(str.length() == 0) 
            return 0;

        int sign = 1;
        int ans = 0, digit;
        bool flag=false;

        for(int i=0; i<str.length(); i++)
        {
            if(flag == false)
            {
                if(str.at(i) == '-' || str.at(i) == '+')
                {
                    sign = (str.at(i) == '+' ? 1 : -1);
                    flag = true;
                }
                else if(str.at(i) > '0' && str.at(i) <='9')
                {
                    ans = str.at(i)-'0';
                    flag = true;
                }
                else if(str.at(i) == ' ' || str.at(i) == '0')
                    continue;
                else
                    break;
            }
            else
            {
                if(str.at(i) >= '0' && str.at(i) <='9')
                {
                    digit = str.at(i)-'0';

                    //Check if overflow
                    if (INT_MAX/10 < ans || (INT_MAX/10 == ans && INT_MAX % 10 < digit))
                        return sign == 1 ? INT_MAX : INT_MIN;

                    ans = ans*10 + digit;
                }
                else
                    break;
            }
        }

        return sign*ans;
    }
};

經過這次的實驗驗證,發現只要前期想得夠透徹,有可能可以把case都想完整。只是這個過程不夠精煉,花了很多時間,而且想出這些case之後,對應到程式碼的功力也還有待提升(例如處理溢位的地方就卡了一陣子),看來目前還沒有把這題學到完全懂的能力,之後值得回來再訪。

results matching ""

    No results matching ""