【设计/数据结构】 题目汇总(1)

理论

关于栈的详细理论分析,详见此篇博客

题目

leetcode705. 设计哈希集合

不使用任何内建的哈希表库设计一个哈希集合(HashSet)。
实现 MyHashSet 类:

  • void add(key) 向哈希集合中插入值 key 。
  • bool contains(key) 返回哈希集合中是否存在这个值 key 。
  • void remove(key) 将给定值 key 从哈希集合中删除。如果哈希集合中没有这个值,什么也不做。

思路分析

为了实现哈希集合这一数据结构,有以下几个关键问题需要解决:

  1. 哈希函数:能够将集合中任意可能的元素映射到一个固定范围的整数值,并将该元素存储到整数值对应的地址上。

  2. 冲突处理:由于不同元素可能映射到相同的整数值,因此需要在整数值出现「冲突」时,需要进行冲突处理。总的来说,有以下几种策略解决冲突:

    2.1 链地址法:为每个哈希值维护一个链表,并将具有相同哈希值的元素都放入这一链表当中。
    2.2 开放地址法:当发现哈希值 hh 处产生冲突时,根据某种策略,从 hh 出发找到下一个不冲突的位置。
       				例如,一种最简单的策略是,不断地检查 h+1,h+2,h+3,\ldotsh+1,h+2,h+3,… 这些整数对应的位置。
    2.3 再哈希法:当发现哈希冲突后,使用另一个哈希函数产生一个新的地址。
    2.4 扩容:当哈希表元素过多时,冲突的概率将越来越大,而在哈希表中查询一个元素的效率也会越来越低。
       			因此,需要开辟一块更大的空间,来缓解哈希表中发生的冲突。
    

以上内容读者可以自行翻阅数据结构的教材,本题解不再阐述,而是直接给出一个最简单的哈希表实现。

方法一:链地址法
设哈希表的大小为 base,则可以设计一个简单的哈希函数:hash(x) = x mod base。
我们开辟一个大小为 base 的数组,数组的每个位置是一个链表。当计算出哈希值之后,就插入到对应位置的链表当中。
由于我们使用整数除法作为哈希函数,为了尽可能避免冲突,应当将 base 取为一个质数。在这里,我们取 base=769。

class MyHashSet {
private:
    vector<list<int>> data;//每个位置是链表的数组
    static const int base = 769;//取模运算的基本值,也是数组的大小,最好取质数,减少重复
    static int hash(int key){//哈希函数:将输入的key映射的一组整数范围内的一个值
        return key % base;
    }

public:
    //不定长拉链法
    /** Initialize your data structure here. */
    MyHashSet() {
        data.resize(base);
    }
    
    void add(int key) {
        int h = hash(key);
        for(auto it = data[h].begin(); it != data[h].end(); it++){
            if((*it) == key) return;
        }
        data[h].push_back(key);
    }
    
    void remove(int key) {
        int h = hash(key);
        for(auto it = data[h].begin(); it != data[h].end(); it++){
            if((*it) == key){
                data[h].erase(it);
                return;
            } 
        }
    }
    
    /** Returns true if this set contains the specified element */
    bool contains(int key) {
        int h = hash(key);
        for(auto it = data[h].begin(); it != data[h].end(); it++){
            if((*it) == key) return true;
        }
        return false;
    }
};

其他方法,可参考其他的题解

leetcode706. 设计哈希映射

不使用任何内建的哈希表库设计一个哈希映射(HashMap)。

实现 MyHashMap 类:

MyHashMap() 用空映射初始化对象 void put(int key, int value) 向 HashMap 插入一个键值对
(key, value) 。如果 key 已经存在于映射中,则更新其对应的值 value 。 int get(int key) 返回特定的
key 所映射的 value ;如果映射中不包含 key 的映射,返回 -1 。 void remove(key) 如果映射中存在 key
的映射,则移除 key 和它所对应的 value 。

