LeetCode12. 整数转罗马数字 / 剑指 Offer 40. 最小的k个数 / 剑指 Offer 41. 数据流中的中位数

Posted Zephyr丶J

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LeetCode12. 整数转罗马数字 / 剑指 Offer 40. 最小的k个数 / 剑指 Offer 41. 数据流中的中位数相关的知识,希望对你有一定的参考价值。

LeetCode12. 整数转罗马数字

2021.5.14 每日一题

题目描述

罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。

字符          数值
I             1
V             5
X             10
L             50
C             100
D             500
M             1000
例如, 罗马数字 2 写做 II ,即为两个并列的 112 写做 XII ,即为 X + II 。 27 写做  XXVII, 即为 XX + V + II 。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

I 可以放在 V (5) 和 X (10) 的左边,来表示 49。
X 可以放在 L (50) 和 C (100) 的左边,来表示 4090。 
C 可以放在 D (500) 和 M (1000) 的左边,来表示 400900。
给定一个整数,将其转为罗马数字。输入确保在 13999 的范围内。


示例 1:

输入: 3
输出: "III"
示例 2:

输入: 4
输出: "IV"
示例 3:

输入: 9
输出: "IX"
示例 4:

输入: 58
输出: "LVIII"
解释: L = 50, V = 5, III = 3.
示例 5:

输入: 1994
输出: "MCMXCIV"
解释: M = 1000, CM = 900, XC = 90, IV = 4.

思路

经典问题了,思路就是模拟吧,从高位到低位依次处理,我也是这样写的:

class Solution {
    public String intToRoman(int num) {
        //经典问题了,必须拿下
        //数字最大3999,因此先除以1000,等于几,添加几个M
        //再除以100,等于9 加 CM ,等于4 加CD,其余如果大于等于5,先加D,再加几个C;小于5,加C
        //下面的处理类似,
        //想想怎么循环起来
        StringBuilder sb = new StringBuilder();
        //用一个Map来存储当前位置(个十百千)对应的罗马数字
        Map<Integer, Character> map = new HashMap<>(){
            {
                put(1, 'I');
                put(5, 'V');
                put(10, 'X');
                put(50, 'L');
                put(100, 'C');
                put(500, 'D');
                put(1000, 'M');
            }
        };
        int base = 1000;
        //统计每位的个数
        while(num > 0){
            int count = num / base;
            num = num - count * base;
            char ge = map.get(base);
            char wu = ' ';
            char shi = ' ';
            if(map.containsKey(base * 5))
                wu = map.get(base * 5);
            if(map.containsKey(base * 10))
                shi = map.get(base * 10);
            
            if(count == 9){
                sb.append(ge);
                sb.append(shi);
                count = 0;
            }
            else if(count == 4){
                sb.append(ge);
                sb.append(wu);
                count = 0;
            }
            else if(count >= 5){
                sb.append(wu);
                count = count - 5;
            }

            for(int i = 0; i < count; i++){
                sb.append(ge);
            }

            base /= 10;
        }
        return sb.toString();
    }
}

没啥问题,看了题解吧,发现也是模拟,只不过把数分的更加详细了,贴一个weiwei哥的代码:

public class Solution {

    public String intToRoman(int num) {
        // 把阿拉伯数字与罗马数字可能出现的所有情况和对应关系,放在两个数组中,并且按照阿拉伯数字的大小降序排列
        int[] nums = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
        String[] romans = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};

        StringBuilder stringBuilder = new StringBuilder();
        int index = 0;
        while (index < 13) {
            // 特别注意:这里是等号
            while (num >= nums[index]) {
                stringBuilder.append(romans[index]);
                num -= nums[index];
            }
            index++;
        }
        return stringBuilder.toString();
    }
}


剑指 Offer 40. 最小的k个数

题目描述

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

 

示例 1:

输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:

输入:arr = [0,1,2,1], k = 1
输出:[0]

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

