线性表基础:队列(三)智力发散题

智力发散题

推荐刷题顺序:
LeetCode #面试题 17.09 第K个数
LeetCode #859 亲密字符串
LeetCode #860 柠檬水找零
LeetCode #969 煎饼排序
LeetCode #621 任务调度
LeeCode #338 比特位计数

1. LeetCode #面试题 17.09 第K个数

题目描述:
有些数的素因子只有 3,5,7,请设计一个算法找出第 k 个数。注意,不是必须有这些素因子,而是必须不包含其他的素因子。例如,前几个数按顺序应该是 1,3,5,7,9,15,21。
在这里插入图片描述

解题思路:

  • 为了叙述方便,我们就把符合题目要求的这些数叫做丑数。
  • 不难发现,一个丑数总是由前面的某一个丑数 x3 / x5 / x7 得到。
    反过来说也是一样的,一个丑数 x3 / x5 / x7 就会得到某一个更大的丑数。
  • 如果把丑数数列叫做 ugly[i],那么考虑一下三个数列:
    • 1. ugly[0]*3,ugly[1]*3,ugly[2]*3,ugly[3]*3,ugly[4]*3,ugly[5]*3……
    • 2. ugly[0]*5,ugly[1]*5,ugly[2]*5,ugly[3]*5,ugly[4]*5,ugly[5]*5……
    • 3. ugly[0]*7,ugly[1]*7,ugly[2]*7,ugly[3]*7,ugly[4]*7,ugly[5]*7……
  • 上面这个三个数列合在一起就形成了新的、更长的丑数数列。
  • 如果合在一起呢?这其实就是一个合并有序线性表的问题。
  • 定义三个index 分别指向上面三个数列,下一个丑数一定是三个 index 代表的值中最小的那个,然后相应 index++ 即可。

动画图解:

首先第一个丑数是1,将其放入数组中(我图画错了数组下标应该从0开始,但是意思不变,懒得改了),然后定义三个下标分别指向数组中第一个元素,通过下标获取数组元素分别与对应的素数相乘,比较取最小值:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class Solution {
    public int getKthMagicNumber(int k) {
        int[] arr = new int[k];
        arr[0] = 1;
        int num = 1;
        int p3 = 0, p5 = 0, p7 = 0;

        while (num < k) {
            int value3 = arr[p3] * 3;
            int value5 = arr[p5] * 5;
            int value7 = arr[p7] * 7;
            int minValue = Math.min(Math.min(value3, value5), value7);
            arr[num] = minValue;

            if (value3 == minValue) p3++;
            if (value5 == minValue) p5++;
            if (value7 == minValue) p7++;

            num++;
        }
        return arr[arr.length - 1];
    }
}

2. LeetCode #859 亲密字符串

题目描述:
给定两个由小写字母构成的字符串 A 和 B ,只要我们可以通过交换 A 中的两个字母得到与 B 相等的结果,就返回 true ;否则返回 false 。

交换字母的定义是取两个下标 i 和 j (下标从 0 开始),只要 i!=j 就交换 A[i] 和 A[j] 处的字符。例如,在 “abcd” 中交换下标 0 和下标 2 的元素可以生成 “cbad” 。
在这里插入图片描述

解题思路:

  • 只有两种情况我们才认为两个字符串为亲密字符串
    • 只有两处不同,并且两处不同是可交换的
    • 两个字符串完全相同,并且至少有一个字符出现两次以上(比如aab,交换第一个和第二个字符仍然相等,也属于亲密字符串)

以下是判断两处不同,并且两处不同是可交换的图解:

首先寻找A字符串和B字符串之间不同的位置
在这里插入图片描述

当找到第一处不同点时,首先判断changeA和changeB是否为null,确保是第一个不同点,然后分别记录两个不同的字符
在这里插入图片描述

继续查找下一个不同点,当找到下一个不同点的时候,判断changeA是否和字符串B第二处不同点的字符相等,changeB是否和字符串A第二处不同点的字符相等

  • 不相等说明两处不同点是不能通过交换保证字符串相等的,返回false
  • 相等,如果当前字符串还没有遍历完,还需要保证之后的每一个字符都相等才能返回true

在这里插入图片描述

class Solution {
    public boolean buddyStrings(String a, String b) {
        // 长度不一样肯定不是亲密字符串
        if (a.length() != b.length()) {
            return false;
        }
        // 如果两个字符串完全相等,判断字符串中是否有重复的字符
        if (a.equals(b)) {
            int[] charNum = new int[26];
            for (char c : a.toCharArray()) {
                int i = c - 'a';
                if (charNum[i] != 0) {
                    return true;
                }
                charNum[i] = 1;
            }
            return false;
        }

        char[] charsA = a.toCharArray();
        char[] charsB = b.toCharArray();
        int first = -1, second = -1;// 记录不同点位置
        for (int i = 0; i < a.length(); i++) {
            if (charsA[i] != charsB[i]) {
                if (first == -1) {
                    first = i;
                } else if (second == -1) {
                    second = i;
                } else {
                    return false;// 说明找到第三处不同点直接返回false
                }
            }
        }
        // 走完循环有可能只找到一个不同点,所以要判断下second不为-1
        return second != -1 && charsA[first] == charsB[second] && charsA[second] == charsB[first];
    }
}

