1. 程式人生 > >[算法總結] 13 道題搞定 BAT 面試——字符串

[算法總結] 13 道題搞定 BAT 面試——字符串

解析 相同字符 反轉 輸出 基本 設置 search 元素 就是

1. KMP 算法

談到字符串問題,不得不提的就是 KMP 算法,它是用來解決字符串查找的問題,可以在一個字符串(S)中查找一個子串(W)出現的位置。KMP 算法把字符匹配的時間復雜度縮小到 O(m+n) ,而空間復雜度也只有O(m)。因為“暴力搜索”的方法會反復回溯主串,導致效率低下,而KMP算法可以利用已經部分匹配這個有效信息,保持主串上的指針不回溯,通過修改子串的指針,讓模式串盡量地移動到有效的位置。

具體算法細節請參考:

  • 字符串匹配的KMP算法
  • 從頭到尾徹底理解KMP
  • 如何更好的理解和掌握 KMP 算法?
  • KMP 算法詳細解析
  • 圖解 KMP 算法
  • 汪都能聽懂的KMP字符串匹配算法【雙語字幕】
  • KMP字符串匹配算法1

1.1 BM 算法

BM算法也是一種精確字符串匹配算法,它采用從右向左比較的方法,同時應用到了兩種啟發式規則,即壞字符規則 和好後綴規則 ,來決定向右跳躍的距離。基本思路就是從右往左進行字符匹配,遇到不匹配的字符後從壞字符表和好後綴表找一個最大的右移值,將模式串右移繼續匹配。
字符串匹配的KMP算法

2. 替換空格

劍指offer:替換空格
請實現一個函數,將一個字符串中的每個空格替換成“%20”。例如,當字符串為We Are Happy.則經過替換之後的字符串為We%20Are%20Happy。

public class Solution {
    public String replaceSpace(StringBuffer str) {
        StringBuffer res = new StringBuffer();
        int len = str.length() - 1;
        for(int i = len; i >= 0; i--){
            if(str.charAt(i) == ‘ ‘)
                res.append("02%");
            else
                res.append(str.charAt(i));
        }
        return res.reverse().toString();
    }
}

3. 最長公共前綴

Leetcode: 最長公共前綴
編寫一個函數來查找字符串數組中的最長公共前綴。如果不存在公共前綴,返回空字符串 ""。

技術分享圖片
首先對字符串數組進行排序,然後拿數組中的第一個和最後一個字符串進行比較,從第 0 位開始,如果相同,把它加入 res 中,不同則退出。最後返回 res

class Solution {
    public String longestCommonPrefix(String[] strs) {
        if(strs == null || strs.length == 0)
            return "";
        Arrays.sort(strs);
        char [] first = strs[0].toCharArray();
        char [] last = strs[strs.length - 1].toCharArray();
        StringBuffer res = new StringBuffer();
        int len = first.length < last.length ? first.length : last.length;
        int i = 0;
        while(i < len){
            if(first[i] == last[i]){
                res.append(first[i]);
                i++;
            }
            else
                break;
        }
        return res.toString();
    }
}

4. 最長回文串

LeetCode: 最長回文串
給定一個包含大寫字母和小寫字母的字符串,找到通過這些字母構造成的最長的回文串。在構造過程中,請註意區分大小寫。比如 "Aa" 不能當做一個回文字符串。

技術分享圖片
統計字母出現的次數即可,雙數才能構成回文。因為允許中間一個數單獨出現,比如“abcba”,所以如果最後有字母落單,總長度可以加 1。

class Solution {
    public int longestPalindrome(String s) {
        HashSet<Character> hs = new HashSet<>();
        int len = s.length();
        int count = 0;
        if(len == 0)
            return 0;
        for(int i = 0; i<len; i++){
            if(hs.contains(s.charAt(i))){
                hs.remove(s.charAt(i));
                count++;
            }else{
                hs.add(s.charAt(i));
            }
        }
        return hs.isEmpty() ? count * 2 : count * 2 + 1;
    }
}

4.1 驗證回文串

Leetcode: 驗證回文串
給定一個字符串,驗證它是否是回文串,只考慮字母和數字字符,可以忽略字母的大小寫。
說明:本題中,我們將空字符串定義為有效的回文串。

兩個指針比較頭尾。要註意只考慮字母和數字字符,可以忽略字母的大小寫。
技術分享圖片

