【完虐演算法】「字串-最長公共字首」5種方法腦洞大開

技術gogogo發表於2021-11-29

大家好!我是Johngo!

今天不準備一個專題的模組進行分享。

最近在專題製作過程中遇到了最長字首公共子串的問題,也是讀者最近校招面試到的一個題目。為什麼拿出這個來說呢?

可怕的是,他居然給了 5 種解題方法。

更可怕的是,因此他直接少了一輪面試,天哪!!

今天順便分享出來,作為「字串」的第 5 個部分。

說在前面

言歸正傳,這一期來說說字串的第五塊內容 「字串 - 最長公共字首」問題

github:https://github.com/xiaozhutec/share_leetcode

文件地址:https://github.com/xiaozhutec/share_leetcode/tree/master/docs

整體架構:

字串 - 最長公共字首

小概念:子串的必須要連續,和子序列不同。

比如說一個字串 "flower"

子串:"flow", "ower", "low" 等等都是它的子串,子串必須要連續;

子序列:"flwer", "fler", "wer" 等等都是它的子序列,可以不連續;

但需要注意的是它們的順序需要和原字串保持一致。

另外,字首,一定是從字串的開頭進行計算的。

今天大概說的就是個這...

對,被框住的合集中,就是公共字首(LCP)!

而且這期只舉一個 LeetCode 中比較簡單的案例來說明。

思路上比較簡單!

但是!就是因為這個思路比較簡單,本期就用 5 種方式進行分析。

分別是 Python 提供的 zip 方式解決、橫向掃描、縱向掃描、分治、二分法

案例 - 14.最長公共字首【簡單】

整體關於字串「最長公共字首」方面的問題。

利用 LeetCode 的 第 14 題,最長公共字首【簡單】來舉例!

編寫一個函式來查詢字串陣列中的最長公共字首。

如果不存在公共字首,返回空字串 ""。

輸入:strs = ["flower","flow","flight"]
輸出:"fl"

方法一 Python zip輕鬆解決

熟悉的我的同學都知道,我們們刷題一直用的是 Python 進行刷題,然後也會用到不少 Python 提供的庫函式進行問題的解決。

不熟悉 zip 作用的同學不要著急,此處不說原理,10 秒鐘用一個例子說明它存在的實際意義。

zip() 函式簡單說來,就是將可迭代物件中,各個對應元素打包成一個一個的元祖。

看例子:

>>> str1 = [1,2,3]
>>> str2 = [4,5,6]
>>> str3 = [7,8,9]
>>> zip(str1, str2, str3)
[(1, 4, 7), (2, 5, 8), (3, 6, 9)]

又或者這個例子:

>>> strs = ["flower", "flow", "flight"]
>>> zip(*strs)
[('f', 'f', 'f'), ('l', 'l', 'l'), ('o', 'o', 'i'), ('w', 'w', 'g')]

*str 有解包的作用,即把字串解為一個一個的字元。

zip() 函式的大概作用明白了吧~

如果仔細看第二個例子的話,其實已經可以看出解決方式了。

將上述各個元祖進行 set 操作去重!

[('f'), ('l'), ('o', 'i'), ('w', 'g')]

繼續對各個進行長度計算操作,如果長度為 1 的,那麼,字首必然相同。

即可求出公共字首了!

圖中:最後長度為 1 的字串,就是我們們要得出來的最長公共字首了。

簡單看下程式碼:

def longestCommonPrefix1(self, strs):
    lcp = ""
    for tmp in zip(*strs):
        if len(set(tmp)) == 1:
            lcp += tmp[0]
        else:
            break
    return lcp

方法二 縱向比較

迴圈比較個字串的各個位置。

在第一次迴圈中比較每個字串的第 0 位,在第二次迴圈中比較每個字串的第 1 位,..., 以此類推,直到匹配到不是相同字元。

以下圖做一個詳細的分析:

tag 表示在比較過程中,是否相同,相同為True,不同為False;

lcp 表示最長公共字首的長度;

第一次迴圈:字元都相同,則,tag=True,lcp+1=1

第二次迴圈:字元都相同,則,tag=True,lcp+1=2

第三次迴圈:字元在第三個字元比較中出現了不同,則,tag=False,退出迴圈,得到最終答案。

lcp的值停留在了上一次迴圈中。。

這就是縱向比較的全部流程,只要遇到不匹配的就退出迴圈。

下面看下程式碼實現:

def longestCommonPrefix2(self, strs):
    s = strs[0]
    size = len(s)
    lcp = 0
    tag = False
    for index in range(size):
        # 迴圈比較每一個位置的字元是否相同
        for item in range(len(strs)):
            # 需要判斷位置 index 在 strs 中字串是否越界
            if index < len(strs[item]) and s[index] == strs[item][index]:
                tag = True
            else:
                # 當匹配不到的時候,退出該次迴圈
                tag = False
                break
        if tag is True:
            lcp += 1
        else:
            break
    return s[:lcp]

下面再看一種方法,是橫向比較,即躺著比較,也稱為“鹹魚比較法”

方法三 橫向比較(鹹魚比較法)

方法一 和 方法二的思路差不多,都是從每一個字串的每一位進行比較。

