算法细节系列(33):再见字符串(1)

算法细节系列(33):再见字符串(1)

详细代码可以fork下Github上leetcode项目,不定期更新。

题目摘自leetcode:

Leetcode 008. String to Integer (atoi)

字符串的题不是难,是烦,要考虑的边界条件太多了,但拿来练练还是不错的。

判断当前String能否转成Integer

此题还不算复杂,遇到空格直接跳过,所以刚开始检测出”+”or”-“,否则返回0,当遇到”+”or”-“时,i++,检测数字就行。此题的难点在于如何解决溢出问题。

刚开始的想法在循环外解决溢出,但发现long也有可能溢出,所以它的解决是,一旦超过int的范围就返回。

解决溢出的判断条件为:

if (Integer.MAX_VALUE / 10 < val || Integer.MAX_VALUE / 10 == val && Integer.MAX_VALUE % 10 < (c[i] - '0'))
                return flag == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;

代码如下:

    public int myAtoi(String str) {
        if (str.isEmpty()) return 0;
        char[] c = str.toCharArray();
        int i = 0;
        while (i < c.length && c[i] == ' ') i++;

        int flag = 1;
        if (c[i] == '+' || c[i] == '-'){
            flag = c[i++] == '-' ? -1 : 1;
        }

        long val = 0;
        while (i < c.length && Character.isDigit(c[i])) {
            if (Integer.MAX_VALUE / 10 < val || Integer.MAX_VALUE / 10 == val && Integer.MAX_VALUE % 10 < (c[i] - '0'))
                return flag == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
            val = val * 10 + (c[i++] - '0');
        }
        val = val * flag;
        return (int)val;
    }

Leetcode 065. Valid Number

这道题有点类似模式匹配的味道,同样需要考虑很多情况。遇到情况较多的字符串匹配,可以用DFA来解决,第一次尝试用DFA来处理字符串匹配问题,很有趣,不过在了解它强大之前,先看看根据规则匹配的直观代码:

public boolean isNumber(String s) {
        if (s.isEmpty()) return false;
        int n = s.length();
        char[] c = s.toCharArray();

        boolean isValid = true;

        int i = 0;

        while (i < n && c[i] == ' ') i++;
        if (i == n || !(c[i] == '+' || c[i] == '-' || c[i] == '.' || Character.isDigit(c[i]))) return false;

        //'+' or '-'
        if (c[i] == '+' || c[i] == '-'){
            i++;
        }

        if (c[i] == '.'){
            i++;

            if (i < n && c[i] == 'e') return false;

            if (i < n && Character.isDigit(c[i])){
                while (i < n && Character.isDigit(c[i])) i++;

                if (i < n && c[i] == 'e'){
                    i++;
                    while (i < n && c[i] == ' ') i++;
                    if (i == n || ! (Character.isDigit(c[i]) || c[i] == '+' || c[i] == '-')) return false;

                    if (i < n && (c[i] == '+' || c[i] == '-')) i++;
                    if (i == n) return false;

                    while (i < n && Character.isDigit(c[i])) i++;
                    while (i < n && c[i] == ' ') i++;
                    if (i == n) return true;
                    return false;
                }

                while (i < n && c[i] == ' ') i++;
                if (i == n) return true;
            }
            return false;
        }

        if (i == n || !Character.isDigit(c[i])) return false;

        long num = 0;
        while (i < n && Character.isDigit(c[i])){
            num = 10 * num + c[i++] - '0';
        }

        while (i < n && c[i] == ' ') i++;
        if (i == n) return isValid;


        if (!(c[i] == '.' || c[i] == 'e')) return false;

        if (!Character.isDigit(c[i-1]) && c[i] == '.') return false;
        if (!Character.isDigit(c[i-1]) && c[i] == 'e') return false;

        if (c[i] == '.'){
            i++;

            if (i < n && c[i] == 'e'){
                    i++;
                    while (i < n && c[i] == ' ') i++;
                    if (i == n || ! (Character.isDigit(c[i]) || c[i] == '+' || c[i] == '-')) return false;

                    if (i < n && (c[i] == '+' || c[i] == '-')) i++;
                    if (i == n) return false;

                    while (i < n && Character.isDigit(c[i])) i++;
                    while (i < n && c[i] == ' ') i++;
                    if (i == n) return true;
                    return false;
            }   

            if (i < n && Character.isDigit(c[i])){
                while (i < n && Character.isDigit(c[i])) i++;

                if (i < n && c[i] == 'e'){
                    i++;
                    while (i < n && c[i] == ' ') i++;
                    if (i == n || ! (Character.isDigit(c[i]) || c[i] == '+' || c[i] == '-')) return false;

                    if (i < n && (c[i] == '+' || c[i] == '-')) i++;
                    if (i == n) return false;

                    while (i < n && Character.isDigit(c[i])) i++;
                    while (i < n && c[i] == ' ') i++;
                    if (i == n) return true;
                    return false;
                }

                while (i < n && c[i] == ' ') i++;
                if (i == n) return true;
                return false;
            }

            while (i < n && c[i] == ' ') i++;
            if (i == n) return true;
            return false;           
        }

        if (i < n && c[i] == 'e'){
            i++;
            while (i < n && c[i] == ' ') i++;
            if (i == n || ! (Character.isDigit(c[i]) || c[i] == '+' || c[i] == '-')) return false;

            if (i < n && (c[i] == '+' || c[i] == '-')) i++;
            if (i == n) return false;

            while (i < n && Character.isDigit(c[i])) i++;
            while (i < n && c[i] == ' ') i++;
            if (i == n) return true;
            return false;
        }

        return isValid;
    }

