巧妙使用两次反转,轻松解决循环移位问题

循环移位是一个看似简单,但是实现非常麻烦的问题,看个例子:

给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
例如:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]

这个题怎么做呢?你是否想到可以逐个移动来实现?理论上可以,但是实现的时候会发现问题重重,不好处理。这里直接介绍一种很简单,但是很奇妙的方法:两轮翻转。

思路如下:

  1. 首先对整个数组实行翻转,这样子原数组中需要翻转的子数组,就会跑到数组最前面。

  2. 这时候,从 k 处分隔数组,左右两数组,各自进行翻转即可。

例如 [1,2,3,4,5,6,7] 我们先将其整体翻转成[7,6,5,4,3,2,1],

然后再根据k将其分成两组 [7,6,5] 和[4,3,2,1],

最后将两个再次翻转:[5,6,7] 和[1,2,3,4],这时候就得到了最终结果[5,6,7,1,2,3,4]

代码如下:

 public static void rotate(int[] nums, int k) {
        k %= nums.length;
        reverse(nums, 0, nums.length - 1);
        reverse(nums, 0, k - 1);
        reverse(nums, k, nums.length - 1);
    }

    public static void reverse(int[] nums, int start, int end) {
        while (start < end) {
            int temp = nums[start];
            nums[start] = nums[end];
            nums[end] = temp;
            start += 1;
            end -= 1;
        }
    }

再写个测试类:

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5, 6, 7};
        rotate(arr, 3);
        System.out.println(Arrays.toString(arr));
    }

这种方法在处理某些字符串问题同样可以使用,例如LeetCode151. 翻转字符串里的单词。

题目要求:

给你一个字符串 `s` ,逐个翻转字符串中的所有 **单词** 。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
请你返回一个翻转 s 中单词顺序并用单个空格相连的字符串。
说明:

* 输入字符串 s 可以在前面、后面或者单词间包含多余的空格。
* 翻转后单词间应当仅用一个空格分隔。
* 翻转后的字符串中不应包含额外的空格。

例如:

示例1:
输入:s = "the sky is blue"
输出:"blue is sky the"

示例2:
输入:s = "hello world"

输出:"world hello"
解释:输入字符串可以在前面或者后面包含多余的空格,但是翻转后的字符不能包括。

这个题也经常出现在很多面试题中,我记得曾经见过有个题是这样出的,要你按照同样的方式反转“ I love youzan”。这个题的关键在于如何处理单词。很多语言提供了相关的特性,因此我们可以首先使用语言的特性来实现:

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

  • 使用 split 将字符串按空格分割成字符串数组;

  • 使用 reverse 将字符串数组进行反转;

  • 使用 join 方法将字符串数组拼成一个字符串。

如图所示:

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);
    }
}

如果我们要自行编写实现函数,对于字符串不可变的语言,例如java中的String,首先得把字符串转化成其他可变的数据结构,同时还需要在转化的过程中去除空格。

而对于字符串可变的语言,就不需要再额外开辟空间了,直接在字符串上原地实现。在这种情况下,反转字符和去除空格可以一起完成。代码有点长,但是并不复杂:

class Solution {
    public String reverseWords(String s) {
        StringBuilder sb = trimSpaces(s);

        // 翻转字符串
        reverse(sb, 0, sb.length() - 1);
        // 翻转每个单词
        reverseEachWord(sb);
        return sb.toString();
    }

    public StringBuilder trimSpaces(String s) {
        int left = 0, right = s.length() - 1;
        // 去掉字符串开头的空白字符
        while (left <= right && s.charAt(left) == ' ') {
            ++left;
        }

        // 去掉字符串末尾的空白字符
        while (left <= right && s.charAt(right) == ' ') {
            --right;
        }

        // 将字符串间多余的空白字符去除
        StringBuilder sb = new StringBuilder();
        while (left <= right) {
            char c = s.charAt(left);

            if (c != ' ') {
                sb.append(c);
            } else if (sb.charAt(sb.length() - 1) != ' ') {
                sb.append(c);
            }
            ++left;
        }
        return sb;
    }

    public void reverse(StringBuilder sb, int left, int right) {
        while (left < right) {
            char tmp = sb.charAt(left);
            sb.setCharAt(left++, sb.charAt(right));
            sb.setCharAt(right--, tmp);
        }
    }

    public void reverseEachWord(StringBuilder sb) {
        int n = sb.length();
        int start = 0, end = 0;

        while (start < n) {
            // 循环至单词的末尾
            while (end < n && sb.charAt(end) != ' ') {
                ++end;
            }
            // 翻转单词
            reverse(sb, start, end - 1);
            // 更新start,去找下一个单词
            start = end + 1;
            ++end;
        }
    }
}

很多算法是需要学习的,而不仅仅是靠自己想出来的,这个题就很好的说明了这一点。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纵横千里,捭阖四方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值