1. 程式人生 > >從這道字串處理的難題,尋找解決複雜問題的套路

從這道字串處理的難題,尋找解決複雜問題的套路

本文始發於個人公眾號:**TechFlow**,原創不易,求個關注

今天是LeetCode專題的第39篇文章,我們一起來看下LeetCode第68題 Text Justification。

這題官方給的難度是Hard,通過率不到1/3。並且624贊同,1505反對。光看這個資料,可能會覺得這題很難,或者是藏著什麼坑點,但其實做下來之後發現並不是這樣的。題目只能算是稍稍複雜,並不算棘手,唯一的可能大概是大家比較畏懼字串處理的問題吧。

題意

題目會給定一系列單詞和一個每行的最長長度maxWidth,要求我們根據這個長度,將這些單詞重新整理,整理得儘可能整齊。

這裡整齊的定義有這麼幾條,首先,重新整理之後的文字的每一行的長度都是固定的,就是maxWidth。為了達成這點,題目保證單詞當中不會出現長度超過這個限制的單詞。

另外,要求用盡量少的行數來存放這些單詞。也就是說每一行要儘可能存放盡可能多的單詞,並且單詞之間的順序不能改變,也就是要按照題目給定的順序來擺放這些單詞。每一行對於單詞的數量沒有限制,可以是一個,也可以是多個。如果一行當中的單詞數量超過1,那麼需要在單詞之間擺放空格。要求單詞之間的空格儘可能均勻,如果不可能保證每個空隙的空格數量完全相等,那麼要保證前面的空格數量大於後面。

文字的最後一行要求進行左對齊,也就是說單詞全部靠左擺放,單詞之間只有一個空格。剩餘的空格全部擺放在行末。

我這樣說起來感覺很麻煩的樣子,但實際上很簡單,我們看個樣例就明白了。

輸入:
words = ["What","must","be","acknowledgment","shall","be"]
maxWidth = 16
輸出:
[
  "What   must   be",
  "acknowledgment  ",
  "shall be        "
]
解釋: 注意最後一行的格式應為 "shall be    " 而不是 "shall     be",
     因為最後一行應為左對齊,而不是左右兩端對齊。       
     第二行同樣為左對齊,這是因為這行只包含一個單詞。

在上面這個例子當中,我們可以看到輸入的單詞被分成了三行,每行16單位的長度。單詞之間被填充了空格,單個成行以及最後一行按照左對齊的方式擺放,也就是所有的空格都在右側。

解法

這題的解法很明顯了,就是題目的意思本身。也就是說這也是一道模擬題,那麼和之前講過的其他模擬題一樣,它的特點就是題目簡單,但是用程式碼做起來麻煩。我們想一下也很容易發現,首先,我們要把單詞切分,找出哪幾個單詞在一行。接著這些單詞的擺放又有講究,單個的單詞和多個單詞的擺放方式不一樣,並且還要判斷是不是最後一行,因為最後一行的擺放方式也不一樣。

這些問題解決了之後又面臨空格的問題,我們需要合理地安排空格,使得單詞擺放儘量均勻。要做到空格儘量均勻,需要先計算究竟有多少個空格又有多少個間隙。然後算出來每個間隙安排多少個空格,但是由於空格的數量並不一定能均分,所以還需要保證前面的間隙空格比後面的多一個。所以,我們又需要計算餘數,算出究竟有多少個間隙空格多一個……

很顯然,這個解法非常麻煩,但是一時之間好像也沒有特別好的方法。如果是新手來做的話,可能會有頭大如鬥、心亂如麻的感覺,或者是一鼓作氣寫了很多程式碼,然後執行之後發現情況完全不對,心態崩潰。

所以在我們開始正式寫程式碼之前,先和大家聊點心得。

之前讀過這麼一則小故事,說是有一個得道高僧,修行多年終於成道。之後記著去採訪他,問他大師你得道了之後,生活有什麼變化嗎?

高僧說,沒有,每天還是挑水、砍柴、做飯。

記者又問,那你成道之前的生活難道不也是這樣嗎?

高僧搖頭,不一樣,成道之前,挑水的時候想看砍柴,砍柴的時候想著做飯,做飯的時候又想著挑水。成道之後,挑水就是挑水,做飯就是做飯。

故事雖然是假的,但是道理是真的。我們每天這麼多事情要做,我經常發現自己有時候一件事情剛開始做或者是剛做到一半,心裡冒出其他的事情來。有的時候意志力薄弱,就被轉移了注意力,去做別的事了。然後別的事情做到一半又跳回到當前的事情上來。不僅做事如此,解題的時候也是如此,有時候眼前的問題明明沒有解決,滿腦子裝的都是以後的問題。顯然,這樣效率很低。

我們雖然成不了得道高僧,但也可以從故事當中得到一點啟發。在列計劃的時候統籌全域性,高瞻遠矚。而在執行的時候就認準腳下,之後的問題之後再想。