这是我的第一个版本,但在写代码时发现,很多模式是重复的,比如判断e后面是否为数字的情况,这一状态的判断将被用在很多处地方,所以完全可以重用很多代码。

上述复杂的代码完全可以用一幅图替代,如下:
alt text

根据规则我们不难写出这样的状态转移图,真的是very badly poor at DFA啊,根据状态转移图写出的DFA代码居然还是那么长,代码如下:

static int[][] DFA = {
            {0,0,0,0,0,0,0,0,0,0},
            {0,1,2,3,4,0,0,0,0,0},
            {0,0,0,3,4,0,0,0,0,0},
            {0,0,0,3,0,5,6,0,0,9},
            {0,0,0,0,0,5,0,0,0,0},
            {0,0,0,0,0,5,6,0,0,9},
            {0,0,0,0,0,0,0,7,8,0},
            {0,0,0,0,0,0,0,0,8,0},
            {0,0,0,0,0,0,0,0,8,9},
            {0,0,0,0,0,0,0,0,0,9}
    };
    public boolean isNumber(String s) {
        if (s.isEmpty()) return false;
        int n = s.length();
        char[] c = s.toCharArray();
        int currentState = 1;
        for (int i = 0; i < n; ++i){
            if (c[i] == ' '){
                if (currentState == 1) currentState = DFA[currentState][1];
                else if (currentState == 9) currentState = DFA[currentState][9];
                else if (currentState == 3) currentState = DFA[currentState][9];
                else if (currentState == 5) currentState = DFA[currentState][9];
                else if (currentState == 8) currentState = DFA[currentState][9];
                else return false;
            }
            else if (c[i] >= '0' && c[i] <= '9'){
                if (currentState ==1) currentState = DFA[currentState][3];
                else if (currentState ==2) currentState = DFA[currentState][3];
                else if (currentState ==3) currentState = DFA[currentState][3];
                else if (currentState ==4) currentState = DFA[currentState][5];
                else if (currentState ==5) currentState = DFA[currentState][5];
                else if (currentState ==7) currentState = DFA[currentState][8];
                else if (currentState ==6) currentState = DFA[currentState][8];
                else if (currentState ==8) currentState = DFA[currentState][8];
                else return false;
            }
            else if (c[i] == '.'){
                if (currentState ==1) currentState = DFA[currentState][4];
                else if (currentState ==2) currentState = DFA[currentState][4];
                else if (currentState ==3) currentState = DFA[currentState][5];
                else return false;
            }
            else if (c[i] == '+' || c[i] == '-'){
                if (currentState ==1) currentState = DFA[currentState][2];
                else if (currentState ==6) currentState = DFA[currentState][7];
                else return false;
            }
            else if (c[i] == 'e'){
                if (currentState == 3) currentState = DFA[currentState][6];
                else if (currentState == 5) currentState = DFA[currentState][6];
                else return false;
            }
            else return false;
        }
        return currentState == 9 || currentState == 3 || currentState == 5 || currentState == 8;
    }

核心思想:遍历所有字符,如果状态没有发生转移的情况return false,其他情况下,有限自动机运行着,如果最终能够抵达3,5,8,9这四个状态说明匹配成功。

这是最直观的代码,而且仔细观察代码会发现会有很多if else的判断,比如:

if (currentState ==1) currentState = DFA[currentState][3];
                else if (currentState ==2) currentState = DFA[currentState][3];
                else if (currentState ==3) currentState = DFA[currentState][3];
                else if (currentState ==4) currentState = DFA[currentState][5];
                else if (currentState ==5) currentState = DFA[currentState][5];
                else if (currentState ==7) currentState = DFA[currentState][8];
                else if (currentState ==6) currentState = DFA[currentState][8];
                else if (currentState ==8) currentState = DFA[currentState][8];
                else return false;

