933.最近的请求次数
- 有新请求就入队,3000ms前发出的请求出队
- 队列的长度就是最近请求次数
/** 时间复杂度:O(n) while循环体 n为踢出对的请求个数
* 空间复杂度:O(n) 定义了一个数组,n为队列的长度
*/
var RecentCounter = function() {
this.q = []
};
RecentCounter.prototype.ping = function(t) {
// 新请求入队
this.q.push(t)
// 把不在时间范围内的老请求出队
while(this.q[0]<t-3000){
this.q.shift()
}
return this.q.length
};
数据流中的第K大元素
int k = 3;
int[] arr = [4,5,8,2];
KthLargest kthLargest = new KthLargest(3, arr);
kthLargest.add(3); // returns 4
kthLargest.add(5); // returns 5
思路:
方法1:保存前K个最大的值;全部排序
方法2:优先队列,每次把最大的放在最上面,或把最小的放在最上面
小顶堆:把最小的放根结点,所以要保证元素个数等于K(意思是这个堆的个数为K个,最顶的是最小的,遍历数组的元素,如果有比它小的就不用进来排队了,如果比它大,就把最小的踢掉,剩下的元素重新调整log2^k)
239.滑动窗口最大值
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 ------> 3
1 [3 -1 -3] 5 3 6 7 ------> 3
1 3 [-1 -3 5] 3 6 7 ------> 5
1 3 -1 [-3 5 3] 6 7 ------> 5
1 3 -1 -3 [5 3 6] 7 ------> 6
1 3 -1 -3 5 [3 6 7] ------> 7
所有的滑动窗口题用队列去处理
- 方法1:暴力求解,枚举窗口的起点位置,起点开始到len-k的最大值,写两个嵌套循环O(n*K)
const maxSlidingWindow = function (nums, k) {
const len = nums.length
const res = []
let left = 0, right = k-1
// 当数组没有被遍历完时,执行循环体内的逻辑
while(right < len){
const max = calMax(nums, left, right)
res.push(max)
// 左右指针前进一步
left++
right++
}
return res
}
function calMax(arr, left, right){
if (!arr || !arr.length) return
// 初始化为第一个元素
let maxNum = arr[left]
for(let i = left; i<=right; i++){
// 谁比我大,我就变成谁
if(arr[i] > maxNum) maxNum = arr[i]
}
return maxNum
}
console.log(maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3))
上述通过遍历来更新最大值产生了K
- 方法2:线性队列,O(n+k)即O(n)一次解决
1)设置一个双端队列,存储nums的下标
2)遍历每个元素,拿元素跟双端队列里的队尾做比较(双端队列:比我小的都踢出去)
当前元素小于队尾,就直接入队,否则,将队尾元素逐个出队,直到队尾元素大于等于当前元素为止(比如,窗口移到2 3 4位置,5进来的时候,要跟窗口其他数对比,只要比5小,都可以从这个栈弹出)
3)检查队头元素,看队头元素是否已经被排除在滑动窗口的范围之外了(即队头小于i-k)。如果是,则将队头元素出队
4)判断滑动窗口状态,被遍历的元素个数小于K,表示还不能动结果数组,只能继续更新队列;如果大于等于k,表示最大值已经出现,队头元素就是最大值
const maxSlidingWindow = function (nums, k) {
// 缓存数组的长度
const len = nums.length;
// 初始化结果数组
const res = [];
// 初始化双端队列
const deque = [];
// 开始遍历数组
for (let i = 0; i < len; i++) {
// 当队尾元素小于当前元素时
while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
// 将队尾元素(索引)不断出队,直至队尾元素大于等于当前元素
deque.pop();
}
// 入队当前元素索引(注意是索引)
deque.push(i);
// 当队头元素的索引已经被排除在滑动窗口之外时
while (deque.length && deque[0] <= i - k) {
// 将队头元素索引出队
deque.shift();
}
// 判断滑动窗口的状态,只有在被遍历的元素个数大于 k 的时候,才更新结果数组
if (i >= k - 1) {
res.push(nums[deque[0]]);
}
}
// 返回结果数组
return res;
}
设计循环队列
循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
循环队列可以高效利用存储空间,普通队列需要很大的存储空间,存储空间会往上跑,循环队列可以避免这个问题。
你的实现应该支持如下操作:
MyCircularQueue(k): 构造器,设置队列长度为 k 。
Front: 从队首获取元素。如果队列为空,返回 -1 。
Rear: 获取队尾元素。如果队列为空,返回 -1 。
enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
isEmpty(): 检查循环队列是否为空。
isFull(): 检查循环队列是否已满。
class MyCircularQueue{
constructor(k) {
// 用来保存数据长度为k的数据结构
this.list = Array(k)
// 队首指针
this.front = 0
// 队尾的指针
this.rear = 0
// 队列的长度
this.max = k
}
// 入队:rear指针指向哪里,哪里就可以入队,且rear指针+1
enQueue(num) {
if (this.isFull()) {
return false
} else {
this.list[this.rear] = num
// 指针向后移动
this.rear = (this.rear + 1) % this.max
return true
}
}
// 出队:front指针指向谁,谁就出队,且front指针+1
deQueue() {
let v = this.list[this.front]
this.list[this.front] = ''
console.log('front----->'+this.front)
// front指针往后移动
this.front = (this.front + 1) % this.max
console.log('front222----->'+this.front)
return v
}
isEmpty() {
return this.front === this.rear && !this.list[this.front]
}
isFull() {
return this.front === this.rear && !!this.list[this.front]
}
Front() {
return this.list[this.front]
}
rear() {
let rear = this.rear - 1
return this.list[rear < 0 ? this.max - 1 : rear]
}
}
const cQueue = new MyCircularQueue(5); // 设置长度为 3
cQueue.enQueue(1); // 返回 true
cQueue.enQueue(2); // 返回 true
cQueue.enQueue(3); // 返回 true
cQueue.enQueue(4); // 返回 false,队列已满
// cQueue.rear(); // 返回 3
// cQueue.isFull(); // 返回 true
cQueue.deQueue(); // 返回 true
cQueue.deQueue(); // 返回 true
cQueue.deQueue(); // 返回 true
// cQueue.enQueue(4); // 返回 true
// cQueue.rear(); // 返回 4
console.log(cQueue)
任务调度器
给定一个用字符数组表示的 CPU 需要执行的任务列表。其中包含使用大写的 A - Z 字母表示的26 种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。CPU 在任何一个单位时间内都可以执行一个任务,或者在待命状态。
然而,两个相同种类的任务之间必须有长度为 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。
你需要计算完成所有任务所需要的最短时间。
示例 :
输入:tasks = [“A”,“A”,“A”,“B”,“B”,“B”], n = 2
输出:8
解释:A -> B -> (待命) -> A -> B -> (待命) -> A -> B.
在本示例中,两个相同类型任务之间必须间隔长度为 n = 2 的冷却时间,而执行一个任务只需要一个单位时间,所以中间出现了(待命)状态。
思路:任务清单keys [‘A’,‘B’] 是用来遍历循环,拿到当前数量最多的任务,然后个push到临时队列tmp中,临时队列只能放到n+1个(3个)任务,不够放的就用’-'来补齐(做到两个相同类型任务之间必须长度为n)。执行完一个临时队列tmp后,再循环判断还有没有任务清单keys,有的话继续前面的动作,直到keys[0]=undefined为止
步骤:
1)先记录每类任务的数量 Q:{A:3, B:3}
2)max记录最多数量的先执行,先被push到临时任务队列tmp中,这个tmp就是AB-AB-AB(不够n+1个的往里面补"-")
- 获取任务清单keys([‘A’,‘B’]),遍历任务清单找到数量最多的key,拿到数量最多的A先push到临时队列tmp中,表示执行这个队列,所以得把Q中A的数量减一,从任务清单keys清除A
- 这个临时队列容量为n+1,所以能被循环小于n次,执行完一个临时队列后,把临时队列累加给p, 再去循环执行下一个临时队列
const leastInterval = (tasks, n) => {
// 表示最终队列执行的结果
let q = ''
// 对归类进行存储
let Q = {}
tasks.forEach(item => {
if (Q[item]) {
Q[item]++
} else {
Q[item] = 1
}
});
// 队列中还有任务就要循环,为什么为1,是因为效率问题,不要一直判断对象Q是否为空,会重复计算
while (1) {
// 处理边界问题
// 任务清单:只要有key值,表示任务队列中还有,key值没有就表示已经都处理掉了,就不用每次去判断Q是否为空
let keys = Object.keys(Q)
if (!keys[0]) {
break
}
// 处理任务:找到哪个数量最多,数量最多的就优先处理
// 声明一个队列用来存储1+n任务单元
let tmp = []
for (let i = 0; i <= n; i++) {
// 记录最大值
let max=0
// 最大值名称
let key
// 最大值索引位置
let pos
// 任务清单中找当前任务还剩下多少
keys.forEach((item, idx)=>{
// 找最大值
if(Q[item]>max){
max = Q[item]
key = item
pos = idx
// console.log(item)
// console.log(idx)
}
})
// 判断是否找到最大值key,如果是就push到临时队列
if(key){
// console.log('11--->'+key)
tmp.push(key)
// 在任务清单中清除
keys.splice(pos, 1) // ["A", "B"] ===> ["B"]
Q[key]--
if(Q[key]<1){
delete Q[key]
}
// console.log('222--->'+keys)
}else{
break
}
}
// 如果不够,后面补什么
q +=tmp.join('').padEnd(n+1,'-')
}
// 边界处理,最后不要出现冷却时间
q = q.replace(/-+$/g, '')
return q.length
}
const tasks = ["A", "A", "A", "B", "B", "B"], n = 2
console.log(leastInterval(tasks, n))