Loading

剑指offer一刷:排序

剑指 Offer 45. 把数组排成最小的数

难度:中等

此题求拼接起来的最小数字,本质上是一个排序问题。设数组 nums 中任意两数字的字符串为 x 和 y,则规定排序判断规则为:

  • 若拼接字符串 x + y > y + x,则 x“大于”y
  • 反之,若 x + y < y + x,则 x“小于”y

“小于”y 代表:排序完成后,数组中 x 应在 y 左边;“大于”则反之

根据以上规则,套用任何排序方法对 nu 执行排序即可。

采用快速排序的方法:

class Solution {
    public String minNumber(int[] nums) {
        String[] strs = new String[nums.length];
        for(int i = 0; i < nums.length; i++)
            strs[i] = String.valueOf(nums[i]);
        quickSort(strs, 0, strs.length - 1);
        StringBuilder res = new StringBuilder();
        for(String s : strs)
            res.append(s);
        return res.toString();
    }
    void quickSort(String[] strs, int l, int r) {
        if(l >= r) return;
        int i = l, j = r;
        String tmp = strs[i];
        while(i < j) {
            while((strs[j] + strs[l]).compareTo(strs[l] + strs[j]) >= 0 && i < j) j--;
            while((strs[i] + strs[l]).compareTo(strs[l] + strs[i]) <= 0 && i < j) i++;
            tmp = strs[i];
            strs[i] = strs[j];
            strs[j] = tmp;
        }
        strs[i] = strs[l];
        strs[l] = tmp;
        quickSort(strs, l, i - 1);
        quickSort(strs, i + 1, r);
    }
}

作者:Krahets
链接:https://leetcode.cn/leetbook/read/illustration-of-algorithm/59ceyt/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

时间复杂度:O(NlogN),空间复杂度:O(N)。

注意

  1. j 和 i 的顺序,谁先谁后
  2. 2 个 while 中的判断条件,等号最好都带上(绝对不能都不带)

剑指 Offer 61. 扑克牌中的顺子

难度:简单

首先,要知道什么是顺子。顺子就是 5 张连续的牌,如 [1,2,3,4,5];大小王可以看成任何数字,如 [0,0,1,2,5] 也算顺子,因为可以把 2 个 0 分别看成 3 和 4。

根据题意,此 5 张牌是顺子的充分条件如下:

  1. 除大小王外,所有牌无重复
  2. 设此 5 张牌中最大的牌为 max,最小的牌为 min(大小王除外),则需满足:

max - min < 5

可以简单设想一下,如果 最大牌 - 最小牌 = 5,那么除了 2 个端点,中间需要 4 张牌才能构成连续牌,3 张显然无法连续。

方法一:集合 Set + 遍历

  • 遍历五张牌,遇到大小王(即 0)直接跳过。
  • 判别重复:利用 Set 实现遍历判重, Set 的查找方法的时间复杂度为 O(1);
  • 获取最大 / 最小的牌:借助辅助变量 max 和 min,遍历统计即可。
class Solution {
    public boolean isStraight(int[] nums) {
        Set<Integer> repeat = new HashSet<>();
        int max = 0, min = 14;
        for(int num : nums) {
            if(num == 0) continue; // 跳过大小王
            max = Math.max(max, num); // 最大牌
            min = Math.min(min, num); // 最小牌
            if(repeat.contains(num)) return false; // 若有重复,提前返回 false
            repeat.add(num); // 添加此牌至 Set
        }
        return max - min < 5; // 最大牌 - 最小牌 < 5 则可构成顺子
    }
}

作者:jyd
链接:https://leetcode.cn/problems/bu-ke-pai-zhong-de-shun-zi-lcof/solution/mian-shi-ti-61-bu-ke-pai-zhong-de-shun-zi-ji-he-se/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

时间复杂度:O(N)=O(5)=O(1),空间复杂度:O(N)=O(5)=O(1)。

