字符串
344. 反转字符串
class Solution {
public void reverseString(char[] s) {
for(int i=0; i<s.length/2; i++){
char t = s[i];
s[i] = s[s.length- 1 - i];
s[s.length- 1 - i] = t;
}
}
}
541. 反转字符串 II
初步设计:笨逼的数学方法
class Solution {
public String reverseStr(String s, int k) {
char[] c = s.toCharArray();
int tail = s.length()-1;
boolean flag =false;
for(int i = 0 ; i < s.length(); i++){
int flayer = i/(2*k);
int index = i%(2*k);
if(index < k/2){
int left = i;
int right = 2*k*flayer+k-1-index;
if(right >s.length()-1){
flag = true;
}
if(flag){
right = tail--;
if(right <= left){
break;
}
}
//right = right < s.length()-1? right: s.length()-index-1 ;
char temp = c[left];
c[left] = c[right];
c[right] = temp;
}
}
return String.valueOf(c);
}
}
用两个循环,还快,1ms
// 解法二还可以用temp来交换数值,会的人更多些
class Solution {
public String reverseStr(String s, int k) {
char[] ch = s.toCharArray();
for(int i = 0;i < ch.length;i += 2 * k){
int start = i;
// 判断尾数够不够k个来取决end指针的位置
int end = Math.min(ch.length - 1,start + k - 1);
while(start < end){
char temp = ch[start];
ch[start] = ch[end];
ch[end] = temp;
start++;
end--;
}
}
return new String(ch);
}
}
标记right/left2
class Solution {
public void reverse(char[] arr, int left, int right) {
while (left < right) {
char temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
}
public String reverseStr(String s, int k) {
char[] s1 = s.toCharArray();
for(int left = 0;left < s.length();left+= 2*k){
int right = left + k -1;
if(right > s.length()-1){
right = s.length()-1;
}
reverse(s1,left,right);
}
return new String(s1);
}
}
剑指 Offer 05. 替换空格
初步思想:利用 StringBuffer
class Solution {
public String replaceSpace(String s) {
char[] s1 = s.toCharArray();
StringBuffer temp = new StringBuffer();
for(int i=0; i<s1.length; i++){
if(s1[i] == ' ') {
temp.append("%20");
}else{
temp.append(s1[i]);
}
}
return temp.toString();
}
}
双指针法
在java中多此一举,并不能节省更多的空间。
从后往前走,不然复杂度为O(n^2),因为后续元素都要移动
//方式二:双指针法
public String replaceSpace(String s) {
if(s == null || s.length() == 0){
return s;
}
//扩充空间,空格数量2倍
StringBuilder str = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if(s.charAt(i) == ' '){
str.append(" ");
}
}
//若是没有空格直接返回
if(str.length() == 0){
return s;
}
//有空格情况 定义两个指针
int left = s.length() - 1;//左指针:指向原始字符串最后一个位置
s += str.toString();
int right = s.length()-1;//右指针:指向扩展字符串的最后一个位置
char[] chars = s.toCharArray();/
while(left>=0){
if(chars[left] == ' '){
chars[right--] = '0';
chars[right--] = '2';
chars[right] = '%';
}else{
chars[right] = chars[left];
}
left--;
right--;
}
return new String(chars);
}
151. 颠倒字符串中的单词
初步设计:借助顺序表
利用StringBuffer取各个单词,再储存到List里面从后往前取。
时间复杂度O(n),空间复杂度O(n)
class Solution {
public String reverseWords(String s) {
char[] s1 = s.toCharArray();
StringBuffer temp = new StringBuffer();
List<String> sequence = new ArrayList<>();
for(int i=0; i<s1.length; i++){
if(s1[i] == ' ') {
if(temp.length() != 0) {
sequence.add(temp.toString());
}
temp.delete( 0,temp.length());
}else{
temp.append(s1[i]);
}
}
if(temp.length()!=0){
sequence.add(temp.toString());
}
temp.delete( 0,temp.length());
for(int i = sequence.size() -1 ; i >0; i--){
temp.append(sequence.get(i));
temp.append(" ");
}
temp.append(sequence.get(0));
return temp.toString();
}
}
改进判断写法:
for(int i=0; i<s1.length; i++){
if(s1[i] == ' ' && temp.length() != 0) {
sequence.add(temp.toString());
temp.delete( 0,temp.length());
}else if(s1[i] != ' '){
temp.append(s1[i]);
}
}
第二次写※:
class Solution {
public String reverseWords(String s) {
char[] c = s.toCharArray();
StringBuilder builder = new StringBuilder();
List<String> list = new ArrayList();
String result = new String();
for(int i = 0; i< c.length ; i++){
if(c[i] != ' '){
builder.append(c[i]);
}
else if( (i> 0&& c[i-1] != ' ' ) ){
list.add(builder.toString());
builder.delete(0,builder.length());
}
}
if(builder.length()!= 0){
list.add(builder.toString());
builder.delete(0,builder.length());
}
for(int i = list.size()-1; i>0; i--){
builder.append(list.get(i));
builder.append(' ');
}
builder.append(list.get(0));
return builder.toString();
}
}
方法一:使用语言特性
部分正则表达式:
符号 | 作用 |
---|---|
\s | 匹配任何空白字符 包括指标符 换行符 。等价于 [ \f\n\r\t\v] |
\S | 匹配任何非空白字符。等价于 [^ \f\n\r\t\v。 |
+ | 如\d+ 匹配前面的子表达式一次或多次。 |
部分用到的函数:
String trim() | 返回字符串的副本,忽略前导空白和尾部空白。 |
---|---|
static String join(CharSequence delimiter, CharSequence… elements) | 返回由 CharSequence elements的副本组成的新String,该副本与指定的 delimiter的副本连接在一起。 |
String[] split(String regex) | 根据给定正则表达式的匹配拆分此字符串。 |
代码:
class Solution {
public String reverseWords(String s) {
// 除去开头和末尾的空白字符
s = s.trim();
// 正则匹配连续的空白字符作为分隔符分割
List<String> wordList = Arrays.asList(s.split("\\s+"));
Collections.reverse(wordList);
return String.join(" ", wordList);
}
}
时间复杂度O(n),空间复杂度O(n)
方法二:自行编写对应的函数
时间复杂度O(n),空间复杂度O(n)
class Solution {
public String reverseWords(String s) {
//去空格
StringBuilder sb = trimSpaces(s);
// 翻转字符串
reverse(sb, 0, sb.length() - 1);
// 翻转每个单词
reverseEachWord(sb);
return sb.toString();
}
public StringBuilder trimSpaces(String s) {
int left = 0, right = s.length() - 1;
// 去掉字符串开头的空白字符
while (left <= right && s.charAt(left) == ' ') {
++left;
}
// 去掉字符串末尾的空白字符
while (left <= right && s.charAt(right) == ' ') {
--right;
}
// 将字符串间多余的空白字符去除
StringBuilder sb = new StringBuilder();
while (left <= right) {
char c = s.charAt(left);
if (c != ' ') {
sb.append(c);
} else if (sb.charAt(sb.length() - 1) != ' ') {//上个位置不是空格,就可以填
sb.append(c);
}
++left;
}
return sb;
}
public void reverse(StringBuilder sb, int left, int right) {
while (left < right) {
char tmp = sb.charAt(left);
sb.setCharAt(left++, sb.charAt(right));
sb.setCharAt(right--, tmp);
}
}
public void reverseEachWord(StringBuilder sb) {
int n = sb.length();
int start = 0, end = 0;
while (start < n) {
// 循环至单词的末尾
while (end < n && sb.charAt(end) != ' ') {
++end;
}
// 翻转单词
reverse(sb, start, end - 1);
// 更新start,去找下一个单词
start = end + 1;
++end;
}
}
}
方法三:双端队列
与初步设计思想一致,都是借助额外空间最终实现逆向访问。
利用队列将元素放在头部实现逆序
时间复杂度O(n),空间复杂度O(n)
class Solution {
public String reverseWords(String s) {
int left = 0, right = s.length() - 1;
// 去掉字符串开头的空白字符
while (left <= right && s.charAt(left) == ' ') {
++left;
}
// 去掉字符串末尾的空白字符
while (left <= right && s.charAt(right) == ' ') {
--right;
}
Deque<String> d = new ArrayDeque<String>();
StringBuilder word = new StringBuilder();
while (left <= right) {
char c = s.charAt(left);
if ((word.length() != 0) && (c == ' ')) {
// 将单词 push 到队列的头部
d.offerFirst(word.toString());
word.setLength(0);
} else if (c != ' ') {
word.append(c);
}
++left;
}
d.offerFirst(word.toString());
return String.join(" ", d);
}
}
剑指 Offer 58 - II. 左旋转字符串
初步设计:简单的StringBuffer
class Solution {
public String reverseLeftWords(String s, int n) {
StringBuilder sequence = new StringBuilder();
char[] s1 = s.toCharArray();
for(int i= n;i<s1.length;i++){
sequence.append(s1[i]);
}
for(int i = 0; i<n ; i++){
sequence.append(s1[i]);
}
return sequence.toString();
}
}
字符串切片
class Solution {
public String reverseLeftWords(String s, int n) {
return s.substring(n, s.length()) + s.substring(0, n);
}
}
局部反转+整体反转
具体步骤为:
- 反转区间为前n的子串
- 反转区间为n到末尾的子串
- 反转整个字符串
class Solution {
public String reverseLeftWords(String s, int n) {
int len=s.length();
StringBuilder sb=new StringBuilder(s);
reverseString(sb,0,n-1);
reverseString(sb,n,len-1);
return sb.reverse().toString();
}
public void reverseString(StringBuilder sb, int start, int end) {
while (start < end) {
char temp = sb.charAt(start);
sb.setCharAt(start, sb.charAt(end));
sb.setCharAt(end, temp);
start++;
end--;
}
}
}
28. 实现 strStr()
class Solution {
public int strStr(String haystack, String needle) {
char[] s1 = haystack.toCharArray();
char[] s2 = needle.toCharArray();
if(s2.length == 0){
return 0;
}
for(int i = 0;i<s1.length;i++){
int k = i;
int j = 0;
while(k<s1.length && j<s2.length && s1[k] == s2[j] ){
k++;
j++;
}
if(j == s2.length){
return i;
}
}
return -1;
}
}
第二种,一样O(m * n)
class Solution {
public int strStr(String haystack, String needle) {
char[] big = haystack.toCharArray();
char[] small = needle.toCharArray();
int fastIndex = 0;
int slowIndex = 0;
while(fastIndex<big.length){
if(big[fastIndex] == small[slowIndex]){
slowIndex++;
fastIndex++;
if(slowIndex == small.length){
return fastIndex - slowIndex;
}
}else{
fastIndex = fastIndex-slowIndex+1;
slowIndex = 0;
}
}
return -1;
}
}
KMP算法
1.前缀表
前缀表要求的就是相同前后缀的长度。
2.利用前缀表匹配
next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。
3.构建next数组
1.初始化
int j = -1;
next[0] = j;
2.处理前后缀不相同情况
找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。
为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。所以要看前一位的 前缀表的数值。
前一个字符的前缀表的数值是2, 所以把下标移动到下标2的位置继续比配。 最后就在文本串中找到了和模式串匹配的子串了。
因为j初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较。
所以遍历模式串s的循环下标i 要从 1开始,代码如下:
for (int i = 1; i < s.size(); i++) {
如果 s[i] 与 s[j+1]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退。
怎么回退呢?next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。
那么 s[i] 与 s[j+1] 不相同,就要找 j+1前一个元素在next数组里的值(就是next[j])。
所以,处理前后缀不相同的情况代码如下:
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
3.处理前后缀相同的情况
如果 s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。
代码如下:
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j;
最后整体构建next数组的函数代码如下:
void getNext(int* next, const string& s){
int j = -1;
next[0] = j;
for(int i = 1; i < s.size(); i++) { // 注意i从1开始
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
代码构造next数组的逻辑流程动画如下:
如果不减一,直接和j比较
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作
j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
4.使用next数组来做匹配
i就从0开始,遍历文本串,代码如下:
for (int i = 0; i < s.size(); i++)
接下来就是 s[i] 与 t[j + 1] (因为j从-1开始的) 进行比较。
如果 s[i] 与 t[j + 1] 不相同,j就要从next数组里寻找下一个匹配的位置。
代码如下:
while(j >= 0 && s[i] != t[j + 1]) {
j = next[j];
}
如果 s[i] 与 t[j + 1] 相同,那么i 和 j 同时向后移动, 代码如下:
if (s[i] == t[j + 1]) {
j++; // i的增加在for循环里
}
如何判断在文本串s里出现了模式串t呢,如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。
那么使用next数组,用模式串匹配文本串的整体代码如下:
int j = -1; // 因为next数组里记录的起始位置为-1
for (int i = 0; i < s.size(); i++) { // 注意i就从0开始
while(j >= 0 && s[i] != t[j + 1]) { // 不匹配
j = next[j]; // j 寻找之前匹配的位置
}
if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t
return (i - t.size() + 1); //模式串出现的第一个位置 (从0开始)
}
}
5.前缀表减一全实现
class Solution {
private int[] getNext(String needle){
//初始化
int n = needle.length();
int[] next = new int[n];
next[0] = -1;
//开始构造
for(int i = 1,j = -1; i < n; i++){ // 注意i从1开始
//前后缀不相同了,匹配失败,调整j位置
while(j >= 0 && needle.charAt(j+1) != needle.charAt(i)){
j = next[j]; // 向前回退
}
//找到相同的前后缀,匹配成功
if(needle.charAt(j+1) == needle.charAt(i)){
j++;
}
next[i] = j;// 将j(前缀的长度)赋给next[i]
}
return next;
}
public int strStr(String haystack, String needle) {
int m = haystack.length();
int n = needle.length();
if(n == 0){
return 0;
}
int[] next = getNext(needle);
// 因为next数组里记录的起始位置为-1,注意i就从0开始
for(int i = 0, j = -1; i< m ;i++){
//不匹配
while(j>= 0 && haystack.charAt(i) != needle.charAt(j+1)){
j = next[j];// j 寻找之前匹配的位置
}
//匹配,i与j同时移动
if(haystack.charAt(i) == needle.charAt(j+1)){
j++;
}
//判断是否满足全匹配
if(j == n-1){
return(i - j);//模式串出现的第一个位置 (从0开始)
}
}
return -1;
}
}
注意,将这两个字符串的长度m与n存储,在循环和判断时不用再次调用length方法,时间从1ms降为0ms。
合并函数
class Solution {
public int strStr(String haystack, String needle) {
int m = haystack.length();
int n = needle.length();
if(n == 0){
return 0;
}
//初始化
int[] next = new int[n];
next[0] = -1;
//开始构造
for(int i = 1,j = -1; i < n; i++){
//匹配失败,调整j位置
while(j >= 0 && needle.charAt(j+1) != needle.charAt(i)){
j = next[j];
}
//匹配成功
if(needle.charAt(j+1) == needle.charAt(i)){
j++;
}
next[i] = j;
}
for(int i = 0, j = -1; i< m ;i++){
//不匹配
while(j>= 0 && haystack.charAt(i) != needle.charAt(j+1)){
j = next[j];
}
//匹配,i与j同时移动
if(haystack.charAt(i) == needle.charAt(j+1)){
j++;
}
//判断是否满足全匹配
if(j == n-1){
return(i - j);
}
}
return -1;
}
}
459. 重复的子字符串
1.暴力解法
子串与父串判断,O(n^2)
class Solution {
public boolean repeatedSubstringPattern(String s) {
int m = s.length();
for(int i = 0; i < m/2; i++){// m/2,判断到中间,再往后整个字符串肯定能构成自己
if(m%(i+1) == 0){
//substring(m,n),m代表想要截取的字符串中初始位置的下角标
//n代表截取至字符串下角标为n的位置(不包括n,m≤n≤str.length)
String sub = s.substring(0,i+1);
if(ifSubString(s,sub)){
return true;
}
}
}
return false;
}
public boolean ifSubString(String s, String sub){
int m = s.length();
int n = sub.length();
for(int i = 0; i < m ; i++){
if(s.charAt(i) != sub.charAt(i%n)){
return false;
}
}
return true;
}
}
第二种优化,依然是O(n^2)但快不少
class Solution {
public boolean repeatedSubstringPattern(String s) {
int n = s.length();
for (int i = 1; i * 2 <= n; ++i) {
if (n % i == 0) {
boolean match = true;
for (int j = i; j < n; ++j) {
if (s.charAt(j) != s.charAt(j - i)) {//每个重复与上一个重复比较!
match = false;
break;
}
}
if (match) {
return true;
}
}
}
return false;
}
}
2.KMP算法
求最大相同前缀后缀next表
class Solution {
public boolean repeatedSubstringPattern(String s) {
//剪枝
if(s.isEmpty()){
return false;
}
//构造next数组
int length = s.length();
int[] next = new int[length];
next[0] = -1;
char[] chars = s.toCharArray();
for(int i = 1,j = -1; i < length; i++){ // 注意i从1开始
while(j >= 0 && chars[j+1] != chars[i]){
j = next[j]; // 向前回退
}
if(chars[j+1] == chars[i]){
j++;
}
next[i] = j;// 将j(前缀的长度)赋给next[i]
}
//最长前后缀
int max = next[length-1] +1;
//存在重复情况下的一个周期长度
int circle = length - max;
//最后判断是否是重复的子字符串
if(max > 0 && length % circle == 0){
return true;
}
return false;
}
}
注意,构造chars数组,chars[i]比 s.charAt(i) 快很多,从8ms将为4ms