【路飞】295. 数据流的中位数、1753. 移除石子的最大得分、264. 丑数 II、313. 超级丑数

103 阅读5分钟

295. 数据流的中位数

中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。

例如,

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

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

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

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

addNum(1) addNum(2) findMedian() -> 1.5 addNum(3) findMedian() -> 2

解题思路:因为每次新增的数据都是无序的,但是题目要求是有序列表的中间数,所有我们需要用到堆,我们可以将有序数组分成两个数组来维护,左边数组保持大顶堆,右边数组是小顶堆,这样两个数组合并就是一个从小到大的有序序列了,每次新增保持左边数组长度大于右边数组长度加一或者等于右边数组,这样如果左边数组大于右边那么中间数就是左边数组第一个(大顶堆),如果两边数组相等,那么就将两个数组第一个数相加除以2就是中位数了,代码如下:

var MedianFinder = function() {
    //左边数组
    this.data = [];
    //右边数组
    this.data2 = [];
};

/** 
 * @param {number} num
 * @return {void}
 */
MedianFinder.prototype.addNum = function(num) {
    //优先给左边数组新增,因为右边数组是小顶堆,所有小于右边最小的一个数都在左边
    if(this.data.length < 1 || num < this.data[0]) {
        //新增并保持左边数组大顶堆特性,第一个数组是左边数组最大,max表示大顶堆
        this.data = push_up(num,this.data,'max');
    } else {
        //新增并维护右边数组小顶堆特性,第一个数组是右边数组最小一个,min表示小顶堆
        this.data2 = push_up(num,this.data2,'min');
    } 
    //保持右边数组小于或等于左边数组
    if(this.data2.length > this.data.length){
        //如果右边数组长度大于左边,就将右边最小数给到左边数组,左边数组新增一个数
        this.data = push_up(this.data2[0],this.data,'max');
        //右边数组删除堆顶元素
        this.data2 =  pop_down(this.data2,'min');
    }
    //因为左边数组只能多右边一个,或者相等,如果超过两个就需要给右边一个
    if(this.data.length > this.data2.length + 1){
        //右边数组新增左数组多的数
        this.data2 = push_up(this.data[0],this.data2,'min');
        // 左边数组删除多余数
        this.data = pop_down(this.data,'max');
    }
};

/**
 * @return {number}
 */
MedianFinder.prototype.findMedian = function() {
    //如果右边数组大,说明中间数就是左边节点堆顶元素
    if(this.data.length > this.data2.length) {
        return this.data[0];
    }else {
        //不相等就等于两个堆顶元素和除以2
        return (this.data[0] + this.data2[0]) / 2;
    }
};
//以下代码是维持堆的特性,前面纹章有介绍
var push_up = function(val,data,type){
    data.push(val);
    let idx = data.length - 1;//当前节点
    let gIdx = parseInt((idx - 1) / 2);//根节点
    while(idx && CMP(idx,gIdx,data,type)){
        data = swap(data[idx],idx,data[gIdx],gIdx,data);
        idx = gIdx;
        gIdx = parseInt((idx - 1)/2);
    }
    return data;
}

var pop_down = function(data,type){
    if(data.length <= 1){
        data = [];
        return data;
    }
    let cnt = data.length;
    //删除最大值,将尾结点放在开头,位置树结构
    data[0] = data.pop();
    cnt --;
    let idx = 0;//当前节点
    let n = cnt -1;//最大节点个数
    let zIdx = 2 * idx + 1;//左子节点
    while(zIdx <= n){
        let temp = idx;//三角区最大值下标
        if(CMP(zIdx,idx,data,type)) temp = zIdx;
        if(zIdx + 1 <= n && CMP(zIdx + 1,temp,data,type)) temp = zIdx + 1;
        if(temp == idx) break;
        data = swap(data[idx],idx,data[temp],temp,data);
        idx = temp;
        zIdx = 2*idx +1;
    }
    return data;
}

var swap = function(v1,i1,v2,i2,data){
    data[i1] = v2;
    data[i2] = v1;
    return data;
}

var CMP = function(val,val2,data,type){
    if(type == 'max'){
        if(data[val] > data[val2]) return true;
        else return false
    }else{
        if(data[val] < data[val2]) return true;
        else return false
    }
}

1753. 移除石子的最大得分

你正在玩一个单人游戏,面前放置着大小分别为 a​​​​​​、b 和 c​​​​​​ 的 三堆 石子。

每回合你都要从两个 不同的非空堆 中取出一颗石子,并在得分上加 1 分。当存在 两个或更多 的空堆时,游戏停止。

给你三个整数 a 、b 和 c ,返回可以得到的 最大分数 。

解题思路:1.将a b c 从小到大排序,a < b < c;
2. 我们可以先将c多余b的部分用a抵消掉;
3.如果 a < c - b,此时将a减掉后c > b 那么最大数就是 a + b;
4.如果 a > c - b, 此时我们可以先抵消 c - b次,经过抵消此时 b = c,我们可以将a分成两份分别抵消b和c,此时b和c还是相等的,所有最大次数就是 (c- b) + parseInt(a/2) + b(经过a抵消的), 代码如下:

var maximumScore = function(a, b, c) {
    //中间遍历便于排序
    let temp = 0;
    //记录执行次数
    let cnt = 0
    if(a > b) {
        temp = a;
        a = b;
        b = temp
    }
    //此时 a < b
    if(b > c){
        temp = b;
        b = c;
        c = temp;
    }
    // 此时 b < c,此时c是最大的
    //交换后再次比较
    if(a > b) {
        temp = a;
        a = b;
        b = temp
    }
    //此时 a < b < c //不排除等号
    //假设执行 c - b 次
    cnt = c - b;
    //cnt >= a,那么经过a的抵消,a为0,c >= b,那么最大此时就是 a + b
    if(cnt >= a) {
        return a + b;
    } else {
       //此时b = c
       //计算a抵消后的数
        a = a - cnt;
       //计算c抵消后的数
        c = c - cnt;
        //如果a为奇数那么分为两部分抵消bc是操作的数就是 a - 1,如果是偶数分别抵消bc的次数就是a
        if(a % 2 != 0){
            a --;
        } 
        b -= a / 2
        c -= a / 2;
        cnt += a ;
        return cnt + b;
    }
};

264. 丑数 II

给你一个整数 n ,请你找出并返回第 n 个 丑数 。

丑数 就是只包含质因数 23 和/或 5 的正整数。 示例 1:

输入:n = 10
输出:12
解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。

示例 2:

输入:n = 1
输出:1
解释:1 通常被视为丑数。

解题思路:这题的题目要求是数组从1开始,右2 3 5的倍数从小到大排序的一组数据,我们可以将这个数组当成一个二叉树来处理,然后利用小顶堆来排序,每次都讲最小值删掉直到第n个数,此时的堆顶就是dik个数,获取第n个值,这棵树的根节点是1,1的子节点有 2 3 5,然后 2的子节点是2分别再乘以 2 3 5, 3 的子节点也是 3 分别乘以 2 3 5,5也是,后面子节点也是一次类推,但是如果所有节点都分别乘以 2 3 5,所以会有重复数,所以我们每次找子节点都只乘以大于当前质因子的数,比如 2 的子节点 2 3 5 的倍数,3 的子节点就是 3 5 的倍数,5的子节点是 5的倍数,代码如下:

var nthUglyNumber = function(n) {
    //如果n= 1,直接返回1就好
    if(n == 1) return 1;
    //默认数组的第一个数是1
    let data2 = [1];
    // 将所有质因子组成一个数组,方便使用
    let ans = [2,3,5];
    while( n > 1 ){
        n --;
        //获取当前节点,判断最大质因子添加子节点然后维护小顶堆
        let val = data2[0];
        //删除堆顶元素,可以找当下一个可以添加子节点的节点
        data2 = pop_down(data2,'min')
        //判断最大质因子确认子节点个数
        if(val % 5 == 0){
             data2 = push_up(val * 5,data2,'min');
        } else if(val % 3 == 0){
            for(let i = 2; i > 0; i --){
                data2 = push_up(val * ans[i],data2,'min');
            }
        }else{
            for(let i = 2; i >= 0; i --){
                data2 = push_up(val * ans[i],data2,'min');
            }
        }
    }
    return data2[0];
};
//一下代码是维护小顶堆,前面文章有介绍,这里就不介绍了
var push_up = function(val,data,type){
    data.push(val);
    let idx = data.length - 1;//当前节点
    let gIdx = parseInt((idx - 1) / 2);//根节点
    while(idx && CMP(idx,gIdx,data,type)){
        data = swap(data[idx],idx,data[gIdx],gIdx,data);
        idx = gIdx;
        gIdx = parseInt((idx - 1)/2);
    }
    return data;
}

var pop_down = function(data,type){
    if(data.length <= 1){
        data = [];
        return data;
    }
    let cnt = data.length;
    //删除最大值,将尾结点放在开头,位置树结构
    data[0] = data.pop();
    cnt --;
    let idx = 0;//当前节点
    let n = cnt -1;//最大节点个数
    let zIdx = 2 * idx + 1;//左子节点
    while(zIdx <= n){
        let temp = idx;//三角区最大值下标
        if(CMP(zIdx,idx,data,type)) temp = zIdx;
        if(zIdx + 1 <= n && CMP(zIdx + 1,temp,data,type)) temp = zIdx + 1;
        if(temp == idx) break;
        data = swap(data[idx],idx,data[temp],temp,data);
        idx = temp;
        zIdx = 2*idx +1;
    }
    return data;
}

var swap = function(v1,i1,v2,i2,data){
    data[i1] = v2;
    data[i2] = v1;
    return data;
}

var CMP = function(val,val2,data,type){
    if(type == 'max'){
        if(data[val] > data[val2]) return true;
        else return false
    }else{
        if(data[val] < data[val2]) return true;
        else return false
    }
}

313. 超级丑数

超级丑数 是一个正整数,并满足其所有质因数都出现在质数数组 primes 中。

给你一个整数 n 和一个整数数组 primes ,返回第 n 个 超级丑数 。

题目数据保证第 n 个 超级丑数 在 32-bit 带符号整数范围内。

解题思路:这题应为质因子个数不确认所有不合适用丑数2的方法来做,可以参考之前的第k个数的做法来做,定义一个数组长度为质子数的长度,用来记录每个质子数的下标,下标对应的是添加数组的下标,每一轮计算当前质子数与数组对应的下标相乘取最小的数插入数组,数组默认是1,代码如下:

var nthSuperUglyNumber = function(n, primes) {
    //定义一个数组的长度为质子数的个数,并默认都是从0开始,记录当前质子数的在data的下标
    let p = new Array(primes.length).fill(0);
    //定义一个数组等于质子数数组
    let nus = [...primes];
    //默认数组第一位数为1
    let data = [1];
    while(n -- > 1){
        // 获得当前所有质子数乘以data下边数的最小值,加入到data里
        let min = Math.min(...nus);
        data.push(min);
        for(let i = 0; i < primes.length; i ++){
            // 根基min找到是那个质子数是相乘最小,所以向后走一步
            if(primes[i] * data[p[i]] == min){
                p[i] ++;
                //重置nus数组
                nus[i] = primes[i] * data[p[i]];
            }
        }
    }
    return data.pop();
};