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^4
1 <= 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 <= 200
1 <= T.length <= 200
S
和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);
}
};