字符串问题一

一 字符串的排列与组合

1.字符串的组合【子序列】

 题目:输入一个字符串,输出该字符串中字符的所有组合。

例子:输入:abc,它的组合有:a、b、c、ab、ac、bc、abc

分析:我们可以将字符串中的每个字符看成二叉树的一个节点,根节点为空,每个节点都会有两种选择:要 和 不要 两种选择 。那么我们就可以利用递归实现。

public class 字符串子序列 {
 
    public static void printAllSubString(String str){
        if(str == null){
            return;
        }
 
        char[] chars = str.toCharArray();
        if(chars.length > 0){
            String pre = new String("");   // pre:用于表示从0到i-1位置上形成的结果
            printAllSubString(0, pre, chars);
        }else{
            System.out.println("");     // 输入空字符串也会打印空
        }
    }
 
    public static void printAllSubString(int i, String pre, char[] chars){
        // 迭代终止条件
        if(i == chars.length){
            // 说明已经到最后一个字符了,所有的选择都已经做完了,应该返回了
            System.out.println(pre);
            return;
        }
 
        // 如果没有到最后一个字符,那么当前字符有两种选择:选择要 和 选择不要
        printAllSubString(i + 1, pre, chars);  // 不要当前字符
        printAllSubString(i + 1, pre + String.valueOf(chars[i]), chars);   // 要当前字符
    }
 
    // 测试
    public static void main(String[] args) {
        printAllSubString("abc");
    }
}

2、字符串的排列

题目:输入一个字符串,打印出该字符串中字符的所有排列。

举例:输入字符串 abc,则输出由字符 a、b、c 所能排列出来的所有字符串 abc、acb、bac、bca、cab 和 cba。

分析:排列和上面的组合问题思想是一样的:上面的组合问题,每个节点只有 “要” 和 “不要” 两种选择,而排列这里每个节点 i 有  n - i 种选择。

排列问题:所有的排列都是包含该字符串中所有的字符,所以不需要像组合那样利用额外的空间 pre 记录选择的过程。

需要注意的是:i 位置在进行选择的时候,会先和 i + 1 位置交换位置,搞定 i + 1 后面的排列后,会再和 i + 2 ~ n - 1 位置上的每个元素交换一次,所以为了保证都是和 i 位置上的元素进行交换,每次递归一次后,必须要将 i 位置元素再换回原来的位置。

可以直观的理解下:加入现在搞定 i 位置上元素,i  一共有 n - i 种选择,每次 i 位置固定后,i + 1 ~ n - 1 位置上的元素都是同样递归实现。

public class 字符串排列 {
 
    public static void printAllSort(String string){
        if(string == null){
            return;
        }
 
        char[] chars = string.toCharArray();
        if(chars.length > 0){
            printAllSort(0, chars);
        }
    }
 
    // 对i及i以后的字符进行全排列
    public static void printAllSort(int i, char[] chars){
        // 递归终止条件
        if(i == chars.length){
            System.out.println(String.valueOf(chars));
        }
 
        for(int j = i; j < chars.length; j++){
            swap(i, j, chars);   // 第 i 个位置有 i ~ (n - 1) 种选择。n 为字符串的长度
            printAllSort(i + 1, chars);
            swap(i, j, chars);   // 保证 i 后面的字符每次都是和 i 位置上的元素进行的交换,还需要将 i 和 j 交换回来
        }
    }
 
    public static void swap(int i, int j, char[] chars){
        int temp = chars[i];
        chars[i] = chars[j];
        chars[j] = chars[i];
    }
 
    public static void main(String[] args) {
        printAllSort("abcc");
    }
}

运行结果:从运行结果可以看出来,很多排列的结果是重复的,这是全排列的效果。

只需要增加一个 hashset,用于保证重复字符不会被再次交换。

public class 字符串排列_去重 {
 
    public static void printAllSort(String string){
        if(string == null){
            return;
        }
 
        char[] chars = string.toCharArray();
        if(chars.length > 0){
            printAllSort(0, chars);
        }
    }
 
