151. 反转字符串中的单词
题目难度
中等
题目描述
给你一个字符串s
,请你反转字符串中单词的顺序。
单词是由非空格字符组成的字符串。s
中使用至少一个空格将字符串中的单词分隔开。
返回单词顺序颠倒且单词之间用单个空格连接的结果字符串。
注意:输入字符串s
中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
示例
示例 1
输入:s = "the sky is blue"
输出:"blue is sky the"
示例 2
输入:s = " hello world "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。
示例 3
输入:s = "a good example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。
提示信息
1 <= s.length <= 104
s
包含英文大小写字母、数字和空格' '
s
中至少存在一个单词。
解题思路
-
去除多余空格:
- 首先,去除输入字符串
s
中的前导空格、尾随空格以及单词之间的多余空格。可以通过遍历字符串,使用一个新的字符串来存储处理后的结果。当遇到非空格字符时,将其添加到新字符串中;当遇到空格且新字符串不为空时,再添加一个空格。这样可以确保新字符串中单词之间只有一个空格。
- 首先,去除输入字符串
-
反转整个字符串:
- 对去除多余空格后的字符串进行反转。可以使用双指针的方法,一个指针从字符串的开头开始,另一个指针从字符串的末尾开始,不断交换两个指针所指的字符,直到两个指针相遇。
-
反转每个单词:
- 再次遍历反转后的字符串,确定每个单词的边界。当遇到空格时,说明一个单词结束,可以对这个单词进行反转。同样可以使用双指针的方法,一个指针指向单词的开头,另一个指针指向单词的末尾,不断交换两个指针所指的字符,直到两个指针相遇。
解法一:反转两次
- 先删除空白
- 整个反转
- 单词反转,转回来了
class Solution:
def reverseWords(self, s: str) -> str:
# 删除前后空白
s = s.strip()
# 反转整个字符串
s = s[::-1]
# 将字符串拆分成单词,并且反转每个单词
s = ' '.join(word[::-1] for word in s.split())
return s
- 因为字符串在python时是不可变类型,所以反转单词时,需要先将其转换成列表,然后通过join函数再将其转换成列表,所以空间复杂度不是 O ( 1 ) O(1) O(1)
解法二:双指针反转
class Solution:
def reverseWords(self, s: str) -> str:
# 将字符串char拆分成单词 '' ,即转换成列表类型
# s = ' hello world '
words = s.split() # words = ['hello','world']
# 双指针反转单词'位置'
left, right = 0, len(words) - 1
while left < right:
words[left], words[right] = words[right], words[left]
left += 1
right -= 1
# words = ['world','hello']
return ' '.join(words)
解法三:拆分字符串+反转列表
class Solution:
def reverseWords(self, s):
words = s.split() # char ---> list = ['','','']
words = words[::-1] # 切片快速反转单词位置
return ' '.join(words)
以下是对三种解法的详细分析:
解法一:反转两次
-
思路:
- 首先使用
strip()
方法去除字符串前后的空白。 - 然后通过切片操作
s[::-1]
反转整个字符串。 - 最后,使用列表推导式将字符串按空格分割成单词列表,对每个单词进行反转,再用
join
方法将反转后的单词重新组合成字符串。
- 首先使用
-
优点:
- 代码较为简洁,利用了 Python 的内置函数和切片操作,容易理解。
-
缺点:
- 正如你所说,由于字符串在 Python 中是不可变类型,在反转单词时需要将字符串转换为列表再转换回字符串,这会带来一定的开销,并且空间复杂度不是 O ( 1 ) O(1) O(1)。
-
时间复杂度:
strip()
的时间复杂度为 O ( n ) O(n) O(n),其中n
是字符串的长度。- 切片操作
s[::-1]
的时间复杂度为 O ( n ) O(n) O(n)。 - 分割字符串和对每个单词进行反转的时间复杂度也为 O ( n ) O(n) O(n),因为涉及遍历整个字符串。所以总体时间复杂度为 O ( n ) O(n) O(n)。
-
空间复杂度:
- 由于需要创建新的列表来存储反转后的单词,所以空间复杂度不是 O ( 1 ) O(1) O(1),具体取决于字符串中的单词数量和长度。
解法二:双指针反转
-
思路:
- 首先使用
split()
方法将字符串拆分成单词列表。 - 然后使用双指针的方法交换单词列表中单词的位置,实现单词顺序的反转。
- 最后,使用
join
方法将反转后的单词列表重新组合成字符串。
- 首先使用
-
优点:
- 不需要对每个单词进行单独的反转操作,只需要进行一次双指针反转单词位置的操作,相对高效。
- 没有创建额外的数据结构来存储反转后的单词,空间复杂度相对较低。
-
缺点:
- 代码相对解法一稍微复杂一些,需要理解双指针的操作。
-
时间复杂度:
split()
方法的时间复杂度为 O ( n ) O(n) O(n),其中n
是字符串的长度。- 双指针反转单词位置的操作时间复杂度为
O
(
m
)
O(m)
O(m),其中
m
是单词的数量。由于单词数量通常小于字符串长度,所以总体时间复杂度为 O ( n ) O(n) O(n)。
-
空间复杂度:
- 只创建了一个单词列表,没有创建其他额外的数据结构,空间复杂度取决于单词的数量和长度,相对较低,可以认为接近
O
(
m
)
O(m)
O(m),其中
m
是单词的数量。
- 只创建了一个单词列表,没有创建其他额外的数据结构,空间复杂度取决于单词的数量和长度,相对较低,可以认为接近
O
(
m
)
O(m)
O(m),其中
解法三:拆分字符串 + 反转列表
-
思路:
- 与解法二类似,首先使用
split()
方法将字符串拆分成单词列表。 - 然后直接使用切片操作
words[::-1]
反转单词列表。 - 最后,使用
join
方法将反转后的单词列表重新组合成字符串。
- 与解法二类似,首先使用
-
优点:
- 代码简洁,利用了切片操作快速反转单词列表。
-
缺点:
- 与解法一类似,需要将字符串拆分成单词列表,可能会带来一定的开销。并且空间复杂度不是 O ( 1 ) O(1) O(1)。
-
时间复杂度:
split()
方法的时间复杂度为 O ( n ) O(n) O(n),其中n
是字符串的长度。- 切片操作
words[::-1]
的时间复杂度为 O ( m ) O(m) O(m),其中m
是单词的数量。总体时间复杂度为 O ( n ) O(n) O(n)。
-
空间复杂度:
- 创建了一个单词列表,空间复杂度取决于单词的数量和长度,不是 O ( 1 ) O(1) O(1)。
综上所述,三种解法的时间复杂度都是 O ( n ) O(n) O(n),但解法二在空间复杂度上相对较低,并且代码也较为高效。解法一和解法三虽然代码简洁,但在空间复杂度上有一定的不足。在实际应用中,可以根据具体情况选择合适的解法。
切片操作是只能对列表进行么?
切片操作words[::-1]
的时间复杂度为
O
(
m
)
O(m)
O(m),其中
m
m
m 是切片操作所作用的序列的长度,在这个例子中就是单词列表words
中单词的数量。
切片操作不仅可以对列表进行,也可以对字符串这种数据类型进行。对于字符串的切片操作,其语法和对列表的切片操作类似,例如:
s = "hello world"
print(s[::-1])
print(s[1:5])
在上述代码中,s[::-1]
会将字符串s
反转,得到dlrow olleh
;s[1:5]
会截取字符串s
中从索引为1的字符到索引为4的字符,得到ello
。
字符串切片操作的时间复杂度也是与切片的长度相关的,对于长度为 n n n 的字符串,切片操作的时间复杂度通常为 O ( k ) O(k) O(k),其中 k k k 是切片的长度,也就是切片操作所选取的字符数量。在最坏的情况下,如果切片选取了整个字符串,时间复杂度就是 O ( n ) O(n) O(n)。
所以,切片操作是一种非常方便且高效的操作,可以用于多种序列类型的数据,包括列表、字符串、元组等,能够帮助我们灵活地获取和处理序列中的部分数据。