这个专题中的题目是我跟随代码随想录的刷题计划,在LeetCode上做的与字符串相关的题目,用于加深对字符串的理解!
下面的内容将会有每一道题目的题意、在代码随想录中对应的参考文章、我的思路以及我所写的Java代码,希望对你有帮助!
目录
1 - LeetCode 344 反转字符串 – 双指针法
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reverse-string
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
示例 1:输入:s = [“h”,“e”,“l”,“l”,“o”]
输出:[“o”,“l”,“l”,“e”,“h”]
示例 2:输入:s = [“H”,“a”,“n”,“n”,“a”,“h”]
输出:[“h”,“a”,“n”,“n”,“a”,“H”]
提示:1 <= s.length <= 105 s[i] 都是 ASCII 码表中的可打印字符
思路:
使用双指针来做这道题,注意这道题与《LeetCode 206 反转链表》的区别,《反转链表》中双指针是贴紧一起往右走,这道题的双指针是一个在头一个在尾不断往中间移动。
以下图片很形象地说明了双指针法在本题如何发挥作用:
补充:很显然这种题目其实调用Java内置的库函数就能解决,但是面试时候面试官肯定不是想看你调用库函数,所以像这种题目关键部分直接调用一个库函数就能解决的题目,建议不要用库函数。如果库函数仅仅是解题过程的一部分,并且我们已经很清楚这个库函数的内部实现原理的话,就可以考虑使用库函数。
本题Java代码:
class Solution {
public void reverseString(char[] s) {
int left = 0, right = s.length - 1;
while (right > left) {
char tmp = s[left];
s[left] = s[right];
s[right] = tmp;
right--;
left++;
}
}
}
2 - LeetCode 541 反转字符串 II – 双指针法
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reverse-string-ii
给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。
- 如果剩余字符少于 k 个,则将剩余字符全部反转。
- 如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。
示例 1:输入:s = “abcdefg”, k = 2
输出:“bacdfeg”
示例 2:输入:s = “abcd”, k = 2
输出:“bacd”
提示:1 <= s.length <= 104 s 仅由小写英文组成 1 <= k <= 104
思路:
这道题一开始剩余字符少于k以及大于或等于k那地方看的还有点绕,后来看懂了发现其实就是就是最后一部分反转的字符如果字符个数少于k就全部反转,否则就跟原本的操作一样。所以我们只需要将这个字符串每2k个字符进行处理就行,用一个for循环,然后 i 每次循环加上 2*k 即可,然后最后一个部分判断一下就行了。
至于如何对字符串进行反转,参考《LeetCode 344 反转字符串》这道题,使用双指针来进行。
本题Java代码:
class Solution {
private void reverse(char[] c, int start, int end) {
while (end > start) {
char tmp = c[start];
c[start] = c[end];
c[end] = tmp;
start++;
end--;
}
}
public String reverseStr(String s, int k) {
char[] c = s.toCharArray();
for (int i = 0; i < c.length; i += (2 * k)) {
if (i + k >= c.length)
reverse(c, i, c.length - 1);
else
reverse(c, i, i + k - 1);
}
return new String(c);
}
}
3 - 剑指 Offer 05 替换空格 – 双指针法
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
示例 1:输入:s = “We are happy.”
输出:“We%20are%20happy.”
思路:
非常简单的一道题,只是在参考文章中使用的是C++来做,所以给出的是不需要额外辅助空间的做法,使用的是双指针的操作,即先对数组进行扩容(扩大的容量为原字符串中的空格数*3,因为替换为了“%20”),然后再将前指针从原字符串最后一个字符开始往前移动,后指针从扩容后的字符串最后一个位置开始往前移动,如果前字符指向的不是空格则后指针处直接复制,如果前指针指向的是空格则后指针指向的位置加上"%20"。
以下图片很形象地说明了这个过程(图片转自代码随想录):
本题C++代码(转自代码随想录):
class Solution {
public:
string replaceSpace(string s) {
int count = 0; // 统计空格的个数
int sOldSize = s.size();
for (int i = 0; i < s.size(); i++) {
if (s[i] == ' ') {
count++;
}
}
// 扩充字符串s的大小,也就是每个空格替换成"%20"之后的大小
s.resize(s.size() + count * 2);
int sNewSize = s.size();
// 从后先前将空格替换为"%20"
for (int i = sNewSize - 1, j = sOldSize - 1; j < i; i--, j--) {
if (s[j] != ' ') {
s[i] = s[j];
} else {
s[i] = '0';
s[i - 1] = '2';
s[i - 2] = '%';
i -= 2;
}
}
return s;
}
};
但是由于我习惯写的是Java代码,而由于Java中String类具有不可变性,所以不能实现上述C++代码中一样的操作。既然使用辅助内存是一定需要的事,那么这道题直接借助StringBuilder类就能很简单地完成了。
本题Java代码:
class Solution {
public String replaceSpace(String s) {
StringBuilder ans = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == ' ')
ans.append("%20");
else
ans.append(s.charAt(i));
}
return ans.toString();
}
}
4 - LeetCode 151 翻转字符串里的单词 – 双指针法
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reverse-words-in-a-string
给你一个字符串 s ,逐个翻转字符串中的所有 单词 。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
请你返回一个翻转 s 中单词顺序并用单个空格相连的字符串。
说明:输入字符串 s 可以在前面、后面或者单词间包含多余的空格。
翻转后单词间应当仅用一个空格分隔。
翻转后的字符串中不应包含额外的空格。
示例 1:输入:s = “the sky is blue”
输出:“blue is sky the”
示例 2:输入:s = " hello world "
输出:“world hello”解释:输入字符串可以在前面或者后面包含多余的空格,但是翻转后的字符不能包括。
示例 3:输入:s = “a good example”
输出:“example good a”
解释:如果两个单词间有多余的空格,将翻转后单词间的空格减少到只含一个。
示例 4:
输入:s = " Bob Loves Alice "
输出:“Alice Loves Bob”
示例 5:输入:s = “Alice does not even like bob”
输出:“bob like even not does
Alice”
提示:1 <= s.length <= 104 s 包含英文大小写字母、数字和空格 ’ ’ s 中 至少存在一个 单词
思路:
这道题综合考察对字符串的多种操作,其中包括:去除指定元素、字符串指定区域翻转。
对于使用Java而言,最主要是熟悉StringBuilder类或StringBuffer类的使用。
这道题的思路主要为(以字符串" the sky is blue "为例):
1、移除多余的空格:“the sky is blue”
2、将整个字符串翻转:“eulb si yks eht”
3、将字符串中的单词反转:“blue is sky the”
关于字符串移除元素的操作可以参考《LeetCode 27 移除元素》,虽然这一道题因为使用Java时在此步骤将String类对象转化为StringBuilder类对象,没有使用到《移除元素》中的知识点,不过也可以参考借鉴
关于字符串指定翻转的操作可以参考《LeetCode 344 反转字符串》这道题
本题Java代码:
class Solution {
public String reverseWords(String s) {
StringBuilder sb = removeSpace(s);
reverse(sb, 0, sb.length() - 1);
reverseEachWord(sb);
return sb.toString();
}
// 移除多余空格
private StringBuilder removeSpace(String s) {
int start = 0, end = s.length() - 1;
while (s.charAt(start) == ' ')
start++;
while (s.charAt(end) == ' ')
end--;
StringBuilder sb = new StringBuilder();
while (start <= end) {
char c = s.charAt(start);
if (c != ' ' || sb.charAt(sb.length() - 1) != ' ')
sb.append(c);
start++;
}
return sb;
}
// 反转字符串中指定的区域
private void reverse(StringBuilder sb, int start, int end) {
while (start < end) {
char tmp = sb.charAt(start);
sb.setCharAt(start, sb.charAt(end));
sb.setCharAt(end, tmp);
start++;
end--;
}
}
// 对字符串中的单词进行反转
private void reverseEachWord(StringBuilder sb) {
int start = 0, end = 1;
while (end < sb.length()) {
if (sb.charAt(end) == ' ') {
reverse(sb, start, end - 1);
start = ++end;
end++;
} else
end++;
}
reverse(sb, start, end - 1);// 反转最后一个单词
}
}
5 - 剑指Offer 58 - II 左旋转字符串 – 双指针法
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
示例 1:
输入: s = “abcdefg”, k = 2
输出: “cdefgab”
示例 2:输入: s = “lrloseumgh”, k = 6
输出: “umghlrlose”
限制:1 <= k < s.length <= 10000
这道题如果直接使用String类自带方法substring(int startIndex, int endIndex)是非常容易就解决的,注意substring方法中截取的子串是不包含坐标为endIndex上的子串的。
class Solution {
public String reverseLeftWords(String s, int n) {
String s1 = s.substring(0, n);
String s2 = s.substring(n, s.length());
return s2 + s1;
}
}
看了参考文章,参考文章中使用的是C++语言,可以做到不额外申请空间就完成本题,做法是先将坐标n前的子串反转,再将坐标n后的子串反转,最后再整个字符串反转,这个做法很有意思,下面这个图很形象地说明了这个做法(图转自代码随想录):
由于Java中使用这个做法需要再使用StringBuilder类来实现,没法做到不额外申请内存,且使用的内存空间也没省下多少,时间也反而慢了很多。(上面的提交结果是上述直接使用String类自带方法substring的做法,下面的提交结果是使用参考文章中做法的方法)
不过参考文章这个做法还是很有意思的,以下是参考文章做法的Java实现,字符串的反转使用到了双指针法,参考《LeetCode 344 反转字符串》这道题
本题Java代码:
class Solution {
public String reverseLeftWords(String s, int n) {
StringBuilder sb = new StringBuilder(s);
reverse(sb, 0, n - 1);
reverse(sb, n, sb.length() - 1);
reverse(sb, 0, sb.length() - 1);
return sb.toString();
}
private void reverse(StringBuilder sb, int start, int end) {
while (start < end) {
char tmp = sb.charAt(start);
sb.setCharAt(start, sb.charAt(end));
sb.setCharAt(end, tmp);
start++;
end--;
}
}
}
6 - LeetCode 28 实现strStr() – KMP
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/implement-strstr
实现 strStr() 函数。
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从0 开始)。如果不存在,则返回 -1 。
说明:当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。
对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr() 以及 Java 的 indexOf()定义相符。
示例 1:输入:haystack = “hello”, needle = “ll”
输出:2
示例 2:输入:haystack = “aaaaa”, needle = “bba”
输出:-1
示例 3:输入:haystack = “”, needle = “”
输出:0
提示:0 <= haystack.length, needle.length <= 5 * 104
haystack 和 needle仅由小写英文字符组成
这道题其实就是KMP的模板题,用来学习KMP算法。关于KMP算法的介绍这里我就不写了,可以看参考文章,我觉得已经写的很明白了。
关于前缀表和next数组:
前缀表:记录下标 i 之前(包括 i )的字符串中,有多大长度的相同前缀后缀,因此记录的是长度
next数组:参考文章里说,next数组可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组,这并不涉及到KMP的原理,而是具体实现,next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。
但是我这里要补充的是为什么我考虑使用前缀表统一减一的方式来表示next数组:
其实我一开始也是考虑直接使用前缀表作为next数组,但是发现写起来很不连贯,有点绕。如果next数组的值与前缀表相同,那么next数组表示的是相同前缀后缀的长度,这时候在代码中 j 的初始值为0,j 表示的含义一会儿体现为相同前缀后缀的长度,一会儿表示needle中当前匹配的字符的下标,不够直观
后来我发现,如果next数组的值为前缀表统一减一,这时候 j 的初始值为-1,在这个过程当中,j 始终可以表示为当前在needle中已匹配的字符的下标,写起来思路明朗了很多
本题Java代码:
class Solution {
public int strStr(String haystack, String needle) {
if (needle.length() == 0)
return 0;
int[] next = getNext(needle);
int j = -1;
for (int i = 0; i < haystack.length(); i++) {
while (j >= 0 && haystack.charAt(i) != needle.charAt(j + 1)) {
j = next[j];
}
if (haystack.charAt(i) == needle.charAt(j + 1))
j++;
if (j == needle.length() - 1)
return i - needle.length() + 1;
}
return -1;
}
private int[] getNext(String needle) {
int j = -1;
int[] next = new int[needle.length()];
next[0] = j;
for (int i = 1; i < needle.length(); i++) {
while (j >= 0 && needle.charAt(i) != needle.charAt(j + 1)) {
j = next[j];
}
if (needle.charAt(i) == needle.charAt(j + 1))
j++;
next[i] = j;
}
return next;
}
}
7 - LeetCode 459 重复的子字符串 – KMP
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/repeated-substring-pattern
给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。
示例 1:输入: “abab”
输出: True
解释: 可由子字符串 “ab” 重复两次构成。
示例 2:输入: “aba”
输出: False
示例 3:输入: “abcabcabcabc”
输出: True
解释: 可由子字符串 “abc” 重复四次构成。 (或者子字符串 “abcabc” 重复两次构成。)
思路:
这道题首先要懂得KMP的原理,知道next数组的含义(在本题的实现当中,next数组代表当前最长前缀后缀长度-1,至于为什么-1以及next数组是什么含义,参考《LeetCode 28 实现strStr()》),其次这道题主要是要发现一个规律,就是如果字符串是由子字符串重复构成,那么它的next数组是特别的,以下图为例(图片转自代码随想录)
我们可以发现重复字符串第一次出现时,它对应的next数组元素值都是-1,而在之后重复出现里边,对应的next数组元素值不断+1,如果能理解next数组的含义,出现这个规律的原因是不难理解的,这是因为最大前缀后缀的长度一直都在增大。
红色圈和蓝色圈中的字符串分别代表相同最大前缀和后缀。设len为字符串的长度,那么 len - ( next [ len-1 ] + 1 ) 则表示重复子串的长度,如果满足 len % len - ( next [ len-1 ] + 1 ) == 0,则表示字符串由重复的子串构成
本题Java代码:
class Solution {
public boolean repeatedSubstringPattern(String s) {
int[] next = getNext(s);
int len = s.length();
if (next[len - 1] >= 0 && len % (len - (next[len - 1] + 1)) == 0)
return true;
return false;
}
private int[] getNext(String s) {
int j = -1;
int[] next = new int[s.length()];
next[0] = j;
for (int i = 1; i < s.length(); i++) {
while (j >= 0 && s.charAt(i) != s.charAt(j + 1))
j = next[j];
if (s.charAt(i) == s.charAt(j + 1))
j++;
next[i] = j;
}
return next;
}
}