    // 对i及i以后的字符进行全排列
    public static void printAllSort(int i, char[] chars){
        // 递归终止条件
        if(i == chars.length){
            System.out.println(String.valueOf(chars));
        }
 
        // 用于保证每次交换的字符不存在重复的字符
        HashSet<Character> set = new HashSet<>();
        for(int j = i; j < chars.length; j++){
            if(!set.contains(chars[j])){
                set.add(chars[j]);
                swap(i, j, chars);   // 第 i 个位置有 i ~ (n - 1) 种选择。n 为字符串的长度
                printAllSort(i + 1, chars);
                swap(i, j, chars);   // 保证 i 后面的字符每次都是和 i 位置上的元素进行的交换,还需要将 i 和 j 交换回来
            }
        }
    }
 
    public static void swap(int i, int j, char[] chars){
        int temp = chars[i];
        chars[i] = chars[j];
        chars[j] = chars[i];
    }
 
    public static void main(String[] args) {
        printAllSort("abcc");
    }
}

二 最长回文子串

先说明几个概念:

1. 子串:小于等于原字符串长度,由原字符串中任意个连续字符组成的子序列;

2. 回文:关于中间字符对称的字符串,例如:"ababa"(单核)、"abccba"(双核);

3. 最长回文子串:回文子串中最长的子串。

1、暴力解法【时间复杂度:O()】

 基本思路:遍历该字符串所有的子串,找出其中是回文子串且长度最长的那个。所以,我们可以倒着遍历该字符串,从最长的子串开始,这样只用找到第一个是回文字符串就可以了,它一定是原字符串最长的回文子串。

  • 从最长的子串开始,遍历该字符串的所有子串(时间复杂度为:O(n^2));
  • 判断当前子串是否为回文串(时间复杂度为:O(n));
  • 当前子串为回文时,则找到了原字符串的最长回文子串,结束遍历;否则,继续遍历,直到遍历完所有子串。
public class LongestPalindrome_5 {
 
    public static void main(String[] args) {
 
        Scanner sc = new Scanner(System.in);
        String str = sc.next();
        getLongestPalindrome1(str);
    }
 
    // 暴力解法
    public static String getLongestPalindrome1(String str){
        if(str.length() <= 1){
            return str;
        }
 
        // 前两层循环是求字符串的所有子串
        for(int i = str.length(); i > 0; i--){
            for(int j = 0; j <= str.length() - i; j++){
                String sub = str.substring(j, i + j);
                int count = 0;
                // 检验当前的子串是否为回文串
                for(int k = 0; k < sub.length() / 2; k++){
                    // k是从0开始的,所以是sub.length-k-1
                    if(sub.charAt(k) == sub.charAt(sub.length() - k - 1)){
                        count++;
                    }
                }
                if(count == sub.length() / 2){
                    System.out.println(sub);
                    return sub;
                }
            }
        }
        return "";  // 没有
    }
}

从分析和代码实现上,可以很明显的看出,暴力解法的时间复杂度是:O(n^3),无论是笔试还是面试显然都是不行的。

2、动态规划【时间复杂度:O(n^2)】

回文字符串的子串也是回文,比如P[i, j](表示以 i 开始,以 j 结束的子串)是回文字符串,那么P[i+1, j-1]也是回文字符串。这样最长回文子串就能分解成一系列子问题了。这样需要额外的空间O(N^2),算法复杂度也是O(N^2)。

首先定义状态方程和转移方程:

  • P[i, j] = false:表示子串[i, j]不是回文串;P[i, j] = true:表示子串[i, j]是回文串。
  • P[i, i] = true:当且仅当P[i+1, j-1] = true && (s[i] == s[j])否则p[i,j] =false;
public class LongestPalindrome_5 {
 
    public static void main(String[] args) {
 
        Scanner sc = new Scanner(System.in);
        String str = sc.next();
        String subStr = getLongestPalindrome3(str);
        System.out.println(subStr);
    }
 