思路分析

如果做过上面那道705题,那么「设计哈希映射」与「设计哈希集合」解法接近,唯一的区别在于我们存储的不是 key 本身,而是 (key,value) 对。除此之外,代码基本是类似的。

class MyHashMap {
private:
    vector<list<pair<int, int>>> data;//拉链法:里面pair存键值对
    static const int base = 769;//取模运算作哈希映射
    static int hash(int key){
        return key % base;
    }

public:
    /** Initialize your data structure here. */
    MyHashMap() {
        data.resize(base);
    }
    
    /** value will always be non-negative. */
    void put(int key, int value) {
        int h = hash(key);
        for(auto it = data[h].begin(); it != data[h].end(); it++){
            if((*it).first == key){
                (*it).second = value;
                return;
            }            
        }
        data[h].push_back(make_pair(key, value));
    }
    
    /** Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key */
    int get(int key) {
        int h = hash(key);
        for(auto it = data[h].begin(); it != data[h].end(); it++){
            if((*it).first == key){
                return (*it).second;
            }            
        }
        return -1;
    }
    
    /** Removes the mapping of the specified value key if this map contains a mapping for the key */
    void remove(int key) {
        int h = hash(key);
        for(auto it = data[h].begin(); it != data[h].end(); it++){
            if((*it).first == key){
                data[h].erase(it);
                return;
            }            
        }
    }
};

leetcode232. 用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列的支持的所有操作(push、pop、peek、empty):

  • 实现 MyQueue 类:

    void push(int x) 将元素 x 推到队列的末尾; int pop() 从队列的开头移除并返回元素; int peek()返回队列开头的元素; boolean empty() 如果队列为空,返回 true ;否则,返回 false

  • 说明:

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

  • 进阶:

    你能否实现每个操作均摊时间复杂度为 O(1) 的队列?换句话说,执行 n 个操作的总时间复杂度为 O(n),即使其中一个操作可能花费较长时间。

思路分析

两个数据结构的概念:

  • 栈:后进先出
  • 队列:先进先出

题目让我们用栈来实现一个队列,就是要让两个栈实现一个先进先出的数据结构。

思路是:

「输入栈」会把输入顺序颠倒;如果把「输入栈」的元素逐个弹出放到「输出栈」,再从「输出栈」弹出元素的时候,则可以负负得正,实现了先进先出。

无论「用栈实现队列」还是「用队列实现栈」,思路都是类似的。
都可以通过使用两个栈/队列来解决。
我们创建两个栈,分别为 out 和 in,用作处理「输出」和「输入」操作。
其实就是两个栈来回「倒腾」。
而对于「何时倒腾」决定了是 O(n) 解法 还是 均摊 O(1) 解法。

1. O(n) 解法

我们创建两个栈,分别为 out 和 in:

  • in 用作处理输入操作 push(),使用 in 时需确保 out 为空
  • out 用作处理输出操作 pop() 和 peek(),使用 out 时需确保 in 为空

2. 均摊 O(1) 解法

事实上,我们不需要在每次的「入栈」和「出栈」操作中都进行「倒腾」。
我们只需要保证,输入的元素总是跟在前面的输入元素的后面,而输出元素总是最早输入的那个元素即可。
可以通过调整「倒腾」的时机来确保满足上述要求,但又不需要发生在每一次操作中:
只有在「输出栈」为空的时候,才发生一次性的「倒腾」

//思路:inStack负责存push进的新元素;pop,peek:将in中的依次存到outStack中,顺序刚好调换过来,符合队列的要求,此时栈顶即为队首元素;当out空时再从in里移入
class MyQueue {
private:
    stack<int> inStack, outStack;//双栈实现队列功能

    //实现inStack移入到outStack的功能
    void in2out(stack<int>& inStack, stack<int>& outStack){
        while(!inStack.empty()){
            outStack.push(inStack.top());
            inStack.pop();
        }
    }
   
public:
    /** Initialize your data structure here. */
    MyQueue() {}
    
