栈与队列 代码随想录 刷题记录

理论基础

栈与队列可以用vector、list、deque作为其底层实现,栈对外提供push、pop等接口

栈的所有元素必须符合先进后出原则。

1.用栈实现队列

自己的思路:

 对于这种定义类别的题目总是拿不定主意,有点不太会,也没啥思路。

正确的思路:

遇到这种类型的题目的时候,第一步要做的是分析这两者的数据结构,并且看从其数据结构的特性上如何实现相关的功能。在这里就要考虑队列的先进先出和栈的先进后出的特性怎么匹配起来。因为题干中也提到了“两个栈”,所以在这一步就可以考虑使用画图求解看怎么样能够实现相关的功能。 

(即哪怕对于具体的代码实现尚未有思路,也可以实现相关功能)

后面正常入栈出栈的时候使用的就是栈本身的底层实现。pop是元素的弹出,top是获取第一个元素。

peek就是获取队列里面的出口处的第一个元素。push是放进去,pop是弹出来。

 int pop() {
        if(stOut.empty()){
            while(!stIn.empty()){//此时已经默认stIn.empty是非空的,所以不必要再进行是否为空的判断
                stOut.push(stIn.top());
                stIn.pop();//top是取值 pop是弹出
                //pop弹出完后才能进行取值的相关操作
            }
        }
        int result = stOut.top();
        stOut.pop();
        return result;//实现队列的一次蹦出一个元素
    }

pop不仅获取相关值,还弹出元素。如果只是为了获取相对应的值的话,使用top获取。

int peek() {
        int res = this -> pop();//直接使用现成的pop函数
        stOut,push(res);
        return res;
        //pop元素再调用
        //再push回去相关的元素
    }

从队列的代码形式就可以看出,如果直接调用pop的话,使用this -> pop(),调用相关的类别。

又因为pop本身带有弹出+返回元素的特性,而peek却无。所以在此处调用了pop函数之后,要将弹出的值进行返回。

this -> pop的用法的含义是:指向当前对象的pop成员。

2.用队列实现栈

自己的想法:

根据上方的相关操作,先模拟如果用队列实现栈。

调转队列的出队的方向是可能实现的吗(单个队列即实现)?还是说继续使用两个队列进行操作?

有想到如果要实现出队的话,可以每次都将要出队的那个元素放置末尾,而前方的元素放到第二个队列中备用。

正确的做法:

上文自己的想法的思路是正确的,但是未思考具体实现。按照这个思路继续做题。

int pop() {
        int size = que1.size() - 1;
        while(size --){
            que2.push(que1.front());
            que1.pop();
        }
        
        int result = que1.front;
        que1.pop();
        que1 = que2;
        while(!ques.empty()){
            que2.pop;
        }
        return result;
    }

注意的是,pop因为会返回被弹出的值,所以要专门设置一个res来记录。另外备用队列的内容也应该在末尾的时候原路返还给que1.

queue.front相当于栈中的top的值。

3.有效的括号

自己的想法:

原文中给的是字符串的形式,是否可以和栈联系起来使用?

题目中给出的例子可以看出,输入的字符串一种是关于中间对称的,一种是单个一对一对匹配完成的。一种是上述两者结合。

都呈现出来的特点是都是偶数形式出现的,但是括号与括号之间要相互对应要怎么完成呢?

将字符串逐个输入栈中,遇到对应的括号,则两个括号一起出栈。最后如果全部能够对应出栈,则说明字符串有效,否则则说明无效。

正确的做法:

自己的想法是正确的,修改后也通过了,但是其实做得有点复杂了。

更快的做法:

遇到左,全部转化为对应的右括号放入栈中。然后遇到右边再进行一一匹配。+ 字符串还没遍历完,右括号就为空了。

注意:(针对自己的做法)

自己的正确版本的代码:

class Solution {
public:
    bool fitCheck(char i, char j){
        if(i == '(' && j == ')')  return true;
        else if(i == '[' && j == ']') return true;
        else if(i == '{' && j =='}')  return true;
        else    return false;
    }
    
    bool isValid(string s) {
        stack<char> stin;
        for(int i = 0; i < s.size(); i++){
            if(stin.empty()) {
                stin.push(s[i]);  // 栈空时直接压入
                continue;
            }
            
            char tmp = stin.top();  // 获取当前栈顶
            
            // 尝试匹配当前字符与栈顶
            if(fitCheck(tmp, s[i])) {
                stin.pop();  // 匹配成功则弹出栈顶
            } else {
                stin.push(s[i]);  // 不匹配则压入新字符
            }
        }
        return stin.empty();  // 最终栈空则有效
    }
};

1.stIn.pop()弹出相关值并不需要参数。

2.fitCheck(s[i - 1], stin.top())这个比较方式是有问题的,因为在被弹出之后,s[i - 1]可能早已经被弹出去了.所以想的是,用一个tmp变量来记录弹完两个值

3.应该用当前元素来匹配栈顶,而不是压入之后来比较当前元素和栈顶的前一个元素。如果匹配成功直接弹出旧的栈顶就可以,不用压进去再弹出。操作冗余,也不好比较。

4.删除字符串中的所有相邻重复项

自己的想法:

用栈来实现,如果栈不为empty就push进去一个值,在判断字符串中即将push的值是否与已经在栈中的值相同。如果相同,直接弹出原有栈中的值,如果不相同则继续push。直到最后返回栈中的值。(输出的时候不能按照栈的顺序输出 要倒转一下)

正确的想法:

思路正确。但是后面的访问有一点问题,因为新创建了一个字符串,并且试图用索引来访问。

  • 原代码:t[j] = stIn.top(); 尝试通过索引赋值

  • 问题:字符串 t 初始为空,任何索引访问都会导致未定义行为(内存越界)

  • 修复:改用 t += stIn.top() 追加字符到字符串末尾

还有更加高效的方法是:直接使用字符串模拟栈的操作,这样就不需要再设置一个栈了。

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

字符串的用法:

for(char s: S)依次将字符串 S 中的每个字符赋值给变量 s

操作作用时间复杂度示例
push_back(char c)在字符串末尾添加字符O(1)s="ab"; s.push_back('c'); // s="abc"
pop_back()删除字符串最后一个字符O(1)s="abc"; s.pop_back(); // s="ab"

5.逆波兰表达式求值

 自己的想法:

逆波兰表达式即后缀表达式,运算符号写在操作数之后,而本题要实现的是将给出的后缀表达式转化为中缀算术表达式并且求出结果。

且逆波兰表达式运用栈来操作,遇到数字即入栈;遇到算符则取出栈顶的两个数字进行计算并将结果压入栈中。

正确的想法:

整体思路大致正确。

需要注意:

1.栈的类型设置为long long,整型数据类型,能表示更大范围的整数。

2.stIn.push(stoi(tokens[i])); 这行代码的含义是将字符串转换为整数后压入栈中。(string to integer)

3.先判断符号,剩下的就是数字了。并且如果不符合符号的相关法则,(如果此时到了字符串的末尾,那也是符号)。可以直接返回相关的值。

6.滑动窗口最大值

自己的想法:

注意:

vector<int>& res(nums.size() - k + 1, 0);//新创立一个数组

创建一个新的数组的时候不需要等于号。

或者创立一个大小为空的数组也可以。直接创建为vector<int> res,不要带引用符号。

在数组中插入相关的值的时候:

res.insert(res.begin() + index, value);  // 在指定索引处插入
// 例如:在开头插入
res.insert(res.begin(), 10);
// 在第三个位置插入
res.insert(res.begin() + 2, 20);

 数组函数中的insert必须带有两个参数值使用,一般插入相关的值是使用函数push_back。

“每次只找滑动窗口中最大的那个值,最终产生的最大值的数目 = 原数组的长度 - k  + 1。滑动窗口的相邻的比较 只需将上一个窗口的最大值 与 滑动窗口新的拓展的值进行比较。这是可以简便计算的点。”——这个思路是不正确的。

只能在限定的窗口大小内选择最大值。

正确的做法:

 创建一个自定义的单调栈,维护单调栈中的元素。如果push进来的元素比前面的元素都大,就消去前面的元素。

如果pop的元素是当前窗口的最大值的话,就从出口处pop出去。

维护当前滑动窗口的出口处的元素为最大值。

7.前 K 个高频元素

自己的想法: 

 怎么确认某个值是出现频次最高的值呢?

新创建一个数组count来记录相关的值,count[2] = 3代表数字为2在nums数组中出现了3次。然后用count值来找出对应的值,然后用前k高的值来找出对应的数字?

然后再设置一个res数组来存放对应的值,通过k--的循环找两次。(好了写到这里就感觉这个思路应该不对 时间复杂度和空间复杂度都太高了)

正确的做法:

优先级队列(披着队列外衣的堆)在这里可以理解为 经过排序了的队列(存在优先级的队列)

map来存放该种数据结构是非常合理的,key是相关元素,value是元素出现的个数。

大顶堆和小顶堆:因为push进去一个元素就要弹出一个元素,所以在这种情况下大顶堆的最大元素会被弹出,反倒无法维护出“前k个最大的元素”的情况。

代码构建思路涉及到新知。

如下:

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;

    }
};

 

小根堆的队列的比较的规则:

bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
    return lhs.second > rhs.second; // 比较两个 pair 的第二个元素
}

它是一个自定义比较函数,用于比较两个 pair<int, int> 对象。

这是一个函数对象(Functor),通常用于 STL 容器(如 priority_queuesort 等)的自定义排序规则。它比较两个 pair<int, int> 对象的 second 成员(即第二个 int 值)

比较逻辑

lhs.second > rhs.second:如果左侧的 second 值 大于 右侧的 second 值,则返回 true。这意味着它会将 较大的 second 值 视为 "更小"(在排序中位置更靠前),从而实现 降序排序

后面在小根堆的具体定义中用到了这个定义规则。

priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;

unordered_map<int, int> map;是在map中常常用到的值,用于定义map的存在。

迭代器的使用:


        // 用固定大小为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();
            }
        }
  • unordered_map<int, int>::iterator:声明一个迭代器类型,专门用于遍历 unordered_map 中的键值对。

  • it:迭代器变量名

  • map.begin():获取哈希表的起始位置迭代器(指向第一个元素)

  • 整体含义:创建一个迭代器 it,初始化为指向哈希表的第一个元素。

pri_que.push(*it)

  • *it:解引用操作符,获取迭代器当前指向的元素值

  • 对于 unordered_map 迭代器,*it 返回的是 pair<const Key, Value> 类型的引用

  • 具体到本例*it 就是 pair<int, int> 类型(键值对)

  • 整体含义:将当前迭代器指向的键值对插入优先队列

 

总结:

栈里面的元素在内存中是不是连续分布的。栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中不一定是连续分布的。

在使用到栈的题目中,注意不是所有的元素都要入栈,,可以先判断,符合条件就弹出/不符合条件就push进去。

利用队列实现滑动窗口的最大值问题,这里的是实现方法有一点不明白。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值