橫向比較方法,是利用每兩個字串相互比較,保留公共字首,將保留下來的公共字首和後面的字串再進行比較。

還是用這個例子進行說明:strs = ["flower","flow","flight"]

將 0 位置的字串作為哨兵,與 1 位置的字串進行比較,得到最長公共字首。

再用得到的最長公共字首再與 2 位置的字串進行比較。得到最後的結果。

以上述例子,看下圖:

第一次比較:字串"flower" 和 "flow" 進行比較,最長公共字首是 4

第二次比較:將上一步中得到的字串 “flow” 與下一個字串再做比較,得到“fl”。

至此,問題解決!

看程式碼實現:

def longestCommonPrefix3(self, strs):
    s = strs[0]
    for index in range(1, len(strs)):
        w_index, size = 0, min(len(s), len(strs[index]))
        for w_index in range(size):
            if s[w_index] != strs[index][w_index]:
                break
            w_index += 1
        s = s[0: w_index]
    return s

再下面的兩種解法是運用了分治和二分的思想。

有很多人可能很容易想到分治的思路,但是立馬想不到二分的思路進行解決。

方法四 分治思想解決【較重要】

分治思想要比前兩種明顯感覺要高階一點。。

前兩種想法設法的去比較,而當引入分治的時候,就要進行用高階的方式進行比較了。

如下如,分治體現的就是分而治之,部分決策。

將一個大問題,拆分為兩個子問題,對子問題繼續向下求解。

這個題目就非常清楚的闡明瞭分治思想的核心。

下面看下這塊的程式碼:

def longestCommonPrefix4(self, strs):
    def lcp(left, right):
        if left == right:
            return strs[left]
        mid = (left + right)//2
        # 得到左右兩個字串
        left_str, right_str = lcp(left, mid), lcp(mid+1, right)
        index, min_len = 0, min(len(left_str), len(right_str))
        while index < min_len:
            if left_str[index] != right_str[index]:
                return left_str[:index]
            index += 1
        return left_str[:index]
    return lcp(0, len(strs)-1)

這塊是一個典型的二分法的運用。

所以,要理解其中遞迴的思維邏輯,這個題目就很好的解決了。

方法五 二分思想解決【較重要】

再有一個方法呢,就是利用二分的思路進行解決。

還是用 ["flower", "flow", "flownlp", "flowcv"]來舉例子。

利用二分查詢,以第一個字串為基準,不斷跟後面字串進行比較。

初始化左右指標以及midleft=0right=len(s)-1, mid=(left+right)/2

"flower" -> left=0,right=5,mid=(left+right)//2=2 => 左:["flo"], 右:["wer"]

如果左:["flo"]在後面每個單詞[left:mid]中,說明左側子串都能夠匹配,需要右面子串進行匹配,則 left=mid+1, mid=(left+right)//2

否則,right=mid, mid=(left+right)//2

所以以上述列表中字串為例:

right=2,mid=1,左:["fl"],右:["o"]

如果左:["fl"]在後面每個單詞[left:mid]中,則 left=mid+1,mid=(left+right)//2

否則,right=mid, mid=(left+right)//2

如果看不清楚,可以看下面圖解!

第一次比較:

left=0,right=5,mid=2,發現字串左半部分"flo"與剩餘的每一個字串都匹配。

所以下面需要進行有半部分的匹配即可,即 left=mid+1!

第二次比較:

left=3,right=5,mid=4,發現子串左半部分"we"與剩餘的每一個字串都不匹配。

因此,需要縮小左半部分的範圍,右指標 right=mid。

第三次比較:

left=3,right=4,mid=3,發現子串左半部分"w"與剩餘的每一個字串都匹配。

此時,已經得到了最後的結果。退出迴圈!

這樣看下來,思路也是很清晰,下面用Python來實現一下:

def longestCommonPrefix5(self, strs):
    s = strs[0]
    lcp = ""
    left, right = 0, len(strs[0])-1
    mid = (left+right)//2
    # 只有一個字串的情況下
    if len(strs) == 1:
        return s
    while left <= right:
        tag = True
        # 輪詢判斷子串與後面每個字串對應位置的子串是否相同
        for i in range(1, len(strs)):
            if s[left:mid+1] not in strs[i][left:mid+1]:
                tag = False
                break

        # 左邊子串存在後面每個字串中
        if tag is True:
            # 將匹配到的子串加入到結果集中
            lcp += s[left:mid+1]
            left = mid+1
            mid = (left+right)//2
        # 左邊子串不存在後面每個字串中
        else:
            if right == mid: # 當 right == mid,說明right指標已經無法靠左移動了,退出迴圈
                break
            else:
                right = mid
            mid = (left+right)//2
    return lcp

以上就是就關於字串「最長公共字首」的全部分享了。

另外,方便的話也在我的github? 加顆星,它是我持續輸出最大最大的動力,感謝大家!

github:https://github.com/xiaozhutec/share_leetcode


如果感覺內容對你有些許的幫助!

點贊、在看!

評論、轉發!

下期想看哪方面的,評論區告訴我!

好了~ 我們們下期見!bye~~

相關文章