线程池与任务队列专题一

240 阅读6分钟

这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战 | 创作学习持续成长,夺宝闯关赢大奖 - 掘金 (juejin.cn)

题目链接

  1. 最近的请求次数 leetcode-cn.com/problems/nu…
  2. 第k个数 leetcode-cn.com/problems/ge…
  3. 亲密字符串 leetcode-cn.com/problems/bu…
  4. 柠檬水找零 leetcode-cn.com/problems/le…
  5. 煎饼排序 leetcode-cn.com/problems/pa…

题解及分析

最近的请求次数

写一个 RecentCounter 类来计算特定时间范围内最近的请求。
请你实现 RecentCounter 类:
RecentCounter()初始化计数器,请求数为0。
int ping(int t)在时间t添加一个新请求,其中t表示以毫秒为单位的某个时间,并返回过去3000毫秒内发生的所有请求数(包括新请求)。确切地说,返回在[t-3000, t]内发生的请求数。
保证每次对ping的调用都使用比之前更大的t值。

这道题我是没看懂的...leetcode的题解如下:

我们只会考虑最近 3000 毫秒到现在的 ping 数,因此我们可以使用队列存储这些 ping 的记录。当收到一个时间 t 的 ping 时,我们将它加入队列,并且将所有在时间 t - 3000 之前的 ping 移出队列。

即是说,需要在每次数据进入队列的时候查看整个队列并移除所有大于t-3000的值

var RecentCounter = function() {
    this.queue = []
};

RecentCounter.prototype.ping = function(t) {
    this.queue.push(t);
    while (this.queue[0] < t - 3000) {
        this.queue.shift();
    }
    return this.queue.length;
};

折腾一点,整个二分法,效率一下子下来了:

var RecentCounter = function() {
  this.arr = [];
};

RecentCounter.prototype.ping = function(t) {
  this.arr.push(t);
  return this.arr.length - bisectLeft(this.arr, t - 3000);
};

// 二分查找
function bisectLeft(arr, target) {
    let left = 0
    let right = arr.length
    while(left < right) {
        const middle = Math.floor((left + right) / 2)
        if(target <= arr[middle]) {
            right = middle
        } else {
            left = middle + 1
        }
    }
    return left
}

第k个数

有些数的素因子只有3,5,7,请设计一个算法找出第k个数。注意,不是必须有这些素因子,而是必须不包含其他的素因子。例如,前几个数按顺序应该是1,3,5,7,9,15,21。

这道题完全看不懂...感谢leetcode的大神题解[第 k 个数]:就是想不通为何三指针? - 第 k 个数 - 力扣(LeetCode) (leetcode-cn.com)

解题重点:一个丑数总是由前面的某一个丑数x3/x5/x7得到 (丑数可以看做是素数)

这道题思路大致如下:

  • 维护一个数组,存放k+1个元素
  • 维护三个指针,分表代表3,5,7倍数的下标
    • 维护这三个指针的意义:从结果的规律上看,大致是第n+3个元素为n的倍数,我们可以以此来取得对应的n值。因此这三个指针的作用是保证尽可能小的素数来取值,以获得尽可能小的乘积
  • 这三个指针每次向数组推入的值都会使对应的指针向后移一位
    • 另一层,指针同时也可以用来互乘时(如3*5)的去重
  • 每次我们从数组中取得上一次值对应的下标,分别乘以对应的值,讲最小值填入数组 (语无伦次.jpg)
var getKthMagicNumber = function(k) {
    const arr = new Array(k + 1).fill(1)
    let p3 = 1
    let p5 = 1
    let p7 = 1
    for(let i = 2; i <= k; i++) {
        let num3 = arr[p3] * 3
        let num5 = arr[p5] * 5
        let num7 = arr[p7] * 7
        arr[i] = Math.min(num3, num5, num7)
        if(arr[i] === num3) p3++;
        if(arr[i] === num5) p5++;
        if(arr[i] === num7) p7++;
    }
    return arr[k]
};

亲密字符串

给你两个字符串s和goal,只要我们可以通过交换s中的两个字母得到与goal相等的结果,就返回true ;否则返回false 。
交换字母的定义是:取两个下标i和j(下标从0开始)且满足i != j,接着交换s[i]和s[j]处的字符。
例如,在"abcd"中交换下标0和下标2的元素可以生成"cbad"。

