Leetcode之字符串(二)

目录

11.regular-expression-matching

12.wildcard-matching

13.count-and-say

14.reverse-integer

15.palindrome-number

16.implement-strstr

17.substring-with-concatenation-of-all-words

18.anagrams

19.zigzag-conversion

20.text-justification

21.simplify-path


11.regular-expression-matching

题目:请实现支持'.'and'*'.的通配符模式匹配。字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)

分析:方法一,见剑指offer面试题19 https://blog.csdn.net/Nibaby9/article/details/103822794 

方法二,采用动态规划,f(i,j)表示字符串的前i个字符与模式串的前j个字符是否匹配,如果当前字符匹配,f(i,j) = f(i-1,j-1);如果模式串当前字符为‘*’,根据匹配串的上个字符与字符串当前字符是否匹配分成两种情况,若不匹配说明‘*’匹配0个字符,f(i,j) = f(i,j-1),若匹配则f(i,j) = f(i,j-1) || f(i-1,j-1) || f(i-1,j),也就是*匹配0个字符,匹配1个字符或者是匹配多个字符。

   public boolean isMatch(String s, String p) {
        int m = s.length(), n = p.length();
        boolean[][] res = new boolean[m + 1][n + 1];
        res[0][0] = true;
        for (int i = 0; i < n; i++) {
            if (p.charAt(i) == '*' && res[0][i - 1])
                res[0][i + 1] = true;
        }
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (p.charAt(j) == '.')
                    res[i + 1][j + 1] = res[i][j];
                if (p.charAt(j) == s.charAt(i))
                    res[i + 1][j + 1] = res[i][j];
                if (p.charAt(j) == '*') {
                    if (s.charAt(i) != p.charAt(j - 1) && p.charAt(j - 1) != '.')
                        res[i + 1][j + 1] = res[i + 1][j - 1];
                    else {
                        //res[i + 1][j - 1] 表示*一个都不匹配;
                        //res[i + 1][j]表示匹配一个
                        //res[i][j + 1]表示匹配多个
                        res[i + 1][j + 1] = res[i + 1][j - 1] || res[i + 1][j] || res[i][j + 1];
                    }
                }
            }
        }
        return res[m][n];
    }

12.wildcard-matching

题目:请实现支持'?'and'*'.的通配符模式匹配.'?' 可以匹配任何单个字符,'*' 可以匹配任何字符序列(包括空序列)

分析:这题与上题非常相似,但是这题如果采用类似递归的方式会超时。这里采用双指针的方法,每次碰到字符 ‘*’ 就标记好匹配串的当前位置和匹配串*的位置,这样在每次无法匹配时,就能回溯,从匹配串标记的下一个位置再重新进行匹配。

   public boolean isMatch(String s, String p) {
       int i = 0,j = 0,backI = -1,backJ = -1;
       while(i < s.length()){
           if(j < p.length() && (p.charAt(j) == '?' || s.charAt(i) == p.charAt(j))){//当前字符匹配
               i++;j++;
           }
           else if(j < p.length() && p.charAt(j) == '*'){//若不匹配,如果模式串当前字符为*,记录匹配串和模式串回溯位置
               backI = i;backJ = j;j++;
           }
           else if(backJ != -1) {//根据上一个*进行回溯
               i = backI + 1;j = backJ + 1;backI = i;
           }
           else
               return false;
       }
       while (j < p.length() && p.charAt(j) == '*')
           j++;
       return j == p.length();
    }

13.count-and-say

题目:count-and-say数列的前几项如下:1, 11, 21, 1211, 111221, ...。1读作“1个1”或11,11读作“2个1“或者21,21读作”1个2,1个1“或者1211。给出一个整数n,请给出序列的第n项。注意:序列中的数字用字符串表示

分析:先依次求出序列的n-1项,再根据第n-1项求出第n项即可,当前项其实就是在前一项的基础上统计各个数字连续出现的次数。

    public String countAndSay(int n) {
        if(n <= 0)
            return "";
        String result = "1";
        for(int i = 1;i < n;i++){
            String temp = "";
            int count = 1;
            int j = 1;
            for(;j < result.length();j++){
                if(result.charAt(j) == result.charAt(j-1))
                    count++;
                else {
                    temp += ("" + count);
                    temp += result.charAt(j - 1);
                    count = 1;
                }
            }
            temp += ("" + count);
            temp += result.charAt(j - 1);
            result = temp;
        }
        return result;
    }

14.reverse-integer

