[LeetCode][C++] 用乾淨簡單的方式解 #8 String to Integer (atoi)

這題 Acceptance rate 是 LeetCode 前幾名最低的,只有 16% 左右,看似複雜但其實可以變成很簡單,只要我們把問題切成小塊來處理就好了。

何謂 Read In?

有沒有發現題目描述提到很多次 "Read in"?

其實每次 Read in 就是把一個 character 給吃掉的意思:


在 Parse 字串的時候有時候你會想要吃掉 character,有時候卻又不想吃掉。像是如果突然讀到一個數字字元 "4",但這時候還在決定正負號怎麼辦?

那當然就是不要吃掉這個字元,留給負責處理數字的步驟去處理。

Easy As 123

既然題目描述都跟你說 1、2、3 步要做什麼了,我們可以就寫相對應的 functions 一個專心處理一件事就好。

第一步我們先看看要不要狂吃空白字元,認不得的東西就算了丟給第二步:


這邊如果你看到我標示的 "Read" 亮框就表示每走過一次箭頭就應該要把一個字元吃掉。

第二步我們來看看正負號,決定最後到底是正是負:


第三步我們就狂吃數字字元,直到沒辦法再吃為止:


如果看了這三張投影片,我們不管用什麼程式語言應該都可以寫出 code 來了。

如果我用 C++ 寫就會寫出像這樣的 code:


class Solution {
 public:
  int myAtoi(std::string s) {
    int index = 0;
    while (runStep1(s, index /*may change*/))
      ;
    while (runStep2(s, index /*may change*/))
      ;
    while (runStep3(s, index /*may change*/))
      ;
    if (!m_positive) m_result *= -1;
    return m_result;
  }

  bool runStep1(const std::string &s, int &index) {
    if (index >= s.size()) return false;
    if (s.at(index) == ' ') {
      index++;
      return true;  // continue
    } else {
      return false;
    }
  }

  bool runStep2(const std::string &s, int &index) {
    if (index >= s.size()) return false;
    if (s.at(index) == '-') {
      m_positive = false;
      index++;
    } else if (s.at(index) == '+') {
      m_positive = true;
      index++;
    } else {
      m_positive = true;
    }
    return false;
  }

  bool runStep3(const std::string &s, int &index) {
    if (index >= s.size()) return false;
    if (s.at(index) >= '0' && s.at(index) <= '9') {
      const int digit = s.at(index) - '0';
      m_result *= 10;
      m_result += digit;
      index++;
      return true;  // continue
    } else {
      return false;
    }
  }

  bool m_positive = false;
  int m_result = 0;
};


是不是看了讓人覺得很清爽?

等等! 456 步呢?

沒錯,這題還有一個麻煩的東西我們還沒處理,就是他希望你要把數字限制在 int 能表示的範圍,不過這個借用一下 LeetCode #7 的解法就好了。

我們可以用一個 function 來 "安全地" 來算 a * 10 + x,overflow 的話就回傳 false:


  bool safeMultiplyAndAdd(const int a, const int x, int &out) {
    // Does a * 10 + x > INT_MAX ?
    // (Step 1) Does a * 10 > INT_MAX ?
    if (a > INT_MAX / 10) return false;
    // (Step 2) Does a * 10 + x > INT_MAX ?
    if (a * 10 > INT_MAX - x) return false;

    out = a * 10 + x;
    return true;
  }


因為 ax 都一定是非負的數字,所以我們可以省去很多其他一般乘法判斷 overflow/underflow 的條件式。

如果途中算到一半 overflow 的話我們不只要保護 int 不超出範圍,我們還要用一個 m_overflow member variable 像記仇一樣記住這件事,最後回傳結果的時候直接傳 INT_MAXINT_MIN:


    if (m_overflow) {
      return m_positive ? INT_MAX : INT_MIN;
    } else {
      return m_positive ? m_result : (-m_result);
    }


一般來說如果 m_resultINT_MIN 的話加上負號其實是會超過 INT_MAX 會 overflow,不過我們只會 m_result * 10 + digit 而且 m_result 一開始是 0 所以 m_result 一定不是負的,自然就不用多去保護他。

所以最後的程式碼就會長這樣:


#include <climits>

class Solution {
 public:
  int myAtoi(std::string s) {
    int index = 0;
    while (runStep1(s, index /*may change*/))
      ;
    while (runStep2(s, index /*may change*/))
      ;
    while (runStep3(s, index /*may change*/))
      ;
    if (m_overflow) {
      return m_positive ? INT_MAX : INT_MIN;
    } else {
      return m_positive ? m_result : (-m_result);
    }
  }

  bool runStep1(const std::string &s, int &index) {
    if (index >= s.size()) return false;
    if (s.at(index) == ' ') {
      index++;
      return true;  // continue
    } else {
      return false;
    }
  }

  bool runStep2(const std::string &s, int &index) {
    if (index >= s.size()) return false;
    if (s.at(index) == '-') {
      m_positive = false;
      index++;
    } else if (s.at(index) == '+') {
      m_positive = true;
      index++;
    } else {
      m_positive = true;
    }
    return false;
  }

  bool runStep3(const std::string &s, int &index) {
    if (index >= s.size()) return false;
    if (s.at(index) >= '0' && s.at(index) <= '9') {
      const int digit = s.at(index) - '0';
      if (!safeMultiplyAndAdd(m_result, digit, m_result /*out*/))
        m_overflow = true;
      index++;
      return true;  // continue
    } else {
      return false;
    }
  }

  bool safeMultiplyAndAdd(const int a, const int x, int &out) {
    // Does a * 10 + x > INT_MAX ?
    // (Step 1) Does a * 10 > INT_MAX ?
    if (a > INT_MAX / 10) return false;
    // (Step 2) Does a * 10 + x > INT_MAX ?
    if (a * 10 > INT_MAX - x) return false;

    out = a * 10 + x;
    return true;
  }

  bool m_positive = false;
  bool m_overflow = false;
  int m_result = 0;
};


結語

Submit 完會得到又快又省記憶體的結果:


這和上一題的原因差不多,因為我們沒有用一些 string 或 long 來判斷 overflow,也沒有用太複雜的方式來解題,所以得到這結果是滿合理的。

其實上面 Steps 1-3 如果我們把那些圖合起來會得到類似 state machine 的圖,像是有人這樣解、或是這樣子。如果我們去看 JSON 官網也會看到類似的圖,其實都是在做類似的事情。我們甚至可以用比較專業的 DFANFA 去表示,不過用一大堆專業術語可不是我的目標。

GitHub

Code 和投影片我都放在 GitHub 上了,有興趣的歡迎參考。

留言

此網誌的熱門文章

[試算表] 追蹤台股 Google Spreadsheet (未實現損益/已實現損益)

[Side Project] 互動式教學神經網路反向傳播 Interactive Computational Graph

[插件] 在 Chrome 網頁做區分大小寫的搜尋