这是因为在不同的状态下,检查到数字情况下的转移过程不一定完全相同,所以我们需要单独判断以应对不同情况。

当然我们也不一定要完全使用DFA,代码还可以简化,如下:

public boolean isNumber(String s) {
        if (s.isEmpty()) return false;
        int n = s.length();
        char[] c = s.toCharArray();
        int currentState = 1;
        for (int i = 0; i < n; ++i){
            if (c[i] == ' '){
                if (currentState == 1) currentState = 1;
                else if (currentState == 9 || currentState == 3) currentState = 9;
                else if (currentState == 5 || currentState == 8) currentState = 9;
                else return false;
            }
            else if (c[i] >= '0' && c[i] <= '9'){
                if (currentState ==1 || currentState == 2 || currentState == 3) currentState = 3;
                else if (currentState ==4 || currentState == 5) currentState = 5;
                else if (currentState ==7 || currentState == 6 || currentState == 8) currentState = 8;
                else return false;
            }
            else if (c[i] == '.'){
                if (currentState ==1 || currentState == 2) currentState = 4;
                else if (currentState ==3) currentState = 5;
                else return false;
            }
            else if (c[i] == '+' || c[i] == '-'){
                if (currentState ==1) currentState = 2;
                else if (currentState ==6) currentState = 7;
                else return false;
            }
            else if (c[i] == 'e'){
                if (currentState == 3 || currentState == 5) currentState = 6;
                else return false;
            }
            else return false;
        }
        return currentState == 9 || currentState == 3 || currentState == 5 || currentState == 8;
    }

所以只要能够画出状态转移图,就能写出简洁的代码。不过这现象很有趣,原本很复杂的代码逻辑,一旦用DFA解决,整个代码结构就变得清晰易懂,这是为什么?

  • 我个人认为DFA的一个最大特点就是对状态的抽象,在一个DFA中,我们能看到很多状态以及状态之间的转移。最先版本的代码中,为了判断一个e后面的逻辑,我们总共写了好几处,无非是因为有很多状态都能抵到e,代码自然复杂,而DFA把e后续的状态用一套逻辑给解决了。
  • 第二点,标记和信息的区别,我们只需要判断当前字符串是否符合某种语法规则,所以我们强调的是对标记的识别而不是信息的采集。

可以把DFA想象成一个有内在逻辑的黑盒,我们给它输入信息,如果信息符合DFA的逻辑,它就会发生状态转移,而如果信息流能够一下抵达DFA的终态,说明匹配成功,而DFA一旦在中间出现卡顿,则匹配失败。DFA之所以能够简化大量代码,是因为它在构造逻辑时就去重了。可见状态抽象的重要性,起码能让我们找到同质的状态。

Leetcode 005. Longest Palindromic Substring

一开始以为palindrome可以不连续,但发现题解中都指明最长palindrome连续,两种做法,一种比较直接,直接从一个字符扩展palindrome,并记录最大值,不断比较就好。

另一种方法使用dp,如果有更大的区间,则更新,注意下遍历方向性问题就好了。

代码如下:(扩展,奇偶性判断挺巧妙的)

int lo, hi, max;
    public String longestPalindrome(String s) {
        int n = s.length();
        if (n == 0) return "";
        for (int i = 0; i < n - 1; ++i){
            extendPalindrome(s.toCharArray(), i, i);
            extendPalindrome(s.toCharArray(), i, i+1);
        }
        return s.substring(lo, hi + 1);
    }

    private void extendPalindrome(char[] c, int i, int j){
        while (i >= 0 && j < c.length && c[i] == c[j]){
                i--;
                j++;
        }
        if (j - i - 1 > max){
            max = j - i - 1;
            lo = i + 1;
            hi = j - 1;
        }
    }

动态规划法:

public String longestPalindrome(String s) {
        int n = s.length();
        if (n == 0) return "";
        char[] cs = s.toCharArray();
        boolean[][] dp = new boolean[n][n];
        String ans = "";
        for (int i = 0; i < n; ++i) dp[i][i] = true;
        ans = s.substring(0, 1);
        for (int i = 0; i < n; ++i){
            for (int j = i - 1; j >= 0; --j){
                if (cs[j] == cs[i]){
                    dp[j][i] = dp[j+1][i-1] && i - j + 1 >= 3 || i - j + 1 == 2;
                }

                if (dp[j][i] && (i - j + 1) > ans.length()){
                    ans = s.substring(j,i+1);
                }
            }
        }
        return ans;
    }

