《JS玩算法系列》海王的鱼塘

918 阅读8分钟

这是大冰块2021年第3篇原创文章,和大冰块一起在前端领域努力吧!!!


写在前面

不知为什么,感觉2020年面试过程中算法题突然流行了起来,不论你是前端后端还是架构师,面试前都得先来两道算法题热热身。

如果你很干脆的回答不会,hr小姐姐就会吃惊地问你:什么?身为一个前端程序员竟然不会算法??? 语气口吻就好比你的亲戚问你:不会吧不会吧!身为一个程序员不会修电脑??? 让你心跳加速脸红娇喘羞愧难当...

为了避免这种尴尬的情况出现,今天趁着领导不在阳光正好,我们一起来摸一条某大厂中等算法题的大鱼试试看,想看《海王的鱼塘》可以直接跳到尾部挑战一下,不过大冰块建议先从《聪明的销售》看起~


《聪明的销售》

销售主管的任务是出售一系列的物品,其中每个物品都有一个编号。 由于出售具有相同编号的商品会更容易,所以销售主管决定删除一些物品。 现在她知道她最多能删除多少物品,她想知道最终袋子里最少可以包含多少种不同编号的物品。 例如,最开始她有n = 6 个物品,编号为:ids = [1,1,1,2,2,3],她最多可以删除 n = 2 个物品。 如果删除两个物品 1,则剩下的物品 ids = [1,2,2,3],此时她拥有三种不同编号的物品。 如果删除两个物品 2,则剩下的物品 ids = [1,1,1,3],此时她拥有两种不同编号的物品。 如果删除物品 2 和物品 3 各 1个,则剩下的物品 ids = [1,1,1,2],此时她拥有两种不同编号的物品。 我们发现,物品最多可以剩下两种不同的编号,所以你的程序要返回 2

ids 的大小不超过 10^5 1 ≤ ids[i] ≤ 1000000 1 ≤ m ≤ 100000

样例:

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

在这里我要吐槽一句:出题人的语文学的极其差不好,在座的各位没意见吧?

实不相瞒,我看了三遍题目,终于看明白了出题人的意思,不就是数组中删掉n个元素,要求剩余的元素中,相同的元素越多越好嘛!

看明白了题目意思,就已经成功了一半。

算法题的难度50%在于题目读不懂,另50%在于题目读不懂不想做下去。 ——《大冰块语录》

既然已经成功了一半,那么趁热打铁,理一下思路吧!

如果想要删掉n个元素,剩余的元素中,相同的元素越多越好,那么:删掉的元素出现的次数肯定是越少越好。

所以这道题其实是求出每个元素出现的次数,只要求出每个元素出现的次数,答案就近在咫尺唾手可得呼之欲出了(大冰块的语文是不是比出题人要好?)。

就这???我微微一下,劈里啪啦敲出如下demo:

let arr = ['苹果','苹果','苹果','香蕉','香蕉','橘子','橘子','橘子','草莓','火龙果','火龙果']
let obj = {}
arr.forEach( item => obj.hasOwnProperty(item) ? obj[item]+=1 : obj[item] = 1 )
console.log(obj)  // 控制台输出:{苹果: 3, 香蕉: 2, 橘子: 3, 草莓: 1, 火龙果: 2}

如果要删掉5个,就从数量最少的水果删,删够5个即可,我一眼就看出来草莓1+火龙果2+香蕉2刚好等于5,删掉它们即可。

可是程序并不知道,所以我们应该先根据水果数量对obj进行排序,才能顺利的从少到多依次删除对应水果。

可是object对象真的能排序吗?我知道js里的object对象存在自动排序机制,但是我没有对object对象做过排序,所以本文暂时不做讨论object对象属性排序的问题。保险起见,我们采用人人都会的数组排序方式来解决。

把代码重构如下:

let arr = ['苹果','苹果','苹果','香蕉','香蕉','橘子','橘子','橘子','草莓','火龙果','火龙果']
let resultArr = []
arr.forEach( item => {
    let obj = {name: item, num: 1}
    let i = resultArr.findIndex(element => element.name == item)
    i !== -1 ? resultArr[i].num+=1 : resultArr.push(obj)
})
resultArr.sort((a,b)=>a.num-b.num);
console.log(resultArr) 
// 控制台输出:[{name: "草莓", num: 1},{name: "香蕉", num: 2},{name: "火龙果", num: 2},{name: "苹果", num: 3},{name: "橘子", num: 3}]
Object.keys(obj).length

排序完成,那么从num最小的元素开始删除吧!

如果需要删除5个,则声明一个变量sum赋值为0,从第一个元素开始,把元素的num累加给sum,这样对比 sum是否大于5即可,如果大于5则不能删除,直接结束。如果小于5,则累加下一个元素的num,直到大于5。

代码重构如下