题目:给出一个 32 位的有符号整数,将这个整数中每位上的数字进行反转。例1:x=123,返回321;例2:x=-123,返回-321。假设我们的环境只能存储得下 32 位的有符号整数,则其数值范围为 [−231,  231 − 1]。请根据这个假设,如果反转后整数溢出那么就返回 0。

分析:通过循环将数字x的每一位拆开,然后在计算新值时每一步都判断是否溢出。

   public int reverse(int x) {
        int result = 0;
        while(x != 0){
            int pop = x % 10;
            x /= 10;
            if (result > Integer.MAX_VALUE/10 || (result == Integer.MAX_VALUE / 10 && pop > 7))
                return 0;
            if (result < Integer.MIN_VALUE/10 || (result == Integer.MIN_VALUE / 10 && pop < -8))
                return 0;
            result = result * 10 + pop;
        }
        return result;
    }

15.palindrome-number

题目:判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

分析:一种方法是将数字转化为字符串,并检查字符串是否为回文,但这需要使用额外的空间。另一种方法是将整数反转,但从上题可以知道,整数翻转可能会出现溢出。为了避免这个问题,我们可以只反转数字的一半,如果该数字是回文,其后半部分反转后应该与原始数字的前半部分相同。首先我们需要处理一些特殊情况,负数不是回文,数字末尾为0也不是回文(0除外),然后按照反转整数的思路依次构建新值,当x的值小于等于新值时意味着我们已经翻转了一半,以12321为例,当新值为123、x为12就已经反转了一半。

   public boolean isPalindrome(int x) {
        if(x < 0 || (x != 0 && x % 10 == 0))
            return false;
        int reverse = 0;
        while(x > reverse){
            reverse = reverse * 10 + x % 10;
            x = x / 10;
        }
        return reverse == x || reverse / 10 == x;
    }

16.implement-strstr

题目:实现函数 strStr。函数声明如下:char *strStr(char *haystack, char *needle),返回一个指针,指向needle第一次在haystack中出现的位置,如果needle不是haystack的子串,则返回null。例如"mississippi","issi",返回"ississippi".

分析:考查KMP算法,它是一种改进的字符串模式匹配算法,可以在O(n+m)的时间复杂度以内完成字符串的匹配操作,其核心思想在于当一趟匹配过程中出现字符不匹配时,不需要回溯主串的指针,而是利用已经得到的“部分匹配”,将模式串尽可能多地向右“滑动”一段距离,然后继续比较。KMP算法先计算模式串的next数组,next[i]表示模式串前i个字符的部分匹配值(”前缀”和”后缀”的最长的公有元素的长度),如”A”的前缀和后缀都为空集,部分匹配值为0,“ABCDAB”的部分匹配值为2。求解next数组的步骤:

  • 初始化next[0]=-1, next[1]=0;(注意如果初始化next[1]的话,需保证next数组长度至少为2,所以可以暂不初始化next[1])
  • 求解next[j]时,令k=next[j-1];
  • 比较T[j-1]与T[k]的值:若相等,则next[j] = k +1;否则令k=next[k],若k等于-1,则next[j]=0,否则跳转到第三步。

求解完next数组后,开始遍历主串来匹配模式串,如果不匹配则利用next数组进行对模式串进行偏移。

    public String strStr(String haystack, String needle) {
        if(needle.length() == 0)
            return haystack;
        if(needle.length() > haystack.length())
            return null;
        int[] next = cal_next(needle);
        int i = 0,j = 0;
        while(i < haystack.length() && j < needle.length()){
            if(haystack.charAt(i) == needle.charAt(j) ){
                i++;j++;
            }
            else if(next[j] == -1){
                j = 0;i++;
            }
            else
                j = next[j];
        }
        if(j == needle.length())
            return haystack.substring(i-j);//易错
        return null;
    }

    private int[] cal_next(String needle) {
        int[] next = new int[needle.length()];
        next[0] = -1;//初始化
        for(int i = 2;i < needle.length();i++){
            int k = next[i-1];
            while(k != -1 && needle.charAt(i-1) != needle.charAt(k))
                k = next[k];
            next[i] = k + 1;
        }
        return next;
    }

17.substring-with-concatenation-of-all-words

题目:给出一个字符串S和一组单词L,L中单词的长度都相等,找出S中的符合以下要求的子串在S中的起始位置索引:子串为L中所有单词串联在一起(单词的顺序随意),L中的每个单词只出现一次,中间不能有其他的字符。例如:给定S="barfoothefoobarman",L为["foo", "bar"],返回的索引应该是[0,9].(不用关心给出索引的顺序)

