知多一點有限狀態自動機

hello~親愛的觀衆老爺們你們好~最近 LeetCode 上的算法已經刷得差很少了(剩下都是 hard,不看答案是不會作了),是時候小結一下在刷題過程當中,學到的一些有意思的知識點。相信你們對 React 都有一點了解,可能也看過相似的說法:「React 把組件當作是一個狀態機(State Machines)。經過與用戶的交互,實現不一樣狀態,而後渲染 UI,讓用戶界面和數據保持一致。」那狀態機究竟是什麼呢?javascript

本文將簡單地介紹狀態機的理念,並經過解答一道算法展現其具體的應用,但願能讓你瞭解多一點狀態機這個有趣的模型,在編程中處理複雜的狀態時,有多一個新選擇~如下是正文:前端

什麼是狀態機

有限狀態機(英語:finite-state machine,縮寫:FSM)又稱有限狀態自動機,簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動做等行爲的數學模型。java

以上摘自維基百科,看起來仍是有點抽象~簡單地說,就是有這麼一個裝置,用戶不斷把輸入塞進去,裝置告訴用戶塞進去的輸入是否合法。其實在編程過程當中,或多或少都會接觸到它,只是未察覺到而已。好比正則,就是狀態機的典型應用。在看具體例子以前,還須要瞭解一下狀態機的特徵:正則表達式

狀態總數是有限的。算法

任一時刻,只處在一種狀態之中。編程

某種條件觸發後,會從一種狀態轉變到另外一種狀態。後端

好了~狀態機的概念介紹完了,能夠關掉本文啦! 看起來比較枯燥是麼,不要緊,咱們看圖說話:優化

這是一張典型的狀態機示意圖,圓圈表示狀態機中的狀態,雙圓圈也是狀態,但它是特殊的,是最終狀態,也就是接受狀態。若根據輸入,狀態機最後的狀態停留在最終狀態,就意味着這個輸入是可被接受的、合法的。箭頭表示狀態的轉移,好比狀態機處於 S2 狀態時,往狀態機輸入 0,那麼根據箭頭,狀態轉移至 S1,輸入 0 則「轉移」到 S2, 也就是原地踏步,保持不變。注意,當輸入 0 或 1 以外的字符時,狀態機沒有任何能適配該字符的轉移,那麼此時便可認爲輸入不合法了。ui

OK~至此,狀態機基本的概念已經瞭解得差很少,下面將結合實例,看看它的實際用途。spa

狀態機的應用

上文說過,正則就是狀態機的典型應用,那麼咱們不妨來實現一個簡單的正則引擎,它能根據輸入的字符串與正則表達式,返回該正則表達式是否匹配該字符串:

給定一個字符串 (s) 和一個字符模式 (p) ,實現一個支持 '?' 和 '*' 的通配符匹配。

    '?' 能夠匹配任何單個字符。
    '*' 能夠匹配任意字符串(包括空字符串)。
    
兩個字符串徹底匹配纔算匹配成功。

說明:

    s 可能爲空,且只包含從 a-z 的小寫字母。
    p 可能爲空,且只包含從 a-z 的小寫字母,以及字符 ? 和 *。
複製代碼

這是 LeetCode 中的一道原題:44. 通配符匹配,你們不妨先暫停閱讀,試試解決這道題。解題思路其實挺多的,但既然是正則相關,那麼狀態機必定能夠解決這個問題。那就先根據示例,畫一下狀態機的示意圖找找感受:

示例 1:

    輸入:
    s = "aa"
    p = "a"
    輸出: false
    解釋: "a" 沒法匹配 "aa" 整個字符串。
複製代碼

根據正則表達式 p,能夠繪製出這個狀態機:

如上圖所示,一開始狀態機的狀態是 S0,當字符 a 輸入後,狀態機的狀態從 S0 轉移到 S1,接着再往狀態機中輸入字符 a,但在 S1 狀態上,沒有任何能夠轉移的路徑,於是該輸入不合法,返回 false

下面咱們多看一個有意思的示例:

示例 4:

    輸入:
    s = "adceb"
    p = "*a*b"
    輸出: true
    解釋: 第一個 '*' 能夠匹配空字符串, 第二個 '*' 能夠匹配字符串 "dce".
複製代碼

根據示例,能夠繪製出這個狀態機:

這個狀態機比較有趣,狀態之間的轉移,再也不是隻有一種可能,存在根據相同的輸入轉移到不一樣狀態的可能。分析一下,狀態機的起始狀態是 S0,往狀態機中輸入字符 a,因爲 * 能夠匹配任意字符串,那麼狀態機既能夠「原地踏步」,也能夠轉移到 S1。但狀態機任一時刻,只處在一種狀態之中,那該怎麼辦呢?其實能夠假設手上有無數個待啓動的狀態機,碰到分支狀況時,再啓動若干個狀態機,轉移對應的狀態。按照這個思路,此時咱們須要啓動兩個狀態機,其中一個狀態的狀態在 S0,另外一個在 S1(從 S0 轉移過去)。

以後不管是逐個輸入 dce 三個字符,兩臺狀態機均「原地踏步」,最後輸入字符 b,根據轉移條件,第一個狀態機仍然只能原地踏步,但第二個狀態機既能轉移到 S2,也能原地踏步,於是再開一個狀態機。第三臺狀態的狀態是 S3。輸入就此結束,檢查已啓動的狀態機,發現有一個狀態機的狀態是最終狀態,於是輸入合法,返回 true

因而可知,只要有一個狀態機的狀態是最終狀態,那麼輸入就是合法的(也就是匹配成功)。如今思路有了,是時候用代碼描述出來了,不妨先試試本身寫對應的程序。如下是個人實現:

/** * @param {string} s * @param {string} p * @return {boolean} */
var isMatch = function(s, p) {
  // 連續的*是沒意義的,算是簡單的優化
  p = p.replace(/\*+/g, '*');
  // 正則對應狀態機的描述
  const map = {};
  // 初始狀態
  let index = 0;
  for (const token of p) {
    map[index] = map[index] || {};
    map[index][token] = true;
    // 只要不是*,那隻要匹配,必定能轉移到下一個狀態
    if (token !== '*') index++;
  }
  // 最大的index,就是狀態機的最終狀態
  const SUCCESS = index;
  // 已啓動狀態機的集合,值爲該狀態機所在的狀態
  let set = new Set();
  set.add(0);
  for (let i = 0; i < s.length; i++) {
    const newSet = new Set();
    const token = s[i];
    for (const status of set) {
      // 若是狀態機沒有任何轉移條件,那就不必繼續下面的判斷,廢棄這個狀態機
      if (!map[status]) continue;
      // 原地踏步的狀況
      if (map[status]['*']) newSet.add(status);
      // 匹配到對應字符,能夠轉移到下一個狀態的狀況
      if (map[status]['?'] || map[status][token]) newSet.add(status + 1);
    }
    if (!newSet.size) return false;
    // 用新的狀態機集合取代舊的集合
    set = newSet;
  }
  return set.has(SUCCESS);
};
複製代碼

代碼我都加上註釋,稍微過一下總體的流程~ map 就是整個狀態機的描述。map 的鍵是對應的狀態,值是一個對象,描述轉移的路徑(也就是那些箭頭)。set 是已啓動狀態機的集合,因爲每一個狀態機其實都同樣,不一樣的是它們此刻的狀態,所以集合的值是狀態機的狀態。以後就是遍歷輸入的字符串,往狀態機中逐個輸入字符,讓狀態機發生轉移。最後判斷啓動的狀態機中是否存在最終狀態,返回結果便可~

LeetCode 中跑出來的結果以下:

小結

上述算法,是存在優化空間的,好比已啓動狀態機的複用,但爲了代碼有更好的可讀性,沒有進行相關的優化。固然,根據跑出來的結果能夠看出,算法的運行速度並非十分理想,只優於 23% 的提交,爲追求速度,用動態規劃的思路解題會更合適。但使用狀態機進行解答,思路是至關清晰的,也能加深對狀態機的理解。往後碰到複雜狀態的切換與維護,不妨考慮用狀態機進行解決。

以上就是全文的內容啦!狀態機其實至關有意思的模型,普遍應用於前端、後端乃至編譯器之中。本文僅顯淺地介紹了相關概念及其應用,詳細的內容還須要翻閱對應的書籍資料~

感謝各位看官大人看到這裏,知易行難,但願本文對你有所幫助~謝謝!

相關文章
相關標籤/搜索