class Solution {
    public boolean isPalindrome(String s) {
        if(s.length() == 0)
             return true;
        int l = 0, r = s.length() - 1;
        while(l < r){
            if(!Character.isLetterOrDigit(s.charAt(l))){
                l++;
            }else if(!Character.isLetterOrDigit(s.charAt(r))){
                r--;
            }else{
                if(Character.toLowerCase(s.charAt(l)) != Character.toLowerCase(s.charAt(r)))
                    return false;
                l++;
                r--;
            } 
        }
        return true;
    }
}

4.2 最長回文子串

LeetCode: 最長回文子串
給定一個字符串 s,找到 s 中最長的回文子串。你可以假設 s 的最大長度為1000。

技術分享圖片
以某個元素為中心,分別計算偶數長度的回文最大長度和奇數長度的回文最大長度。

class Solution {
    private int index, len;
    public String longestPalindrome(String s) {
        if(s.length() < 2)
            return s;
        for(int i = 0; i < s.length()-1; i++){
            PalindromeHelper(s, i, i);
            PalindromeHelper(s, i, i+1);
        }
        return s.substring(index, index+len);
    }
    public void PalindromeHelper(String s, int l, int r){
        while(l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)){
            l--;
            r++;
        }
        if(len < r - l - 1){
            index = l + 1;
            len = r - l - 1;
        }
    }
}

4.3 最長回文子序列

LeetCode: 最長回文子序列
給定一個字符串s,找到其中最長的回文子序列。可以假設s的最大長度為1000。
最長回文子序列和上一題最長回文子串的區別是,子串是字符串中連續的一個序列,而子序列是字符串中保持相對位置的字符序列,例如,"bbbb"可以使字符串"bbbab"的子序列但不是子串。

技術分享圖片
動態規劃:
dp[i][j] = dp[i+1][j-1] + 2 if s.charAt(i) == s.charAt(j)
otherwise, dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1])

class Solution {
    public int longestPalindromeSubseq(String s) {
        int len = s.length();
        int [][] dp = new int[len][len];
        for(int i = len - 1; i>=0; i--){
            dp[i][i] = 1;
            for(int j = i+1; j < len; j++){
                if(s.charAt(i) == s.charAt(j))
                    dp[i][j] = dp[i+1][j-1] + 2;
                else
                    dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);
            }
        }
        return dp[0][len-1];
    }
}

5. 字符串的排列

Leetcode: 字符串的排列
給定兩個字符串 s1 和 s2,寫一個函數來判斷 s2 是否包含 s1 的排列。
換句話說,第一個字符串的排列之一是第二個字符串的子串。

技術分享圖片
我們不用真的去算出s1的全排列,只要統計字符出現的次數即可。可以使用一個哈希表配上雙指針來做。

class Solution {
    public boolean checkInclusion(String s1, String s2) {
        int l1 = s1.length();
        int l2 = s2.length();
        int [] count = new int [128];
        if(l1 > l2)
            return false;
        for(int i = 0; i<l1; i++){
            count[s1.charAt(i) - ‘a‘]++;
            count[s2.charAt(i) - ‘a‘]--;
        }
        if(allZero(count))
            return true;
        for(int i = l1; i<l2; i++){
            count[s2.charAt(i) - ‘a‘]--;
            count[s2.charAt(i-l1) - ‘a‘]++;
            if(allZero(count))
                return true;
        }
        return false;
    }
    public boolean allZero(int [] count){
        int l = count.length;
        for(int i = 0; i < l; i++){
            if(count[i] != 0)
                return false;
        }
        return true;
    }
}

6. 打印字符串的全排列

劍指offer:字符串的排列
輸入一個字符串,按字典序打印出該字符串中字符的所有排列。例如輸入字符串abc,則打印出由字符a,b,c所能排列出來的所有字符串abc,acb,bac,bca,cab和cba。

把問題拆解成簡單的步驟:
第一步求所有可能出現在第一個位置的字符(即把第一個字符和後面的所有字符交換[相同字符不交換]);
第二步固定第一個字符,求後面所有字符的排列。這時候又可以把後面的所有字符拆成兩部分(第一個字符以及剩下的所有字符),依此類推。這樣,我們就可以用遞歸的方法來解決。

public class Solution {
    ArrayList<String> res = new ArrayList<String>();
    public ArrayList<String> Permutation(String str) {
        if(str == null)
            return res;
        PermutationHelper(str.toCharArray(), 0);
        Collections.sort(res);
        return res;
    }
    public void PermutationHelper(char[] str, int i){
        if(i == str.length - 1){
            res.add(String.valueOf(str));
        }else{
            for(int j = i; j < str.length; j++){
                if(j!=i && str[i] == str[j])
                    continue;
                swap(str, i, j);
                PermutationHelper(str, i+1);
                swap(str, i, j);
            }
        }
    }
    public void swap(char[] str, int i, int j) {
        char temp = str[i];
        str[i] = str[j];
        str[j] = temp;
    }
}

