前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >算法细节系列(33):再见字符串(1)

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

作者头像
用户1147447
发布2019-05-26 09:52:48
6060
发布2019-05-26 09:52:48
举报
文章被收录于专栏:机器学习入门机器学习入门

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://cloud.tencent.com/developer/article/1434806

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

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

题目摘自leetcode:

Leetcode 008. String to Integer (atoi)

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

判断当前String能否转成Integer

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

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

解决溢出的判断条件为:

代码语言:javascript
复制
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;

代码如下:

代码语言:javascript
复制
    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来处理字符串匹配问题,很有趣,不过在了解它强大之前,先看看根据规则匹配的直观代码:

代码语言:javascript
复制
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后面是否为数字的情况,这一状态的判断将被用在很多处地方,所以完全可以重用很多代码。

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

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

代码语言:javascript
复制
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的判断,比如:

代码语言:javascript
复制
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,代码还可以简化,如下:

代码语言:javascript
复制
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,如果有更大的区间,则更新,注意下遍历方向性问题就好了。

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

代码语言:javascript
复制
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;
        }
    }

动态规划法:

代码语言:javascript
复制
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

思路:

代码语言:javascript
复制
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住)。

总结:

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

代码如下:

代码语言:javascript
复制
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);
    }

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

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

代码语言:javascript
复制
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])

如:

代码语言:javascript
复制
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;

代码如下:

代码语言:javascript
复制
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)统一更新,这样,更新规则可以完全合并,如下:

代码语言:javascript
复制
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用一句话表示即可,代码如下:

代码语言:javascript
复制
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了。

代码语言:javascript
复制
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)为回文,返回

代码如下:

代码语言:javascript
复制
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的时候再来看看它的精髓吧。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017年06月12日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 算法细节系列(33):再见字符串(1)
    • Leetcode 008. String to Integer (atoi)
      • Leetcode 065. Valid Number
        • Leetcode 005. Longest Palindromic Substring
          • Leetcode 076. Minimum Window Substring
            • Leetcode 003. Longest Substring Without Repeating Characters
              • Leetcode 214.Shortest Palindrome
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档