LeetCode每日一题——Day27

文章介绍了如何使用Java的API(split,reverse,join)以及双指针方法来反转字符串中的单词顺序。在处理过程中,需要考虑单词间的多余空格,并确保结果中没有前导或尾随空格。提供的代码示例展示了这两种方法的实现,并给出了执行时间和内存消耗。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。

思路分析:

        方法一:调用JavaAPI 实现

        很多语言对字符串提供了 split(拆分),reverse(翻转)和 join(连接)等方法,因此我们可以简单的调用内置的 API 完成操作:

        使用 split 将字符串按空格分割成字符串数组;
        使用 reverse 将字符串数组进行反转;
        使用 join 方法将字符串数组拼成一个字符串。

        下面我们来详细了解一下Java中的split,reverse,join方法。

        split() 方法根据匹配给定的正则表达式来拆分字符串。语法如下:

public String[] split(String regex, int limit)
//regex 正则表达式
//limit 切分个数

        下面我们来分析一下Java中split的源码实现,源码如下:

public String[] split(String regex, int limit) {
        /* fastpath if the regex is a
         (1)one-char String and this character is not one of the
            RegEx's meta characters ".$|()[{^?*+\\", or
         (2)two-char String and the first char is the backslash and
            the second is not the ascii digit or ascii letter.
            以上是源码的注释
          正则表达式只要满足下面其中一种情况使用就fastpath
          只有一个字符或者2个字符,但第一个字符是转移字符的话,就启用快速分割模式
          (1): 如果只有一个字符并且这个字符不是正则表达式使用的超文本符号(meta characters)
          (2): 如果是两个字符的字符串,并且第一个字符是一个反斜线,第二个不是一个ascii的大小写字母或者符号
          否者使用正则表达式提供的split函数
         */
        char ch = 0;
        if (((regex.value.length == 1 &&
             ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
             (regex.length() == 2 &&
              regex.charAt(0) == '\\' &&
              (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
              ((ch-'a')|('z'-ch)) < 0 &&
              ((ch-'A')|('Z'-ch)) < 0)) &&
            (ch < Character.MIN_HIGH_SURROGATE ||
             ch > Character.MAX_LOW_SURROGATE))
        {
        	// 偏移量
            int off = 0;  
            // 下一个地址
            int next = 0;
            // 判断是否限制切片
            boolean limited = limit > 0;
            // 创建一个list来存放保存的结果
            ArrayList<String> list = new ArrayList<>();
            while ((next = indexOf(ch, off)) != -1) {
                if (!limited || list.size() < limit - 1) {
                    list.add(substring(off, next));
                    off = next + 1;
                } else {    // last one
                    //assert (list.size() == limit - 1);
                    list.add(substring(off, value.length));
                    off = value.length;
                    break;
                }
            }
            // If no match was found, return this
            if (off == 0)
                return new String[]{this};
             // Add remaining segment
             // 如果不限制,或者已经放入的还小于限制长度,把最后剩余的也全都放入进去
            if (!limited || list.size() < limit)
                list.add(substring(off, value.length));
            // Construct result
            // 如果不限制长度,从后往前,把找到的元素遍历一遍
            // 
            // 去掉结尾元素是空的元素(长度为0的)
            // 但是无法去掉中间元素是空的!
            int resultSize = list.size();
            if (limit == 0) {
                while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
                    resultSize--;
                }
            }
            // 将结果返回
            String[] result = new String[resultSize];
            return list.subList(0, resultSize).toArray(result);
        }
        // 不满足如上条件,这使用正则表达式自带的split函数
        return Pattern.compile(regex).split(this, limit);
    }

        第一次看到源码可能会云里雾里,我们逐个分析,先看这让人迷茫的if语句。

if (
		(    
			(  // 是一个字符,但是不包括正则表达式
				regex.value.length == 1 &&
             	".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1
            ) 
            ||
            (   // 是两个字符,并且第一个字符是反斜杠,然后判断第二个字符是否是数字或者大小写字母
             	regex.length() == 2 &&
              	regex.charAt(0) == '\\' &&
              	(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
              	((ch-'a')|('z'-ch)) < 0 &&
              	((ch-'A')|('Z'-ch)) < 0
            )
       	) 
		&&
		// 判断不是unicode的高代理和低代理
        (   ch < Character.MIN_HIGH_SURROGATE ||
            ch > Character.MAX_LOW_SURROGATE
        )
)

        if语句解决后,再看源码思路便清晰不少。

        reverse方法将此字符序列用其反转形式取代。语法如下:

public StringBuffer reverse()

         我们顺便分析一下Java中String的底层源码:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];


        可以看到在源码中,value数组使用了final关键字修饰,即String底层的数据结构使用的是final修饰的字符数组,所以是不能修改的。

        我们再来看看Java中的StringBuffer和StringBuilder,这里以StringBuffer为例,因为他们都是继承了AbstractStringBuilder。

public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
    /**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache;
 
abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

        StringBuffer的底层仅仅只是一个char字符数组,所以可以被更改。

        那我们再来想想String类型为什么是不可更改类型:

        在Java的堆内存中,有一个特殊的存储区域字符串常量,当创建了一个新的String对象时,假设字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用当前已经存在的对象,帮我们节省了空间。
     

    String s1 = "花朝九日";
    String s2 = "花朝九日";
    System.out.println(s1==s2);
    //结果为ture


        String被许多的Java类和库用来当做参数,比如url,path等等。如果String不是固定不变的将会引起各种安全隐患。

       String resource = "mybatis-config.xml";
        //创建一个resource对象引入mybatis配置文件

        再来看Java中reverse的源码

    public AbstractStringBuilder reverse() {
	boolean hasSurrogate = false;
	int n = count - 1;
 
	for (int j = (n-1) >> 1; j >= 0; --j) {//此处右移一位就是除以2
	    char temp = value[j];
	    char temp2 = value[n - j];
	    if (!hasSurrogate) {
		hasSurrogate = (temp >= Character.MIN_SURROGATE && temp <= Character.MAX_SURROGATE)
		    || (temp2 >= Character.MIN_SURROGATE && temp2 <= Character.MAX_SURROGATE);
	    }
	    value[j] = temp2;
	    value[n - j] = temp;
	}
	if (hasSurrogate) {
	    // Reverse back all valid surrogate pairs
	    for (int i = 0; i < count - 1; i++) {
		char c2 = value[i];
		if (Character.isLowSurrogate(c2)) {
		    char c1 = value[i + 1];
		    if (Character.isHighSurrogate(c1)) {
			value[i++] = c1;
			value[i] = c2;
		    }
		}
	    }
	}
	return this;
    }

       大致可以看出是前后字符对换的思想。

        最后是join方法

        String.join() 方法返回使用指定分隔符拼接一个字符串。语法如下:

public static String join(CharSequence delimiter, CharSequence... elements)  
public static String join(CharSequence delimiter, Iterable<? extends CharSequence> elements)  
// delimiter表示每个元素要添加的分隔符
//elements表示需要添加分隔符的字符串(表示被连接的数组(也可以是集合),或者是要连接的多个字符串)

        源码如下:

//delimiter 分隔符
//elements 需要连接的元素
public static String join(CharSequence delimiter, CharSequence... elements) {
    //判断是否为null,如果为null,抛出NullPointerException
    Objects.requireNonNull(delimiter);
    Objects.requireNonNull(elements);
    //构造一个分隔符为delimiter的实例
    StringJoiner joiner = new StringJoiner(delimiter);
    //循环拼接
    for (CharSequence cs: elements) {
        joiner.add(cs);
    }
    return joiner.toString();
}

        StringJoiner构造器源码:

//1个参数构造器
public StringJoiner(CharSequence delimiter) {
    //调用3个参数构造器
    this(delimiter, "", "");
}
 
//3个参数构造器
//delimiter 分隔符
//prefix 前缀
//suffix 后缀
public StringJoiner(CharSequence delimiter,
                    CharSequence prefix,
                    CharSequence suffix) {
    //判断是否为null,如果为null,抛出NullPointerException
    Objects.requireNonNull(prefix, "The prefix must not be null");
    Objects.requireNonNull(delimiter, "The delimiter must not be null");
    Objects.requireNonNull(suffix, "The suffix must not be null");
    //为成员变量赋值
    //前缀
    this.prefix = prefix.toString();
    //分隔符
    this.delimiter = delimiter.toString();
    //后缀
    this.suffix = suffix.toString();
    this.emptyValue = this.prefix + this.suffix;
}

        String.join()方法中是通过add方法拼接字符串的,add()源码如下:

public StringJoiner add(CharSequence newElement) {
    //prepareBuilder()返回参数,调用append()方法
    prepareBuilder().append(newElement);
    return this;
}

        perpareBuilder()方法源码如下:

private StringBuilder prepareBuilder() {
    if (value != null) {
        value.append(delimiter);
    } else {
        value = new StringBuilder().append(prefix);
    }
    return value;
}

        方法二:双指针遍历

        设立left = n - 1,right = n两个指针,left递减,当left指向的字符为空格时,且left在right的左侧时,就可以添加(left + 1,right)范围内的字符,然后重置right的位置,right = left。重复上述操作即可。

代码实现:

        API 

class Solution {
    public String reverseWords(String s) {
        s = s.trim();
        List<String> wordList = Arrays.asList(s.split("\\s+"));
        Collections.reverse(wordList);
        return String.join(" ", wordList);
    }
}

        双指针

class Solution {
    public String reverseWords(String s) {
    s = ' ' + s;
    StringBuilder ans = new StringBuilder();
    for (int left = s.length() - 1, right = s.length(); left >= 0; left--) {
        if (s.charAt(left) == ' ') {
            if (left + 1 < right) {
                ans.append(s, left + 1, right).append(' ');
            }
            right = left;
        }
    }
    return ans.substring(0, ans.length() - 1);
}
}

提交结果

        API 

58 / 58 个通过测试用例

状态:通过

执行用时: 9 ms

内存消耗: 41.8 MB

提交时间:9 分钟前

        双指针

58 / 58 个通过测试用例

状态:通过

执行用时: 3 ms

内存消耗: 41.4 MB

提交时间:16 分钟前

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值