方法二:排序 + 遍历

  • 先对数组执行排序。
  • 判别重复:排序数组中的相同元素位置相邻,因此可通过遍历数组,判断 nums[i] = nums[i + 1] 是否成立来判重。
  • 获取最大 / 最小的牌:排序后,数组末位元素 nums[4] 为最大牌;元素 nums[joker] 为最小牌,其中 joker 为大小王的数量。
class Solution {
    public boolean isStraight(int[] nums) {
        int joker = 0;
        Arrays.sort(nums); // 数组排序
        for(int i = 0; i < 4; i++) {
            if(nums[i] == 0) joker++; // 统计大小王数量
            else if(nums[i] == nums[i + 1]) return false; // 若有重复,提前返回 false
        }
        return nums[4] - nums[joker] < 5; // 最大牌 - 最小牌 < 5 则可构成顺子
    }
}

作者:jyd
链接:https://leetcode.cn/problems/bu-ke-pai-zhong-de-shun-zi-lcof/solution/mian-shi-ti-61-bu-ke-pai-zhong-de-shun-zi-ji-he-se/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

时间复杂度:O(NlogN)=O(5log5)=O(1),空间复杂度:O(1)。

剑指 Offer 40. 最小的 k 个数

难度:简单

方法一:基于快速排序的数组划分

题目只要求返回最小的 k 个数,对这 k 个数的顺序并没有要求。因此,只需要将数组划分为最小的 k 个数其他数字两部分即可,而快速排序的哨兵划分可完成此目标。

根据快速排序原理,如果某次哨兵划分后基准数正好是第 k+1 小的数字,那么此时基准数左边的所有数字便是题目所求的最小的 k 个数

根据此思路,考虑在每次哨兵划分后,判断基准数在数组中的索引是否等于 k,若 true 则直接返回此时数组的前 k 个数字即可。

getLeastNumbers() 函数

  1. 若 k 大于数组长度,则直接返回整个数组;
  2. 执行并返回 quick_sort() 即可;

quick_sort() 函数

  1. 哨兵划分
    • 划分完毕后,基准数为 arr[i],左 / 右子数组区间分别为 [l, i - 1], [i + 1, r];
  2. 递归或返回
    • 若 k < i,代表第 k + 1 小的数字在左子数组中,则递归左子数组;
    • 若 k > i,代表第 k + 1 小的数字在右子数组中,则递归右子数组;
    • 若 k = i,代表此时 arr[k] 即为第 k + 1 小的数字,则直接返回数组前 k 个数字即可;
class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if (k >= arr.length) return arr;
        return quickSort(arr, k, 0, arr.length - 1);
    }
    private int[] quickSort(int[] arr, int k, int l, int r) {
        int i = l, j = r;
        while (i < j) {
            while (i < j && arr[j] >= arr[l]) j--;
            while (i < j && arr[i] <= arr[l]) i++;
            swap(arr, i, j);
        }
        swap(arr, i, l);
        if (i > k) return quickSort(arr, k, l, i - 1);
        if (i < k) return quickSort(arr, k, i + 1, r);
        return Arrays.copyOf(arr, k);
    }
    private void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/solution/jian-zhi-offer-40-zui-xiao-de-k-ge-shu-j-9yze/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

时间复杂度:O(N)(N+N/2+N/4+……=2N-1),空间复杂度:O(logN)(递归深度)。

方法二:大根堆(前 k 小)

本题是求前 k 小,因此用一个容量为 k 的大根堆,每次 poll 出最大的数,那堆中保留的就是前 k 小了。

注意不是小根堆!小根堆的话需要把全部的元素都入堆,时间复杂度达到 O(NlogN),就不是 O(Nlogk) 了。

// 保持堆的大小为K,然后遍历数组中的数字,遍历的时候做如下判断:
// 1. 若目前堆的大小小于K,将当前数字放入堆中。
// 2. 否则判断当前数字与大根堆堆顶元素的大小关系,如果当前数字比大根堆堆顶还大,这个数就直接跳过;
//    反之如果当前数字比大根堆堆顶小,先poll掉堆顶,再将该数字放入堆中。
class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if (k == 0 || arr.length == 0) {
            return new int[0];
        }
        // 默认是小根堆,实现大根堆需要重写一下比较器。
        Queue<Integer> pq = new PriorityQueue<>((v1, v2) -> v2 - v1);
        for (int num: arr) {
            if (pq.size() < k) {
                pq.offer(num);
            } else if (num < pq.peek()) {
                pq.poll();
                pq.offer(num);
            }
        }
        
        // 返回堆中的元素
        int[] res = new int[pq.size()];
        int idx = 0;
        for(int num: pq) {
            res[idx++] = num;
        }
        return res;
    }
}