3. LeetCode #860 柠檬水找零

题目描述:
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。

顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
在这里插入图片描述

解题思路:
由于顾客只可能给你三个面值的钞票,而且我们一开始没有任何钞票,因此我们拥有的钞票面值只可能是 5 美元,10 美元和 20 美元三种。基于此,我们可以进行如下的分类讨论。

  • 5 美元,由于柠檬水的价格也为 5 美元,因此我们直接收下即可。
  • 10 美元,我们需要找回 5 美元,如果没有 5 美元面值的钞票,则无法正确找零。
  • 20 美元,我们需要找回 15 美元,此时有两种组合方式,一种是一张 10 美元和 5 美元的钞票,一种是 3 张 5 美元的钞票,如果两种组合方式都没有,则无法正确找零。当可以正确找零时,两种找零的方式中我们更倾向于第一种,即如果存在 5 美元和 10 美元,我们就按第一种方式找零,否则按第二种方式找零,因为需要使用 5 美元的找零场景会比需要使用 10 美元的找零场景多,我们需要尽可能保留 5 美元的钞票。

基于此,我们维护两个变量 five 和 ten 表示当前手中拥有的 5 美元和 10 美元钞票的张数,从前往后遍历数组分类讨论即可。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

无法找零的情况:
在这里插入图片描述

class Solution {
    public boolean lemonadeChange(int[] bills) {
        int five = 0;
        int ten = 0;
        for (int bill : bills) {
            switch (bill) {
                case 5:
                    // 5元直接收走
                    five++;
                    break;
                case 10:
                    if (five == 0) return false;
                    // 10元找5元
                    five--;
                    ten++;
                    break;
                case 20:
                    if (five > 0 && ten > 0) {
                        //20元优先找一张10和一张5
                        five--;
                        ten--;
                    } else if (five >= 3) {
                        // 否则找3张5
                        five -= 3;
                    } else {
                        return false;
                    }
                    break;
            }
        }
        return true;
    }
}

4. LeetCode #969 煎饼排序

题目描述:
给你一个整数数组 arr ,请使用 煎饼翻转 完成对数组的排序。

一次煎饼翻转的执行过程如下:

  • 选择一个整数 k ,1 <= k <= arr.length
  • 反转子数组 arr[0…k-1](下标从 0 开始)

例如,arr = [3,2,1,4] ,选择 k = 3 进行一次煎饼翻转,反转子数组 [3,2,1] ,得到 arr = [1,2,3,4] 。

以数组形式返回能使 arr 有序的煎饼翻转操作所对应的 k 值序列。任何将数组排序且翻转次数在 10 * arr.length 范围内的有效答案都将被判断为正确。
在这里插入图片描述

解题思路:

  • 煎饼排序简单来说,就是每次只能反转数组中的第一个元素到第K个元素的子数组,然后我们每完成反转一次,就将k值记录下来,直到这个数组变成有序递增,就输出我们记录的所有k的值。
  • 每次将第N大的元素先翻转到第1位,再翻转到第N位,这样第N位就无需在后续进程中再进行处理,只需要考虑前N-1位即可。
  • 由于每个元素只需要2次翻转即可归位,因此所需的次数最多只需2N次,符合题目需求。
    • 对于这种做法,可以做些小优化:
    • 一是可以去除最后一次值“1”的翻转(值为“1”的翻转相当于未操作);
    • 二是可以跳过已经在正确位置上的元素。
  • 对于煎饼排序的最优解,在《编程之美》中有更加详细的讨论,感兴趣的同学可以自行阅读。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