分析:符合条件的子串长度必然为len = L[0].length * L.length,所以可以依次遍历S的第i到i+len-1个字符,判断是否符合子串条件,为了方便判断,可以将L中各个单词的个数存在HashMap中。

    public ArrayList<Integer> findSubstring(String S, String[] L) {
        ArrayList<Integer> res = new ArrayList<>();
        if(L.length == 0 || S.length() == 0)
            return res;
        int len = L[0].length(),num = L.length;
        HashMap<String,Integer> map = new HashMap<>();
        for(String s : L){
            if(map.containsKey(s))
                map.put(s,map.get(s) + 1);
            else
                map.put(s,1);
        }
        //从i开始,后面至少要有len * num个字符
        HashMap<String,Integer> cur = new HashMap<>();
        for(int i = 0;i <= S.length() - len * num;i++){
            cur.putAll(map);//map的深拷贝
            int j = i;
            for(;j < i + len * num;j+=len){//判断[j,j+len*num)是否符合子串条件
                String str = S.substring(j,j+len);
                if(cur.containsKey(str) && cur.get(str) > 0){
                    cur.put(str,cur.get(str)-1);
                }
                else
                    break;
            }
            if(j == i+len*num)
                res.add(i);
        }
        return res;
    }

18.anagrams

题目:给出一个字符串数组,返回所有互为“换位词(anagrams)”的字符串的组合。(换位词就是包含相同字母,但字母顺序可能不同的字符串)备注:所有的输入都是小写字母。例如:输入["tea","and","ate","eat","den"],返回["tea","ate","eat"]

分析:方法一:根据换位词按字母排序是同一个字符串的特点,将这个排好序的字符串作为HashMap的key,而value就是对应的所有换位词,最后只要遍历map将这些list加入到结果集合中即可。时间复杂度为O(NKlogK)(NKlogK),空间复杂度为O(NK)。

方法二:让每个字符对应于一个质数,于是这些数值一定不会有公约数,不在一组的数,它们的乘积一定不相等。因此可以将每个字符串映射成Integer值,作为HashMap的key。时间复杂度和空间复杂度均为O(NK),相比于方法一显然节省了排序的时间。

//方法一:利用内部排好序的String作为HashMap的key
 public List<List<String>> groupAnagrams(String[] strs) {
        List<List<String>> res = new ArrayList<>();
        if(strs.length == 0)
            return res;
        HashMap<String,ArrayList<String>> map = new HashMap<>();
        for(String s : strs){
            char[] c = s.toCharArray();
            Arrays.sort(c);
            String key = String.valueOf(c);
            if(!map.containsKey(key))
                map.put(key,new ArrayList());
            map.get(key).add(s);
        }
        for(String s : map.keySet())
            res.add(map.get(s));
        return res;
    }
    //方法二:使用质数作为乘法因子得到HashMap的key
    public List<List<String>> groupAnagrams(String[] strs) {
        List<List<String>> res = new ArrayList<>();
        if(strs.length == 0)
            return res;
        int[] primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37,
                41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101};
        HashMap<Integer,ArrayList<String>> map = new HashMap<>();
        for(String s : strs){
            int key = 1;
            for(int i = 0;i < s.length();i++)
                key *= primes[s.charAt(i) - 'a'];
            if(!map.containsKey(key))
                map.put(key,new ArrayList<>());
            map.get(key).add(s);
        }
        for(Integer i : map.keySet())
            res.add(map.get(i));
        return res;
    }

19.zigzag-conversion

题目:将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。比如输入字符串为 "LEETCODEISHIRING" 行数为 3 时,排列如下:

L     C     I   R
E T O E S I I G
E    D     H  N
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"LCIRETOESIIGEDHN"。编写代码完成将字符串转化为指定行数的Z字形字符串。

分析:假设转换后Z字形的行数分别为0~nRows-1,可以发现,在按顺序遍历字符串时,我们是依次将字符放入到第0~nRows-1行,再反过来放入nRows-2~0行,因此我们可以模拟这个行索引的变化,在遍历 s 时把每个字符填到正确的行 ss[i],在达到 Z字形转折点时,执行反向。

   public String convert(String s, int nRows) {
        if(nRows <= 1)
            return s;
        StringBuilder[] ss = new StringBuilder[nRows];
        for(int i = 0;i < nRows;i++)
            ss[i] = new StringBuilder();
        int flag = -1,row = 0;
        for(int i = 0;i < s.length();i++){
            ss[row].append(s.charAt(i));
            if(row == 0 || row == nRows - 1)
                flag = -flag;
            row += flag;
        }
        StringBuilder result = new StringBuilder();
        for(int i = 0;i < nRows;i++)
            result.append(ss[i]);
        return result.toString();
    }

