题目描述
给定一个字符串,逐个翻转字符串中的每个单词。
说明:无空格字符构成一个单词。输入字符串可以在前面或后面包含多余的空格,但是反转后的字符不能包括。如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
数据结构
- 数组+双指针、双端队列、字符串
算法思维
- 遍历、逆序、字符串操作
解题要点
- 熟练使用 Java 语言的 String 字符串特性
- 了解 trim() / split() / join() / substring() 等函数的底层原理
- 妥善使用 String 特性函数,高效实现功能
解题步骤
一. Comprehend 理解题意
解法一:把单词看成整体,翻转单词的顺序
- 把字符串按空格切割,得到多个单词
- 翻转单词顺序,拼接成新的字符串
- 处理多余空格
解法二:先翻转字符串,再翻转字母顺序
- 翻转整个字符串,单词的顺序正确了
- 翻转单词的每个字母
- 处理多余空格
解法三:双端队列解法
- 向双端队列头部依次存入每个单词
- 从双端队列头部依次取出每个单词
二. Choose 选择数据结构与算法
解法一:翻转每个单词的顺序
- 数据结构:数组 / 栈 / 双端队列
- 算法思维:遍历、逆序
解法二:翻转单词的字母
- 数据结构:数组
- 算法思维:遍历、双指针
解法三:双端队列
- 数据结构:双端队列
- 算法思维:遍历、后进先出
三. Code 编码实现基本解法
解法一:Java 语言特性 实现
- 将字符串按空格切割成单词数组
- 翻转单词顺序
使用数组工具类转成集合
使用集合工具类进行翻转 - 重新将单词与空格拼接成新字符串
使用String类的静态方法join进行拼接
class Solution {
public String reverseWords01(String s) {
if (s == null || "".equals(s = s.trim()))
return "";
//细节;正则匹配多个空格
// 1.将字符串按空格切割成单词数组
String[] strings = s.split("\\s+");
// 2.翻转单词顺序。细节:数组、集合工具类的使用
List list = Arrays.asList(strings);
Collections.reverse(list);
// 3.重新将单词与空格拼接成字符串
return String.join(" ", list);
}
}
时间复杂度:O(n)
• 切割过程进行遍历查找:O(n)
• 翻转与拼接:O(n) + O(n)
空间复杂度:O(n)
• 切割使用了 2个数组:O(n)
• join 使用了 1个数组:O(n)
执行耗时:7 ms,击败了 46.81% 的Java用户
内存消耗:39.3 MB,击败了 20.75% 的Java用户
解法二:数组+双指针 实现
- 按字符串长度定义新数组,临时存储
- 倒序遍历字符串,定位单词起止索引
- 读取单词起止索引范围内的字符,写入新数组
- 还原指针,用以定位下个单词
- 将新数组中合法数据生成新字符串
边界问题
- 以字符串中的空格为单词分界
- 字符串首尾的空格应跳过
细节问题
- 倒序遍历时,先定义单词尾指针
- 读取到下一个空格,索引+1定位单词开始指针
- 注意单词间的多个空格,只保留一个
class Solution {
public String reverseWords(String s) {
int len;
if (s == null || (len = s.length()) == 0)
return "";
// 1.准备工作:初始化新数组,定义单词起止索引
char[] chars = new char[len]; //新字符数组
int first = -1, last = -1, index = 0; //单词起止索引
// 2.倒序遍历字符串,定位单词起止索引
for (int i = len - 1; i >= 0; i--) {
char c = s.charAt(i);
if (c != ' ') { //非空格:第一个非空格为单词结尾字符
if (last == -1) last = i; // 2.1.定位last
if (i == 0) first = i; //细节:处理字符串首字符不是空格
} else { //空格:以“空格+1”为单词开始索引
if (last != -1) first = i + 1; // 2.2.定位first
}
// 3.读取单词起止索引范围内的字符,写入新数组
if (first >= 0 && last >= 0 ) {
//细节:如果新数组中已经有数据,先存放一个空格,再放数据
if (index > 0) chars[index++] = ' ';
while (first <= last) {
chars[index++] = s.charAt(first);
first++;
}
first = last = -1; // 4.还原指针,用以定位下个单词
}
}
return String.valueOf(chars, 0, index); // 5.将新数组中合法数据生成新字符串返回
}
}
时间复杂度:O(n)
• 倒序遍历字符串:O(n)
• 读取所有单词:O(n)
空间复杂度:O(n)
• 需要一个临时数组:O(n)
• 两个指针:O(1)
• 最后重新生成一个数组:O(n)
执行耗时:3 ms,击败了 75.57% 的Java用户
内存消耗:39.1 MB,击败了 43.42% 的Java用户
解法三:双端队列 解法思路
- 往双端队列头部依次存入每个单词
• 以空格为单词分界,将单词字符存入缓冲区
• 从缓冲区取出单词存入双端队列头部
• 注意过滤掉首尾、单词间多余空格 - 从双端队列头部依次取出每个单词
• 使用join方法,将空格拼接到每个单词之间
• 注意不要遗漏最后一个单词
class Solution{
public String reverseWords(String s) {
int left = 0, right = s.length() - 1;
Deque d = new ArrayDeque();
StringBuilder word = new StringBuilder();
while (left <= right) {
char c = s.charAt(left);
if (c != ' ') {
word.append(c);
} else {
if (word.length() != 0) {
// 将单词 push 到队列的头部
d.offerFirst(word.toString());
word.setLength(0);
}
}
++left;
}
if (word.length() > 0) d.offerFirst(word.toString());
return String.join(" ", d);
}
}
时间复杂度:O(n)
• 遍历字符串:O(n)
• 读取所有单词:O(n)
• 双端队列扩容:O(n)
空间复杂度:O(n)
• 需要一个双端队列:O(n)
• 一个字符串缓冲区:O(n)
• 最后重新生成一个数组:O(n)
执行耗时:7 ms,击败了 46.81% 的Java用户
内存消耗:38.7 MB,击败了 93.19% 的Java用户
四. Consider 思考更优解
剔除无效代码或优化空间消耗
- 新建数组的容量不确定,用字符串长度比较浪费空间,是否有缓冲区可以使用?
- 数据结构(栈、双端队列)的使用是必要的吗?
寻找更好的算法思维
- 使用语言特性:切割 + 反向遍历
- 参考其它算法
五. Code 编码实现最优解
最优解:切割+反向遍历
- 将字符串按空格进行切割
• 按单个字符 " " 切割,降低处理复杂度
• 切割结果是一个字符串数组,可能包含 " " - 反向遍历数组中的每个单词
- 将每个单词存入字符串缓冲区中
• 存入前加空格作为前缀
• 遍历完成后进行截取
class Solution {
public String reverseWords(String s) {
if (s == null || "".equals(s = s.trim()))
return "";
// 按空格进行切割,而不是 \s+
String[] strs = s.split(" ");
StringBuilder sb = new StringBuilder();
// 反向遍历
for (int i = strs.length - 1; i >= 0; i--) {
if (strs[i].length() != 0)
// 拼接单词前加一个空格
sb.append(" ").append(strs[i]);
}
// 截掉第一个空格
return sb.substring(1);
}
}
时间复杂度:O(n)
• 生成数组过程遍历字符串:O(n)
• 读取所有单词:O(n)
空间复杂度:O(n)
• 生成一个数组:O(n)
• 一个字符串缓冲区:O(n)
• 最后重新生成一个数组:O(n)
执行耗时:1 ms,击败了 99.99% 的Java用户
内存消耗:38.6 MB,击败了 97.51% 的Java用户
六. Change 变形与延伸
题目变形
- (练习)自己实现String类的trim、split方法和字符串缓冲区,实现本题
- (练习)使用栈翻转单词