找到次大值3:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class Solution {
    public List<Integer> pancakeSort(int[] arr) {
        List<Integer> result = new ArrayList<>();
        // 题意可知,最大值就是数组的个数
        // 我们定义的i既控制循环次数,也代表当前循环的最(次)大值
        for (int i = arr.length; i > 1; i--) {// 最后一个1可以不处理
            // 寻找最大值索引位
            int index = 0;
            while (arr[index] != i) {
                index++;
            }

            if (index == i - 1) {
                // 第一种情况,当前值已经在正确的位置,不要反转
                continue;
            } else if (index == 0) {
                // 当前值已经在队首,只需要反转一次
                flip(arr, i - 1);
                result.add(i);
            } else {
                // 先将次大值反转到队首,再反转到i-1
                flip(arr, index);
                result.add(index + 1);
                flip(arr, i - 1);
                result.add(i);
            }

        }
        return result;
    }

    // 翻转
    private void flip(int[] arr, int k) {
        for (int i = 0, j = k; j > i; i++, j--) {
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
}

5. LeetCode #621 任务调度

题目描述:
给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。

然而,两个 相同种类 的任务之间必须有长度为整数 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。

你需要计算完成所有任务所需要的 最短时间 。
在这里插入图片描述

解题思路:

  • 思路一:
    按照时间顺序,依次给每一个时间单位分配任务。那么如果当前有多种任务不在冷却中,那么我们应该如何挑选执行的任务呢?直觉上,我们应当选择剩余执行次数最多的那个任务,将每种任务的剩余执行次数尽可能平均,使得 CPU 处于待命状态的时间尽可能少。(具体实现与证明略)
  • 思路二:
    相同任务会有时间执行间隔,中间插入其他任务能够节省时间
    有数量大于1的任务都应该与最大任务数量的任务间隔相排
    • 我们首先考虑所有任务种类中执行次数最多的那一种,有这样的任务列表{“A”: 5, “B”: 5, “C”: 3, “D”: 4},且冷却时间n=3为例。

    • 记执行次数最多的任务执行次数为max,那么无论如何安排,任务A都至少要花费(max - 1) * (1 + n) + 1的时间,即(5-1)*(1+3)+1,我们使用一个宽为 n+1 的矩阵可视化地展现执行 A 的时间点:

      在这里插入图片描述

      转换成矩阵:
      在这里插入图片描述
      蓝色为冷却时间

    • 如果还有其它也需要执行max次的任务,我们也需要将它们依次排布成列

      在这里插入图片描述

      如果需要执行 max 次的任务的数量为 maxCount,那么类似地可以得到对应的总时间为:(max − 1)(n + 1) + maxCount

    • 处理完执行次数为 max 次的任务,剩余任务的执行次数一定都小于max,那么我们应当如何将它们放入矩阵中呢?一种方式是,从倒数第二行开始,先放入靠左侧的列,同一列中先放入下方的行,依次放入每一种任务,并且同一种任务需要连续地填入。例如还有任务 C,D时,我们会按照下图的方式依次放入这些任务。

      在这里插入图片描述

    • 对于任意一种任务而言,一定不会被放入同一行两次(否则说明该任务的执行次数大于等于max),并且由于我们是按照列优先的顺序放入这些任务,因此任意两个相邻的任务之间要么间隔 n(例如上图中位于同一列的相同任务),要么间隔 n+1(例如上图中第一行和第二行的D),都是满足题目要求的。因此如果我们按照这样的方法填入所有的任务,那么就可以保证总时间不变,仍然为:(max − 1)(n + 1) + maxCount

    • 考虑一种情况,任务列表{“A”: 5, “B”: 5, “C”: 3, “D”: 4}中多了一种任务E,执行次数为3,按照上面方式排列,则会超出n+1列:

      在这里插入图片描述

    • 此时n+1=4,但是我们填了5列,看上去我们需要 (5 - 1) * 5 + 2 = 22的时间来执行所有任务,但实际上如果我们填「超出」了 n+1 列,那么所有的 CPU 待命状态都是可以省去的(图中红色部分)。这是因为 CPU 待命状态本身只是为了规定任意两个相邻任务的执行间隔至少为 n,但如果列数超过了 n+1,那么就算没有这些待命状态,任意两个相邻任务的执行间隔肯定也会至少为 n。此时,总执行时间就是任务的总数task。

    • 因此,在任意的情况下,需要的最少时间就是 (max - 1)(n + 1) + maxCount 和 task 中的较大值。

因此我们只需要如下做:

  • 统计出现次数最多的任务的次数
  • 统计一共有多少种任务出现次数和最多次数一样次数的任务数量
  • 最终在 任务总数量 和 格子数量 之间取最大值
class Solution {
    public int leastInterval(char[] tasks, int n) {
        // 统计每种任务执行次数
        int[] num = new int[26];
        for (char task : tasks) {
            num[task - 'A'] += 1;
        }
        // 排序
        Arrays.sort(num);

        // 统计有几种任务次数 和 最大次数一样
        int maxCount = 1;
        for (int i = 24; i >= 0 && num[25] == num[i]; i--, maxCount++);

        return Math.max(tasks.length, (num[25] - 1) * (n + 1) + maxCount);
    }
}

6. LeeCode #338 比特位计数

题目描述:
给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。
在这里插入图片描述

解题思路:

  • x和x-1,由于从x二进制位的最后一个1开始,和x-1不一样
  • 如果做与运算的话可以得到一个相对于x二进制位刚好少一个1的数的二进制位
  • 因此我们可以把 x&(x-1),看成去掉x二进制最后一位1的操作
  • 设f(x)可以得到x的二进制位中1的个数,那么推出公式f(x) - f( x&(x-1) ) = 1

在这里插入图片描述

class Solution {
    public int[] countBits(int num) {
        // 111000 56
        // 110111 55
        // 做与运算
        // 110000 48
        // 56 比 48 多一个1
        // f(x) - f(x & (x -1)) = 1
        int[] result = new int[num + 1];
        result[0] = 0;
        for (int i = 1; i <= num; i++) {
            result[i] = result[i & (i - 1)] + 1;
        }
        return result;
    }
}

这个做法的时间复杂度是O(n)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

犬豪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值