    /** Push element x to the back of queue. */
    void push(int x) {
        inStack.push(x);
    }
    
    /** Removes the element from in front of queue and returns that element. */
    int pop() {
        if(outStack.empty()){
            in2out(inStack, outStack);
        }
        int x = outStack.top();
        outStack.pop();

        return x;
    }
    
    /** Get the front element. */
    int peek() {
        if(outStack.empty()){
            in2out(inStack, outStack);
        }

        return outStack.top();
    }
    
    /** Returns whether the queue is empty. */
    bool empty() {
        return inStack.empty() && outStack.empty();
    }
};

leetcode225. 用队列实现栈

暂未记录

leetcode150. 逆波兰表达式求值

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

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

思路分析

  • 逆波兰表达式:
    逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。
    平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
    该算式的逆波兰表达式写法为 ( ( 1 2 + ) (3 4 + ) * ) 。
    逆波兰表达式主要有以下两个优点:

    去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
    适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
    

我们平常用的是中缀表达式,也就是从小到大数学课本里
计算式子。题目中的是逆波兰式,也叫后缀表达式,一个好处就是只需要运算符,去掉括号,也不会产生歧义。
计算法则就是,每次找到运算符位置的前两个数字,然后进行计算。 该后缀表达式是非常符合计算机计算逻辑的。

  • 直接用栈写了代码,遇到操作数就入栈,遇到操作符就将栈顶的两个元素弹出进行操作,将结果继续入栈即可。

后面其他计算器相关题,其中一个解法就是 将其字符串 转化成 后缀表达式,再计算。

本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么这岂不就是一个相邻字符串消除的过程,这和 1047. 删除字符串中的所有相邻重复项 是差不多的,只不过本题不要相邻元素做消除了,而是做运算!

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> num;//数字栈
        int ans;

        for(const string& s : tokens){
            if(s == "+" || s == "-" || s == "*" || s == "/"){
                int b = num.top();num.pop();
                int a = num.top();num.pop();
                //取栈顶两个值计算后再入栈
                if(s == "+") 
                    num.push(a+b);
                else if(s == "-")
                    num.push(a-b);
                else if(s == "*")
                    num.push(a*b);
                else if(s == "/")
                    num.push(a/b);
            }else{
                num.push(stoi(s));
            }                
        }
        ans = num.top();

        return ans;
    }
};

leetcode227. 基本计算器 II

实现一个基本的计算器来计算一个简单的字符串表达式的值。
字符串表达式仅包含非负整数,+, - ,*,/ 四种运算符和空格 。 整数除法仅保留整数部分。

思路分析

计算器的题目基本都和栈有关,这道题也不例外。
由题目信息可知,s 中一共包含以下几种数据: 空格 数字 操作符
这里有 + - * / 而对于操作符来说又可以进一步细分:

  • 一元操作符 + -
  • 二元操作符 * /

1.对于一元操作符来说,我们只需要知道一个操作数即可。这个操作数就是操作符右边的数字。为了达到这个效果,我们需要一点小小的 trick。

  1 + 2 我们可以在前面补充一个 + 号,变成:+1 + 2 可看成 (+1)(+2) 
  再比如: (-1)(+2)(+3)(-4) 括号只是逻辑分组,实际并不存在。下同,不再赘述。

2.而对于二元操作符来说,我们需要知道两个操作数,这两个操作数分别是操作符两侧的两个数字。

  (5) / (2) 
  再比如 (3) * (4)

简单来说就是,一元操作符绑定一个操作数。而二元操作符绑定两个操作数。

算法:

  • 从左到右遍历 s :
  • 如果是数字,则更新数字
  • 如果是空格,则跳过
  • 如果是运算符,则按照运算符规则计算,并将计算结果重新入栈,具体见代码。最后更新 pre_flag 即可。(此处比较的永远是 当前数前面一个操作符,所以最后要加一个哨兵,不然最后一个数会漏掉没压入栈中)
    为了简化判断,使用了两个哨兵。一个是 s 末尾的$,另一个是最开始的 pre_flag。