7. 第一個只出現一次的字符

劍指offer: 第一個只出現一次的字符
在一個字符串(0<=字符串長度<=10000,全部由字母組成)中找到第一個只出現一次的字符,並返回它的位置, 如果沒有則返回 -1.

先在hash表中統計各字母出現次數,第二次掃描直接訪問hash表獲得次數。也可以用數組代替hash表。

import java.util.HashMap;
public class Solution {
    public int FirstNotRepeatingChar(String str) {
        int len = str.length();
        if(len == 0)
            return -1;
        HashMap<Character, Integer> map = new HashMap<>();
        for(int i = 0; i < len; i++){
            if(map.containsKey(str.charAt(i))){
                int value = map.get(str.charAt(i));
                map.put(str.charAt(i), value+1);
            }else{
                map.put(str.charAt(i), 1);
            }
        }
        for(int i = 0; i < len; i++){
            if(map.get(str.charAt(i)) == 1)
                return i;
        }
        return -1;
    }
}

8. 翻轉單詞順序列

劍指offer: 翻轉單詞順序列
LeetCode: 翻轉字符串裏的單詞

借助trim()和 split()就很容易搞定

public class Solution {
    public String reverseWords(String s) {
        if(s.trim().length() == 0)
            return s.trim();
        String [] temp = s.trim().split(" +");
        String res = "";
        for(int i = temp.length - 1; i > 0; i--){
            res += temp[i] + " ";
        }
        return res + temp[0];

    }
}

9. 旋轉字符串

Leetcode: 旋轉字符串
給定兩個字符串, A 和 B。
A 的旋轉操作就是將 A 最左邊的字符移動到最右邊。 例如, 若 A = ‘abcde‘,在移動一次之後結果就是‘bcdea‘ 。如果在若幹次旋轉操作之後,A 能變成B,那麽返回True。

技術分享圖片
一行代碼搞定

class Solution {
    public boolean rotateString(String A, String B) {
        return A.length() == B.length() && (A+A).contains(B);
    }
}

9.1 左旋轉字符串

劍指offer: 左旋轉字符串
匯編語言中有一種移位指令叫做循環左移(ROL),現在有個簡單的任務,就是用字符串模擬這個指令的運算結果。對於一個給定的字符序列S,請你把其循環左移K位後的序列輸出。例如,字符序列S=”abcXYZdef”,要求輸出循環左移3位後的結果,即“XYZdefabc”。是不是很簡單?OK,搞定它!

在第 n 個字符後面將切一刀,將字符串分為兩部分,再重新並接起來即可。註意字符串長度為 0 的情況。

public class Solution {
    public String LeftRotateString(String str,int n) {
        int len = str.length();
        if(len == 0)
            return "";
        n = n % len;
        String s1 = str.substring(n, len);
        String s2 = str.substring(0, n);
        return s1+s2;
    }
}

9.2 反轉字符串

LeetCode: 反轉字符串
編寫一個函數,其作用是將輸入的字符串反轉過來。

技術分享圖片

class Solution {
    public String reverseString(String s) {
        if(s.length() < 2)
            return s;
        int l = 0, r = s.length() - 1;
        char [] strs = s.toCharArray(); 
        while(l < r){
            char temp = strs[l];
            strs[l] = strs[r];
            strs[r] = temp;
            l++;
            r--;
        }
        return new String(strs);
    }
}

10. 把字符串轉換成整數

劍指offer: 把字符串轉換成整數
將一個字符串轉換成一個整數(實現Integer.valueOf(string)的功能,但是string不符合數字要求時返回0),要求不能使用字符串轉換整數的庫函數。 數值為0或者字符串不是一個合法的數值則返回0。

public class Solution {
    public int StrToInt(String str) {
        if(str.length() == 0)
            return 0;
        int flag = 0;
        if(str.charAt(0) == ‘+‘)
            flag = 1;
        else if(str.charAt(0) == ‘-‘)
            flag = 2;
        int start = flag > 0 ? 1 : 0;
        long res = 0;
        while(start < str.length()){
            if(str.charAt(start) > ‘9‘ || str.charAt(start) < ‘0‘)
                return 0;
            res = res * 10 + (str.charAt(start) - ‘0‘);
            start ++;
        }
        return flag == 2 ? -(int)res : (int)res;
    }
}

11. 正則表達式匹配