    // 动态规划
    public static String getLongestPalindrome2(String str){
        if(str == null && str.length() <= 0){
            return "";
        }
 
        int len = str.length();
        int start = 0;            // 记录字符串起始的位置
        int maxLength = 1;        // 记录回文串的最大长度
        boolean dp[][] = new boolean[str.length()][str.length()];
 
        // 长度为1和2的子串的初始化
        for(int i = 0; i < len; i++){
            // 初始化所有长度为1的子串
            dp[i][i] = true;
            // 初始化所有长度为2的子串
            if(i < len - 1 && str.charAt(i) == str.charAt(i + 1)){
                dp[i][i + 1] = true;
                start = i;
                maxLength = 2;
            }
        }
 
        // 以字符串长度为1和2的子串为基础,推导长度:3~len 的子串的dp
        for(int strlen = 3; strlen <= len; strlen++){
            // 从头开始,遍历长度为strlen的子串,并判断它们是否为回文串
            for(int i = 0; i <= len - strlen; i++){
                int j = i + strlen - 1;   // 子串结束位置的下标
                if(dp[i + 1][j - 1] && str.charAt(i) == str.charAt(j)){
                    dp[i][j] = true;
                    // 更新最大回文子串长度为当前子串长度,因为子串长度是不断增加的,所以最后一个回文串肯定是最长的
                    maxLength = strlen;
                    start = i;  // 记录回文串开始位置的下标
                }
            }
        }
 
        if(maxLength > 0){
            return str.substring(start, start + maxLength);
        }
        return "";
    }
}

3、中心扩展法【时间复杂度:O()】

事实上,只需使用恒定的空间,我们就可以在 O(n^2) 的时间内解决这个问题。

我们观察到回文中心的两侧互为镜像。因此,回文可以从它的中心展开,并且只有 2n −1 个这样的中心。

你可能会问,为什么会是 2n - 1 个,而不是 n 个中心?原因在于所含字母数为偶数的回文的中心可以处于两字母之间(例如 {“abba”}的中心在两个{‘b’}之间)。【和在两个字符之间添加“#”是一个道理】

public class LongestPalindrome_5 {
 
    public static void main(String[] args) {
 
        Scanner sc = new Scanner(System.in);
        String str = sc.next();
        String subStr = getLongestPalindrome3(str);
        System.out.println(subStr);
    }
 
    // 中心扩展法
    public static String getLongestPalindrome3(String str){
        if(str == null && str.length() <= 0){
            return "";
        }
 
        int maxLength = 1;
        int start = 0;
 
        // 类似于aba这种情况,以i为中心向两边扩展
        for(int i = 0; i < str.length(); i++){
            int j = i - 1;
            int k = i + 1;
            while((j >= 0 && k < str.length()) && str.charAt(j) == str.charAt(k)){
                if(k - j + 1 > maxLength){
                    maxLength = k - j + 1;
                    start = j;
                }
                j--;
                k++;
            }
        }
 
        // 类似于abba这种情况,以i,i+1为中心向两边扩展
        for(int i = 0; i < str.length(); i++){
            int j = i;
            int k = i + 1;
            while((j >= 0 && k < str.length()) && str.charAt(j) == str.charAt(k)){
                if(k - j + 1 > maxLength){
                    maxLength = k - j + 1;
                    start = j;
                }
                j--;
                k++;
            }
        }
        if(maxLength > 0){
            return str.substring(start, start + maxLength);
        }
        return "";
    }
}

可以看到上面代码非常长,可以发现主要就是奇回文和偶回文两种情况的处理,代码高度一致。可以精简下,这里贴出 leetcode 官方提供的代码。

其实对于笔试来说,通过最重要,还是推荐上面的代码,虽然长,但是思路比较清晰。

public class LongestPalindrome_5 {
 
    public static void main(String[] args) {
 
        Scanner sc = new Scanner(System.in);
        String str = sc.next();
        String subStr = getLongestPalindrome4(str);
        System.out.println(subStr);
    }
 
