code freethinking--栈和队列 * 重点笔记 *核心代码*

理论基础

  栈和队列是STL(C++标准库)里面的两个数据结构,要先知道实现原理的底层版本

  1. HP STL 其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。

  2. P.J.Plauger STL 由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。

  3. SGI STL 由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。

  4. 栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。

 栈的底层实现可以是vector,deque,list,主要是数组和链表的底层实现

同理,SGI STL中队列一样是以deque为缺省情况下的底部结构。要对数据结构深挖其内部原理

std::stack<int, std::vector<int> > third;  // 使用vector为底层容器的栈
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列

用栈实现队列

使用栈实现队列的下列操作:

push(x) -- 将一个元素放入队列的尾部。
pop() -- 从队列首部移除元素。
peek() -- 返回队列首部的元素。
empty() -- 返回队列是否为空。

示例:

MyQueue queue = new MyQueue();
queue.push(1);
queue.push(2);
queue.peek();  // 返回 1
queue.pop();   // 返回 1
queue.empty(); // 返回 false

说明:

  • 你只能使用标准的栈操作 -- 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
  • 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)。

用栈模拟队列需要两个栈来实现输入输出,从in转入到out里再输出才可以

示例参考:

queue.push(1);
queue.push(2);
queue.pop(); 输出栈(1)
queue.push(3);
queue.push(4);
queue.pop();输出栈(2)
queue.pop();3
queue.pop();4
queue.empty();

class MyQueue {
public:
    stack<int>stIn;
    stack<int>stOut;
    MyQueue() {

    }
    
    void push(int x) {
        stIn.push(x);
    }
    
    int pop() {
        if(stOut.empty()){
            while(!stIn.empty()){
                stOut.push(stIn.top());
                stIn.pop();
            }
        }
        int result = stOut.top();
        stOut.pop();
        return result;
    }
    
    int peek() {
        int res = this->pop();
        stOut.push(res);
        return res;
    }

用队列实现栈

 使用队列实现栈的下列操作:

  • push(x) -- 元素 x 入栈
  • pop() -- 移除栈顶元素
  • top() -- 获取栈顶元素
  • empty() -- 返回栈是否为空

注意:

  • 你只能使用队列的基本操作-- 也就是 push to back, peek/pop from front, size, 和 is empty 这些操作是合法的。
  • 你所使用的语言也许不支持队列。 你可以使用 list 或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
  • 你可以假设所有操作都是有效的(例如, 对一个空的栈不会调用 pop 或者 top 操作)

 这道题跟前面那道题不一样,因为队列是双端开口的,所以不能使用两个队列posh pop实现,但可以通过主队列和拷贝队列,对输入的元素进行移动,输出最后的,再移动,只输出最后的元素

模拟代码如下:

queue.push(1);        
queue.push(2);        
queue.pop();   // 注意弹出的操作       
queue.push(3);        
queue.push(4);       
queue.pop();  // 注意弹出的操作    
queue.pop();    
queue.pop();    
queue.empty();  

上面这种比较好理解,但优化的方法是使用一个队列来实现即可,通过把队列头的元素放到队尾,每次都先输出队尾元素就是栈的顺序了。 学习一下语句和入队出队的基本操作是重点。

int pop 出队   int top 读取队头元素

class MyStack {
public:
    queue<int> que;
    MyStack() {

    }

    void push(int x) {
        que.push(x);
    }

    int pop() {
        int size = que.size();
        size--;
        while (size--) {
            que.push(que.front());
            que.pop();
        }
        int result = que.front();
        que.pop();
        return result;
    }

    int top() {
        int size = que.size();
        size--;
        while (size--) {
            que.push(que.front());
            que.pop();
        }
        int result = que.front();
        que.push(que.front());
        que.pop();
        return result;
    }

    bool empty() {
        return que.empty();
    }
};

删除字符串中的所有相领重复项

给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。

在 S 上反复执行重复项删除操作,直到无法继续删除。

在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

示例:

  • 输入:"abbaca"
  • 输出:"ca"
  • 解释:例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。

提示:

  • 1 <= S.length <= 20000
  • S 仅由小写英文字母组成。

这道题只需要做一个判断就好,栈用来处理匹配的问题很合适,只需要判断放入的元素和原来已有的元素是否一样,如果一样,栈也出元素,如果不一样才放入

class Solution {
public:
    string removeDuplicates(string s) {
        string result;
        for(char S:s){
            if(result.empty()||result.back() != S){
                result.push_back(S);
            }
            else{
                result.pop_back();
            }
        }
        return result;
    }
};
​

有效的括号

给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。

有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。
  • 注意空字符串可被认为是有效字符串。

示例 1:

  • 输入: "()"
  • 输出: true

示例 2:

  • 输入: "()[]{}"
  • 输出: true

示例 3:

  • 输入: "(]"
  • 输出: false

括号匹配是栈用法的经典问题,编译器在 词法分析的过程中处理括号、花括号等这个符号的逻辑,也是使用了栈这种数据结构。

有个小技巧:在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了

  • 时间复杂度: O(n)
  • 空间复杂度: O(n)
class Solution {
public:
    bool isValid(string s) {
        if(s.size() % 2 !=0) return false;
        stack<char>st;
        for(int i = 0;i<s.size();i++){
            if(s[i]=='(')st.push(')');
            else if(s[i]=='[')st.push(']');
            else if(s[i]=='{')st.push('}');
            else if(st.empty()||st.top() != s[i]) return false;
            else st.pop();
        }
        return st.empty();
    }
};

逆波兰表达式求值

根据 逆波兰表示法,求表达式的值。

有效的运算符包括 + ,  - ,  * ,  / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

说明:

整数除法只保留整数部分。 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

示例 1:

