Leetcode——栈和队列(2)
滑动窗口最大值
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶:
你能在线性时间复杂度内解决此题吗?
示例:
输入: 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 <= nums.length <= 10^5-10^4 <= nums[i] <= 10^41 <= k <= nums.length
multiset
STL 自带的 multiset 是一种基于红黑树的数据结构,可以自动对元素进行排序,又允许有重复值。
遍历每个数字,即窗口右移,若超过了k,则需要把左边界值删除,这里不能直接删除 nums[i-k],因为集合中可能有重复数字,我们只想删除一个,而 erase 默认是将所有和目标值相同的元素都删掉,所以我们只能提供一个 iterator,代表一个确定的删除位置,先通过 find() 函数找到左边界 nums[i-k] 在集合中的位置,再删除即可。
然后将当前数字插入到集合中,此时看若 i >= k-1,说明窗口大小正好是k,就需要将最大值加入结果 res 中,而由于 multiset 是按升序排列的,最大值在最后一个元素,我们可以通过 rbeng() 来取出。
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> res;
multiset<int> st;
for (int i = 0; i < nums.size(); ++i) {
if (i >= k) st.erase(st.find(nums[i - k]));
st.insert(nums[i]);
if (i >= k - 1) res.push_back(*st.rbegin());
}
return res;
}
};
优先队列
里面放一个 pair 对儿,由数字和其所在位置组成的,这样我们就可以知道每个数字的位置了,而不用再进行搜索了。
在遍历每个数字时,进行 while 循环,假如优先队列中最大的数字此时不在窗口中了,就要移除,判断方法就是将队首元素的 pair 对儿中的 second(位置坐标)跟 i-k 对比,小于等于就移除。
然后将当前数字和其位置组成 pair 对儿加入优先队列中。此时看若 i >= k-1,说明窗口大小正好是k,就将最大值加入结果 res 中即可。
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> res;
priority_queue<pair<int, int>> q;
for (int i = 0; i < nums.size(); ++i) {
while (!q.empty() && q.top().second <= i - k) q.pop();
q.push({nums[i], i});
if (i >= k - 1) res.push_back(q.top().first);
}
return res;
}
};
deque
用双向队列保存数字的下标,遍历整个数组,如果此时队列的首元素是 i-k 的话,表示此时窗口向右移了一步,则移除队首元素。
然后比较队尾元素和将要进来的值,如果小的话就都移除,然后此时我们把队首元素加入结果中即可。
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> res;
deque<int> q;
for (int i = 0; i < nums.size(); ++i) {
if (!q.empty() && q.front() == i - k) q.pop_front();
while (!q.empty() && nums[q.back()] < nums[i]) q.pop_back();
q.push_back(i);
if (i >= k - 1) res.push_back(nums[q.front()]);
}
return res;
}
};
滑动窗口中位数
中位数是有序序列最中间的那个数。如果序列的大小是偶数,则没有最中间的数;此时中位数是最中间的两个数的平均数。
例如:
[2,3,4],中位数是3[2,3],中位数是(2 + 3) / 2 = 2.5
给你一个数组 nums,有一个大小为 k 的窗口从最左端滑动到最右端。窗口中有 k 个数,每次窗口向右移动 1 位。你的任务是找出每次窗口移动后得到的新窗口中元素的中位数,并输出由它们组成的数组。
示例:
给出 nums = [1,3,-1,-3,5,3,6,7],以及 k = 3。
窗口位置 中位数
--------------- -----
[1 3 -1] -3 5 3 6 7 1
1 [3 -1 -3] 5 3 6 7 -1
1 3 [-1 -3 5] 3 6 7 -1
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] 6
因此,返回该滑动窗口的中位数数组 [1,-1,-1,3,5,6]。
提示:
- 你可以假设
k始终有效,即:k始终小于输入的非空数组的元素个数。 - 与真实值误差在
10 ^ -5以内的答案将被视作正确答案。
multiset
用一个multiset集合,和一个指向最中间元素的iterator。
我们首先将数组的前k个数组加入集合中,由于multiset自带排序功能,所以我们通过k/2能快速的找到指向最中间的数字的迭代器mid,
如果k为奇数,那么mid指向的数字就是中位数;
如果k为偶数,那么mid指向的数跟前面那个数求平均值就是中位数。
当我们添加新的数字到集合中,multiset会根据新数字的大小加到正确的位置,然后我们看如果这个新加入的数字比之前的mid指向的数小,那么中位数肯定被拉低了,所以mid往前移动一个,再看如果要删掉的数小于等于mid指向的数(注意这里加等号是因为要删的数可能就是mid指向的数),则mid向后移动一个。
然后我们将滑动窗口最左边的数删掉,我们不能直接根据值来用erase来删数字,因为这样有可能删掉多个相同的数字,而是应该用lower_bound来找到第一个不小于目标值的数,通过iterator来删掉确定的一个数字。
class Solution {
public:
vector<double> medianSlidingWindow(vector<int>& nums, int k) {
vector<double> res;
multiset<double> ms(nums.begin(), nums.begin() + k);
auto mid = next(ms.begin(), k / 2);
for (int i = k; ; ++i) {
res.push_back((*mid + *prev(mid, 1 - k % 2)) / 2);
if (i == nums.size()) return res;
ms.insert(nums[i]);
if (nums[i] < *mid) --mid;
if (nums[i - k] <= *mid) ++mid;
ms.erase(ms.lower_bound(nums[i - k]));
}
}
};
两个堆
维护small和large两个堆,分别保存有序数组的左半段和右半段的数字,保持small的长度大于等于large的长度。我们开始遍历数组nums,
如果i>=k,说明此时滑动窗口已经满k个了,再滑动就要删掉最左值了,我们分别在small和large中查找最左值,有的话就删掉。然后处理增加数字的情况(分两种情况:
-
如果small的长度小于large的长度,再看如果large是空或者新加的数小于等于large的首元素,我们把此数加入small中。否则就把large的首元素移出并加入small中,然后把新数字加入large。
-
如果small的长度大于large,再看如果新数字大于small的尾元素,那么新数字加入large中,否则就把small的尾元素移出并加入large中,把新数字加入small中)。最后我们再计算中位数并加入结果res中,根据k的奇偶性来分别处理。
class Solution {
public:
vector<double> medianSlidingWindow(vector<int>& nums, int k) {
vector<double> res;
multiset<int> small, large;
for (int i = 0; i < nums.size(); ++i) {
if (i >= k) {
if (small.count(nums[i - k])) small.erase(small.find(nums[i - k]));
else if (large.count(nums[i - k])) large.erase(large.find(nums[i - k]));
}
if (small.size() <= large.size()) {
if (large.empty() || nums[i] <= *large.begin()) small.insert(nums[i]);
else {
small.insert(*large.begin());
large.erase(large.begin());
large.insert(nums[i]);
}
} else {
if (nums[i] >= *small.rbegin()) large.insert(nums[i]);
else {
large.insert(*small.rbegin());
small.erase(--small.end());
small.insert(nums[i]);
}
}
if (i >= (k - 1)) {
if (k % 2) res.push_back(*small.rbegin());
else res.push_back(((double)*small.rbegin() + *large.begin()) / 2);
}
}
return res;
}
};
比较含退格的字符串
给定 S 和 T 两个字符串,当它们分别被输入到空白的文本编辑器后,判断二者是否相等,并返回结果。 # 代表退格字符。
注意:如果对空文本输入退格字符,文本继续为空。
示例 1:
输入:S = "ab#c", T = "ad#c"
输出:true
解释:S 和 T 都会变成 “ac”。
示例 2:
输入:S = "ab##", T = "c#d#"
输出:true
解释:S 和 T 都会变成 “”。
示例 3:
输入:S = "a##c", T = "#a#c"
输出:true
解释:S 和 T 都会变成 “c”。
示例 4:
输入:S = "a#c", T = "b"
输出:false
解释:S 会变成 “c”,但 T 仍然是 “b”。
提示:
1 <= S.length <= 2001 <= T.length <= 200S和T只含有小写字母以及字符'#'。
遍历
对S和T串分别处理完退格操作后再进行比较,那么就可以使用一个子函数来进行字符串的退格处理,
在子函数中,我们新建一个结果 res 的空串,然后遍历输入字符串,
当遇到退格符的时候,判断若结果 res 不为空,则将最后一个字母去掉;
若遇到的是字母,则直接加入结果 res 中即可。
这样S和T串同时处理完了之后,再进行比较即可
class Solution {
public:
bool backspaceCompare(string S, string T) {
return helper(S) == helper(T);
}
string helper(string str) {
string res = "";
for (char c : str) {
if (c == '#') {
if (!res.empty()) res.pop_back();
} else {
res.push_back(c);
}
}
return res;
}
};
for 循环来处理S和T串,分别建立s和t的空串,然后进行退格操作,最后比较s和t串是否相等即可
class Solution {
public:
bool backspaceCompare(string S, string T) {
string s = "", t = "";
for (char c : S) c == '#' ? s.size() > 0 ? s.pop_back() : void() : s.push_back(c);
for (char c : T) c == '#' ? t.size() > 0 ? t.pop_back() : void() : t.push_back(c);
return s == t;
}
};
双指针
我们采用从后往前遍历,因为退格是要删除前面的字符,所以倒序遍历要好一些。
用变量i和j分别指向S和T串的最后一个字符的位置,然后还需要两个变量 cnt1 和 cnt2 来分别记录S和T串遍历过程中连续出现的井号的个数,
因为在连续井号后,要连续删除前面的字母,如何知道当前的字母是否是需要删除,就要知道当前还没处理的退格符的个数。
现在进行 while 循环,条件是i和j至少有一个要大于等于0,然后对S串进行另一个 while 循环,条件是当i大于等于0,且当前字符是井号,或者 cnt1 大于0,
若当前字符是退格符,则 cnt1 自增1,否则 cnt1 自减1,然后i自减1,这样就相当于跳过了当前的字符,不用进行比较。对T串也是做同样的 while 循环处理。
之后若i和j有一个小于0了,那么可以根据i和j是否相等的情况进行返回。
否则再看若S和T串当前的字母不相等,则返回 false,因为当前位置的退格符已经处理完了,剩下的字母是需要比较相等的,若不相等就可以直接返回 false 了。
最后当外层的 while 循环退出后,返回i和j是否相等
class Solution {
public:
bool backspaceCompare(string S, string T) {
int i = (int)S.size() - 1, j = (int)T.size() - 1, cnt1 = 0, cnt2 = 0;
while (i >= 0 || j >= 0) {
while (i >= 0 && (S[i] == '#' || cnt1 > 0)) S[i--] == '#' ? ++cnt1 : --cnt1;
while (j >= 0 && (T[j] == '#' || cnt2 > 0)) T[j--] == '#' ? ++cnt2 : --cnt2;
if (i < 0 || j < 0) return i == j;
if (S[i--] != T[j--]) return false;
}
return i == j;
}
};
设计循环队列
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k): 构造器,设置队列长度为 k 。Front: 从队首获取元素。如果队列为空,返回 -1 。Rear: 获取队尾元素。如果队列为空,返回 -1 。enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。isEmpty(): 检查循环队列是否为空。isFull(): 检查循环队列是否已满。
示例:
MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3
circularQueue.enQueue(1); // 返回 true
circularQueue.enQueue(2); // 返回 true
circularQueue.enQueue(3); // 返回 true
circularQueue.enQueue(4); // 返回 false,队列已满
circularQueue.Rear(); // 返回 3
circularQueue.isFull(); // 返回 true
circularQueue.deQueue(); // 返回 true
circularQueue.enQueue(4); // 返回 true
circularQueue.Rear(); // 返回 4
提示:
- 所有的值都在 0 至 1000 的范围内;
- 操作数将在 1 至 1000 的范围内;
- 请不要使用内置的队列库。
使用 size 来记录环形队列的最大长度之外,还要使用三个变量,head,tail,cnt,分别来记录队首位置,队尾位置,和当前队列中数字的个数,
将head初始化为 k - 1,tail初始化为 0,head 是指向数组范围内的起始位置,tail 指向数组范围内的结束位置。
Front() 函数,由于我们要返回起始位置的数字,为了不越界,进行环形走位,还要对 size 取余,于是就变成了 head % size,
Rear() 函数,我们要返回结束位置的数字,为了不越界,并且环形走位,tail 要先加上size,再对 size 取余,于是就变成了 (tail+size) % size。
判空就看当前个数 cnt 是否为0,判满就看当前个数 cnt 是否等于 size。
取首尾元素,先进行判空,然后根据 head 和tail 取即可,记得使用上循环数组的性质,要对 size 取余。
进队列函数,先进行判满,tail 要移动到下一位,为了避免越界,我们使用环形数组的经典操作,加1之后对长度取余,然后将新的数字加到当前的tail位置,cnt 再自增1即可。
出队列函数先进行判空,队首位置 head 要向后移动一位,同样进行加1之后对长度取余的操作,到这里就可以了,不用真正的去删除数字,因为 head 和 tail 限定了我们的当前队列的范围,然后 cnt 自减1。
class MyCircularQueue {
public:
MyCircularQueue(int k) {
size = k; head = k - 1; tail = 0; cnt = 0;
data.resize(k);
}
bool enQueue(int value) {
if (isFull()) return false;
data[tail] = value;
tail = (tail + 1) % size;
++cnt;
return true;
}
bool deQueue() {
if (isEmpty()) return false;
head = (head + 1) % size;
--cnt;
return true;
}
int Front() {
return isEmpty() ? -1 : data[(head + 1) % size];
}
int Rear() {
return isEmpty() ? -1 : data[(tail - 1 + size) % size];
}
bool isEmpty() {
return cnt == 0;
}
bool isFull() {
return cnt == size;
}
private:
vector<int> data;
int size, cnt, head, tail;
};
设计循环双端队列
设计实现双端队列。
你的实现需要支持以下操作:
- MyCircularDeque(k):构造函数,双端队列的大小为k。
- insertFront():将一个元素添加到双端队列头部。 如果操作成功返回 true。
- insertLast():将一个元素添加到双端队列尾部。如果操作成功返回 true。
- deleteFront():从双端队列头部删除一个元素。 如果操作成功返回 true。
- deleteLast():从双端队列尾部删除一个元素。如果操作成功返回 true。
- getFront():从双端队列头部获得一个元素。如果双端队列为空,返回 -1。
- getRear():获得双端队列的最后一个元素。 如果双端队列为空,返回 -1。
- isEmpty():检查双端队列是否为空。
- isFull():检查双端队列是否满了。
示例:
MyCircularDeque circularDeque = new MycircularDeque(3); // 设置容量大小为3
circularDeque.insertLast(1); // 返回 true
circularDeque.insertLast(2); // 返回 true
circularDeque.insertFront(3); // 返回 true
circularDeque.insertFront(4); // 已经满了,返回 false
circularDeque.getRear(); // 返回 2
circularDeque.isFull(); // 返回 true
circularDeque.deleteLast(); // 返回 true
circularDeque.insertFront(4); // 返回 true
circularDeque.getFront(); // 返回 4
提示:
- 所有值的范围为 [1, 1000]
- 操作次数的范围为 [1, 1000]
- 请不要使用内置的双端队列库。
使用size来记录环形队列的最大长度之外,还要使用三个变量,head,tail,cnt,分别来记录队首位置,队尾位置,和当前队列中数字的个数,
将head初始化为k-1,tail初始化为0。
判空就看当前个数cnt是否为0,判满就看当前个数cnt是否等于size。
取首尾元素,先进行判空,然后根据head和tail分别向后和向前移动一位取即可,记得使用上循环数组的性质,要对size取余。
删除末尾函数,先进行判空,然后tail向前移动一位,使用循环数组的操作,然后cnt自减1。
删除开头函数,先进行判空,队首位置head要向后移动一位,同样进行加1之后对长度取余的操作,然后cnt自减1。
插入末尾函数,先进行判满,然后将新的数字加到当前的tail位置,tail移动到下一位,为了避免越界,我们使用环形数组的经典操作,加1之后对长度取余,然后cnt自增1即可。
插入开头函数,先进行判满,然后将新的数字加到当前的head位置,head移动到前一位,然后cnt自增1
class MyCircularDeque {
public:
/** Initialize your data structure here. Set the size of the deque to be k. */
MyCircularDeque(int k) {
size = k; head = k - 1; tail = 0, cnt = 0;
data.resize(k);
}
/** Adds an item at the front of Deque. Return true if the operation is successful. */
bool insertFront(int value) {
if (isFull()) return false;
data[head] = value;
head = (head - 1 + size) % size;
++cnt;
return true;
}
/** Adds an item at the rear of Deque. Return true if the operation is successful. */
bool insertLast(int value) {
if (isFull()) return false;
data[tail] = value;
tail = (tail + 1) % size;
++cnt;
return true;
}
/** Deletes an item from the front of Deque. Return true if the operation is successful. */
bool deleteFront() {
if (isEmpty()) return false;
head = (head + 1) % size;
--cnt;
return true;
}
/** Deletes an item from the rear of Deque. Return true if the operation is successful. */
bool deleteLast() {
if (isEmpty()) return false;
tail = (tail - 1 + size) % size;
--cnt;
return true;
}
/** Get the front item from the deque. */
int getFront() {
return isEmpty() ? -1 : data[(head + 1) % size];
}
/** Get the last item from the deque. */
int getRear() {
return isEmpty() ? -1 : data[(tail - 1 + size) % size];
}
/** Checks whether the circular deque is empty or not. */
bool isEmpty() {
return cnt == 0;
}
/** Checks whether the circular deque is full or not. */
bool isFull() {
return cnt == size;
}
private:
vector<int> data;
int size, head, tail, cnt;
};
棒球比赛
你现在是棒球比赛记录员。
给定一个字符串列表,每个字符串可以是以下四种类型之一:
-
整数(一轮的得分):直接表示您在本轮中获得的积分数。 -
"+"(一轮的得分):表示本轮获得的得分是前两轮有效 回合得分的总和。 -
"D"(一轮的得分):表示本轮获得的得分是前一轮有效回合得分的两倍。
-
"C"(一个操作,这不是一个回合的分数):表示您获得的最后一个有效回合的分数是无效的,应该被移除。
每一轮的操作都是永久性的,可能会对前一轮和后一轮产生影响。
你需要返回你在所有回合中得分的总和。
示例 1:
输入: ["5","2","C","D","+"]
输出: 30
解释:
第1轮:你可以得到5分。总和是:5。
第2轮:你可以得到2分。总和是:7。
操作1:第2轮的数据无效。总和是:5。
第3轮:你可以得到10分(第2轮的数据已被删除)。总数是:15。
第4轮:你可以得到5 + 10 = 15分。总数是:30。
示例 2:
输入: ["5","-2","4","C","D","9","+","+"]
输出: 27
解释:
第1轮:你可以得到5分。总和是:5。
第2轮:你可以得到-2分。总数是:3。
第3轮:你可以得到4分。总和是:7。
操作1:第3轮的数据无效。总数是:3。
第4轮:你可以得到-4分(第三轮的数据已被删除)。总和是:-1。
第5轮:你可以得到9分。总数是:8。
第6轮:你可以得到-4 + 9 = 5分。总数是13。
第7轮:你可以得到9 + 5 = 14分。总数是27。
注意:
- 输入列表的大小将介于1和1000之间。
- 列表中的每个整数都将介于-30000和30000之间。
class Solution {
public:
int calPoints(vector<string>& ops) {
vector<int> v;
for (string op : ops) {
if (op == "+") {
v.push_back(v.back() + v[v.size() - 2]);
} else if (op == "D") {
v.push_back(2 * v.back());
} else if (op == "C") {
v.pop_back();
} else {
v.push_back(stoi(op));
}
}
//accumulate统计vector<int>容器对象中的元素之和。
return accumulate(v.begin(), v.end(), 0);
}
};

浙公网安备 33010602011771号