trick解析:

  • 记录 pre_flag,即上一次出现的操作符
  • 使用哨兵简化操作。一个是 s 的 $ ,另一个是 pre_flag 的 +
class Solution {
public:
    int calculate(string s) {
        stack<int> numStk;//存数字的栈
        //num和pre_op的预先初始化(加个哨兵),可以解决s开头是负数的情况
        s += '$';//给原字符串最后加个哨兵,是为了把最后一个数压进栈里
        int num = 0;//计算当前数的值,初始化为0
        char pre_op = '+';//记录的总是当前数 前面的操作符,初始化为+号

        for(const auto& c : s){
            if(c == ' ') continue;//空格跳过
            else if(c >= '0' && c<= '9')//数字就统计
                num = num * 10 + (c - '0');
            else{//遇到计算符号时,判断的是当前数Num之前的计算符,即可拿栈顶元素与当前数num直接算乘除
                if(pre_op == '+')
                    numStk.push(num);
                else if(pre_op == '-')
                    numStk.push(-num);
                else if(pre_op == '*'){
                    int pre = numStk.top();//取出前一个数
                    numStk.pop();
                    numStk.push(pre*num);
                }else if(pre_op == '/'){
                    int pre = numStk.top();
                    numStk.pop();
                    numStk.push(pre/num);
                }
                num = 0;//入栈一个数后,勿忘置0重新统计后面的数
                pre_op = c;//更新下一个操作符状态   
            }
        }
        int ans = 0;
        //将栈中数全部累加即可得到答案
        while(!numStk.empty()){
            ans += numStk.top();
            numStk.pop();
        }

        return ans;
    }
};

leetcode224. 基本计算器

实现一个基本的计算器来计算一个简单的字符串表达式 s 的值。

1. 栈 + 递归

思路分析

这道题是上面 227. 基本计算器 II 的扩展版,多了括号而已。不妨先做一下 227. 基本计算器 II 入手。 拿题目中的例子来说
“(1+(4+5+2)-3)+(6+8)”。我们可以将其拆分为:

(6+8 )= 14
(4 + 5 + 2) = 11
(11)- 3 = 8
(1 + 8) = 9
(9 + 14) = 23
简单来说就是将括号里面的内容提取出来,提取出来就是上面的问题了。用上面的方法计算出结果,然后将结果作为一个数字替换原来的表达式。
比如我们先按照上面的算法计算出 6 + 8 的结果是 14,然后将 14 替换原来的(6+8),那么原问题就转化为了(1+(4+5+2)-3)+14 。这样一步一步就可以得到答案。

因此我们可以使用递归,每次遇到 ‘(’ 则开启一轮新的递归,遇到 ‘)’ 则退出一层递归即可

通过递归实现 由内层(…)到外层(…)值的计算:

  • 遇到‘(’ 就递归调用本身计算的函数,勿忘下标的后移;
  • 遍历代码块最后 遇到‘)’就跳出循环(代表当前内层括号内的值以全压入栈中),计算累加值并返回;

详见代码 和 详细注释~