20.text-justification

题目:给定一个单词数组和长度L,将该单词数组中文本两端对齐(左边和右边),使每一行都有L个字符。你要在每一行中尽可能多地填充单词。在必要时填充额外的空格' ',使每行正好有L个字符。单词之间的额外空格要尽可能均匀地分布。如果一行的空格数不能在单词之间平均分配,请在左边分配更多的空格对于最后一行文本,它应该左对齐,并且单词之间不插入额外的空格。例如,单词数组为:["This", "is", "an", "example", "of", "text", "justification."],L:16.

输出:[
   "This    is    an",
   "example  of text",
   "justification.  "
]

分析:先计算出一行最多能够容纳的个数,再将空格数先平均分配给各个单词中间,再将多余的空格从前往后依次分配。注意有两种特殊情况,一种是 一行只有一个单词,只要在单词右侧填充空格,另一种是如果当前行为最后一行时,前面单词与单词中间只有一个空格,最后一个单词右侧填满空格。

   public ArrayList<String> fullJustify(String[] words, int L) {
        ArrayList<String> res = new ArrayList<>();
        StringBuffer cur;
        int i = 0;
        while(i < words.length){
            cur = new StringBuffer(words[i]);
           int len = words[i].length(),word_num = 1,j = i + 1;
           while(j < words.length && len + words[j].length() < L){
               len += words[j].length() + 1;
               word_num++;j++;
           }
           if(j == i+1)//只有一个单词
               cur.append(nspace(L - words[i].length()));
           else if(j == words.length){//最后一行
               for(int k = i+1;k < j;k++)
                   cur.append(" " + words[k]);
               cur.append(nspace(L - cur.length()));
           }
           else{
               int space = (L - len) / (word_num - 1),remain = (L - len) % (word_num - 1);
               String add = nspace(space + 1);
               for(int k = i+1;k < j;k++){
                   if(k-i <= remain)
                        cur.append(" " + add + words[k]);
                   else
                       cur.append(add + words[k]);
               }
           }
           res.add(cur.toString());
           i = j;//勿漏,更新i的值
        }
        return res;
    }

    private String nspace(int n){
        if(n == 0)
            return "";
        String a = nspace(n / 2);
        if((n & 1) == 1)
            return a + a + " ";
        else
            return a + a;
    }

21.simplify-path

题目:请简化给出的Unix样式的文件绝对路径,也就是转换成规范路径。在Unix样式的文件系统中, .代表当前目录,.. 表示将目录向上移动一级,更多的介绍可以查看 Absolute path vs relative path in Linux/Unix。请注意,返回的规范路径必须以斜杠“/”开头,并且两个目录名之间只能有一个斜杠“/”开头。如果存在的最后一级目录的话不能以“/”结尾。另外,转化出的规范路径必须是能表示给出的绝对路径的最短字符串。例如:文件路径 = "/home/", =>"/home"文件路径 = "/a/./b/../../c/", =>"/c"特殊样例:你有考虑过样例 文件路径 ="/../"吗? 这个样例应该返回"/".另一种特殊样例是路径中可能相邻的有多个“/”,例如“/home//foo/”。这种情况下应该忽略多余的“/”,这个样例应该回"/home/foo".

分析:因为需要对".."进行返回上一级的操作,如果从前往后遍历会比较麻烦,所以可以从后往前遍历,如果遇到“..”,意味着要跳过“..”前面的一个有效目录,对这个需要跳过的目录数量进行累加。如果当前目录不是空目录也不是“.”,并且跳过的目录数量为0,就将目录名添加到简化的目录之前。

   public String simplifyPath(String path) {
        String[] name = path.split("/");
        StringBuilder res = new StringBuilder();
        int skip = 0;
        for(int i = name.length - 1;i >= 0;i--){
            //跳过空目录和当前目录
            if(name[i].length() == 0 || name[i].equals("."))
                continue;
            //记录好跳过的目录数量
            else if(name[i].equals(".."))
                skip++;
            else if(skip > 0){
                skip--;
                continue;
            }
            else
                res.insert(0,"/" + name[i]);
        }
        if(res.length() == 0)
            return "/";
        else
            return res.toString();
    }

猜你喜欢

转载自blog.csdn.net/Nibaby9/article/details/104526092