作者:sweetiee
链接:https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/solution/3chong-jie-fa-miao-sha-topkkuai-pai-dui-er-cha-sou/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

时间复杂度:O(Nlogk)(大根堆实时维护前 k 小值,所以插入删除都是 O(logk) 的时间复杂度,最坏情况下数组里 N 个数都会插入,所以一共需要 O(Nlogk) 的时间复杂度),空间复杂度:O(k)(大根堆里最多 k 个数)。

两种方法的优劣性比较

在面试中,另一个常问的问题就是这两种方法有何优劣。看起来分治法的快速选择算法的时间、空间复杂度都优于使用堆的方法,但是要注意到快速选择算法的几点局限性:

第一,算法需要修改原数组,如果原数组不能修改的话,还需要拷贝一份数组,空间复杂度就上去了。

第二,算法需要保存所有的数据。如果把数据看成输入流的话,使用堆的方法是来一个处理一个不需要保存数据只需要保存 k 个元素的最大堆。而快速选择的方法需要先保存下来所有的数据,再运行算法。当数据量非常大的时候,甚至内放不下的时候,就麻烦了。所以当数据量大的时候还是用基于堆的方法比较好

剑指 Offer 41. 数据流中的中位数

难度:困难

建立一个小顶堆 A 和大顶堆 B ,各保存列表的一半元素,且规定:

  • A 保存较大的一半,长度为 N/2(N 为偶数)或 (N+1)/2(N 为奇数);
  • B 保存较小的一半,长度为 N/2(N 为偶数)或 (N-1)/2(N 为奇数);

随后,中位数可仅根据 A, B 的堆顶元素计算得到。

算法流程

设元素总数为 N = m + n,其中 m 和 n 分别为 A 和 B 中的元素个数。

addNum(num) 函数

  1. 当 m = n(即 N 为偶数):需向 A 添加一个元素。实现方法:将新元素 num 插入至 B,再将 B 堆顶元素插入至 A
  2. 当 m ≠ n(即 N 为奇数):需向 B 添加一个元素。实现方法:将新元素 num 插入至 A,再将 A 堆顶元素插入至 B

假设插入数字 num 遇到情况 1. 。由于 num 可能属于“较小的一半”(即属于 B),因此不能将 num 直接插入至 A。而应先将 num 插入至 B,再将 B 堆顶元素插入至 A。这样就可以始终保持 A 保存较大一半、B 保存较小一半

findMedian() 函数

  1. 当 m = n(N 为偶数):则中位数为 ( A 的堆顶元素 + B 的堆顶元素 ) / 2
  2. 当 m ≠ n(N 为奇数):则中位数为 A 的堆顶元素
class MedianFinder {
    Queue<Integer> A, B;
    public MedianFinder() {
        A = new PriorityQueue<>(); // 小顶堆,保存较大的一半
        B = new PriorityQueue<>((x, y) -> (y - x)); // 大顶堆,保存较小的一半
    }
    public void addNum(int num) {
        if(A.size() != B.size()) {
            A.add(num);
            B.add(A.poll());
        } else {
            B.add(num);
            A.add(B.poll());
        }
    }
    public double findMedian() {
        return A.size() != B.size() ? A.peek() : (A.peek() + B.peek()) / 2.0;
    }
}

作者:Krahets
链接:https://leetcode.cn/leetbook/read/illustration-of-algorithm/5v0zcc/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

时间复杂度:查找数字 O(1);添加数字 O(logN)

空间复杂度:O(N)

注意大顶堆的构造

posted @   幻梦翱翔  阅读(46)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
点击右上角即可分享
微信分享提示