我們把這個思路套用到解題上來,這題雖然細節很多,但是我們大體上劃分一下無非也就兩個主要的流程。第一個流程是切分,也就是單詞的切分,哪些單詞成為一行。第二個流程是填充,也就是在單詞之間填充上合適的空格數量,使其符合題意。

我們怎麼判斷這一行究竟要包含幾個單詞?當然是根據單詞的長度來判斷,所以我們需要維護單詞的總長度,還需要一個list儲存當前候選成為一行的候選詞。

什麼情況下可以新增新的單詞?顯然,只有新的單詞的長度加入不會超過限制的時候才可以。否則就要把這個單詞放到下一行去。

所以我們根據這個思路,可以寫出切分的程式碼:

curLen, curWords = 0, []

for w in words:
  # 由於要保證單詞之間至少有一個空格,所以還需要加上候選詞的數量
  if curLen + len(w) + len(curWords) <= maxWidth:
    curLen += len(w)
    curWords.append(w)
    else:
   # TODO: 在curWords當中填充空格
      # 把單詞w放入下一行中
      curLen, curWords = len(w), [w]
      
      
# 最後一行單獨處理
if len(curWords) > 0:
  # TODO 填充最後一行

這樣我們切分的邏輯就寫好了,很明顯它和填充沒有任何關聯。作為一個獨立的演算法元件,我們可以單獨測試這個部分,保證它的正確性。這樣也方便之後我們寫完所有程式碼進行整體的debug。

填充的邏輯看起來麻煩一些,但是仔細列舉一下也還好,所有的變數都是可以確定的。首先需要填充的空格數量是確定的,它是maxWidth減去目前選出的單詞總長。填充的空隙數量也是確定的,就是單詞的數量-1。所以我們用空格數量除以空隙數量就得到了每個空隙分到的空格數。我們用空格的數量對空隙數取模,得到的就是分到空格多一個的空隙的數量。

def process(self, curLen, curWords, maxWidth):
  # 空格數量
  num_space = maxWidth - curLen
  # 如果只有一個單詞就沒必要考慮分配,直接填充空格即可
  if len(curWords) == 1:
     return curWords[0] + ' ' * (maxWidth - curLen)
  # 每個空隙分到的空格數量
  num_sep = num_space // (len(curWords) - 1)
  # 分到空格數量多一個的空隙
  head_sep = num_space % (len(curWords) - 1)
  cur = ''
  # 分配
  for i in range(len(curWords) - 1):
      cur = cur + curWords[i] + (' ' * (num_sep + 1) if i < head_sep else ' ' * num_sep)
  # 分配結束之後把最後一個單詞連上
  cur = cur + curWords[-1]
  return cur

我們把這兩個元件串聯在一起就得到了最終的結果:

class Solution:
    
    def process(self, curLen, curWords, maxWidth):
        num_space = maxWidth - curLen
        if len(curWords) == 1:
            return curWords[0] + ' ' * (maxWidth - curLen)
        num_sep = num_space // (len(curWords) - 1)
        head_sep = num_space % (len(curWords) - 1)
        cur = ''
        for i in range(len(curWords) - 1):
            cur = cur + curWords[i] + (' ' * (num_sep + 1) if i < head_sep else ' ' * num_sep)
        cur = cur + curWords[-1]
        return cur
        
    def fullJustify(self, words: List[str], maxWidth: int) -> List[str]:
        ret = []
        curLen, curWords = 0, []
        
        for w in words:
            if curLen + len(w) + len(curWords) <= maxWidth:
                curLen += len(w)
                curWords.append(w)
            else:
                ret.append(self.process(curLen, curWords, maxWidth))
                curLen, curWords = len(w), [w]
                
        # 單獨處理最後一行
        if len(curWords) > 0:
            cur = ''
            for i in range(len(curWords) - 1):
                cur = cur + curWords[i] + ' '
            cur = cur + curWords[-1]
            cur += ' ' * (maxWidth - len(cur))
            ret.append(cur)
        return ret

總結

到這裡,我們這道題就算是解決了。看起來非常複雜的問題,解決之後其實也不過只有三十多行而已。不知道有沒有比你想的要簡單呢?

有沒有發現,我們把事情切分之後也非常符合程式設計的慣例?其實程式碼當中的函式起到的就是一個小模組的作用,而一個複雜的功能也正是這些互相之間彼此獨立的小模組組合而成的。從這點上來說,我們做事情化整為零、由繁到簡的過程和程式設計過程當中模組設計的道理很多是相通的,所謂大道至簡,也許就是這個道理吧。

希望大家喜歡今天的問題,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

![](https://user-gold-cdn.xitu.io/2020/5/26/1724e9d40c86e001?w=258&h=258&f=png&