记录新手小白的做题历程。
题目:
给你一个字符串 s ,颠倒字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
返回单词顺序颠倒且单词之间用单个空格连接的结果字符串。
注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
且题目样例中有其他的要求,首先,最前面的空格与后面的空格的不能存在,单词与单词之间只能有一个空格。
学习了一下别人的解法。
// 版本一
class Solution {
public:
// 反转字符串s中左闭又闭的区间[start, end]
void reverse(string& s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
swap(s[i], s[j]);
}
}
// 移除冗余空格:使用双指针(快慢指针法)O(n)的算法
void removeExtraSpaces(string& s) {
int slowIndex = 0, fastIndex = 0; // 定义快指针,慢指针
// 去掉字符串前面的空格
while (s.size() > 0 && fastIndex < s.size() && s[fastIndex] == ' ') {
fastIndex++;
}
for (; fastIndex < s.size(); fastIndex++) {
// 去掉字符串中间部分的冗余空格,
if (fastIndex - 1 > 0//这个条件是因为后面fast要减1,所以这里要大于1,而不需要等于0,因为第一个一定是字母
&& s[fastIndex - 1] == s[fastIndex]//这个是重复空格
&& s[fastIndex] == ' ') {//确定是空格
continue;//继续移动前面的指针,不放到后面去
} else {
s[slowIndex++] = s[fastIndex];
}
}//这里结束以后,慢指针是指向空格或者是最后一个元素的
if (slowIndex - 1 > 0 && s[slowIndex - 1] == ' ') { // 去掉字符串末尾的空格
s.resize(slowIndex - 1);//是空
} else {
s.resize(slowIndex); // ,最后一个元素,重新设置字符串大小
}
}
string reverseWords(string s) {
removeExtraSpaces(s); // 去掉冗余空格
reverse(s, 0, s.size() - 1); // 将字符串全部反转
int start = 0; // 反转的单词在字符串里起始位置
int end = 0; // 反转的单词在字符串里终止位置
bool entry = false; // 标记枚举字符串的过程中是否已经进入了单词区间
for (int i = 0; i < s.size(); i++) { // 开始反转单词
if (!entry) {
start = i; // 确定单词起始位置
entry = true; // 进入单词区间
}
// 单词后面有空格的情况,空格就是分词符
if (entry && s[i] == ' ' && s[i - 1] != ' ') {
end = i - 1; // 确定单词终止位置
entry = false; // 结束单词区间
reverse(s, start, end);
}
// 最后一个结尾单词之后没有空格的情况
if (entry && (i == (s.size() - 1)) && s[i] != ' ' ) {
end = i;// 确定单词终止位置
entry = false; // 结束单词区间
reverse(s, start, end);
}
}
return s;
}
// 当然这里的主函数reverseWords写的有一些冗余的,可以精简一些,精简之后的主函数为:
/* 主函数简单写法
string reverseWords(string s) {
removeExtraSpaces(s);
reverse(s, 0, s.size() - 1);
for(int i = 0; i < s.size(); i++) {
int j = i;
// 查找单词间的空格,翻转单词
while(j < s.size() && s[j] != ' ') j++;
reverse(s, i, j - 1);
i = j;
}
return s;
}
*/
};
收获:
1.双指针可以用来跳过一个字符串中不需要的字符,这道题中去除空格的步骤很关键
2.resize(n)函数:调整容器的长度大小,使其能容纳n个元素。如果n小于容器的当前的size,则删除多出来的元素。如果n大于size,则将其它元素初始化。而resize(n,t)就是将多余空格初始化为t
记录自己写代码时的错误:
1.
if(first>1&&s[first]==' '&&s[first-1]==' '){
first++;//这里不是移动first,不然就漏字符了,而是continue
}
2.
if(s[last]==' '){//错的,可能只有一个字母
s.resize(last-1);
}
if(s[last]==‘ ’&&last>1)
3.reverse(s,0,n);//主函数的反转不能这么写,结尾应是n-1
4.
for(int i=0;i<n;i++){
int j=i;//j为结尾
while(i<n&&s[i]!=' ')//这里写错了,移动的应该是j,作为尾巴
i++;
reverse(s,j,i-1);//这下面少了一步,要将i=j,从一个单词结尾继续往下
}
下面是我的代码不知道那里出错了检查也检查不出来,有空复查。
class Solution {
public:
void reverse(string& s,int i,int j){
for(;i<j;i++,j--){
swap(s[i],s[j]);
}
}
void move(string& s,int size){
int last=0,first=0;
while(size>0&&s[first]==' '&&first<size){
first++;
}
for(;first<size;first++){
if(first>1&&s[first]==' '&&s[first-1]==' '){
continue;
}
else{
s[last++]=s[first];
}
}
if(s[last]==' '&&last>1){
s.resize(size-1);
}
else{
s.resize(size);
}
return;
}
string reverseWords(string s) {
int n=s.size();
move(s,n);
reverse(s,0,n-1);
for(int i=0;i<n;i++){
int j=i;
while(s[j]!=' '&&j<n)
j++;
reverse(s,i,j-1);
i=j;
}
return s;
}
};
官方代码:
方法一:
与上面大佬的类似,但写法更精简
class Solution {
public:
string reverseWords(string s) {
// 反转整个字符串
reverse(s.begin(), s.end());
int n = s.size();
int idx = 0;
for (int start = 0; start < n; ++start) {
if (s[start] != ' ') {
// 填一个空白字符然后将idx移动到下一个单词的开头位置
if (idx != 0) s[idx++] = ' ';
// 循环遍历至单词的末尾
int end = start;
while (end < n && s[end] != ' ') s[idx++] = s[end++];
// 反转整个单词
reverse(s.begin() + idx - (end - start), s.begin() + idx);
// 更新start,去找下一个单词
start = end;
}
}
s.erase(s.begin() + idx, s.end());
return s;
}
};
方法二:双端队列
class Solution {
public:
string reverseWords(string s) {
int left = 0, right = s.size() - 1;
// 去掉字符串开头的空白字符
while (left <= right && s[left] == ' ') ++left;
// 去掉字符串末尾的空白字符
while (left <= right && s[right] == ' ') --right;
deque<string> d;
string word;
while (left <= right) {
char c = s[left];
if (word.size() && c == ' ') {
// 将单词 push 到队列的头部
d.push_front(move(word));
word = "";
}
else if (c != ' ') {
word += c;
}
++left;
}
d.push_front(move(word));
string ans;
while (!d.empty()) {
ans += d.front();
d.pop_front();
if (!d.empty()) ans += ' ';
}
return ans;
}
};
这串代码写得很漂亮!
在这段代码的收获:
1.move函数的应用
std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义,即意思是:告诉编译器,虽然我们有一个左值,但是我们希望可以像右值一样处理。
我个人的理解是,右值不能变,而有时候我的是左值(会变)(左值赋值需要拷贝)不想拷贝浪费空间,就直接用move化成右值直接将它的地址安在它要去的地方。
左值引用有持久的状态;右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象(不管编译器怎么做,但这个我们需要遵守),所以:
- 所引用的对象将要被销毁
- 该对象没有其他用户(保证安全,在使用的时候一定要特别确定这一点)
而且,使用右值的代码可以自由地接管所引用的对象的资源。
在移动之后,要谨慎操作原对象,一般不操作,因为我们不确定移动操作做了哪些内容,原对象也是处于一种不确定的状态。
即这里的放入word可以不用move,但节省空间,用!
下一题!