1.933. 最近的请求次数
写一个 RecentCounter 类来计算特定时间范围内最近的请求。
请你实现 RecentCounter 类:
RecentCounter() 初始化计数器,请求数为 0 。
int ping(int t) 在时间 t 添加一个新请求,其中 t 表示以毫秒为单位的某个时间,并返回过去 3000 毫秒内发生的所有请求数(包括新请求)。确切地说,返回在 [t-3000, t] 内发生的请求数。
保证 每次对 ping 的调用都使用比之前更大的 t 值。
示例 1:
输入:
[“RecentCounter”, “ping”, “ping”, “ping”, “ping”]
[[], [1], [100], [3001], [3002]]
输出:
[null, 1, 2, 3, 3]
返回 3
recentCounter.ping(3002); // requests = [1, 100, 3001, 3002],范围是 [2,3002],返回 3
提示:
1 <= t <= 10^9
保证每次对 ping 调用所使用的 t 值都 严格递增
至多调用 ping 方法 10^4 次
简单的队列应用题,对于每个ping操作去检查队首是否是当前时间的3000ms之前的时间,如果是就删除队首到t的3000ms之内的时间。最后返回队列的长度。
class RecentCounter {
queue<int> q;
public:
RecentCounter() {
while(!q.empty()){
q.pop();
}
}
int ping(int t) {
q.push(t);
while(!q.empty()&&q.front()<t-3000){
q.pop();
}
return q.size();
}
};
2.2073. 买票需要的时间
有 n 个人前来排队买票,其中第 0 人站在队伍 最前方 ,第 (n - 1) 人站在队伍 最后方 。
给你一个下标从 0 开始的整数数组 tickets ,数组长度为 n ,其中第 i 人想要购买的票数为 tickets[i] 。
每个人买票都需要用掉 恰好 1 秒 。一个人 一次只能买一张票 ,如果需要购买更多票,他必须走到 队尾 重新排队(瞬间 发生,不计时间)。如果一个人没有剩下需要买的票,那他将会 离开 队伍。
返回位于位置 k(下标从 0 开始)的人完成买票需要的时间(以秒为单位)。
示例 1:
输入:tickets = [2,3,2], k = 2
输出:6
示例 2:
输入:tickets = [5,1,1,1], k = 0
输出:8
提示:
n == tickets.length
1 <= n <= 100
1 <= tickets[i] <= 100
0 <= k < n
也是类似于循环排队的问题,这里终止的条件是下标为k的元素的tickets为0即可返回他所用的时间。
我们利用结构体数组来模拟队列,front为队首,rear为队尾,当front<rear的时候我们就继续遍历下去也相当于队列不为空,每次模拟到一个元素时间加一,并且观察当前元素的票数是否为0,如果不是入队否则观察他的原始下表是否为k,如果是直接返回,否则继续遍历。
typedef struct {
int idx;//原始下标
int need;//需要的票数
}Q;
int timeRequiredToBuy(int* tickets, int ticketsSize, int k){
Q q[15000],tmp;
int i=0;
int front=0,rear=0;
for(int i=0;i<ticketsSize;i++){
q[rear].idx=i;
q[rear].need=tickets[i];
rear++;
}
int time=0;
while(front<rear)
{
tmp=q[front++];
tmp.need--;
time++;
if(tmp.need){
q[rear++]=tmp;
}
else if(tmp.idx==k){
return time;
}
}
return 0;
}
3.641. 设计循环双端队列
设计实现双端队列。
实现 MyCircularDeque 类:
MyCircularDeque(int k) :构造函数,双端队列最大为 k 。
boolean insertFront():将一个元素添加到双端队列头部。 如果操作成功返回 true ,否则返回 false 。
boolean insertLast() :将一个元素添加到双端队列尾部。如果操作成功返回 true ,否则返回 false 。
boolean deleteFront() :从双端队列头部删除一个元素。 如果操作成功返回 true ,否则返回 false 。
boolean deleteLast() :从双端队列尾部删除一个元素。如果操作成功返回 true ,否则返回 false 。
int getFront() ):从双端队列头部获得一个元素。如果双端队列为空,返回 -1 。
int getRear() :获得双端队列的最后一个元素。 如果双端队列为空,返回 -1 。
boolean isEmpty() :若双端队列为空,则返回 true ,否则返回 false 。
boolean isFull() :若双端队列满了,则返回 true ,否则返回 false 。
示例 1:
输入
[“MyCircularDeque”, “insertLast”, “insertLast”, “insertFront”, “insertFront”, “getRear”, “isFull”, “deleteLast”, “insertFront”, “getFront”]
[[3], [1], [2], [3], [4], [], [], [], [4], []]
输出
[null, true, true, true, false, 2, true, true, true, 4]
提示:
1 <= k <= 1000
0 <= value <= 1000
insertFront, insertLast, deleteFront, deleteLast, getFront, getRear, isEmpty, isFull 调用次数不大于 2000 次
读过这道题后我们会发现,并不需要我们设计一个循环队列,观察数据量k=1000,我们只需要设计一个4000大小的数组即可,并且定义一个center为2000,front为center,rear为front-1,这样是为了方便求出当前队列的长度和头尾插入。
模拟队列有两种方式,链表和数组,各有优缺点,使用链表模拟方便插入和获得首位元素,使用数组方便访问随即元素操作也简单。链表的问题就是随机访问的时间复杂度为O(N),数组的问题是随机插入的时间复杂度为O(N)这里我就选择的是数组。
数组的长度为raer-front+1,头插尾插的时候先判断当前队列长度是否超过了先前初始化时候给我们的k,之后先对头尾进行移动再插入。头尾删除的时候我们先判断队列是否为空,在删除元素的时候也不需要删除元素只需要移动头尾指针即可。get操作就直接返回首位指针指向的元素即可。
class MyCircularDeque {
#define CENTER 2500
int data[5000];//这里的center定义的稍大了,其实2000即可
int cap;
int front,rear;
public:
int getcount(){
return rear-front+1;
}
MyCircularDeque(int k) {
cap=k;
front=CENTER;
rear=front-1;
}
bool insertFront(int value) {
if(isFull()){
return false;
}else {
data[--front]=value;
}
return true;
}
bool insertLast(int value) {
if(isFull()){
return false;
}else {
data[++rear]=value;
}
return true;
}
bool deleteFront() {
if(isEmpty()){
return false;
}
++front;
return true;
}
bool deleteLast() {
if(isEmpty()){
return false;
}
rear--;
return true;
}
int getFront() {
if(isEmpty()){
return -1;
}
return data[front];
}
int getRear() {
if(isEmpty()){
return -1;
}
return data[rear];
}
bool isEmpty() {
return getcount()==0?1:0;
}
bool isFull() {
return getcount()==cap?1:0;
}
};
4.1670. 设计前中后队列
请你设计一个队列,支持在前,中,后三个位置的 push 和 pop 操作。
请你完成 FrontMiddleBack 类:
FrontMiddleBack() 初始化队列。
void pushFront(int val) 将 val 添加到队列的 最前面 。
void pushMiddle(int val) 将 val 添加到队列的 正中间 。
void pushBack(int val) 将 val 添加到队里的 最后面 。
int popFront() 将 最前面 的元素从队列中删除并返回值,如果删除之前队列为空,那么返回 -1 。
int popMiddle() 将 正中间 的元素从队列中删除并返回值,如果删除之前队列为空,那么返回 -1 。
int popBack() 将 最后面 的元素从队列中删除并返回值,如果删除之前队列为空,那么返回 -1 。
请注意当有 两个 中间位置的时候,选择靠前面的位置进行操作。
比方说:
将 6 添加到 [1, 2, 3, 4, 5] 的中间位置,结果数组为 [1, 2, 6, 3, 4, 5] 。
从 [1, 2, 3, 4, 5, 6] 的中间位置弹出元素,返回 3 ,数组变为 [1, 2, 4, 5, 6] 。
输入:
[“FrontMiddleBackQueue”, “pushFront”, “pushBack”, “pushMiddle”, “pushMiddle”, “popFront”, “popMiddle”, “popMiddle”, “popBack”, “popFront”]
[[], [1], [2], [3], [4], [], [], [], [], []]
输出:
[null, null, null, null, null, 1, 3, 4, 2, -1]
提示:
1 <= val <= 10^9
最多调用 1000 次 pushFront, pushMiddle, pushBack, popFront, popMiddle 和 popBack 。
和上题说的一样,不同的是我们这里要对中间元素来进行插入和删除,如果使用链表的话这个操作会十分简单,但是要得到它的值就是O(N)的复杂度了,不过也可以在该过程记录中间节点来省去便利的操作,但是实际操作过后我们会遇到各种各样的问题,比如尾结点的记录,是否要设计哨兵位结点,插入和得到中间结点的时候除了他本身要记录还要找到他之前和之后的元素。如果使用数组的话插入和删除的时候我们要对数组整体进行移动这个操作的时间复杂的也是O(N),但是得到中间元素的操作是O(1).为了简化操作这里继续选择利用数组了。
头和尾和上题一样不再解释了,中间元素的插入我们需要观察队列的元素数量来去对应中间元素的下标,如果元素数量为0或者1利用元素数量除以2得到的是0,这个时候我们要进行头插。当队列长度除以2不为0的时候我们会发现要插入的位置就是front往后队列长度一半的地方。这里可以自己观察一下很容易找到这个规律。
而删除中间元素,也是可以找到一个规律,即下标为front+(cnt-1)/2。
这里解释一下:
队列长度为1,直接头删。
队列长度为2或者3,删除下标为1的元素。
队列长度为4或者5,删除下标为2的元素。
剩下的依次类推,这里我们先判断队列是否为空,然后将其长度减一之后除以2就是相对于front的中间元素的偏移量。
class FrontMiddleBackQueue {
#define CENTER 2500
int data[5000];
int front,rear;
public:
FrontMiddleBackQueue() {
front=CENTER;
rear=front-1;
}
int getcnt(){
return rear-front+1;
}
void pushFront(int val) {
data[--front]=val;
}
void pushMiddle(int val) {
if(getcnt()==1){
pushFront(val);
return ;//特殊情况处理
}
int x=getcnt();
int index=front+x/2;
for(int i=rear+1;i>index;--i){
data[i]=data[i-1];
}
data[index]=val;
++rear;
}
void pushBack(int val) {
data[++rear]=val;
}
int popFront() {
if(getcnt()==0){
return -1;
}
return data[front++];
}
int popMiddle() {
if(getcnt()==0){
return -1;
}
int x=getcnt();
int index=front+(x-1)/2;
int ans=data[index];
for(int i=index;i<rear;++i){
data[i]=data[i+1];
}
--rear;
return ans;
}
int popBack() {
if(getcnt()==0){
return -1;
}
return data[rear--];
}
};