前者速度居然要快些,可能是少了很多subString的操作。

Leetcode 076. Minimum Window Substring

思路:

S = "ADOBECODEBANC"
T = "ABC"

先构造一个有效的窗口,在S中,有效窗口为"ADOBEC",里面还有"ABC"

如何判断有效窗口?(letterCnt计数)

更新最小窗口,很简单,在后续加入新的字符时,判断是否为T中的元素?
否:不做任何操作
是:又分为两种情况
a. "ADOBECODEB",此时不能更新窗口,因为一旦A被删除,此窗口就少了A元素
b. "ADOBECODEBA",此时可以删除A元素,得到最新的窗口为"CODEBA",这里还连带删除了B元素

这是思路,可程序怎么写?

不管是a还是b,窗口更新都是从左至右一个一个元素比较,所以我们可以定义一个slow指针,用来表示窗口的左侧。

此时就需要判断每个slow指向的元素是否能够删除,所以我们需要定一个合法窗口,用
int[] window = new int[128] 表示
其实就是一个map,但这map功能强大
第一,可以记录窗口中T中每个字符的个数,这样我们就可以判断slow指向的元素是否可以删除。
第二,窗口真正的含义被改写,不再是数组中的某个固定的区间,简化了大量判断逻辑(很有用的想法,没hold住)。

总结:

  • 把握住了窗口的性质,只需要根据窗口符合的性质建立数据结构即可,而不是真的定义一个“区间窗口”,否则带来的边界更新复杂又痛苦。
  • 窗口滑动,可以看成两个指针在数组中相对位置的改变,用双指针同样降低了代码复杂性。

代码如下:

public String minWindow(String s, String t) {
        if (t.length() > s.length()) return "";

        int[] map = new int[128];
        int[] window = new int[128];

        char[] ts = t.toCharArray();
        for (int i = 0; i < t.length(); ++i){
            map[ts[i] - 'A']++;
        }

        char[] ss = s.toCharArray();
        int letterCnt = 0;
        int slow = 0;
        int min = Integer.MAX_VALUE;
        int lo = -1, hi = -1;
        for (int i = 0; i < s.length(); ++i){
            if (map[ss[i] - 'A'] != 0){
                if (letterCnt < t.length() && window[ss[i] - 'A'] < map[ss[i] - 'A']){
                    letterCnt++;
                }
                window[ss[i] - 'A']++;
            }
            if(letterCnt >= t.length()){
                while(slow < s.length() && (window[ss[slow] - 'A'] == 0 || window[ss[slow] - 'A'] > map[ss[slow] - 'A'])){
                    if (window[ss[slow] - 'A'] != 0){
                        window[ss[slow] - 'A']--;
                    }
                    slow++;
                }

                if (i - slow + 1 < min){
                    min = i - slow + 1;
                    lo = slow;
                    hi = i;
                }
            }
        }
        if (lo == -1 && hi == -1) return "";
        return s.substring(lo, hi + 1);
    }

此处隐含了很多细节问题,自行体会。

这是我最先的版本,无法通过一些个别案例,对窗口的建立没把握住,所导致的代码复杂度也成倍上升。(有趣的课题,可以研究研究)

public String minWindow(String s, String t) {
        int[] map = new int[128];
        int[] pos = new int[128];
        for (int i = 0; i < t.length(); ++i){
            map[t.charAt(i) - 'A']++;
            pos[t.charAt(i) - 'A']++;
        }
        int letterCounter = 0;

        int n = s.length();
        char[] cs = s.toCharArray();
        int[] queue = new int[s.length()];
        int fir = 0, lst = 0;
        int min = Integer.MAX_VALUE,lo = -1, hi = -1;
        for (int i = 0; i < n; ++i){
            if (map[cs[i]-'A'] != 0 && letterCounter != t.length()){
                queue[lst++] = i;
                map[cs[i]- 'A']--;
                letterCounter++;
                if (letterCounter == t.length()){
                    //total
                    int len = queue[lst-1] - queue[fir] + 1;
                    min = len;
                    lo = queue[fir];
                    hi = queue[lst -1];
                    continue;
                }
            }

            if (letterCounter == t.length()){
                if (pos[cs[i]-'A'] != 0){
                    queue[lst++] = i;
                    int l = fir;
                    int r = lst - 1;
                    while (l < r){
                        char key = cs[queue[l]];
                        for (int k = r; k >= l; --k){
                            if (cs[queue[k]] == key){ 
                                if (k == l){
                                    r = l;
                                    break;
                                }
                                l++;
                                r--;
                                break;
                            }
                        }
                    }
                    fir = l;
                    int len = queue[lst -1] - queue[fir] + 1;
                    if (len < min){
                        min = len;
                        lo = queue[fir];
                        hi = queue[lst - 1];
                    }
                }
            }
        }
        if (lo == -1 && hi == -1) return "";
        return s.substring(lo, hi+1);
    }

