LeetCode 每日一题767. 重构字符串

767. 重构字符串

给定一个字符串S,检查是否能重新排布其中的字母,使得两相邻的字符不同。

若可行,输出任意可行的结果。若不可行,返回空字符串。

示例 1:

输入: S = "aab"
输出: "aba"

示例 2:

输入: S = "aaab"
输出: ""

注意:

  • S 只包含小写字母并且长度在[1, 500]区间内。

解题思路:

先思考不能重构的情况,当有一个字母的次数大于整个字符串的一半,即 maxCount > (S.length() + 1) / 2。反之,字符串可以重构。

可以重构的前提下,暴力一点的思路:

  • 因为只有26个小写字母,用 int[26] 存储每个字母出现的次数。
  • S 长度为 n,定义 char[n]存字符,每次取「不等于上一个且出现次数最多」 的字母放入char[],对应次数减一。
  • 一共取 n 次,每次需要遍历26个字母取值,所以总的时间复杂度为 O(n * 26)。

基于上面的思路,思考优化:每次都遍历 26 个字母取值似乎有点傻~。

  • 使用优先队列辅助维护每个字符的次数,次数从到大小排列。
  • 因为需要不同的字母,每次从优先队列中弹出次数最多的两个字母。如果没用完,再放回优先队列。
  • 基于优先队列,可以将时间复杂度缩短为 O(n * log26)。

方法一:优先队列

public String reorganizeString(String S) {
    // 统计每个字母出现的次数,顺便维护最大出现次数
    int n = S.length();
    int[] counts = new int[26];
    int maxCount = 0;
    for (int i = 0; i < n; i++) {
        char c = S.charAt(i);
        counts[c - 'a']++;
        maxCount = Math.max(maxCount, counts[c - 'a']);
    }

    // 如果最大次数大于数组的一半,则一定不能重构
    if (maxCount > (n + 1) / 2) {
        return "";
    }

    // 根据出现次数从大到小 构建优先队列
    PriorityQueue<Character> queue = new PriorityQueue<>(Comparator.comparingInt(c -> -counts[c - 'a']));
    for (int i = 0; i < 26; i++) {
        if (counts[i] > 0) {
            queue.offer((char) (i + 'a'));
        }
    }

    // 重构字符串,每次取出最多的2个字符放到数组,如果没用完再放回去
    int index = 0;
    char[] cs = new char[n];
    while (queue.size() > 1) {
        char c1 = queue.poll();
        char c2 = queue.poll();
        cs[index++] = c1;
        cs[index++] = c2;

        if (--counts[c1 - 'a'] > 0) {
            queue.offer(c1);
        }
        if (--counts[c2 - 'a'] > 0) {
            queue.offer(c2);
        }
    }
    // 最后可能还剩一个字母
    if (queue.size() > 0) {
        cs[index] = queue.poll();
    }
    return new String(cs);
}

方法二:基于计数的贪心算法

此方法参考官方解答,自己没想出来~

可以重构的前提下:

  • 用 int[26] 存储每个字母出现的次数
  • S 长度为 n,定义 char[n]存字符,并将位置分为 偶数角标 和 奇数角标
  • 在不考虑特殊情况时,遍历 int[26] 将字母先放偶数角标,偶数角标放满后再放奇数角标。基于此,隔开相同的字母。

示例:

假设 S 为:abzabz。偶数角标:0,2,4;奇数角标:1,3,5。int[26] 如下图:
在这里插入图片描述
遍历 int[26] 步骤如下:

  1. 两个 a 放到偶数角标 0 和 2
  2. 两个 b 先放一个到 4,此时偶数角标放满了,另外一个放到奇数角标 1
  3. 两个 z 放到奇数角标 3 和 5
  4. 最后返回 abazbz

特殊情况: S = aaccc,3个 c 必须放到偶数角标0,2,4才能保证重构成功。若按照上面的流程,a 先放了偶数角标,重构字符串为acacc,重构失败。

为了满足特殊情况,在遍历时如果当前字母的出现次数小于或等于 n / 2,我们应该先放奇数位置,把偶数位置留给特殊情况。

public String reorganizeString(String S) {
    // 统计每个字母出现的次数,顺便维护最大出现次数
    int n = S.length();
    int[] counts = new int[26];
    int maxCount = 0;
    for (int i = 0; i < n; i++) {
        char c = S.charAt(i);
        counts[c - 'a']++;
        maxCount = Math.max(maxCount, counts[c - 'a']);
    }

    // 如果最大次数大于数组的一半,则一定不能重构
    if (maxCount > (n + 1) / 2) {
        return "";
    }

    // 重构字符串
    char[] cs = new char[n];
    int evenIdx = 0, oddIdx = 1;
    int half = n / 2;
    for (int i = 0; i < 26; i++) {
        char c = (char) ('a' + i);
        // 不为特殊情况时,优先使用奇数位置
        while (counts[i] > 0 && counts[i] <= half && oddIdx < n) {
            cs[oddIdx] = c;
            counts[i]--;
            oddIdx += 2;
        }
        while (counts[i] > 0) {
            cs[evenIdx] = c;
            counts[i]--;
            evenIdx += 2;
        }
    }
    return new String(cs);
}

执行结果:

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值