四、字符串算法学习(代码随想录学习)

1. 反转字符串

leetcode链接

思路: 可以利用reverse函数直接反转,也可使用双指针从边界循环交换元素进行反转

class Solution {
public:
	void reverseString(vector<char>& s) {
		int l = 0 , r = s.size() - 1;
		while (l < r)
			swap(s[l++],s[r--]);
	}
};

时间复杂度: O(n)

2. 反转字符串Ⅱ

leetcode链接
思路: 判断剩余字符串的长度,进行不同的反转字符串的操作

class Solution {
public:
	string reverseStr(string s, int k) {
		int length = s.size();  // 记录字符串剩余长度
		// index记录当前操作的起始下标
		int index = 0,l = 0,r = length -1;
		while (length > 0) {
			l = index;
			if (length >= 2 * k)
				r = index + k - 1;
			else if (length >= k)
				r = index + k - 1;
			else
				r = s.size() - 1;
			while (l < r)
				swap(s[l++], s[r--]);
			length -= 2 * k;
			index += 2 * k;
		}
		return s;
	}
};

时间复杂度: O(n)

3. 替换数字

题目链接
思路: 循环遍历,将数字进行替换即可。若使用空间扩充,则不需要开辟辅助数组。若对新字符串从后往前进行替换,则在O(n)的时间复杂度即可完成,而从前往后进行替换,每次需向后移动字符,需O(n²)的复杂度。

#include<iostream>
using namespace std;

int main(){
    string s;
    while(cin >> s){
        int count = 0; // 统计数字字符的数量
        int index = s.size() - 1 ;
        for(int i = 0; i < s.size();i++){
            if(s[i] >= '0' && s[i] <= '9')
            count++;
        }
        // 将s进行扩充
        s.resize(s.size() + 5 * count);
        int newIndex = s.size() - 1;
        while(newIndex >= 0){
            if(s[index] >= '0' && s[index] <= '9'){
                s[newIndex--] = 'r';
                s[newIndex--] = 'e';
                s[newIndex--] = 'b';
                s[newIndex--] = 'm';
                s[newIndex--] = 'u';
                s[newIndex--] = 'n';
            }
            else
                s[newIndex--] = s[index];
            index--;
        }
        cout << s << endl;
    }
    
}

时间复杂度: O(n)
空间复杂度: O(1)

4.反转字符串中的单词

leetcode链接
思路: 先去除多余空格,将字符串转变为标准的字符串。然后将整个字符串反转,再将每个单词反转,即可得到符合条件的字符串。

去除多余空格: 使用双指针的方法去除,不会开辟新的空间
版本一:使用一个布尔变量标记是否需要添加空格,然后最后修正字符串长度。

void removeExtraSpaces(string &s) {
		// 定义快慢指针,去除多余空格
		int fast = 0, slow = 0;
		// 表示是否需要添加空格 ,默认为false可去除串首的空格
		// 若字符串末尾有空格,则末尾会保留一位空格
		bool need = false; 
		for (; fast < s.size(); fast++) {
			if (s[fast] != ' ') {
				s[slow++] = s[fast];
				need = true;
				continue;
			}
			if (need) {
				s[slow++] = ' ';
				need = false;
			}
		}
		// 重设字符串大小
		if (s[slow - 1] == ' ')
			s.resize(slow - 1);
		else
			s.resize(slow);
	}

版本二:也是双指针的方法移除空格,去除所有空格,然后在单词间手动添加一个空格

void removeExtraSpaces(string &s) {
		int slow = 0;
		for (int i = 0; i < s.size(); ++i) {
			if (s[i] != ' ') { // 当碰到单词开头时
				if (slow != 0) // 且不为第一个单词时
					s[slow++] = ' '; // 手动添加一个空格,保证每个单词间仅有一个空格
				while (s[i] != ' ' && i < s.size())  // 将后续单词写入由慢指针写入
					s[slow++] = s[i++];
			}
		}
		s.resize(slow);
	}

总体代码为:

class Solution {
public:
	void reverse(string &s, int start, int end) {  // 翻转函数(选定范围的翻转)
		for (int i = start, j = end; i < j; i++, j--) {
			swap(s[i], s[j]);
		}
	}