Leetcode 003. Longest Substring Without Repeating Characters

核心思路:

  • 记录每个字符对应的下标,用map存,如果遇到map中存在的字符,进行更新操作,更新规则为当前下标i - map.get(ss[i])

如:

a b c a b c
0 1 2 3 4 5

map : {a = 0, b = 1, c = 2}
当遇到下一个a时,i = 3
len = i - {a = 0} = 3
每次更新成最小的即可。

遇到"abc"这种情况怎么处理?
a b c
0 1 2

此时无法根据map.contains(key)来判断,用一个slow指针,表示当前更新窗口的合法左区间

slow = 0
if (!map.contains[ss[i]]) 更新长度

if (map.constains[ss[i]]) 两种情况
lst = map.get(ss[i])
a. 当前窗口左区间slow > lst,说明合法窗口在slow,而不在lst,更新规则为:max = i - slow + 1; 
b. 当前窗口左区间slow < lst,说明合法窗口要调整右移,slow = lst + 1; 更新规则为:max = i - lst;

代码如下:

public int lengthOfLongestSubstring(String s) {
        if (s.isEmpty()) return 0;
        Map<Character, Integer> map = new HashMap<>();
        int max = 0;
        char[] ss = s.toCharArray();
        int n = ss.length;
        int slow = 0;
        for (int i = 0; i < n; ++i){
            if(!map.containsKey(ss[i])){
                map.put(ss[i],i);
                if (i - slow + 1 > max){
                    max = i - slow + 1;
                }
            }else{
                int lst = map.get(ss[i]);
                if (lst < slow){
                    if (i - slow + 1 > max){
                        max = i - slow + 1;
                    }
                }
                else{
                    if (i - lst > max){
                        max = i - lst;
                    }
                    slow = lst + 1;
                }
                map.put(ss[i], i);
            }
        }
        return max;
    }

现在可以开始神奇的代码约简了,首先slow,可以用slow = Math.max(slow, lst + 1)统一更新,这样,更新规则可以完全合并,如下:

public int lengthOfLongestSubstring(String s) {
        if (s.isEmpty()) return 0;
        Map<Character, Integer> map = new HashMap<>();
        int max = 0;
        char[] ss = s.toCharArray();
        int n = ss.length;
        int slow = 0;
        for (int i = 0; i < n; ++i){
            if(map.containsKey(ss[i])){
                int lst = map.get(ss[i]);
                slow = Math.max(slow, lst + 1);
            }
            if (i - slow + 1 > max){
                max = i - slow + 1;
            }
            map.put(ss[i],i);
        }
        return max;
    }

最后再把max用一句话表示即可,代码如下:

public int lengthOfLongestSubstring(String s) {
        if (s.isEmpty()) return 0;
        Map<Character, Integer> map = new HashMap<>();
        int max = 0;
        char[] ss = s.toCharArray();
        int n = ss.length;
        int slow = 0;
        for (int i = 0; i < n; ++i){
            if(map.containsKey(ss[i])){
                int lst = map.get(ss[i]);
                slow = Math.max(slow, lst + 1);
            }
            max = Math.max(max, i - slow + 1);
            map.put(ss[i],i);
        }
        return max;
    }

Leetcode 214.Shortest Palindrome

思路:
比较暴力的一种做法,但居然AC了。

a a c e c a a a
0 1 2 3 4 5 6 7

isPalindrome(s,0,7) 判断字符串s在0-7内是否为回文.

if not
添加“a”,判断isPalindrome(s,0,6)是否为回文。

直到余下的某个i,isPalindrome(s,0,i)为回文,返回

代码如下:

public String shortestPalindrome(String s) {
        if (s.length() <= 1) return s;
        int n = s.length();
        char[] cs = s.toCharArray();
        StringBuilder sb = new StringBuilder();
        for (int i = n - 1; i >= 0; --i) {
            if (isPalindrome(cs, 0, i)) return sb.toString() + s.substring(0, i+1) + sb.reverse().toString();
            else sb.append(cs[i]);
        }
        return "";
    }

    private boolean isPalindrome(char[] c, int i, int j){
        while (i < j){
            if (c[i] != c[j]) return false;
            i++;
            j--;
        }
        return true;
    }

此题还有一种更聪明的做法,采用KMP Table,学习KMP的时候再来看看它的精髓吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值