每日算法24_2020-11-30

来源:题库 - 力扣 (LeetCode) (leetcode-cn.com)。侵删。

题目一

767. 重构字符串

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

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

示例 1:

输入: S = “aab”
输出: “aba”
示例 2:

输入: S = “aaab”
输出: “”
注意:

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

我没做出来

没悟到maxCharCounts > (length + 1) / 2和使用优先级队列。

官方方法一:基于最大堆的贪心算法

class Solution {
    public String reorganizeString(String S) {
        if (S.length() < 2) {
            return S;
        }

        // 26个字母计数数组
        int[] counts = new int[26];
        int maxCount = 0; // 字母最多数量计数
        int length = S.length();
        for (int i = 0; i < length; i++) {
            char c = S.charAt(i);
            counts[c - 'a']++;
            maxCount = Math.max(maxCount, counts[c - 'a']);
        }

        // 如果最多字符的个数比 字符串+1的一半还要多就说明题目无解
        // 比如 aaab 3 > (4+1)/2 -> 3 > 2 比如 aaa 3 > (3+1)/2 -> 3 > 2
        // 只要不是这种情况,那么肯定能够构造成满足题意的字符串
        if (maxCount > (length + 1) / 2) {
            return "";
        }

        // 优先级队列    字符数量大的放在最前面
        PriorityQueue<Character> queue = new PriorityQueue<Character>(new Comparator<Character>() {
            public int compare(Character letter1, Character letter2) {
                return counts[letter2 - 'a'] - counts[letter1 - 'a'];
            }
        });
        for (char c = 'a'; c <= 'z'; c++) {
            if (counts[c - 'a'] > 0) {
                queue.offer(c); // 按顺序将不为0 的字母加进队列中
            }
        }
        StringBuilder sb = new StringBuilder();
        while (queue.size() > 1) { // 因为一次取两个字符所以控制条件是队列得大于1
            char letter1 = queue.poll(); // 取出一个字母
            char letter2 = queue.poll(); // 再取出一个字母

            // 拼接字符串
            sb.append(letter1);
            sb.append(letter2);

            // 得到该字母在计数数组中的索引
            int index1 = letter1 - 'a', index2 = letter2 - 'a';
            counts[index1]--;
            counts[index2]--; // 拼接了一次就减去一下

            // 如果该字母计数没减到0就放进队列继续排队,等下一轮的--
            if (counts[index1] > 0) {
                queue.offer(letter1);
            }
            if (counts[index2] > 0) {
                queue.offer(letter2);
            }
        }

        // 这时队列中就剩一个字母了
        if (queue.size() > 0) {
            sb.append(queue.poll()); // 拼接到字符串上即可
        }
        return sb.toString();
    }
}

复杂度分析

  • 时间复杂度:O(nlog∣Σ∣+∣Σ∣),其中 n 是字符串的长度,Σ 是字符集,在本题中字符集为所有小写字母,∣Σ∣=26。
    遍历字符串并统计每个字母的出现次数,时间复杂度是O(n)。
    将每个字母加入最大堆,字母个数最多为 ∣Σ∣,这里设真正出现的小写字母数量为∣Σ ′∣,那么时间复杂度是 O(∣Σ∣) 加上 O(∣Σ ′∣log∣Σ ′∣) 或 O(∣Σ ′∣)。前者是对数组进行遍历的时间复杂度 O(∣Σ∣),而后者取决于是将每个字母依次加入最大堆,时间复杂度为 O(∣Σ ′∣log∣Σ ′∣);还是直接使用一次堆的初始化操作,时间复杂度为 O(∣Σ ′∣)。
    重构字符串需要对最大堆进行取出元素和添加元素的操作,取出元素和添加元素的次数都不会超过 n 次,每次操作的时间复杂度是 O(log∣Σ ′∣),因此总时间复杂度是 O(nlog∣Σ ′∣)。由于真正出现的小写字母数量为 ∣Σ ′∣ 一定小于等于字符串的长度 n,因此上面的时间复杂度中O(n),O(∣Σ ′∣log∣Σ ′∣) 和 O(∣Σ ′∣) 在渐进意义下均小于 O(nlog∣Σ ′∣),只需要保留 O(∣Σ∣)。由于 ∣Σ ′∣≤∣Σ∣,为了不引入额外符号,可以将时间复杂度 O(nlog∣Σ ′∣) 写成 O(nlog∣Σ∣)。
    总时间复杂度是 O(nlog∣Σ∣+∣Σ∣)。

  • 空间复杂度:O(∣Σ∣),其中 Σ 是字符集,在本题中字符集为所有小写字母,∣Σ∣=26。这里不计算存储最终答案字符串需要的空间(以及由于语言特性,在构造字符串时需要的额外缓存空间),空间复杂度主要取决于统计每个字母出现次数的数组和优先队列。

方法二:基于计数的贪心算法
class Solution {
    public String reorganizeString(String S) {
        if (S.length() < 2) {
            return S;
        }
        int[] counts = new int[26];
        int maxCount = 0;
        int length = S.length();
        for (int i = 0; i < length; i++) {
            char c = S.charAt(i);
            counts[c - 'a']++;
            maxCount = Math.max(maxCount, counts[c - 'a']);
        }
        if (maxCount > (length + 1) / 2) {
            return "";
        }
        char[] reorganizeArray = new char[length];
        int evenIndex = 0, oddIndex = 1;
        int halfLength = length / 2;
        for (int i = 0; i < 26; i++) {
            char c = (char) ('a' + i);
            while (counts[i] > 0 && counts[i] <= halfLength && oddIndex < length) {
                reorganizeArray[oddIndex] = c;
                counts[i]--;
                oddIndex += 2;
            }
            while (counts[i] > 0) {
                reorganizeArray[evenIndex] = c;
                counts[i]--;
                evenIndex += 2;
            }
        }
        return new String(reorganizeArray);
    }
}