	void removeExtraSpaces(string &s) {
		int slow = 0;
		for (int i = 0; i < s.size(); ++i) {
			if (s[i] != ' ') { // 当碰到单词开头时
				if (slow != 0) // 且不为第一个单词时
					s[slow++] = ' '; // 手动添加一个空格,保证每个单词间仅有一个空格
				while (s[i] != ' ' && i < s.size())  // 将后续单词写入由慢指针写入
					s[slow++] = s[i++];
			}
		}
		s.resize(slow);
	}

	string reverseWords(string s) {
		removeExtraSpaces(s);// 移除多余空格
		reverse(s, 0, s.size() - 1); // 翻转字符串
		int start = 0; // 每个单词的起始下标
		for (int i = 0; i <= s.size(); i++) {
			if (i == s.size() || s[i] == ' ') {
				reverse(s, start, i - 1);
				start = i + 1; // 更新start
			}
		}
		return s;
	}
};

时间复杂度: O(n)
空间复杂度: O(1)

5. 右旋转字符串

题目链接

思路1: 可创建一个与输入字符串同等长度的字符数组,将字符按要求填入新的字符数组,空间复杂度为O(n)

#include <iostream>
#include<vector>
#include<algorithm>
#include<string>
using namespace std;
 
void reverse(string &s,int start ,int end) {
    for (int i = start,j = end; i < j; i++, j--)
        swap(s[i], s[j]);
}
 
int main() {
    int k;
    string s;
    cin >> k;
    cin >> s;
    reverse(s, 0, s.size() - 1);
    reverse(s, 0, k - 1);
    reverse(s, k, s.size() - 1);
    cout << s;
}

思路2: 不开辟新的空间,在原字符串上进行操作。先将整体字符串进行反转,在根据k的值将字符串分为两部分分别再反转,即可得到所需字符串。

#include <iostream>
#include<vector>
#include<algorithm>
#include<string>
using namespace std;

void reverse(string &s,int start ,int end) {
	for (int i = start,j = end; i < j; i++, j--)
		swap(s[i], s[j]);
}

int main() {
	int k;
	string s;
	cin >> k;
	cin >> s;
	reverse(s, 0, s.size() - 1);
	reverse(s, 0, k - 1);
	reverse(s, k, s.size() - 1);
	cout << s;
}

时间复杂度: O(n)
空间复杂度: O(1)

6. 实现strStr()

leetcode链接

思路: KMP算法,实现next数组与匹配字符串

next数组实现: 本题使用最原始的next数组,即next[i]表示 i 以前(包括i)的最长公共前后缀
用 j 指向前缀的末尾,用 i 指向后缀的末尾,匹配 i 的next值
在这里插入图片描述
A1与A2为 i - 1 最长公共前后缀,求 i 的最长前后缀
当 i 与 j指向的元素匹配时,next[ i ] = next[ i - 1 ] (即A1的 长度) + 1

// 当前前后缀相匹配(i与j前的k位匹配的字符串),则新的公共前后缀为k+1
	if (s[i] == s[j]) { 
		next[i] = j + 1;
		i++;
		j++;
	}

当不匹配时, 将指针j移动到A1部分的公共前后缀的位置,即B1后的位置,递归循环此操作,直到匹配成功或j == 0

	if (j > 0)
		j = next[j - 1];
	else {
		next[i] = 0;
		i++;
	}

字符串匹配的过程: 两指针j 和 i分别指向匹配串和原串,依次循环匹配并移动两指针。当两指针不相匹配时 , 根据next[ j ] 的值移动 j 指针,直接匹配成功或遍历完整个原串。

整体代码:

class Solution {
public:
	void getnext(vector <int> &next,const string &s) {
		// next[i]表示i以前(包括i)的最长匹配前后缀
		next[0] = 0;
		int j = 0, i = 1; // j表示前缀的尾指针,i表示后缀的尾指针
		while (i < s.size()) {
			// 当前前后缀相匹配(i与j前的k位匹配的字符串),则新的公共前后缀为k+1
			if (s[i] == s[j]) { 
				next[i] = j + 1;
				i++;
				j++;
			}
			// 当前位置不匹配时,比较更短距离的前后缀
			else {
				if (j > 0)
					j = next[j - 1];
				else {
					next[i] = 0;
					i++;
				}
			}
		}
	}
	