  • 输入: ["2", "1", "+", "3", " * "]
  • 输出: 9
  • 解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

示例 2:

  • 输入: ["4", "13", "5", "/", "+"]
  • 输出: 6
  • 解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

示例 3:

  • 输入: ["10", "6", "9", "3", "+", "-11", " * ", "/", " * ", "17", "+", "5", "+"]

  • 输出: 22

  • 解释:该算式转化为常见的中缀算术表达式为:

    ((10 * (6 / ((9 + 3) * -11))) + 17) + 5       
    = ((10 * (6 / (12 * -11))) + 17) + 5       
    = ((10 * (6 / -132)) + 17) + 5     
    = ((10 * 0) + 17) + 5     
    = (0 + 17) + 5    
    = 17 + 5    
    = 22    
    

逆波兰表达式:是一种后缀表达式,所谓后缀就是指运算符写在后面。

平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。

该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。

逆波兰表达式主要有以下两个优点:

  • 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。

  • 适合用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。

逆波兰表达式可以看成是用后序遍历的方式把二叉树序列化了

stoll 是string to longlong 的缩写,把字符串转化成长整型,这是leetcode题目要求

题目还要一点,结果会向0 截断。所以0会变成1,也避免出现除数为 0 的情况

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<long long>st;
        for(int i=0;i<tokens.size();i++){
            if(tokens[i]=="+"||tokens[i]=="-"||tokens[i]=="*"||tokens[i]=="/"){
                long long num1 = st.top();
                st.pop();
                long long num2 = st.top();
                st.pop();
                if(tokens[i] == "+")st.push(num2 + num1);
                if(tokens[i] == "-")st.push(num2 - num1);
                if(tokens[i] == "*")st.push(num2 * num1);
                if(tokens[i] == "/")st.push(num2 / num1);
            }
            else{
                st.push(stoll(tokens[i]));
            }
        }
        int result = st.top();
        st.pop();
        return result;
    }
};

在1970年代和1980年代,惠普在其所有台式和手持式计算器中都使用了RPN(后缀表达式),直到2020年代仍在某些模型中使用了RPN。

这种后缀计算方式对计算机非常友好

滑动窗口的最大值

 

给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

进阶:

你能在线性时间复杂度内解决此题吗?

提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

单调队列经典问题 

单调队列

 维护滑动窗口中大的几个字值即可,不需要全都维护,需要自己实现这种方法

设计单调队列的时候,pop,和push操作要保持如下规则:

  1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
  2. push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止

也就是说,只有进入队列的元素小于入口元素的数值才可以进入,否则,进入之后将入口元素全都弹出。 代码方面先定义一个单调队列实现方法,然后再把要滑动的窗口值先放进去。得到最大的,再继续一个个往里面放,一边找到他的结果

时间复杂度: O(n)        空间复杂度: O(k)

class Solution {
    public:
	class MyQueue{
	public:
	deque<int>que;
	void pop(int value) {
		if (!que.empty() && value == que.front()) {
			que.pop_front();
		}
	}
	void push(int value) {
		while (!que.empty() && value > que.back()) {
			que.pop_back();
		}
		que.push_back(value);
	}
	int front() {
		return que.front();
	}
};
	vector<int>maxSlidingWindow(vector<int>& nums, int k) {
		MyQueue que;
		vector<int>result;
		for (int i = 0; i < k; i++) {
			que.push(nums[i]);
		}
		result.push_back(que.front());
		for (int i = k; i < nums.size(); i++) {
			que.pop(nums[i - k]);
			que.push(nums[i]);
			result.push_back(que.front());
		}
		return result;
	}
};

前K个高频元素

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

示例 1:

  • 输入: nums = [1,1,1,2,2,3], k = 2
  • 输出: [1,2]

示例 2:

  • 输入: nums = [1], k = 1
  • 输出: [1]

提示:

  • 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
  • 你的算法的时间复杂度必须优于 $O(n \log n)$ , n 是数组的大小。
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
  • 你可以按任意顺序返回答案。

这道题的要点是:要统计元素出现频率       对频率排序        找出前K个高频元素

对队列进行排序可以使用一种容器适配器,也就是优先级队列,优先级队列是披着队列外衣的堆,优先级队列内部元素完全按照元素权值进行排列,对于大顶堆,小顶堆的选择还要多加思考

时间复杂度: O(nlogk)空间复杂度: O(n)

class Solution {
public:
    // 小顶堆
    class mycomparison {
    public:
        bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
            return lhs.second > rhs.second;
        }
    };
    vector<int> topKFrequent(vector<int>& nums, int k) {
        // 要统计元素出现频率
        unordered_map<int, int> map; // map<nums[i],对应出现的次数>
        for (int i = 0; i < nums.size(); i++) {
            map[nums[i]]++;
        }

        // 对频率排序
        // 定义一个小顶堆,大小为k
        priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;

        // 用固定大小为k的小顶堆,扫面所有频率的数值
        for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
            pri_que.push(*it);
            if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
                pri_que.pop();
            }
        }

        // 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
        vector<int> result(k);
        for (int i = k - 1; i >= 0; i--) {
            result[i] = pri_que.top().first;
            pri_que.pop();
        }
        return result;

    }
};

小总结

栈里面的元素在内存中是连续分布的么?

这个问题有两个陷阱:

陷阱1:栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中不一定是连续分布的。

陷阱2:缺省情况下,默认底层容器是deque,那么deque在内存中的数据分布是什么样的呢? 答案是:不连续的,下文也会提到deque。

使用抽象程度越高的语言,越容易忽视其底层实现

所以要更加多加思考~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值