第一种:堆排序,用最大堆存储数组中最小的k个数,如果堆顶大于新来的数,就弹出,加入当前数,最后堆中就是k个最小的数

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        //最小的数可以求,最小的两个数可以求,那么最小的四个数其实也可以写,只不过有点复杂,判断条件多了
        //或者排序
        //用最大堆,堆中存储4个最小的数字,如果新的数字小于堆顶,进行替换
        if(k == 0)
            return new int[]{};
        PriorityQueue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>(){
            public int compare(Integer a, Integer b){
                return b - a;
            }
        });

        for(int i = 0; i < arr.length; i++){
            if(pq.size() < k){
                pq.offer(arr[i]);
            }else{
                if(pq.peek() > arr[i]){
                    pq.poll();
                    pq.offer(arr[i]);
                }
            }
        }
        int[] res = new int[k];
        for(int i = 0; i < k; i++){
            res[i] = pq.poll();
        }
        return res;
    }   
}

第二种:快排思想

先来复习一下快排,快排思路就是将数组划分为小于基准数和大于基准数两部分,最重要的是细节
刚开始写了下面的代码:

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        //来写一下快排
        quickSort(arr, 0, arr.length - 1);
        return Arrays.copyOf(arr, k);
    }   

    public void quickSort(int[] arr, int l, int r){
        if(l >= r)
            return;
        //基准值取最左边的值
        int base = arr[l];
        int i = l; 
        int j = r;
        while(i < j){
            while(i < j && arr[i] <= base)
                i++;
            while(i < j && arr[j] >= base)
                j--;
            swap(arr, i, j);
        }
        swap(arr, i, l);
        quickSort(arr, l, i - 1);
        quickSort(arr, i + 1, r);

    }

    public void swap(int[] arr, int i, int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    
}

这个报错了,[0,1,2,1] 1 这个例子倒下了,我输出的是1,答案应该是0
然后我把下面两个循环的顺序颠倒了,就过了

			while(i < j && arr[j] >= base)
                j--;
			while(i < j && arr[i] <= base)
                i++;

这说明第一种写法,排序结果不对,为什么呢,看一下错误的例子,
第一轮排序,基准是0,第一轮两个循环结束,i == 1,j == 1,交换不变,最后交换基准,把0和1的位置交换了,所以发生错误
第二种写法,先移动j,第一轮两个循环结束,j = 0,i = 0,交换不变,最后交换基准,也不变,即0排在最前面,正确
因此,第一种快排模板,很简洁明了,背过

第二种模板,也还行吧,也顺带记一下

 	private int partition(int[] nums, int lo, int hi) {
        int v = nums[lo];
        int i = lo, j = hi + 1;
        while (true) {
            while (++i <= hi && nums[i] < v);
            while (--j >= lo && nums[j] > v);
            if (i >= j) {
                break;
            }
            int t = nums[j];
            nums[j] = nums[i];
            nums[i] = t;
        }
        nums[lo] = nums[j];
        nums[j] = v;
        return j;
    }

然后针对这个问题,对快排进行改进,因为要返回的是最小的k个数,因此不需要对数组进行排序,只需要将数组划分为长度为k和剩余部分两个部分就可以了。
因此只需要找到基准位置的下标为k就可以了,此时,左边的数正好是k个

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        //来写一下快排——> 快排思想
        //如果k比数组长度还大,就直接返回数组
        if(k >= arr.length)
            return arr;
        return quickSort(arr, 0, arr.length - 1, k);
    }   

    public int[] quickSort(int[] arr, int l, int r, int k){
        //基准值取最左边的值
        int base = arr[l];
        int i = l; 
        int j = r;
        while(i < j){
            while(i < j && arr[j] >= base)
                j--;
            while(i < j && arr[i] <= base)
                i++;
            swap(arr, i, j);
        }
        swap(arr, i, l);
        //如果当前基准数的位置大于k,说明在左边
        if(i > k) return quickSort(arr, l, i - 1, k);
        if(i < k) return quickSort(arr, i + 1, r, k);
        return Arrays.copyOf(arr, k);

    }

    public void swap(int[] arr, int i, int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

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

题目描述

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。
示例 1:

输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]
示例 2:

