算法 1.9 双端队列:翻转字符串里的单词【leetcode 151】

题目描述

给定一个字符串,逐个翻转字符串中的每个单词。
说明:无空格字符构成一个单词。输入字符串可以在前面或后面包含多余的空格,但是反转后的字符不能包括。如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

数据结构

  • 数组+双指针、双端队列、字符串

算法思维

  • 遍历、逆序、字符串操作

解题要点

  • 熟练使用 Java 语言的 String 字符串特性
  • 了解 trim() / split() / join() / substring() 等函数的底层原理
  • 妥善使用 String 特性函数,高效实现功能


解题步骤

一. Comprehend 理解题意
解法一:把单词看成整体,翻转单词的顺序
  1. 把字符串按空格切割,得到多个单词
  2. 翻转单词顺序,拼接成新的字符串
  3. 处理多余空格
解法二:先翻转字符串,再翻转字母顺序
  1. 翻转整个字符串,单词的顺序正确了
  2. 翻转单词的每个字母
  3. 处理多余空格
解法三:双端队列解法
  1. 向双端队列头部依次存入每个单词
  2. 从双端队列头部依次取出每个单词


二. Choose 选择数据结构与算法
解法一:翻转每个单词的顺序
  • 数据结构:数组 / 栈 / 双端队列
  • 算法思维:遍历、逆序
解法二:翻转单词的字母
  • 数据结构:数组
  • 算法思维:遍历、双指针
解法三:双端队列
  • 数据结构:双端队列
  • 算法思维:遍历、后进先出


三. Code 编码实现基本解法
解法一:Java 语言特性 实现
  1. 将字符串按空格切割成单词数组
  2. 翻转单词顺序
    使用数组工具类转成集合
    使用集合工具类进行翻转
  3. 重新将单词与空格拼接成新字符串
    使用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. 按字符串长度定义新数组,临时存储
  2. 倒序遍历字符串,定位单词起止索引
  3. 读取单词起止索引范围内的字符,写入新数组
  4. 还原指针,用以定位下个单词
  5. 将新数组中合法数据生成新字符串
边界问题
  • 以字符串中的空格为单词分界
  • 字符串首尾的空格应跳过
细节问题
  • 倒序遍历时,先定义单词尾指针
  • 读取到下一个空格,索引+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用户


解法三:双端队列 解法思路
  1. 往双端队列头部依次存入每个单词
    • 以空格为单词分界,将单词字符存入缓冲区
    • 从缓冲区取出单词存入双端队列头部
    • 注意过滤掉首尾、单词间多余空格
  2. 从双端队列头部依次取出每个单词
    • 使用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 编码实现最优解
最优解:切割+反向遍历
  1. 将字符串按空格进行切割
    • 按单个字符 " " 切割,降低处理复杂度
    • 切割结果是一个字符串数组,可能包含 " "
  2. 反向遍历数组中的每个单词
  3. 将每个单词存入字符串缓冲区中
    • 存入前加空格作为前缀
    • 遍历完成后进行截取
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方法和字符串缓冲区,实现本题
  • (练习)使用栈翻转单词

你可能感兴趣的:(算法 1.9 双端队列:翻转字符串里的单词【leetcode 151】)