zoukankan      html  css  js  c++  java
  • 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中查找最左值,有的话就删掉。然后处理增加数字的情况(分两种情况:

    1. 如果small的长度小于large的长度,再看如果large是空或者新加的数小于等于large的首元素,我们把此数加入small中。否则就把large的首元素移出并加入small中,然后把新数字加入large。

    2. 如果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;
        }
    };
    

    比较含退格的字符串

    给定 ST 两个字符串,当它们分别被输入到空白的文本编辑器后,判断二者是否相等,并返回结果。 # 代表退格字符。

    注意:如果对空文本输入退格字符,文本继续为空。

    示例 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. 1 <= S.length <= 200
    2. 1 <= T.length <= 200
    3. ST 只含有小写字母以及字符 '#'

    遍历

    对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;
        }
    };
    

    双指针

    我们采用从后往前遍历,因为退格是要删除前面的字符,所以倒序遍历要好一些。

    用变量ij分别指向S和T串的最后一个字符的位置,然后还需要两个变量 cnt1 和 cnt2 来分别记录S和T串遍历过程中连续出现的井号的个数

    因为在连续井号后,要连续删除前面的字母,如何知道当前的字母是否是需要删除,就要知道当前还没处理的退格符的个数。

    现在进行 while 循环,条件是i和j至少有一个要大于等于0,然后对S串进行另一个 while 循环,条件是当i大于等于0,且当前字符是井号,或者 cnt1 大于0,

    若当前字符是退格符,则 cnt1 自增1,否则 cnt1 自减1,然后i自减1,这样就相当于跳过了当前的字符,不用进行比较。对T串也是做同样的 while 循环处理。

    之后若ij有一个小于0了,那么可以根据ij是否相等的情况进行返回。

    否则再看若ST串当前的字母不相等,则返回 false,因为当前位置的退格符已经处理完了,剩下的字母是需要比较相等的,若不相等就可以直接返回 false 了。

    最后当外层的 while 循环退出后,返回ij是否相等

    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;
    };
    

    棒球比赛

    你现在是棒球比赛记录员。
    给定一个字符串列表,每个字符串可以是以下四种类型之一:

    1. 整数(一轮的得分):直接表示您在本轮中获得的积分数。

    2. "+"(一轮的得分):表示本轮获得的得分是前两轮有效 回合得分的总和。

    3. "D"(一轮的得分):表示本轮获得的得分是前一轮有效回合得分的两倍。

    4. "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); 
        }
    };
    
  • 相关阅读:
    Qt中的SIGNAL和SLOT
    Android单个模块编译
    decoupling of objetctoriented systems
    设计模式之Objectifier
    代码示例:调用SPS提供的remoting服务,在线把Office文档转换成html文档
    利用WSS做后台存储设计一个统一的信息发布平台
    元数据(metadata)在企业应用开发中的作用
    面向对象的软件设计中应当遵守的原则
    使用NUnit在.Net编程中进行单元测试
    最近在使用sps类库过程中发现了一个让我比较疑惑的问题(有关items属性的)
  • 原文地址:https://www.cnblogs.com/wwj99/p/12952786.html
Copyright © 2011-2022 走看看