劍指offer:正則表達式匹配
請實現一個函數用來匹配包括’.’和’*’的正則表達式。模式中的字符’.’表示任意一個字符,而’*’表示它前面的字符可以出現任意次(包含0次)。 在本題中,匹配是指字符串的所有字符匹配整個模式。例如,字符串”aaa”與模式”a.a”和”ab*ac*a”匹配,但是與”aa.a”和”ab*a”均不匹配

技術分享圖片
動態規劃:
這裏我們采用dp[i+1][j+1]代表s[0..i]匹配p[0..j]的結果,結果自然是采用布爾值True/False來表示。
首先,對邊界進行賦值,顯然dp[0][0] = true,兩個空字符串的匹配結果自然為True;
接著,我們對dp[0][j+1]進行賦值,因為 i=0 是空串,如果一個空串和一個匹配串想要匹配成功,那麽只有可能是p.charAt(j) == ‘*‘ && dp[0][j-1]
之後,就可以愉快地使用動態規劃遞推方程了。

public boolean isMatch(String s, String p) {
    if (s == null || p == null) {
        return false;
    }
    boolean[][] dp = new boolean[s.length()+1][p.length()+1];
    dp[0][0] = true;
    for (int j = 0; i < p.length(); j++) {
        if (p.charAt(j) == ‘*‘ && dp[0][j-1]) {
            dp[0][j+1] = true;
        }
    }
    for (int i = 0 ; i < s.length(); i++) {
        for (int j = 0; j < p.length(); j++) {
            if (p.charAt(j) == ‘.‘) {
                dp[i+1][j+1] = dp[i][j];
            }
            if (p.charAt(j) == s.charAt(i)) {
                dp[i+1][j+1] = dp[i][j];
            }
            if (p.charAt(j) == ‘*‘) {
                if (p.charAt(j-1) != s.charAt(i) && p.charAt(j-1) != ‘.‘) {
                    dp[i+1][j+1] = dp[i+1][j-1];
                } else {
                    dp[i+1][j+1] = (dp[i+1][j] || dp[i][j+1] || dp[i+1][j-1]);
                }
            }
        }
    }
    return dp[s.length()][p.length()];
}

12. 表示數值的字符串

劍指offer: 表示數值的字符串
請實現一個函數用來判斷字符串是否表示數值(包括整數和小數)。例如,字符串”+100″,”5e2″,”-123″,”3.1416″和”-1E-16″都表示數值。 但是”12e”,”1a3.14″,”1.2.3″,”+-5″和”12e+4.3″都不是。

設置三個標誌符分別記錄“+/-”、“e/E”和“.”是否出現過。

public class Solution {
    public boolean isNumeric(char[] str) {
        int len = str.length;
        boolean sign = false, decimal = false, hasE = false;
        for(int i = 0; i < len; i++){
            if(str[i] == ‘+‘ || str[i] == ‘-‘){
                if(!sign && i > 0 && str[i-1] != ‘e‘ && str[i-1] != ‘E‘)
                    return false;
                if(sign && str[i-1] != ‘e‘ && str[i-1] != ‘E‘)
                    return false;
                sign = true;
            }else if(str[i] == ‘e‘ || str[i] == ‘E‘){
                if(i == len - 1)
                    return false;
                if(hasE)
                    return false;
                hasE = true;
            }else if(str[i] == ‘.‘){
                if(hasE || decimal)
                    return false;
                decimal = true;
            }else if(str[i] < ‘0‘ || str[i] > ‘9‘)
                return false;
        }
        return true;
    }
}

13. 字符流中第一個不重復的字符

劍指offer: 字符流中第一個不重復的字符
請實現一個函數用來找出字符流中第一個只出現一次的字符。例如,當從字符流中只讀出前兩個字符”go”時,第一個只出現一次的字符是”g”。當從該字符流中讀出前六個字符“google”時,第一個只出現一次的字符是”l”。

用一個哈希表來存儲每個字符及其出現的次數,另外用一個字符串 s 來保存字符流中字符的順序。

import java.util.HashMap;
public class Solution {
    HashMap<Character, Integer> map = new HashMap<Character, Integer>();
    StringBuffer s = new StringBuffer();
    //Insert one char from stringstream
    public void Insert(char ch)
    {
        s.append(ch);
        if(map.containsKey(ch)){
            map.put(ch, map.get(ch)+1);
        }else{
            map.put(ch, 1);
        }
    }
  //return the first appearence once char in current stringstream
    public char FirstAppearingOnce()
    {
        for(int i = 0; i < s.length(); i++){
            if(map.get(s.charAt(i)) == 1)
                return s.charAt(i);
        }
        return ‘#‘;
    }
}

[算法總結] 13 道題搞定 BAT 面試——字符串