class Solution {
public:
    int dfs(string& s, int& i){
        stack<int> numStk;//存数字的栈        
        //num和pre_op的预先初始化(加个哨兵),可以解决s开头是负数的情况
        int num = 0;//计算当前数的值,初始化为0
        char pre_op = '+';//记录的总是当前数 前面的操作符,初始化为+号

        for(; i < s.size(); i++){
            if(s[i] == ' ') continue;
            if(s[i] == '('){//遇到(,下一个数字开始递归
                num = dfs(s, ++i);
                i++;//递归结束时在')' 要i++ 
            }
            if(isdigit(s[i]))//遇到数字,isdigit()可以判断char类型
                num = num * 10 + (s[i] - '0');
            else{//遇到其他符号
                if(pre_op == '+')//
                    numStk.push(num);
                else if(pre_op == '-')
                    numStk.push(-num);
                else if(pre_op == '*'){//* /题中没有,可以不写
                    int pre = numStk.top();//取出前一个数
                    numStk.pop();
                    numStk.push(pre*num);
                }else if(pre_op == '/'){
                    int pre = numStk.top();
                    numStk.pop();
                    numStk.push(pre/num);
                }
                num = 0;//入栈一个数后,勿忘置0重新统计后面的数
                pre_op = s[i];//更新下一个操作符状态  
            }
            if(s[i] == ')') break;//递归终止条件:找到一对内层(...)括号对,就退出循环,算出该括号内的值
        }
        int ans = 0;
        //计算当前栈中的值
        while(!numStk.empty()){
            ans += numStk.top();
            numStk.pop();
        }
        return ans;
    }

    int calculate(string s) {
        s += "$";//末尾加个哨兵,是为了把最后一个数压进栈里
        int i = 0;
        return dfs(s, i);//不能直接传0,因为非常量引用的初始值必须为左值
    }
};

2.逆波兰表达式/后缀表达式法等等

思路分析
  1. 利用栈完成中缀表达式到后缀表达式的转换;
  2. 利用栈对后缀表达式完成计算。
#include <iostream>
#include <stack>
#include <cctype>
using namespace std;

class Solution {
private:
    stack<int> num;
    stack<char> op;
    int pri(char a){
        switch(a){
        case '+': return 1;
        case '-': return 1;
        case '*': return 2;
        case '/': return 2;
        case '(': return 3;
        default: return -1;
        }
    }
    void cal(){
        int b=num.top();num.pop();
        int a=num.top();num.pop();
        switch(op.top()){
        case '+':num.push(a+b);break;
        case '-':num.push(a-b);break;
        case '*':num.push(a*b);break;
        case '/':num.push(a/b);break;
        }
        op.pop();
    }
public:
    int calculate(string s) {
                string ss;
        for(int i=0;i<(int)s.size();i++){
            if(isdigit(s[i]))
                ss+=s[i];
            else if(s[i]==' ') continue;
            else{
                if(!ss.empty()){
                    num.push(stoi(ss));
                    ss.clear();
                }
                if(op.empty()||s[i]=='('|| pri(op.top())<pri(s[i]) )
                    op.push(s[i]);
                else if(s[i]==')'){
                    while(op.top()!='(') cal();
                    op.pop();
                }
                else{
                    while(!op.empty()&&pri(op.top())<=pri(s[i])) cal();
                    op.push(s[i]);
                }
            }
        }
        if(!ss.empty()) num.push(stoi(ss));
        while(!op.empty()) cal();
        return num.top();
    }
};

int main(){
    Solution s;
    cout<<s.calculate("(1+(4+5+2)-3)+(6+8)")<<endl;
}

其他更多解法详见 此解答

leetcode331. 验证二叉树的前序序列化

序列化二叉树的一种方法是使用前序遍历。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如#。
在这里插入图片描述
例如,上面的二叉树可以被序列化为字符串 “9,3,4,#,#,1,#,#,2,#,6,#,#”,其中 # 代表一个空节点。
给定一串以逗号分隔的序列,验证它是否是正确的二叉树的前序序列化。编写一个在不重构树的条件下的可行算法。
每个以逗号分隔的字符或为一个整数或为一个表示 null 指针的 ‘#’ 。
你可以认为输入格式总是有效的,例如它永远不会包含两个连续的逗号,比如 “1,3” 。

1.栈模拟

思路分析