let arr = ['苹果','苹果','苹果','香蕉','香蕉','橘子','橘子','橘子','草莓','火龙果','火龙果']
let resultArr = []
arr.forEach( item => {
    let obj = {name: item, num: 1}
    let i = resultArr.findIndex(element => element.name == item)
    i !== -1 ? resultArr[i].num+=1 : resultArr.push(obj)
})
resultArr.sort((a,b)=>a.num-b.num);
let sum = 0
resultArr.map((item,index)=>{
    sum += item.num
    sum > 5 ? '' : delete resultArr[index]
})
console.log(resultArr) 
// 控制台输出:[empty × 3,{name: "苹果", num: 3},{name: "橘子", num: 3}]

虽然map()方法看起来代码精简了不少,但是从如上代码运行角度来说,map()不能终止,在多个元素的情况下效率并没有for循环高,所以我们把代码优化如下:

let arr = ['苹果','苹果','苹果','香蕉','香蕉','橘子','橘子','橘子','草莓','火龙果','火龙果']
let resultArr = []
arr.forEach( item => {
    let obj = {name: item, num: 1}
    let i = resultArr.findIndex(element => element.name == item)
    i !== -1 ? resultArr[i].num+=1 : resultArr.push(obj)
})
resultArr.sort((a,b)=>a.num-b.num);
let sum = 0
for (let index = 0; index < resultArr.length; index++) {
    sum += resultArr[index].num
    // 三元运算符中不能使用return和break,因为三元运算符中要写表达式。所以暂时写if判断吧~
    if(sum > 5){ 
        break
    }else{
        resultArr.splice(index,1)
        index-- // splice()删除元素后,会改变原数组length,所以要index--
    }
}
console.log(resultArr) 
// 控制台输出:[{name: "苹果", num: 3},{name: "橘子", num: 3}]
		

看起来好像满足了题目的要求,这也没什么难度嘛~下面来封装一下试试看吧:

const smartSale = (array=[],number=0)=>{
    let resultArr = []
    array.forEach( item => {
        let obj = {name: item, num: 1}
        let i = resultArr.findIndex(element => element.name == item)
        i !== -1 ? resultArr[i].num+=1 : resultArr.push(obj)
    })
    resultArr.sort((a,b)=>a.num-b.num);
    let sum = 0
    for (let index = 0; index < resultArr.length; index++) {
        sum += resultArr[index].num
        if(sum > number){ 
            break  // 三元运算符中不能使用return和break,因为三元运算符中要写表达式。所以暂时写if判断吧~
        }else{
            resultArr.splice(index,1)
            index--  // splice()删除元素后,会改变原数组length,所以要index--
        }
    }
    return resultArr.length
}
let arr = ['苹果','苹果','苹果','香蕉','香蕉','橘子','橘子','橘子','草莓','火龙果','火龙果']
smartSale(arr,3)
// 控制台输出:3

测试完美运行!那么考虑一下题目下面的限定条件吧:

ids 的大小不超过 10^5 1 ≤ ids[i] ≤ 1000000 1 ≤ m ≤ 100000

即传入的数组length : 1 ≤ array.length ≤ 1000000,传入的number : 1 ≤ number ≤ 100000

const smartSale = (array=[],number=0)=>{
    if (array.length < 1 || array.length > 1000000) throw new Error("数组的长度范围限制为1-1000000")
    if (number < 1 || number > 100000) throw new Error("要删除的数量范围限制为1-100000")
    let resultArr = []
    array.forEach( item => {
        let obj = {name: item, num: 1}
        let i = resultArr.findIndex(element => element.name == item)
        i !== -1 ? resultArr[i].num+=1 : resultArr.push(obj)
    })
    resultArr.sort((a,b)=>a.num-b.num);
    let sum = 0
    for (let index = 0; index < resultArr.length; index++) {
        sum += resultArr[index].num
        if(sum > number){ 
            break  // 三元运算符中不能使用return和break,因为三元运算符中要写表达式。所以暂时写if判断吧~
        }else{
            resultArr.splice(index,1)
            index--  // splice()删除元素后,会改变原数组length,所以要index--
        }
    }
    return resultArr.length
}

根据这道算法题的思路,我也编了一道算法题供诸君思考:

《海王的鱼塘》

海王的鱼塘里有n条不同的鱼,但是海王的精力和鱼塘面积有限,只能满足其中m条鱼的需求。海王决定根据每条鱼的送给自己的礼物数量,以及这条鱼的外貌情况,衡量优质鱼的价值,然后决定是否放生这条鱼。

海王衡量优质鱼的价值=礼物数量 * 5 + 外貌 * 50

相同情况下,海王会优先放生外貌分最低的鱼。

目前每个海王鱼塘里都有50 — 1500条鱼,同时每个海王的的精力和鱼塘面积可容纳鱼的数量不同,范围是10—1000。鱼送的礼物数量随机从10 — 50不等,外貌为随机80 — 100分不等。

海王该怎么放生才能保证自己的鱼塘是满的,同时都是优质鱼?

掏出你的大脑袋,开动你的脑筋吧~ 把你的答案码在评论区,供各位海王参考一下~

写在后面

原创不易,如有错误,欢迎指正。

如果有帮助到你,请给大冰块悄悄点赞关注,你的点赞关注就是我写下去的动力。

让我们一起在前端的路上进步吧~😜