字符串翻转相关问题主要涉及字符串整体的翻转(Leetcode 344),字符串间隔分组翻转(Leetcode 541),字符串中单词词序翻转(Leetcode 151),以及字符串的旋转(Leetcode 剑指Offer58-II)。
经典题目:Leetcode 344 。建议不直接使用库函数,可以采用前后双指针法,交换字符直至相遇。对于交换过程的操作既可以采用temp储存法,也可以采用异或法。利用异或的性质:a ^ (a ^ b) = b,进行位运算,避免引入temp,其原理:a ^ (a ^ b) = (a ^ a) ^ b,又因为a ^ a = 0,原式可以简化成0 ^ b = b,但异或法除了装逼没有其他用更耗时。
//假设交换前s[i] = a, s[j] = b
s[i] ^= s[j];//此时s[i] = a ^ b, s[j] = b
s[j] ^= s[i];//此时s[i] = a ^ b, s[j] = a
s[i] ^= s[j];//此时s[i] = b, s[j] = a, 完成交换
//Leetcode344
//时间复杂度:O(n)。
//空间复杂度:O(1)。
public void reverseString(char[] s) {
int i = 0;
int j = s.length - 1;
//利用异或的性质a^(a^b) = b,进行位运算,避免引入temp
//推导:a^(a^b)=(a^a)^b,又因为a^a=0,原式可以简化成0^b=b。
while(i < j){
//假设交换前s[i] = a, s[j] = b
s[i] ^= s[j];//此时s[i] = a ^ b, s[j] = b
s[j] ^= s[i];//此时s[i] = a ^ b, s[j] = a
s[i] ^= s[j];//此时s[i] = b, s[j] = a, 完成交换
i++;
j--;
}
}
经典题目:Leetcode 541。需要做到每2k个字符一组,翻转组内前k个字符。做最后一组不足k,则全部翻转。只需要将边界移动周期设定为2k即可,注意最后一次交换的特殊情况。
//Leetcode541
//时间复杂度:O(n)。
//空间复杂度:O(n)。
public String reverseStr(String s, int k) {
int n = s.length();
char[] cS = s.toCharArray();
int left = 0;
int right = k - 1;
int i, j;
char temp;
while (left < n){
i = left;
j = Math.min(right, n - 1);
while(i < j){
//change
temp = cS[i];
cS[i++] = cS[j];
cS[j--] = temp;
}
left += 2 * k;
right += 2 * k;
}
return new String(cS);
}
经典题目:Leetcode 151。将字符串中单词词序翻转,但是不翻转一个单词中的字母序,因此可以在去除前后空格之后,先将整个字符串翻转(Leetcode344),再将每个单词翻转。或采取模拟的方法,新建一个长度为去除掉首尾空格和内部重复空格后的res数组用于存放最终答案。从前向后遍历原始字符数组,将每个单词从后向前地存放如新的res数组。注意:填充res时,从后向前填充单词,但是对于每一个单词内字母的填充,仍然从前向后填充。
//Leetcode151
//时间复杂度:O(n)。
//空间复杂度:O(n)。
public String reverseWords(String s) {
if(s.length() <= 1){
return s;
}
char[] cS = s.toCharArray();
int i = 0;
while(cS[i] == ' '){
i++;//将i定位到第一个非空格字符
}
int j = s.length() - 1;
while(cS[j] == ' '){
j--;//将j定位到最后一个非空格字符
}
int len = j - i + 1;
for(int k = i; k <= j; k++){
if(cS[k] == ' ' && cS[k + 1] == ' '){
//内部有连续空格的,只计数一次。
len--;
}
}
//此时len为真正的新串长度
char[] res = new char[len];
int left = i;
int right = i;
int count = 0;
int temp = len - 1;
while(true){
right = left;
while(right <= j && cS[right] != ' '){
right++;//right最终移动至本单词的后一个空格位置
}
count = right - left;//count记录本单词长度
if(right > j){
break;//操作最后一个单词时会出现该情况
}
//填充res时,从后向前填充单词,但是对于每一个单词内字母的填充,仍然从前向后填充。
for(int k = count - 1; k >= 0; k--){
res[temp - k] = cS[left++];
}
res[temp - count] = ' ';
temp = temp - count - 1;
while(cS[left] == ' '){
left++;//过滤掉内部空格
}
}
//填充最后一个单词
int l = 0;
while(left < right){
res[l++] = cS[left++];
}
return new String(res);
}
经典题目:Leetcode 剑指Offer58-II,左旋字符串n位,可以采用多次字符串翻转进行。可以证明,通过:
① 将字符串前n个字符翻转,即下标范围在[0, n - 1]的子字符串翻转。
② 将字符串后部分翻转,即下标范围在[n, s.length() - 1]的子字符串翻转。
③ 将整个字符串翻转,即下标范围在[0, s.length() - 1]的字符串翻转。
此方法只需要对char[]数组进行操作即可,不需要创建StringBuilder对象,节省时间。
//Leetcode 剑指Offer58-II
//时间复杂度:O(n)。
//空间复杂度:O(n)。
public String reverseLeftWords(String s, int n) {
//翻转前n个字符,翻转后字符,再翻转整个字符串。
int len = s.length();
char[] cS = s.toCharArray();
reverseByIndex(cS, 0, n - 1);
reverseByIndex(cS, n, len - 1);
reverseByIndex(cS, 0, len - 1);
return new String(cS);
}
public static void reverseByIndex(char[] cS, int start, int end){
char temp;
for(int i = start, j = end; i < j; i++, j--){
temp = cS[i];
cS[i] = cS[j];
cS[j] = temp;
}
}
将字符串中的空格替换为"%20",经典题目:Leetcode 剑指Offer 05.替换空格。需要通过一次遍历确定原串中的空格数量,来确定新串的容量。新串填充过程中,需要在遇到原串空格时,改填充为’%', ‘2’, ‘0’ 三个字符。
//Leetcode 剑指Offer05
//时间复杂度:O(n)。
//空间复杂度:O(n)。
public String replaceSpace(String s) {
int n = s.length();
char[] cS = s.toCharArray();
StringBuilder sb = new StringBuilder();
for(int i = 0; i < n; i++){
if(cS[i] == ' '){
sb.append(" ");
}
}
s += sb.toString();
cS = s.toCharArray();
int len = s.length();
int left = n - 1;
int right = len - 1;
while(left >= 0){
if(cS[left] == ' '){
cS[right--] = '0';
cS[right--] = '2';
cS[right--] = '%';
}else{
cS[right--] = cS[left];
}
left--;
}
return new String(cS);
}
算法中next数组的求解主要分为三部分:回退、自增、赋值
① 当left大于0并且匹配不成功时,left需要回退至匹配成功或回到0,回退操作原理参考题解(反复多看)。
多图预警详解 KMP 算法 - 实现 strStr() - 力扣(LeetCode)
while(left > 0 && p.charAt(left) != p.charAt(right)){
left = next[left - 1];
}
② 当匹配成功时,left自增继续匹配
if(p.charAt(left) == p.charAt(right)){
left++;
}
③ 每一次外层循环中,都要对next[right]赋值,其值为进行过①②操作之后的left值。
next[right] = left;
算法中匹配过程主要含有三部分:跳转、自增、检验
① 匹配不成功则根据next数组**跳转 **
while(j > 0 && s.charAt(i) != p.charAt(j)){
j = next[j - 1];
}
② 匹配成功自增
if(s.charAt(i) == p.charAt(j)){
j++;
}
③ 时刻检验是否匹配完毕子串
if(j == pLen){
return i - j + 1;
}
经典题目:Leetcode28。
//Leetcode28
//时间复杂度:O(n × m)。
//空间复杂度:O(1)。
public int strStr(String haystack, String needle) {
String s = haystack;
String p = needle;
int sLen = s.length();
int pLen = p.length();
if(pLen == 0){
return -1;
}
//获取next数组
int[] next = new int[pLen];
//next[0] = 0;
for(int left = 0, right = 1; right < pLen; right++){
while(left > 0 && p.charAt(left) != p.charAt(right)){
left = next[left - 1];
}
if(p.charAt(left) == p.charAt(right)){
left++;
}
next[right] = left;
}
//进行KMP搜索:内含三部分:1)匹配不成功则根据next数组跳转 2)匹配成功后移 3)时刻检验是否匹配完毕子串
for(int i = 0, j = 0; i < sLen; i++){
while(j > 0 && s.charAt(i) != p.charAt(j)){
j = next[j - 1];
}
if(s.charAt(i) == p.charAt(j)){
j++;
}
if(j == pLen){
return i - j + 1;
}
}
return -1;
}
经典题目:Leeetcode459,检查一个字符串是否可以通过由它的一个子串重复多次构成。考虑如果字符串可以由它的一个子串重复构成,即字符串中存在重复的子串。将字符串进行左旋(右旋也可),旋转次数在1 ~ s.length() - 1次范围内,总能与原字符串相同(匹配)。考虑到进行旋转并遍历耗时严重,故引入新的字符串为原串与原串的相加,即String str = s + s
,在新的str中,从第1位到第str.length() - 2位的子串中,包含了原串左旋1 ~ s.length() - 1次的结果,故只需要在新的str从第1位到第str.length() - 2位的子串中匹配出原串即可说明满足题意。涉及字符串匹配,采用KMP,注意:此处传入KMP方法的参数列表需要增加起始下标和结束下标,即此处必须在str的第1位到第str.length() - 2位的子串中匹配。
//Leetcode459
//时间复杂度:O(n)。
//空间复杂度:O(n)。
public boolean repeatedSubstringPattern(String s) {
String sCopy = s + s;
return indexKMP(sCopy, s, 1, sCopy.length() - 1);
}
//在字符串s的[start, end)子串中匹配p
public boolean indexKMP(String s, String p, int start, int end){
int sLen = end - start;
int pLen = p.length();
char[] sC = s.toCharArray();
char[] pC = p.toCharArray();
if(pLen == 0){
return false;
}
//获取next数组
int[] next = new int[pLen];
//next[0] = 0
for(int left = 0, right = 1; right < pLen; right++){
while(left > 0 && pC[left] != pC[right]){
left = next[left - 1];
}
if(pC[left] == pC[right]){
left++;
}
next[right] = left;
}
//KMP匹配
for(int i = start, j = 0; i < end; i++){
if(j > 0 && pC[j] != sC[i]){
j = next[j - 1];
}
if(pC[j] == sC[i]){
j++;
}
if(j == pLen){
return true;
}
}
return false;
}