我们可以定义一个概念,叫做槽位。一个槽位可以被看作「当前二叉树中正在等待被节点填充」的那些位置
二叉树的建立也伴随着槽位数量的变化。每当遇到一个节点时:

  • 如果遇到了空节点,则要消耗一个槽位;
  • 如果遇到了非空节点,则除了消耗一个槽位外,还要再补充两个槽位。
  • 此外,还需要将根节点作为特殊情况处理。 在这里插入图片描述

我们使用栈来维护槽位的变化。栈中的每个元素,代表了对应节点处剩余槽位的数量,而栈顶元素就对应着下一步可用的槽位数量

  • 当遇到空节点时,仅将栈顶元素减 1;
  • 当遇到非空节点时,将栈顶元素减 1 后,再向栈中压入一个 2。
  • 无论何时,如果栈顶元素变为 0,就立刻将栈顶弹出。

遍历结束后:

  • 若栈为空,说明没有待填充的槽位,因此是一个合法序列;
  • 否则若栈不为空,则序列不合法。
  • 此外,在遍历的过程中,若槽位数量不足,则序列不合法。
class Solution {
public:
    bool isValidSerialization(string preorder) {
        int n = preorder.length();
        int i = 0;
        stack<int> stk;
        stk.push(1);
        while (i < n) {
            if (stk.empty()) {
                return false;
            }
            if (preorder[i] == ',') {
                i++;
            } else if (preorder[i] == '#'){
                stk.top() -= 1;
                if (stk.top() == 0) {
                    stk.pop();
                }
                i++;
            } else {
                // 读一个数字
                while (i < n && preorder[i] != ',') {
                    i++;
                }
                stk.top() -= 1;
                if (stk.top() == 0) {
                    stk.pop();
                }
                stk.push(2);
            }
        }
        return stk.empty();
    }
};

2.直接计数

思路分析

能否将方法一 时间空间复杂度都是O(n) 的空间复杂度优化至 O(1) 呢?
回顾方法一的逻辑,如果把栈中元素看成一个整体,即所有剩余槽位的数量,也能维护槽位的变化。
因此,我们可以只维护一个计数器,代表栈中所有元素之和,其余的操作逻辑均可以保持不变。

class Solution {
public:
    //加入一个数就多两个槽位,并占去一个槽位;遇到#(叶子节点)就减少一个槽位
    bool isValidSerialization(string preorder) {
        int n = preorder.size(), i = 0;
        int slots = 1;//初始槽位为1,放根节点
        while(i < n){
            if(slots == 0)//若还没遍历完就没有槽位了,
                return false;
            if(preorder[i] == ',')//','跳过
                i++;
            else if(preorder[i] == '#'){//‘#’槽位减1
                slots--;
                i++;
            }else{//数字可能是多位,需要判断连续读取
                while(i < n && preorder[i] != ','){
                    i++;
                }
                slots++;//slots = slots - 1 + 2;
            }
       }
       return slots == 0;
    }
};

leetcode1603. 设计停车系统

请你给一个停车场设计一个停车系统。停车场总共有三种不同大小的车位:大,中和小,每种尺寸分别有固定数目的车位。

请你实现 ParkingSystem 类:

  • ParkingSystem(int big, int medium, int small) 初始化 ParkingSystem类,三个参数分别对应每种停车位的数目。
  • bool addCar(int carType) 检查是否有 carType 对应的停车位。
    carType 有三种类型:大,中,小,分别用数字 1, 2 和 3 表示。一辆车只能停在 carType 对应尺寸的停车位中。如果没有空车位,请返回 false ,否则将该车停入车位并返回 true 。

思路

本题很简单,但有很多拓展情况或高级解法需要学习,详情参考 解答