输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

还是想办法优化时间复杂度,因为如果用数组每次插入找到位置,然后移动的话,时间复杂度为O(n)
然后就想起之前做过的用两个对顶堆求中位数的方法;中位数把一组数分成了大数部分和小数部分,用一个小顶堆存放大数部分,堆顶就是大数部分最小数;大顶堆存放小数部分,堆顶就是小数部分最大数
主要是怎么维护这两个堆,首先插入元素,如果大于小顶堆堆顶,放入小顶堆;小于大顶堆堆顶,放在大顶堆;介于中间,也放在小顶堆
然后调整两个堆的大小,使得大小之差小于等于1
仔细想了一下后,写了下面代码:

class MedianFinder {
    //怎么快速计算中位数,
    //如果之前是偶数个数,中位数是m,m = (l + r)/ 2 
    //现在添加了一个t,如果t大于r,中位数变成r;小于l,中位数变成l;l和r之间,中位数为t
    //现在是奇数,中位数是m,记录左边的数l和右边的数r,添加一个数,如果大于r,更新mid = (r + mid) / 2,l = mid,r = r;
    //同理处理小于r和在l和r之间的情况,不行。。。。。。。。。。。

    //那么怎么办呢,突然想起来之前做过的一道题,两个堆找中位数,就是一个大根堆,一个小跟堆,两个顶部的数就是中位数
    //想想逻辑怎么实现,小根堆里存大数,大根堆里存小数
    //先把第一个数放进任意一个堆中,例如小根堆中,
    //然后放第二个数,如果第二个数大于第一个数,那么就把第一个数弹出,放入大根堆中,再把第二个数放在小根堆中
    //放第三个数,和两个堆顶的数进行比较,如果大于小根堆堆顶,放在小根堆中;小于大根堆堆顶,放在大根堆里;介于之间,放在小根堆
    //放入以后,调整两个堆的数个数,使得两个堆个数之差不大于1
    //这样,如果奇数,就取小根堆堆顶的数;如果偶数,就两个堆堆顶的数平均
    //放一个数的时间复杂度为O(logn),计算中位数的时间复杂度为O(1)


    /** initialize your data structure here. */
    PriorityQueue<Integer> small;
    PriorityQueue<Integer> big;

    public MedianFinder() {
        small = new PriorityQueue<>();       //小根堆,放大数
        big = new PriorityQueue<>(new Comparator<Integer>(){     //大根堆,放小数
            public int compare(Integer a, Integer b){
                return b - a;
            }   
        });
    }
    
    public void addNum(int num) {
        //刚开始放在小根堆里
        if(small.isEmpty() && big.isEmpty()){
            small.add(num);
            return; 
        }
        //如果大于小根堆堆顶,说明是大数部分的,放在小根堆里面
        if(!small.isEmpty() && num > small.peek())
            small.offer(num);
        //如果小于大根堆堆顶,说明是小数部分的,放在大根堆里面
        else if(!big.isEmpty() && num < big.peek())
            big.offer(num);
        //介于中间的,也放在小根堆堆顶
        else
            small.offer(num);

        //调整两个堆元素的数量
        while(small.size() - big.size() > 1){
            big.offer(small.poll());
        }
        while(big.size() - small.size() > 1){
            small.offer(big.poll());
        }
    }
    
    public double findMedian() {
        if(small.size() > big.size())
            return small.peek();
        else if(small.size() < big.size())
            return big以上是关于LeetCode12. 整数转罗马数字 / 剑指 Offer 40. 最小的k个数 / 剑指 Offer 41. 数据流中的中位数的主要内容,如果未能解决你的问题,请参考以下文章

总结leetcode剑指offer分类学习速成

[leetcode] 12. 整数转罗马数字

LeetCode 12 整数转罗马数字

LeetCode刷题笔记 - 12. 整数转罗马数字

LeetCode 12. 整数转罗马数字

Leetcode12. 整数转罗马数字(贪心)