    // 中心扩展法:精简版
    public static String getLongestPalindrome4(String str){
        if(str == null || str.length() < 1){
            return "";
        }
        int start = 0, end = 0;
        for(int i = 0; i < str.length(); i++){
            int len1 = expandAroundCenter(str, i, i);            // 偶数回文
            int len2 = expandAroundCenter(str, i, i + 1);  // 奇数回文
            int len = Math.max(len1,len2);
            if(len > end - start){
                start = i - (len - 1) / 2;
                end = i + len / 2;
            }
        }
        return str.substring(start, end + 1);
    }
 
    public static int expandAroundCenter(String str, int left, int right){
        while(left >= 0 && right < str.length() && str.charAt(left) == str.charAt(right)){
            left--;
            right++;
        }
        // 算的是左右两边的中间的长度
        return right - left - 1;
    }
}

4.Manacher算法(马拉车算法)

字符串初始化处理

奇数长和偶数长的回文子串的字符中心是不一样的,不好判断,为了解决这个问题,Manacher算法将所有字符串的长度都变成奇数,方法就是在原字符串的相邻字符之间加分隔符(比如说#),例如ababc,变成#a#b#a#b#c#;abab,变成#a#b#a#b#。所有字符串长度都是奇数,还不影响回文性。

回文半径存储数组的定义

Manacher算法还建立了一个辅助数组,用来存储以str[i]为中心的回文字符串的回文半径的长度。例如回文字符串:#a#b#a#b#a#,以第三个a为中心,即str[5]为中心,这个回文字符串的长度为11,而回文半径是回文字符串长度的一半,即(11+1)/2=6,(要包括回文中心字符),所以len[5]=6。

这里要注意一点的是:调整后加入#的字符串str,以str[i]为中心的回文字符串的长度为2×len[i]-1,而其中有len[i]个分隔符#,所以原字符串的长度为len[i]-1。也就是说最后只要输出len中最大的数字-1就是最长回文子串的长度。

回文半径存储数组的计算

Manacher算法的精髓就是只需要从头到尾遍历str一次!!!

如何实现呢?我们需要设置两个标志位,centre和right,centre是当前回文子串的中心点,right是当前回文子串的右边界(right并不包含在回文子串中!)。一开始初始化,centre=right=-1。当i从0遍历到str.length-1时,需要动态的改变centre和right值,就可以计算出以str[i]为中心的回文半径存入len数组中。

这里需要注意的是,i一定>=centre,有了i的推进,centre和right才会更新。

什么时候要改变centre和right值呢,有两大情况:

当i值在right的左边:即 centre

图中 l 是 以cen为中心的回文子串的左边界,j 为 i 以 cen 为中心的对称点。我们要时刻注意到回文特性,即 str[l+1...cen-1] 与 str[r-1...cen+1]是相同的,所以以 i 为中心的回文子串有可能与以 j 为中心的回文子串是一样的,至少有一部分是相同的。

接下来,这里根据 以 j 为中心的回文子串的长度不同,会再细分成两种情况:

(1)以 j 为中心的回文子串很短,如下图

这种情况下,len[i]至少跟len[j]一样大,为什么至少一样大,什么时候len[i]比len[j]大呢,以上图为例,以 i 为中心的回文子串的右边界还可以向右扩展,这扩展的部分在 j 是无法匹配的。所以可以先将len[i]保存为len[j],再以i为中心左右两边扩展。

(2)以 j 为中心的回文子串很长,如下图

以 j 为中心的回文子串很长,可以看出左边界已经超过了 l ,但是以 i 为中心的回文字符长度,如果只按照回文性来判断,len[i]的值现在只能确定为 r-i ,len[i]

上面两个情况合并一下,即当i值在right的左边时,因为不知道当前j符合哪一种情况,所以 len=Math.min(len[2*cen-i], right-i),取两个中间的最小值即可。

当i值在right的右边:即 i>=right

说明以 i 为中心的回文子串还没有被访问过,所以当前len[i]的长度为1,即为i本身,再以i为中心左右两边扩展。

三 正则表达式匹配

题目:请实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。

例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配

当模式中的第二个字符不是“*”时:

1、如果字符串第一个字符和模式中的第一个字符相匹配,那么字符串和模式都后移一个字符,然后匹配剩余的。

2、如果 字符串第一个字符和模式中的第一个字符相不匹配,直接返回false。

而当模式中的第二个字符是“*”时:

如果字符串第一个字符跟模式第一个字符不匹配,则模式后移2个字符,继续匹配。如果字符串第一个字符跟模式第一个字符匹配,可以有3种匹配方式:

1、模式后移2字符,相当于x*被忽略(因为*前面可以是0个);

2、字符串后移1字符,模式后移2字符(相当于第3位是.匹配1个);

3、字符串后移1字符,模式不变,即继续匹配字符下一位,因为*可以匹配多位;

public class StringMatching {
 
	public boolean match(char[] str, char[] pattern){
		if(str == null || pattern == null){
			return false;
		}
		int strIndex = 0;
		int patternIndex = 0;
		return matchCore(str, strIndex, pattern, patternIndex);
	}
 
	private boolean matchCore(char[] str, int strIndex, char[] pattern, int patternIndex) {
		// 有效性检验,str到尾、pattern到尾,则匹配成功
		if(strIndex == str.length && patternIndex == pattern.length){
			return true;
		}
		// pattern先到尾,匹配失败
		if(strIndex != str.length && patternIndex == pattern.length){
			return false;
		}
		// 模式第2个是*,且字符串第1个跟模式串第1个匹配,分3种匹配模式;如果不匹配,则模式后移2位
		if(patternIndex + 1 < pattern.length && pattern[patternIndex + 1] == '*'){
			if((strIndex != str.length && pattern[patternIndex] == str[strIndex]) ||
					(pattern[patternIndex] == '.' && strIndex != str.length)){
				return matchCore(str, strIndex, pattern, patternIndex + 2) // 模式后移2,视为x*匹配0个字符
						|| matchCore(str, strIndex + 1, pattern, patternIndex + 2)  // 视为模式匹配1个字符
						|| matchCore(str, strIndex + 1, pattern, patternIndex); // *匹配1个,再匹配str中的下一个
			}else{
				return matchCore(str, strIndex, pattern, patternIndex + 2);
			}
		}
		// 模式第2个不是*,且字符串第1个跟模型第1个匹配,则都后移1位,否则直接返回false
		if((strIndex != str.length && pattern[patternIndex] == str[strIndex])
				|| pattern[patternIndex] == '.' && strIndex != str.length){
			return matchCore(str, strIndex + 1, pattern, patternIndex + 1);
		}
		return false;
	}
}

情况2可以被情况1和情况3包含。执行一次情况3,再执行一次情况1,就相当于情况2。所以上诉代码中的下面这种情况可以删去。

matchCore(str, strIndex + 1, pattern, patternIndex + 2)  // 视为模式匹配1个字符

四 替换空格

题目:请实现一个函数,把字符串中的每个空格替换成 “%20”。 例如输入 “We are happy.”,则输出”We%20are%20happy.”。

1.时间复杂度为O(n^2)的解法

最直观的做法就是从头到尾扫描字符串,每次碰到空格字符的时候就进行替换。由于是把1个字符替换成3个字符,因此必须把空格后面的所有字符都要后移2个字节,否则就会有两个字符被覆盖了。

假设字符串的长度是n,对每个空格字符,需要移动后面O(n)个字符,因此对于含有O(n)个空格字符的字符串而言,总的时间效率是O(n^2)。

public class BlankReplace {
 
	public static String replaceBlank(String input){
		if(input == null){
			return null;
		}
		
		StringBuffer outputBuffer = new StringBuffer();
		for (int i = 0; i < input.length(); i++) {
			if(input.charAt(i) == ' '){				
				outputBuffer.append("%");
				outputBuffer.append("2");
				outputBuffer.append("0");
			}else{
				outputBuffer.append(String.valueOf(input.charAt(i)));
			}	
		}
		return new String(outputBuffer);
	}
	
	public static void main(String[] args) {
		String a = "We are happy.";
		System.out.println(replaceBlank(a));
	}
}

上面的思路是:从前往后扫描,下面我们改变下思路,从后往前扫描。

2.时间复杂度为O(n)的解法

 我们可以先遍历一次字符串,这样就能够统计出字符串中的空格总数,则就可以计算出替换之后的字符串的总长度。每替换一个空格,长度增加2,因此替换以后字符串的长度等于原来的长度加上2乘以空格数目。

我们从字符串的后面开始复制和替换,首先准备两个指针,P1和P2,P1指向原始字符串的末尾,而P2指向替换之后的字符串的末尾。

接下来我们向前移动指针P1,逐个把它指向的字符复制到P2指向的位置,直到碰到空格为止。碰到空格后,把P1向前移动1格,在P2之前插入字符串”%20“,由于”%20“的长度为3,同时也要把P2向前移动3格。后面每次遇到空格后都这样做,直到P1和P2指向同一个位置,表明所有的空格已经替换完毕。

这种从后外向前的替换过程,所有的字符串只复制(移动)一次,因此这个算法的时间效率是O(n)。代码如下:

public class replaceSpace {
 
	public static void replaceSpace(String str){
		if(str == null || str.length() <= 0){
			throw new IllegalArgumentException("输入的参数有问题");
		}
		
		// 字符串的初始长度
		int length = str.length();
		// 字符串替换后的长度
		int newLength = str.length() + getBlankNum(str) * 2;
		
		char[] tempArr = new char[newLength];
		// 将str复制到新的 tempArr 数组中
		System.arraycopy(str.toCharArray(), 0, tempArr, 0, str.toCharArray().length);
		int indexOfOriginal = length - 1;
		int indexOfNew = newLength - 1;
		System.out.println("未替换空格时的字符串:");
		printArray(str.toCharArray());
		
		while(indexOfOriginal >= 0 && indexOfOriginal != indexOfNew){
			if(tempArr[indexOfOriginal] == ' '){
				tempArr[indexOfNew--] = '0';
				tempArr[indexOfNew--] = '2';
				tempArr[indexOfNew--] = '%';
			}else{
				tempArr[indexOfNew--] = tempArr[indexOfOriginal];
			}
			indexOfOriginal--;  // 从后先前遍历
		}
		
		System.out.println("替换后的字符串为:");
		printArray(tempArr);
	}
 
	// 获取空格数
	private static int getBlankNum(String str) {
		int count = 0;   // 空格数 
		for (int i = 0; i < str.length(); i++) {
			// 获取str中下标为i的元素,判断它是否为空格
			String tempStr = String.valueOf(str.charAt(i));
			if(tempStr.equals(" ")){
				count++;
			}
		}
		return count;
	}
	
	// 打印char[]数组
	public static void printArray(char[] arr){
		for(char i : arr){
			System.out.print(i);
		}
		System.out.println();
	}
	
	// 测试
	public static void main(String[] args) {
		String str = "We are happy";
		replaceSpace(str);
	}
}

五 字符串的翻转和旋转及其应用

一、字符串的翻转

1、StringBuilder 实现

可以直接利用 StringBuilder 类的 reverse 方法直接实现。

public class StringReverse {
 
    public static String reverseString(String str){
        if(str == null || str.length() < 1){
            return str;
        }
 
        StringBuilder sb = new StringBuilder(str);
        return sb.reverse().toString();
    }
}

2、双指针实现

public class StringReverse {
 
    public static String reverseString(String str){
        if(str == null || str.length() == 0){
            return str;
        }
        char[] res = str.toCharArray();
        int left = 0;
        int right = str.length() - 1;
        while(left < right){
            char temp = res[left];
            res[left++] = res[right];
            res[right--] = temp;
        }
        return String.valueOf(res);
    }
}

3、字符串翻转应用

LeetCode 151题:https://leetcode-cn.com/problems/reverse-words-in-a-string/

题目:给定一个字符串,逐个翻转字符串中的每个单词。

示例 1:

输入: "the sky is blue"

输出: "blue is sky the"

示例 2:

输入: "  hello world!  "

输出: "world! hello"

解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

示例 3:

输入: "a good   example"

输出: "example good a"

解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

说明:

1、无空格字符构成一个单词;

2、输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括;

3、如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

public class ReverseWords_151 {
 
    public String reverseWords(String s) {
        if(s == null || s.length() < 1){
            return s;
        }
 
        // split("\s+") 按空格,制表符等进行拆分
        String str[] = s.trim().split("\\s+");
        String res = "";
        for(int i = str.length - 1; i > 0; i--){
            res += str[i] + " ";
        }
        // str[0]在这里加是避免在循环中加了后会在新的字符串结尾多一个空格
        return res + str[0];
    }
}

知识点:

  1. \s表示 空格、回车、换行等空白符,+号表示一个或多个的意思;
  2. split("\s+") 按空格、制表符等进行拆分;
  3. split(" +") 按空格进行拆分(也就是说只有按空格键流出来的空白才会是拆分的一句),这里用这个也可以。

Example 1: Input: "Let's take LeetCode contest"

Output: "s'teL ekat edoCteeL tsetnoc"

public class Solution {  
    public String reverseWords(String s) {  
    String[] strs = s.split(" ");  
    StringBuilder sb = new  StringBuilder();  
    for(String str: strs){  
        StringBuilder temp = new StringBuilder(str);  
        sb.append(temp.reverse());  
        sb.append(" ");  
    }  
    // 去掉最后一个空格
    return sb.toString().trim();  
    }  
}

二、字符串的旋转

旋转字符串:给定一个字符串和一个偏移量,根据偏移量旋转字符串(从左向右旋转)。

样例

对于字符串 "abcdefg".

offset = 0 => "abcdefg"

offset = 1 => "gabcdef"

offset = 2 => "fgabcde"

offset = 3 => "efgabcd"

1、三步翻转实现旋转【重点】

三步反转的思维比较巧妙,做法分为三步:

  • 将字符串分为两部分,要移位的 m 个字符 X 和剩下的字符 Y。如 “abcdef”,则 X=“abc”,Y=“def”;
  • 将 X 进行反转,得到 “cba”;将 Y 进行反转,得到 “fed”;
  • 将反转后的 XY 合并字符串在进行反转,最后得到 “defabc”。
public class RotateString {
    /**
     * 字符串旋转
     * @param str :原字符串
     * @param m :要旋转的字符串长度
     * @return
     */
    private static String rotateString(String str, int m){
        char[] chars = str.toCharArray();
        reverseString(chars, 0, m - 1);             // 旋转前半部分
        reverseString(chars, m, str.length() - 1);  // 旋转后半部分
        reverseString(chars, 0, str.length() - 1);  // 旋转整个数组
        return String.valueOf(chars);
    }
 
    private static void reverseString(char[] chars, int left, int right){
        while(left < right){
            char temp = chars[left];
            chars[left++] = chars[right];
            chars[right--] = temp;
        }
    }
    public static void main(String[] args) {
        System.out.println(rotateString("abcdef", 2));  // cdefab
    }
}

2、String 的 substring 方法实现

public class RotateString {
 
    public static String rotateString2(String str, int m){
        if(str == null || str.length() < 1 || m < 0){
            return str;
        }
 
        if(m == str.length() - 1){
            return str;
        }else{
            return str.substring(m, str.length()) + str.substring(0, m);
        }
    }
 
    public static void main(String[] args) {
        System.out.println(rotateString2("abcdef", 3));  // cdefab
    }
}

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值