	int strStr(string haystack, string needle) {
		if (needle.size() == 0) // 匹配串长度为0,返回0
			return 0;
		vector <int> next(needle.size());
		getnext(next, needle);  // 获取next数组
		int i = 0, j = 0; // i指向原串,j指向匹配串
		while (i < haystack.size()) {
			if (haystack[i] == needle[j]) {  // 当当前位置匹配时,向后移动两指针
				i++;
				j++;
			}
			else {   // 若不匹配时
				if (j > 0)  
					// j移动到失配字符前一位的公共前后缀的后一位,与当前i指向的元素进行比较
					j = next[j - 1];  
				else  // 若循环到匹配串起点也不匹配时,向后移动i
					i++;
			}
			if (j == needle.size()) // 匹配串匹配成功时,直接返回
				return  i - j;
		}
		return -1;
	}
};

时间复杂度: O(n + m)
空间复杂度: O(m)

7. 重复的子字符串

leetcode链接

思路1: 移动匹配,通过双倍字符串s+s中找原字符串s,判断s是否有某一子字符串构成。

必要性证明: 当s有某一子串重复构成,则可在双倍字符串中找到s

  • 假设 s 可由 子串 x 重复 n 次构成,即 s = nx
  • 则有 s+s = 2nx
  • 移除 s+s 开头和结尾的字符,变为 (s+s)[1:-1],则破坏了开头和结尾的子串 x
  • 此时只剩 2n-2 个子串
  • 若 s 在 (s+s)[1:-1] 中,则有 2n-2 >= n,即 n >= 2
  • 即 s 至少可由 x 重复 2 次构成
  • 否则,n < 2,n 为整数,只能取 1,说明 s 不能由其子串重复多次构成

充分性证明: 当双倍字符串可找到s时,则s由某一子串重复构成。
在这里插入图片描述
图中,因为中间拼接成了s,根据红色框 可以知道 s[4] = s[0], s[5] = s[1], s[0] = s[2], s[1] = s[3] s[2] = s[4] ,s[3] = s[5];说明这个字符串,是由 两个字符 s[0] 和 s[1] 重复组成的!
s的其他组成方式同理也可推断出其由子串重复组成

因此,必要性与充分性都得到证明。

class Solution {
public:
	bool repeatedSubstringPattern(string s) {
		string t = s + s;
		t.erase(t.begin());
		t.erase(t.end()- 1 );
		//如果查找成功,则返回子串位置的值(起始位置);
		// 如果查找失败,则返回特殊值 string::npos,表示没有找到。
		if (t.find(s) != string::npos)
			return true;
		else
			return false;
	}
};

时间复杂度: O(n)
空间复杂度: O(n)

思路2: 使用KMP算法,判断s是否存在公共前后缀。若s由子串重复组成,s的最长公共前后缀的剩余部分即为符合条件的子串。
充分性与必要性的证明:代码随想录

class Solution {
public:
	void getnext(vector <int> &next, const string &s) {
		// next数组 从0开始的
		next[0] = 0;
		int j = 0, i = 1; // j为前缀尾指针,i为后缀尾指针
		while (i < s.size()) {
			if (s[j] == s[i]) {
				next[i] = j + 1;
				j++;
				i++;
			}
			else {
				if (j > 0)
					j = next[j - 1];
				else
					next[i++] = 0;
			}
		}
	}

	bool repeatedSubstringPattern(string s) {
		if (s.size() == 0)
			return false;
		vector <int> next(s.size(), 0);
		getnext(next, s);
		int len = s.size();
		// 当s存在公共前后缀,且公共前后缀的剩余部分 的整数倍为 字符串的长度
		if (next[len - 1] != 0 && len % (len - next[len - 1]) == 0)
			return true;
		else
			return false;
	}
};

时间复杂度: O(n)
空间复杂度: O(n)

8. 总结

代码随想录:字符串总结篇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值