复杂度分析

  • 时间复杂度:O(n+∣Σ∣),其中 n 是字符串的长度,Σ 是字符集,在本题中字符集为所有小写字母,∣Σ∣=26。
    遍历字符串并统计每个字母的出现次数,时间复杂度是O(n)。
    重构字符串需要进行 n 次放置字母的操作,并遍历每个字母得到出现次数,时间复杂度是 O(n+∣Σ∣)。
    总时间复杂度是 O(n+∣Σ∣)。

  • 空间复杂度:O(∣Σ∣),其中 n 是字符串的长度,Σ 是字符集,在本题中字符集为所有小写字母,∣Σ∣=26。这里不计算存储最终答案字符串需要的空间(以及由于语言特性,在构造字符串时需要的额外缓存空间),空间复杂度主要取决于统计每个字母出现次数的数组和优先队列。空间复杂度主要取决于统计每个字母出现次数的数组。

题目二

136. 只出现一次的数字

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

示例 1:

输入: [2,2,1]
输出: 1
示例 2:

输入: [4,1,2,1,2]
输出: 4

暴力排序Hash表能解出答案,但是不能满足不使用额外空间

因此这题需要用到异或的思想!

class Solution {
    public int singleNumber(int[] nums) {
        /*
        System.out.println(2 ^ 2);  // 0
        System.out.println(2 ^ 1);  // 3
        System.out.println(0 ^ 1);  // 1
        System.out.println(0 ^ 2);  // 2
        System.out.println(singleNumber(new int[]{2, 2, 1})); // 1
        System.out.println(singleNumber(new int[]{4, 1, 2, 1, 2})); // 4
        System.out.println(4 ^ 1); // 5
        System.out.println(5 ^ 2); // 7
        System.out.println(7 ^ 1); // 6
        System.out.println(6 ^ 2); // 4

        异或满足三条定律:
            1. 交换律:a ^ b ^ c == a ^ c ^ b
            2. 0与任何数异或为任何数:0 ^ a == a
            3. 相同的数异或为0:a ^ a == 0

        */
        for (int i = 1; i < nums.length; i++) {
            nums[0] = nums[0] ^ nums[i];
        }

        return nums[0];
    }
}

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组长度。只需要对数组遍历一次。
  • 空间复杂度:O(1)。

题目三

350. 两个数组的交集 II

给定两个数组,编写一个函数来计算它们的交集。

示例 1:

输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2,2]
示例 2:

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[4,9]

说明:

输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。
我们可以不考虑输出结果的顺序。
进阶:

如果给定的数组已经排好序呢?你将如何优化你的算法?
如果 nums1 的大小比 nums2 小很多,哪种方法更优?
如果 nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?

我的答案

class Solution {
    public int[] intersect(int[] nums1, int[] nums2) {
        HashMap<Integer, Integer> count = new HashMap<>();

        for(int i : nums1){
            count.put(i, count.getOrDefault(i, 0)+1);
        }

        List<Integer> ret = new ArrayList<>();
        for(int i : nums2){
            if(count.get(i) != null && count.get(i) != 0){
                ret.add(i); // 确保该数在nums1中存在,且计数到减到0
                count.put(i, count.get(i)-1);
            }
        }

        // 想要转换成int[]类型,就得先转成IntStream。
        // 这里就通过mapToInt()把Stream<Integer>调用Integer::valueOf来转成IntStream
        // 而IntStream中默认toArray()转成int[]。
        return ret.stream().mapToInt(Integer::valueOf).toArray();
    }
}

复杂度分析

  • 时间复杂度:O(m+n),其中 m 和 n 分别是两个数组的长度。需要遍历两个数组并对哈希表进行操作,哈希表操作的时间复杂度是 O(1),因此总时间复杂度与两个数组的长度和呈线性关系。

  • 空间复杂度:O(m)或O(n),其中 m 和 n 分别是两个数组的长度。

官方答案方法二:排序

class Solution {
    public int[] intersect(int[] nums1, int[] nums2) {
        Arrays.sort(nums1);
        Arrays.sort(nums2);
        int length1 = nums1.length, length2 = nums2.length;
        int[] intersection = new int[Math.min(length1, length2)];
        int index1 = 0, index2 = 0, index = 0;
        while (index1 < length1 && index2 < length2) {
            if (nums1[index1] < nums2[index2]) {
                index1++;
            } else if (nums1[index1] > nums2[index2]) {
                index2++;
            } else {
                intersection[index] = nums1[index1];
                index1++;
                index2++;
                index++;
            }
        }
        return Arrays.copyOfRange(intersection, 0, index);
    }
}

复杂度分析

  • 时间复杂度:O(mlogm+nlogn),其中 m 和 n 分别是两个数组的长度。对两个数组进行排序的时间复杂度是 O(mlogm+nlogn),遍历两个数组的时间复杂度是O(m+n),因此总时间复杂度是O(mlogm+nlogn)。

  • 空间复杂度:O(min(m,n)),其中 m 和 n 分别是两个数组的长度。为返回值创建一个数组 intersection,其长度为较短的数组的长度。不过在 C++ 中,我们可以直接创建一个 vector,不需要把答案临时存放在一个额外的数组中,所以这种实现的空间复杂度为 O(1)。

另外:

如果 nums2 的元素存储在磁盘上,磁盘内存是有限的,并且你不能一次加载所有的元素到内存中。那么就无法高效地对nums2进行排序,因此推荐使用方法一而不是方法二。在方法一中,nums2只关系到查询操作,因此每次读取 nums2中的一部分数据,并进行处理即可。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值