亲密字符串需要满足的条件:

  • 长度相等
  • 如果两个字符串相同,那么字符串中一定存在相同的字符
  • 如果两个字符串(s和goal)不同,那么字符串中差异的位置必定存在s[i]=goal[j],s[j]=goal[i]
var buddyStrings = function(s, goal) {
    if (s.length != goal.length) {
        return false;
    }
    
    if (s === goal) {
        const count = new Array(26).fill(0);
        for (let i = 0; i < s.length; i++) {
            // 感谢leetcode,让我又了解了一个api的用法
            count[s[i].charCodeAt() - 'a'.charCodeAt()]++;
            if (count[s[i].charCodeAt() - 'a'.charCodeAt()] > 1) {
                return true;
            }
        }
        return false;
    } else {
        let first = -1, second = -1;
        for (let i = 0; i < s.length; i++) {
            if (s[i] !== goal[i]) {
                if (first === -1)
                    first = i;
                else if (second === -1)
                    second = i;
                else
                    return false;
            }
        }

        return (second !== -1 && s[first] === goal[second] && s[second] === goal[first]);
    }
};

柠檬水找零

在柠檬水摊上,每一杯柠檬水的售价为5美元。顾客排队购买你的产品,(按账单bills支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付5美元、10美元或20美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组bills,其中bills[i]是第i位顾客付的账。如果你能给每位顾客正确找零,返回true,否则返回false 。

(这道题有算法吗?)

var lemonadeChange = function(bills) {
    let five = 0
    let ten = 0;
    for (const bill of bills) {
        if(bill === 5) {
            five++
        } else if(bill === 10) {
           ten++
           five--
        } else if(bill === 20) {
            if (five  && ten ) {
                five--;
                ten--;
            } else if (five >= 3) {
                five -= 3;
            } else {
                return false;
            }
        }
        if(ten < 0 || five < 0) {
            return false
        }
    }
    return true;
};

煎饼排序

给你一个整数数组 arr ,请使用 煎饼翻转 完成对数组的排序。
一次煎饼翻转的执行过程如下:
选择一个整数 k ,1 <= k <= arr.length
反转子数组 arr[0...k-1](下标从 0 开始)
例如,arr=[3,2,1,4],选择k=3进行一次煎饼翻转,反转子数组[3,2,1],得到arr=[1,2,3,4]。
以数组形式返回能使 arr 有序的煎饼翻转操作所对应的k值序列。任何将数组排序且翻转次数在10*arr.length范围内的有效答案都将被判断为正确。

这道题目实际上是一种队列的处理。按照题意,我们得出的思路如下:

  • 首先找出队列中的最大的值,将它放在队首
  • 然后将整个队列翻转一次,将最大值放到最后一位,并派出在排序队列之外(折腾)
  • 在剩余的队列中找出最大的值,重复前两步,直到队列排序完毕
var pancakeSort = function(arr) {
    const res = []
    let maxIndex
    while(arr.length > 1) {
        maxIndex = findMaxIndex(arr)
        // maxIndex为0时默认省去一次翻转
        maxIndex > 0 && res.push(maxIndex + 1)
        reverse(arr, maxIndex)
        reverse(arr, arr.length - 1)
        res.push(arr.length)
        // 去掉最后一位,只需要处理剩下的数组即可
        arr.pop()
    }
    return res
};

// 查找最大值的index
var findMaxIndex = arr => {
    const max = Math.max(...arr)
    return arr.findIndex(i => i === max)
}

// 反转队列的方法,只反翻转个部分(0到k)的队列
var reverse = (arr, k) => {
    if(k < 1) return
    let i = 0
    let j = k
    while(i < j) {
        [arr[i], arr[j]] = [arr[j], arr[i]]
        i++
        j--
    }
}

值得一提的是,这道题示例中的答案是错的...(好家伙,搞心态是吧)

题目总结

不难看到,这些题都是队列类型的题目。队列在js中主要体现为数组,这类题目复杂在多种不同场景的归纳,处理和维护(听着像是业务逻辑?),而2题需要的更多是数学类的规律总结和抽象(最艰难的部分)。