class ParkingSystem:

    def __init__(self, big: int, medium: int, small: int):
        self.size = [0, big, medium, small]
        self.car = [0, big, medium, small]
        self.dic = []

    def addCar(self, carType: int) -> bool:
        if self.car[carType] <= 0:
            return False
        self.car[carType] -= 1
        return True
    
    # 如果允许车离开停车位,该怎么做?
    def removeCar(self, carType: int) -> bool:
        if self.car[carType] >= self.size[carType]:
            return False
        self.car[carType] += 1
        return True

    # 如果允许小车停放在比它更大的停车位上,该怎么做?
    def addSmallCar(self, carType: int) -> bool:
        if sum(self.car[1:carType + 1]) <= 0:
            return False
        for i in range(carType ,0 ,-1): 
            if self.car[i] : # 小车优先小车位
                self.car[i] -= 1
                return True 
    # 如果给每个车增加 id,车离开车位的时候必须按照指定的顺序,比如先进先出,该怎么做?
    # 我理解是要存一个,hashmap然后,弹出指定的车辆
    def addCarList(self, carType: int) -> bool:
        dic.append(carType)
        return self.addCar(carType)
    def removeCarList(self) -> bool:
        if not dic : return False
        return self.removeCar(dic.pop(0))

    # 如果在并发场景的停入和离开车,如何保证结果正确?
    # 是否可以参考concurrenthashmap分段锁来设计,蹲一个大佬

leetcode341. 扁平化嵌套列表迭代器

给你一个嵌套的整型列表。请你设计一个迭代器,使其能够遍历这个整型列表中的所有整数。
列表中的每一项或者为一个整数,或者是另一个列表。其中列表的元素也可能是整数或是其他列表。

思路分析

今天的题意略难理解,需要我翻译一下,理解题意的朋友请跳过。
本题定义了一个类 NestedInteger ,这个类可以存储 int 或 List;所以称它是一个「嵌套列表」。类似于一棵多叉树,每个节点都可以有很多子节点。
它有三个方法:

  • isInteger() ,判断当前存储的对象是否为 int;
  • getInteger() , 如果当前存储的元素是 int 型的,那么返回当前的结果 int,否则调用会失败;
  • getList() ,如果当前存储的元素是 List 型的,那么返回该 List,否则调用会失败。

而「扁平化嵌套列表迭代器」说的是,我们需要设计一个迭代器,这个迭代器是把「嵌套列表」铺平(拆包)成各个 int,然后每次调用 hasNext() 来判断是否有下一个整数,通过 next() 返回下一个整数。

注意迭代器是一种按照特定顺序对数据结构遍历的方式,它的调用方式是:

i, v = NestedIterator(nestedList), []
while i.hasNext():
	v.append(i.next())

本文有两种主要的思路:

  1. 在构造函数中提前「扁平化」整个嵌套列表;(先预处理所有)
  2. 在调用 hasNext() 或者 next() 方法的时候扁平化当前的嵌套的子列表。(边用边处理)

详细解析可参考 解答

1.先解析处所有整数到vector或deque(深搜-递归实现)
class NestedIterator {
private:
    vector<int> vals;
    vector<int>::iterator cur;

    void dfs(const vector<NestedInteger> &nestedList) {
        for (auto &nest : nestedList) {
            if (nest.isInteger()) {
                vals.push_back(nest.getInteger());
            } else {
                dfs(nest.getList());
            }
        }
    }

public:
    NestedIterator(vector<NestedInteger> &nestedList) {
        dfs(nestedList);
        cur = vals.begin();
    }

    int next() {
        return *cur++;
    }

    bool hasNext() {
        return cur != vals.end();
    }
};
2. 调用hasnext()时再解析,逆序存入栈中
/**
 * // This is the interface that allows for creating nested lists.
 * // You should not implement it, or speculate about its implementation
 * class NestedInteger {
 *   public:
 *     // Return true if this NestedInteger holds a single integer, rather than a nested list.
 *     bool isInteger() const;
 *
 *     // Return the single integer that this NestedInteger holds, if it holds a single integer
 *     // The result is undefined if this NestedInteger holds a nested list
 *     int getInteger() const;
 *
 *     // Return the nested list that this NestedInteger holds, if it holds a nested list
 *     // The result is undefined if this NestedInteger holds a single integer
 *     const vector<NestedInteger> &getList() const;
 * };
 */

