1. 程式人生 > >經典演算法實現——字串(一)

經典演算法實現——字串(一)

這篇文章主要介紹字串相關的題目。

處理字串操作相關問題時,常見的做法是從字串尾部開始編輯,從後往前逆向操作。這麼做的原因是因為字串的尾部往往有足夠空間,可以直接修改而不用擔心覆蓋字串前面的資料。

摘自《程式設計師面試金典》

問題描述:

對於一個給定的源字串和一個目標字串,你應該輸出在這個源字串中匹配到的第一個索引。如果源字串中不存在目標字串,就返回-1.

例如:

源字串為“source”和目標字串為"target",就返回-1;

源字串為“abcdabcdefg”和目標字串為"bcd",就返回1;

題解

對於字串查詢問題,可使用雙重 for 迴圈解決,效率更高的則為 KMP 演算法。雙重 for 迴圈的使用較有講究,因為這裡需要考慮目標字串比源字串短的可能。對目標字串的迴圈肯定是必要的,所以可以優化的地方就在於如何訪問源字串了。簡單直觀的解法是利用源字串的長度作為 for 迴圈的截止索引,這種方法需要處理源字串中剩餘長度不足以匹配目標字串的情況,而更為高效的方案則為僅遍歷源字串中有可能和目標字串匹配的部分索引。

Python

class Solution:
    def strStr(self, source, target):
        if source is None or target is None:
            return -1

        for i in range(len(source) - len(target) + 1):
            for j in range(len(target)):
                if source[i + j] != target[j]:
                    break
            else:  # no break
                return i
        return -1

C

int strStr(char* haystack, char* needle) {
    if (haystack == NULL || needle == NULL) return -1;

    const int len_h = strlen(haystack);
    const int len_n = strlen(needle);
    for (int i = 0; i < len_h - len_n + 1; i++) {
        int j = 0;
        for (; j < len_n; j++) {
            if (haystack[i+j] != needle[j]) {
                break;
            }
        }
        if (j == len_n) return i;
    }

    return -1;
}

C++

class Solution {
public:
    int strStr(string haystack, string needle) {
        if (haystack.empty() && needle.empty()) return 0;
        if (haystack.empty()) return -1;
        if (needle.empty()) return 0;
        // in case of overflow for negative
        if (haystack.size() < needle.size()) return -1;

        for (int i = 0; i < haystack.size() - needle.size() + 1; i++) {
            string::size_type j = 0;
            for (; j < needle.size(); j++) {
                if (haystack[i + j] != needle[j]) break;
            }
            if (j == needle.size()) return i;
        }
        return -1;
    }
};

Java

public class Solution {
    public int strStr(String haystack, String needle) {
        if (haystack == null && needle == null) return 0;
        if (haystack == null) return -1;
        if (needle == null) return 0;

        for (int i = 0; i < haystack.length() - needle.length() + 1; i++) {
            int j = 0;
            for (; j < needle.length(); j++) {
                if (haystack.charAt(i+j) != needle.charAt(j)) break;
            }
            if (j == needle.length()) return i;
        }

        return -1;
    }
}

原始碼分析

  1. 邊界檢查:haystack(source)needle(target)有可能是空串。
  2. 邊界檢查之下標溢位:注意變數i的迴圈判斷條件,如果用的是i < source.length()則在後面的source.charAt(i + j)時有可能溢位。
  1. 程式碼風格:
  2. 運算子==兩邊應加空格
  • 變數名不要起s1``s2這類,要有意義,如target``source
  • Java 程式碼的大括號一般在同一行右邊,C++ 程式碼的大括號一般另起一行
  • int i, j;`宣告前有一行空格,是好的程式碼風格
  1. 是否在for的條件中宣告i,j,這個視情況而定,如果需要在迴圈外再使用時,則須在外部初始化,否則沒有這個必要。
  2. 需要注意的是有些題目要求並不是返回索引,而是返回字串,此時還需要呼叫相應語言的substring方法。Python3 中用range替換了xrange,Python2 中使用xrange效率略高一些。 另外需要注意的是 Python 程式碼中的else接的是for 而不是if, 其含義為no break, 屬於比較 Pythonic 的用法。

複雜度分析

雙重 for 迴圈,時間複雜度最壞情況下為 O((nm)m)O((n-m)*m)O((nm)m).

題解1-hashmap 統計字頻

判斷兩個字串是否互為變位詞,若區分大小寫,考慮空白字元時,直接來理解可以認為兩個字串的擁有各不同字元的數量相同。對於比較字元數量的問題常用的方法為遍歷兩個字串,統計其中各字元出現的頻次,若不等則返回false. 有很多簡單字串類面試題都是此題的變形題。

Python

class Solution:
    """
    @param s: The first string
    @param b: The second string
    @return true or false
    """
    def anagram(self, s, t):
        return collections.Counter(s) == collections.Counter(t)
C++
class Solution {
public:
    /**
     * @param s: The first string
     * @param b: The second string
     * @return true or false
     */
    bool anagram(string s, string t) {
        if (s.empty() || t.empty()) {
            return false;
        }
        if (s.size() != t.size()) {
            return false;
        }

        int letterCount[256] = {0};

        for (int i = 0; i != s.size(); ++i) {
            ++letterCount[s[i]];
            --letterCount[t[i]];
        }
        for (int i = 0; i != t.size(); ++i) {
            if (letterCount[t[i]] != 0) {
                return false;
            }
        }

        return true;
    }
};
原始碼分析
  1. 兩個字串長度不等時必不可能為變位詞(需要注意題目條件靈活處理)。
  2. 初始化含有256個字元的計數器陣列。
  3. 對字串 s 自增,字串 t 遞減,再次遍歷判斷letterCount陣列的值,小於0時返回false.
在字串長度較長(大於所有可能的字元數)時,還可對第二個for迴圈做進一步優化,即t.size() > 256時,使用256替代t.size(), 使用i替代t[i]. 複雜度分析 兩次遍歷字串,時間複雜度最壞情況下為 O(n)O(n)O(n), 使用了額外的陣列,空間複雜度 O(1)O(1)O(1). 題解2-排序字串
另一直接的解法是對字串先排序,若排序後的字串內容相同,則其互為變位詞。題解1中使用 hashmap 的方法對於比較兩個字串是否互為變位詞十分有效,但是在比較多個字串時,使用 hashmap 的方法複雜度則較高。 Python
class Solution:
    """
    @param s: The first string
    @param b: The second string
    @return true or false
    """
    def anagram(self, s, t):
        return sorted(s) == sorted(t)

C++
class Solution {
public:
    /**
     * @param s: The first string
     * @param b: The second string
     * @return true or false
     */
    bool anagram(string s, string t) {
        if (s.empty() || t.empty()) {
            return false;
        }
        if (s.size() != t.size()) {
            return false;
        }

        sort(s.begin(), s.end());
        sort(t.begin(), t.end());

        if (s == t) {
            return true;
        } else {
            return false;
        }
    }
};
原始碼分析
對字串 s 和 t 分別排序,而後比較是否含相同內容。對字串排序時可以採用先統計字頻再組裝成排序後的字串,效率更高一點。 複雜度
C++的 STL 中 sort 的時間複雜度介於
O(n)O(n)O(n)O(n2)O(n^2)O(n2)之間,判斷s == t時間複雜度最壞為 O(n)O(n)O(n).