题目描述
给你一个字符串 s ,请你反转字符串中 单词 的顺序。单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
进阶:如果字符串在你使用的编程语言中是一种可变数据类型,请尝试使用 O(1) 额外空间复杂度的 原地 解法。
解析
这题其实不难想到方法,最简单的就是通过空格分隔来找单词,存储下来后反转就好,查找的方式可以使循环或者类似官方使用正则表达式,但是这样无疑会引入很大的额外空间开销。包括官方的解法二,和解法三,除了解法二的C++方法,其他也都用到了额外的栈或者其他数据结构存储。(解法2的Python用output = []存储新的结果串,Java版本在反转单词的时候创建了新的StringBuilder对象来反转,如果单词长度和原串类似,那么即使除开输出新串的StringBuilder这个不能避免的0(n)空间,也依旧要引入额外的0(n)空间)。
这里提供一种四指针的方法来逆序,只遍历一遍字符串的同时,使用的额外空间是O(1)。方法是直接修改字符串而不是新建一个字符串后再去拼接,但使用的不是逆序方法。
首先Java本身字符串不支持可变,但是StringBuilder是可以的,所以创建了一个StringBuilder对象复制s的数值后后面就不再用s了,用StringBuilder对象模拟输入的是可变的数据类型,也就是说,实际提交上去的额外空间依然是O(n),但实际上只要把输入的s的类型由String改成StringBuilder,额外空间就是O(1),因为串s的作用就是创建StringBuilder对象,后面再也没有用到。
思路就是前后找单词,创建四个指针left1,left2,right1,right2。前面找的同时就把字符往后面加,后面找的同时就把字符往前面拼,完事儿后删除left1/left2和right1right2。主要麻烦的就是在前面新增或者删除字符后需要改对应的指针,不过在算法过程中会将原字符串扩充到原字符串+最长的单词的长度。
class Solution {
public String reverseWords(String s) {
if(s.indexOf(' ') == -1){
return s;
}
StringBuilder res = new StringBuilder(s.trim());
int left1 = 0;
int left2 = 0;
int right1 = res.length() - 1;
int right2 = right1;
while(left2 <= right2){
// 前找单词,放到后面
int temp = right1;
while(left2 <= right2 && res.charAt(left2) != ' '){
res.insert(temp + 1, res.charAt(left2));
temp ++;
left2 ++;
}
// 这个空格可以直接加,因为第一个一定会执行
res.insert(temp + 1, ' ');
// 后面找单词,放到前面,然后改指针
temp = left1;
left1 ++;
left2 ++;
right1 ++;
right2 ++;
// 这个空格需要判断后再加,因为不一定会执行
if (left2 <= right2 && res.charAt(right2) != ' ') {
res.insert(temp, ' ');
}
while(left2 <= right2 && res.charAt(right2) != ' '){
res.insert(temp, res.charAt(right2));
left1 ++;
left2 ++;
right1 ++;
}
// 找空格
while(left2 <= right2 && res.charAt(left2) == ' '){
left2 ++;
}
while(left2 <= right2 && res.charAt(right2) == ' '){
right2 --;
}
// 删除左边已经搜索过的
res.delete(left1, left2);
// 修改删除后的指针
int x = left2 - left1;
left2 -= x;
right2 -= x;
right1 -= x;
// 删除右边已搜索过的
res.delete(right2 + 1, right1 + 1);
// 重置指针
left1 = left2;
right1 = right2;
}
res.deleteCharAt(res.length() - 1);
return res.toString();
}
}
可以看到,效率方面还是比较好的。Java版本额外的O(n)作为输出无法避免。如果能修改输入类型,能够减少O(n)的额外空间,达到O(1)级别的空间开销。
空间换时间
这个就挺简单了,从后往前扫描,然后把扫描到的单词放到新的字符数组中即可,由于长度已知就不需要使用StringBuilder了,这样能够节省时间。
public class Solution {
public String reverseWords(String s) {
char[] c = s.toCharArray();
char[] result = new char[s.length()];
int fast = s.length() - 1;
int index = 0;
while (fast >= 0) {
// 跳过末尾空格
while (fast >= 0 && c[fast] == ' ') fast--;
if (fast < 0) break; // 如果全部是空格,则直接退出
int slow = fast;
// 定位单词的起始位置
while (fast >= 0 && c[fast] != ' ') fast--;
// 复制单词到result中
if (index > 0) result[index++] = ' '; // 在添加单词前,如果不是第一个单词,先添加空格
for (int i = fast + 1; i <= slow; i++) {
result[index++] = c[i];
}
}
// 返回时不包含末尾的空格
return new String(result, 0, index);
}
}