class NestedIterator {
private:
    stack<NestedInteger> st;//注意:栈里存的是NestedInteger对象

public:
    //存入栈中,在调用 hasNext() 或者 next() 方法的时候再拆开当前嵌套的子列表。
    NestedIterator(vector<NestedInteger> &nestedList) {
        for(int i = nestedList.size() - 1; i >= 0; i--)
            st.push(nestedList[i]);
    }
    
    int next() {
        NestedInteger cur = st.top();//里面都是按NestedInteger类存储的
        st.pop();
        return cur.getInteger();//调用接口返回整型
    }
    
    bool hasNext() {
        while(!st.empty()){
            NestedInteger cur = st.top();
            if(cur.isInteger()){
                return true;
            }
            st.pop();//当前NestedInteger对象不是整数,是list,就是从栈中拿出,再次逆序解析成整数存入
            for(int i = cur.getList().size() - 1; i >= 0; i--){
                st.push(cur.getList()[i]);
            }
        }
        return false;
    }
};

leetcode456. 132模式

给定一个整数序列:a1, a2, …, an,一个132模式的子序列 ai, aj, ak 被定义为:当 i < j < k 时,ai < ak < aj。设计一个算法,当给定有 n 个数字的序列时,验证这个序列中是否含有132模式的子序列。
注意:n 的值小于15000。

思路分析

1. 直接三个下标暴力枚举

题目只让我们找出序列中是否存在,那么我们可以在合理的i<j<k情况下,找较大的nums[j],和较小的nums[i]作为比较的根据,再存在大小在两者之间的nums[k],说明该序列存在132模式。否则不存在。

class Solution {
public:
    bool find132pattern(vector<int>& nums) {
        int n = nums.size();
        if(n < 3) return false;
        int i = 0, j = 0, k = 0;//对应1,3,2模式
        //暴力枚举模式1(的起点下标i)
        while(i < n){
            while(i < n - 2 && nums[i] >= nums[i+1])//1->3:滤掉连续的递增
                i++;
            j = i + 1;
            while(j < n - 1 && nums[j] <= nums[j+1])//3->2:滤掉连续递减
                j++;
            //枚举右侧2的所有可能值
            for(k = j + 1; k < n; k++){
                if(nums[k] < nums[j] && nums[k] > nums[i])
                    return true;
            }
            i = j + 1;//更新模式1的起点i
        }
        return false;
    }
};

2. 单调栈

  1. 满足132模式,我们可以保证最大的3和次大的2 是 数组里尽可能大的值,那么留给最小的1可选空间就足够大了,如果这样都找不到,肯定就不满足了。
  2. q且下标要满足i<j<k,根据132模型找最小的1,可以想到逆序是最合适;、
  3. 用一个栈保存最大值,second代表最小值,从后往前遍历,遇到更大的就依次更新掉 最大值 和 次大值;
  4. 若过程中,左边遇到小于他俩的 就说明找到满足条件的数了,返回ture;遍历完都没遇到就返回false;
class Solution {
public:
    //枚举最小(保证3,2尽量大):用栈存放最大的元素,次大的元素用second存放,遍历方式从后往前。若找到比second小的元素则说明存在132模式。
    bool find132pattern(vector<int>& nums) {
        int n = nums.size();
        if(n < 3) return false;
        stack<int> maxStk;//维护一个栈,存最大值
        int second = INT_MIN;//存次大值        
        //逆序遍历
        for(int i = n - 1; i >= 0; i--){
            if(nums[i] < second) return true;
            //找到更大的就更新最大值和次大值
            while(!maxStk.empty() && nums[i] > maxStk.top()){
                second = maxStk.top();
                maxStk.pop();
            }
            maxStk.push(nums[i]);
        }
        return false;
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值