【优选算法】(实战:栈、队列、优先级队列高频考题通关全解)


🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在算法面试与刷题实战中,栈、队列及优先级队列是三大基础且高频的数据结构,它们看似简单,却贯穿了从基础入门到进阶难题的各类考点,是解锁数组、字符串、树、图等复杂算法题的"钥匙".无论是字节、阿里等大厂面试中的经典追问,还是LeetCode中占比极高的基础应用题,几乎都能看到这三种结构的身影------栈的"先进后出"适配括号匹配、单调栈求解最值,队列的"先进先出"支撑滑动窗口、层次遍历,优先级队列(堆)则是TopK、贪心算法的核心载体.很多学习者在掌握了三种结构的基本原理后,仍会陷入"懂原理、不会做题"的困境.本文聚焦"实战通关"核心,摒弃冗余的理论堆砌,直击栈、队列、优先级队列的高频考点与经典考题.我们将从考点拆解、解题模板、真题精讲三个维度,逐一剖析每种结构的核心应用场景,拆解常见考题的解题思路,总结易错点与避坑技巧,帮助学习者快速掌握解题规律,吃透高频考点,摆脱"刷题低效"的困境,顺利通关算法面试与笔试中的相关考题,夯实算法基础,为进阶学习筑牢根基.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录

1.栈和队列以及优先级队列思想背景

栈和队列以及优先级队列思想背景,源于对"数据有序操作"的实际需求,是计算机科学中最基础且应用广泛的线性数据结构思想,其设计逻辑贴合现实场景,核心是通过规范数据的"进出顺序",解决不同场景下的高效处理问题.

栈的核心思想是"先进后出"(LIFO),源于现实中"堆叠"场景的抽象------如同叠盘子,先放入的盘子在最底层,最后放入的最先取出,这种思想最初用于解决函数调用、表达式求值、括号匹配等"后进先处理"的问题,是算法中"回溯、单调操作"的核心支撑.

队列的核心思想是"先进先出"(FIFO),对应现实中的"排队"场景,比如超市结账、任务调度,先进入队列的元素先被处理,其思想主要用于解决"顺序处理、缓冲"类问题,例如滑动窗口、广度优先搜索(BFS)、消息队列等,确保数据处理的有序性和公平性.

优先级队列则是在队列"先进先出"思想的基础上,增加了"优先级排序"逻辑------不再遵循严格的进出顺序,而是始终优先处理优先级最高的元素,其思想源于现实中"按重要性处理任务"的场景,比如急诊病人优先救治、操作系统的进程调度,核心依赖堆结构实现高效排序,是贪心算法、TopK问题的核心载体.


2.删除字符串中的所有相邻重复项---栈(OJ题)


算法思路:解法(栈):

本题极像我们玩过的开⼼消消乐游戏,仔细观察消除过程,可以发现本题与我们之前做过的括号匹配问题是类似的.当前元素是否被消除,需要知道上⼀个元素的信息,因此可以⽤栈来保存信息.

但是,如果使⽤stack来保存的话,最后还需要把结果从栈中取出来.不如直接⽤数组模拟⼀个栈结构:在数组的尾部尾插尾删,实现栈的进栈和出栈.那么最后数组存留的内容,就是最后的结果.

核心代码

cpp 复制代码
class Solution 
{
public:
    string removeDuplicates(string s) 
    {
        string ret; //搞⼀个数组,模拟栈结构即可
        for (auto ch : s) 
        {
            if (ret.size() && ch == ret.back())
                ret.pop_back(); //出栈
            else
                ret += ch; //⼊栈
        }
        return ret;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <string>

using namespace std;

//字符串模拟栈,删除相邻重复项)
class Solution
{
public:
    string removeDuplicates(string s)
    {
        string ret;         // 用字符串模拟栈:back()=栈顶,push_back=入栈,pop_back=出栈
        for (auto ch : s)  // 遍历字符串的每一个字符
        {
            // 栈不为空 + 当前字符 == 栈顶字符 → 相邻重复,删除栈顶(消除重复)
            if (ret.size() && ch == ret.back())
                ret.pop_back();
                // 不重复 → 字符入栈
            else
                ret += ch;
        }
        return ret; // 栈中剩余字符就是最终结果
    }
};

int main() {
    Solution sol;
    
    string s1 = "abbaca";
    cout << "输入: " << s1 << " → 输出: " << sol.removeDuplicates(s1) << endl;
    
    string s2 = "aabb";
    cout << "输入: " << s2 << " → 输出: " << sol.removeDuplicates(s2) << endl;
    
    string s3 = "aaaaa";
    cout << "输入: " << s3 << " → 输出: " << sol.removeDuplicates(s3) << endl;
    
    string s4 = "abba";
    cout << "输入: " << s4 << " → 输出: " << sol.removeDuplicates(s4) << endl;
    
    string s5 = "x";
    cout << "输入: " << s5 << " → 输出: " << sol.removeDuplicates(s5) << endl;
    
    string s6 = "";
    cout << "输入: 空字符串 → 输出: " << sol.removeDuplicates(s6) << endl;

    return 0;
}

3.比较含退格的字符串---栈(OJ题)


算法思路:解法(⽤数组模拟栈):

由于退格的时候需要知道前⾯元素的信息,⽽且退格也符合后进先出的特性.因此我们可以使⽤栈结构来模拟退格的过程.

  • 当遇到⾮#字符的时候,直接进栈;
  • 当遇到#的时候,栈顶元素出栈.

为了⽅便统计结果,我们使⽤数组来模拟实现栈结构.

核心代码

cpp 复制代码
class Solution 
{
public:
    bool backspaceCompare(string s, string t) 
    {
        return changeStr(s) == changeStr(t);
    }
    string changeStr(string& s) 
    {
        string ret; //⽤数组模拟栈结构
        for (char ch : s) 
        {
            if (ch != '#')
                ret += ch;
            else {
                if (ret.size())
                    ret.pop_back();
            }
        }
        return ret;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <string>

using namespace std;

//(栈思想解决退格字符串比较)
class Solution
{
public:
    //处理两个字符串后比较是否相等
    bool backspaceCompare(string s, string t)
    {
        return changeStr(s) == changeStr(t);
    }

    //用字符串模拟栈,处理#退格字符
    string changeStr(string& s)
    {
        string ret; // 用string模拟栈:back()栈顶,push_back入栈,pop_back出栈
        for (char ch : s)
        {
            // 字符不是#,直接入栈
            if (ch != '#')
                ret += ch;
                // 字符是#,执行退格:栈不为空则弹出栈顶(删除前一个字符)
            else {
                if (ret.size())
                    ret.pop_back();
            }
        }
        return ret; // 返回处理后的最终字符串
    }
};

int main() {
    Solution sol;
    
    string s1 = "ab#c", t1 = "ad#c";
    cout << boolalpha;  // 让cout输出true/false,而不是1/0
    cout << "测试1:s=\"" << s1 << "\", t=\"" << t1 << "\" → "
         << sol.backspaceCompare(s1, t1) << endl;
    
    string s2 = "a##c", t2 = "#a#c";
    cout << "测试2:s=\"" << s2 << "\", t=\"" << t2 << "\" → "
         << sol.backspaceCompare(s2, t2) << endl;
    
    string s3 = "a#c", t3 = "b";
    cout << "测试3:s=\"" << s3 << "\", t=\"" << t3 << "\" → "
         << sol.backspaceCompare(s3, t3) << endl;
    
    string s4 = "###", t4 = "#";
    cout << "测试4:s=\"" << s4 << "\", t=\"" << t4 << "\" → "
         << sol.backspaceCompare(s4, t4) << endl;
    
    string s5 = "", t5 = "";
    cout << "测试5:空字符串对比 → "
         << sol.backspaceCompare(s5, t5) << endl;

    return 0;
}

4.基本计算器--栈(OJ题)


算法思路:解法(栈):

由于表达式⾥⾯没有括号,因此我们只⽤处理加减乘除混合运算即可.根据四则运算的顺序,我们可以先计算乘除法,然后再计算加减法.由此,我们可以得出下⾯的结论:

  • 当⼀个数前⾯是 '+' 号的时候,这⼀个数是否会被⽴即计算是不确定的,因此我们可以先压⼊栈中;
  • 当⼀个数前⾯是 '-' 号的时候,这⼀个数是否被⽴即计算也是不确定的,但是这个数已经和前⾯ 的 '-' 号绑定了,因此我们可以将这个数的相反数压⼊栈中;
  • 当⼀个数前⾯是 '*' 号的时候,这⼀个数可以⽴即与前⾯的⼀个数相乘,此时我们让将栈顶的元素乘上这个数;
  • 当⼀个数前⾯是 '/' 号的时候,这⼀个数也是可以⽴即被计算的,因此我们让栈顶元素除以这个数.

当遍历完全部的表达式的时候,栈中剩余的元素之和就是最终结果.

核心代码

cpp 复制代码
class Solution 
{
public:
    int calculate(string s) 
    {
        vector<int> st; //⽤数组来模拟栈结构
        int i = 0, n = s.size();
        char op = '+';
        while (i < n) {
            if (s[i] == ' ')
                i++;
            else if (s[i] >= '0' && s[i] <= '9') 
            {
                //先把这个数字给提取出来
                int tmp = 0;
                while (i < n && s[i] >= '0' && s[i] <= '9')
                    tmp = tmp * 10 + (s[i++] - '0');
                if (op == '+')
                    st.push_back(tmp);
                else if (op == '-')
                    st.push_back(-tmp);
                else if (op == '*')
                    st.back() *= tmp;
                else
                    st.back() /= tmp;
            } else 
            {
                op = s[i];
                i++;
            }
        }
        int ret = 0;
        for (auto x : st)
            ret += x;
        return ret;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>

using namespace std;

class Solution
{
public:
    int calculate(string s)
    {
        vector<int> st; //用数组模拟栈
        int i = 0, n = s.size();
        char op = '+'; //初始运算符为+,处理第一个数字
        while (i < n) {
            //跳过空格
            if (s[i] == ' ')
                i++;
                //提取连续数字
            else if (s[i] >= '0' && s[i] <= '9')
            {
                int tmp = 0;
                while (i < n && s[i] >= '0' && s[i] <= '9')
                    tmp = tmp * 10 + (s[i++] - '0');
                //根据运算符处理数字:加减入栈,乘除直接计算栈顶
                if (op == '+')
                    st.push_back(tmp);
                else if (op == '-')
                    st.push_back(-tmp);
                else if (op == '*')
                    st.back() *= tmp;
                else
                    st.back() /= tmp;
            }
                //记录当前运算符
            else
            {
                op = s[i];
                i++;
            }
        }
        //栈中所有元素求和即为最终结果
        int ret = 0;
        for (auto x : st)
            ret += x;
        return ret;
    }
};

int main() {
    //创建Solution对象
    Solution sol;
    
    vector<string> testCases = {
            "3+2*2",          //基础混合运算 → 7
            " 3/2 ",          //带空格+除法 → 1
            " 3+5 / 2 ",      //带空格+混合运算 →5
            "10-3*2",         //减法+乘法 →4
            "14-3/2",         //减法+除法(取整)→13
            "2*3*4",          //连续乘法 →24
            "100-20*3+16/4",  //复杂混合运算 →44
            "0"               //单个数字 →0
    };
    
    for (string& expr : testCases) {
        int res = sol.calculate(expr);
        cout << "表达式: \"" << expr << "\"\t计算结果: " << res << endl;
    }

    return 0;
}

5.字符串解码--栈(OJ题)


算法思路:解法(两个栈):

对于3[ab2[cd]] ,我们需要先解码内部的,再解码外部(为了⽅便区分,使⽤了空格):

3[ab2[cd]] -> 3[abcd cd] -> abcdcd abcdcd abcdcd;在解码 cd 的时候,我们需要保存 3 ab 2 这些元素的信息,并且这些信息使⽤的顺序是从后往前,正好符合栈的结构,因此我们可以定义两个栈结构,⼀个⽤来保存解码前的重复次数k(左括号前的数字),⼀个⽤来保存解码之前字符串的信息(左括号前的字符串信息).

细节:字符串这个栈中,先放入一个空串
分情况讨论:

  1. 遇到数字:提取出这个数字,放入"数字栈"中;
  2. 遇到 '[':把后面的字符串提取出来,放入"字符串栈"中;
  3. 遇到 ']':解析,然后放到"字符串栈"栈顶的字符串后面;
  4. 遇到单独的字符:提取出来这个字符串,直接放在"字符串栈"栈顶的字符串后面.

核心代码

cpp 复制代码
class Solution 
{
public:
    string decodeString(string s) 
    {
        stack<int> nums;
        stack<string> st;
        st.push("");
        int i = 0, n = s.size();
        while (i < n) {
            if (s[i] >= '0' && s[i] <= '9') 
            {
                int tmp = 0;
                while (s[i] >= '0' && s[i] <= '9') 
                {
                    tmp = tmp * 10 + (s[i] - '0');
                    i++;
                }
                nums.push(tmp);
            } else if (s[i] == '[') 
            {
                i++; // 把括号后⾯的字符串提取出来
                string tmp = "";
                while (s[i] >= 'a' && s[i] <= 'z') 
                {
                    tmp += s[i];
                    i++;
                }
                st.push(tmp);
            } else if (s[i] == ']') 
            {
                string tmp = st.top();
                st.pop();
                int k = nums.top();
                nums.pop();
                while (k--) 
                {
                    st.top() += tmp;
                }
                i++; // 跳过这个右括号
            } else 
            {
                string tmp;
                while (i < n && s[i] >= 'a' && s[i] <= 'z') 
                {
                    tmp += s[i];
                    i++;
                }
                st.top() += tmp;
            }
        }
        return st.top();
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <stack>
#include <string>
using namespace std;

class Solution
{
public:
    string decodeString(string s)
    {
        stack<int> nums;    //存储重复次数
        stack<string> st;   //存储字符串片段
        st.push("");        //初始化栈,放入空串
        int i = 0, n = s.size();

        while (i < n) {
            //1.处理数字(支持多位数)
            if (s[i] >= '0' && s[i] <= '9')
            {
                int tmp = 0;
                while (s[i] >= '0' && s[i] <= '9')
                {
                    tmp = tmp * 10 + (s[i] - '0');
                    i++;
                }
                nums.push(tmp);
            }
                //2.处理左括号 [
            else if (s[i] == '[')
            {
                i++;
                string tmp = "";
                //提取括号内的字母
                while (s[i] >= 'a' && s[i] <= 'z')
                {
                    tmp += s[i];
                    i++;
                }
                st.push(tmp);
            }
                //3.处理右括号 ],执行字符串重复拼接
            else if (s[i] == ']')
            {
                string tmp = st.top();
                st.pop();
                int k = nums.top();
                nums.pop();
                //循环拼接k次
                while (k--)
                {
                    st.top() += tmp;
                }
                i++;
            }
                //4.处理普通字母,直接拼接
            else
            {
                string tmp;
                while (i < n && s[i] >= 'a' && s[i] <= 'z')
                {
                    tmp += s[i];
                    i++;
                }
                st.top() += tmp;
            }
        }
        return st.top();
    }
};

int main() {
    Solution sol;
    // 经典测试用例(覆盖所有核心场景)
    vector<string> testCases = {
            "3[a]2[bc]",          // 基础用例 → aaabcbc
            "3[a2[c]]",           // 嵌套用例 → accaccacc
            "2[abc]3[cd]ef",      // 连续拼接 → abcabccdcdcdef
            "abc3[cd]xyz",        // 前缀+解码+后缀 → abccdcdcdxyz
            "10[a]",              // 多位数重复 → aaaaaaaaaa
            "2[2[a]b]"            // 多层嵌套 → aabaab
    };
    
    for (string& str : testCases) {
        string res = sol.decodeString(str);
        cout << "编码字符串: \"" << str << "\"" << endl;
        cout << "解码结果:   \"" << res << "\"" << endl << "-------------------------" << endl;
    }

    return 0;
}

6.验证栈序列--栈(OJ题)


算法思路:解法(栈):

⽤栈来模拟进出栈的流程.

⼀直让元素进栈,进栈的同时判断是否需要出栈.当所有元素模拟完毕之后,如果栈中还有元素,那么就是⼀个⾮法的序列.否则,就是⼀个合法的序列.

核心代码

cpp 复制代码
class Solution 
{
public:
    bool validateStackSequences(vector<int>& pushed, vector<int>& popped) 
    {
        stack<int> st;
        int i = 0, n = popped.size();
        for (auto x : pushed) 
        {
            st.push(x);
            while (st.size() && st.top() == popped[i]) 
            {
                st.pop();
                i++;
            }
        }
        return i == n;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <stack>
using namespace std;

class Solution
{
public:
    bool validateStackSequences(vector<int>& pushed, vector<int>& popped)
    {
        stack<int> st;
        int i = 0, n = popped.size();
        //遍历所有压入元素
        for (auto x : pushed)
        {
            st.push(x);
            //栈顶匹配弹出序列,持续弹出
            while (st.size() && st.top() == popped[i])
            {
                st.pop();
                i++;
            }
        }
        //弹出序列全部匹配则合法
        return i == n;
    }
};

int main() {
    Solution sol;


    vector<int> pushed1 = {1,2,3,4,5};
    vector<int> popped1 = {4,5,3,2,1};

    vector<int> pushed2 = {1,2,3,4,5};
    vector<int> popped2 = {4,3,5,1,2};

    vector<int> pushed3 = {};
    vector<int> popped3 = {};

    vector<int> pushed4 = {1};
    vector<int> popped4 = {1};

    vector<int> pushed5 = {1};
    vector<int> popped5 = {2};

    vector<int> pushed6 = {2,1,0};
    vector<int> popped6 = {1,2,0};


    cout << "测试用例1:" << (sol.validateStackSequences(pushed1, popped1) ? "合法" : "非法") << endl;
    cout << "测试用例2:" << (sol.validateStackSequences(pushed2, popped2) ? "合法" : "非法") << endl;
    cout << "测试用例3:" << (sol.validateStackSequences(pushed3, popped3) ? "合法" : "非法") << endl;
    cout << "测试用例4:" << (sol.validateStackSequences(pushed4, popped4) ? "合法" : "非法") << endl;
    cout << "测试用例5:" << (sol.validateStackSequences(pushed5, popped5) ? "合法" : "非法") << endl;
    cout << "测试用例6:" << (sol.validateStackSequences(pushed6, popped6) ? "合法" : "非法") << endl;

    return 0;
}

7.N叉树的层序遍历--队列+宽搜(BFS)(OJ题)


算法思路:解法(队列+宽搜):

层序遍历即可,仅需多加⼀个变量,⽤来记录每⼀层结点的个数就好了.

核心逻辑

  1. 用队列实现层序遍历 :队列里始终只存放当前待处理层的节点;
  2. 按层分组 :每次循环先记录当前层的节点数量,保证只处理完本层再处理下一层;
  3. N叉树适配 :用 for 循环遍历节点的所有子节点,统一入队(区别于二叉树的left/right);
  4. 结果存储:每一层的节点值存入一个数组,最终所有层的数组合并为二维数组返回.

执行流程举例

比如一颗简单的N叉树:

复制代码
    1
   /|\
  2 3 4

遍历过程:

  1. 队列初始:[1],当前层大小=1 → 处理节点1,值[1],子节点2、3、4入队;
  2. 队列:[2,3,4],当前层大小=3 → 处理节点2、3、4,值[2,3,4];
  3. 队列为空,循环结束;
  4. 最终结果:[[1], [2,3,4]].

核心代码

cpp 复制代码
class Solution 
{
public:
    //函数功能:对N叉树进行层序遍历,返回 二维数组(每一层对应一个一维数组)
    //参数:root -> N叉树的根节点
    //返回值:vector<vector<int>> -> 外层是层,内层是当前层所有节点的值
    vector<vector<int>> levelOrder(Node* root) 
    {
        //1.定义结果容器:存储最终的层序遍历结果
        vector<vector<int>> ret;
        //2.定义队列:层序遍历的核心工具(BFS用队列,先进先出,保证按层处理)
        queue<Node*> q;

        //3.边界条件:如果根节点为空(空树),直接返回空结果
        if (root == nullptr)
            return ret;

        //4.初始化队列:将根节点入队,作为第一层的起始节点
        q.push(root);

        //5.外层循环:遍历每一层(队列不为空,说明还有节点未处理)
        while (q.size()) 
        {
            //核心步骤:获取当前队列的大小 = 当前层的节点总数
            //必须先固定这个值!因为后续子节点入队会改变队列大小
            int sz = q.size();
            // 定义临时数组:存储当前层所有节点的值
            vector<int> tmp;

            //6.内层循环:处理当前层的所有 sz 个节点
            for (int i = 0; i < sz; i++) 
            {
                //取出队首节点(当前要处理的节点)
                Node* t = q.front();
                //节点出队(已经取到,不需要留在队列里了)
                q.pop();
                //把当前节点的值加入临时数组(记录当前层的值)
                tmp.push_back(t->val);

                //7.遍历当前节点的所有子节点,让下一层节点入队
                //范围for:遍历 children 数组里的每一个子节点
                for (Node* child : t->children) 
                {
                    //安全判断:子节点非空才入队
                    if (child != nullptr)
                        q.push(child);
                }
            }

            //8.当前层处理完毕,把临时数组加入最终结果
            ret.push_back(tmp);
        }

        //9.所有层遍历完成,返回结果
        return ret;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

//1.定义N叉树节点结构
class Node {
public:
    int val;
    vector<Node*> children;

    //构造函数
    Node() {}
    Node(int _val) {
        val = _val;
    }
    Node(int _val, vector<Node*> _children) {
        val = _val;
        children = _children;
    }
};

class Solution
{
public:
    vector<vector<int>> levelOrder(Node* root)
    {
        vector<vector<int>> ret;
        queue<Node*> q;
        if (root == nullptr)
            return ret;
        q.push(root);
        while (q.size())
        {
            int sz = q.size();
            vector<int> tmp;
            for (int i = 0; i < sz; i++)
            {
                Node* t = q.front();
                q.pop();
                tmp.push_back(t->val);
                for (Node* child : t->children)
                {
                    if (child != nullptr)
                        q.push(child);
                }
            }
            ret.push_back(tmp);
        }
        return ret;
    }
};

void printResult(vector<vector<int>>& res) {
    cout << "层序遍历结果:[";
    for (int i = 0; i < res.size(); i++) {
        cout << "[";
        for (int j = 0; j < res[i].size(); j++) {
            cout << res[i][j];
            if (j != res[i].size() - 1) cout << ",";
        }
        cout << "]";
        if (i != res.size() - 1) cout << ",";
    }
    cout << "]" << endl << endl;
}

int main() {
    Solution sol;
    
    // 树结构:
    //      1
    //    / | \
    //   2  3  4
    Node* root1 = new Node(1);
    Node* node2 = new Node(2);
    Node* node3 = new Node(3);
    Node* node4 = new Node(4);
    root1->children = {node2, node3, node4};
    cout << "测试用例1:";
    vector<vector<int>> res1 = sol.levelOrder(root1);
    printResult(res1);
    
    // 树结构:
    //        1
    //    /   |   \
    //   3    2    4
    //  / \
    // 5   6
    Node* root2 = new Node(1);
    Node* node3_2 = new Node(3);
    Node* node2_2 = new Node(2);
    Node* node4_2 = new Node(4);
    Node* node5 = new Node(5);
    Node* node6 = new Node(6);
    node3_2->children = {node5, node6};
    root2->children = {node3_2, node2_2, node4_2};
    cout << "测试用例2:";
    vector<vector<int>> res2 = sol.levelOrder(root2);
    printResult(res2);
    
    Node* root3 = nullptr;
    cout << "测试用例3:";
    vector<vector<int>> res3 = sol.levelOrder(root3);
    printResult(res3);

    return 0;
}

8.二叉树的锯齿形层序遍历--队列+宽搜(BFS)(OJ题)


算法思路:解法(层序遍历):

在正常的层序遍历过程中,我们是可以把⼀层的结点放在⼀个数组中去的.既然我们有这个数组,在合适的层数逆序就可以得到锯⻮形层序遍历的结果.
核心功能

对二叉树进行锯齿形层序遍历

  • 奇数层:从左到右输出
  • 偶数层:从右到左输出
  • 最终返回二维数组(每一层对应一个一维数组)

核心逻辑

  1. 基础框架 :用队列实现标准的二叉树层序遍历(BFS);
  2. 按层处理 :每次循环先记录当前层节点数量,保证只处理完本层再处理下一层;
  3. 锯齿效果 :用level标记层数,偶数层反转数组,奇数层保持正序;
  4. 结果存储:每层的节点值存入一个数组,最终合并为二维数组返回.

执行流程举例

给一个二叉树:

复制代码
      3
     / \
    9  20
       / \
      15  7

遍历过程:

  1. 第1层(奇数,正序) :节点3 → 数组[3]
  2. 第2层(偶数,逆序) :节点9,20 → 反转成[20,9]
  3. 第3层(奇数,正序) :节点15,7 → 数组[15,7]

最终结果:
[[3], [20,9], [15,7]]

核心代码

cpp 复制代码
class Solution 
{
public:
    //函数功能:锯齿形层序遍历二叉树,返回二维数组
    //参数:root 二叉树根节点
    vector<vector<int>> zigzagLevelOrder(TreeNode* root) 
    {
        vector<vector<int>> ret;  //存储最终结果(二维数组:每层一个数组)
        if (root == nullptr)      //边界条件:空树直接返回空结果
            return ret;
        
        queue<TreeNode*> q;        //队列:BFS核心工具,存储待遍历的节点
        q.push(root);              //根节点入队,开始遍历
        int level = 1;             //标记当前层数:初始为第1层(奇数层,正序)

        //外层循环:遍历每一层(队列不为空 = 还有节点未处理)
        while (q.size()) 
        {
            int sz = q.size();     //固定当前层的节点总数
                                   //必须先存下来!子节点入队会改变队列大小
            vector<int> tmp;       //临时数组:存储当前层的节点值

            //内层循环:处理当前层的所有 sz 个节点
            for (int i = 0; i < sz; i++) 
            {
                auto t = q.front();//取出队首节点(当前要处理的节点)
                q.pop();           //节点出队
                tmp.push_back(t->val); //把节点值加入临时数组

                //子节点入队(先左后右,保证层序顺序)
                if (t->left)
                    q.push(t->left);
                if (t->right)
                    q.push(t->right);
            }

            //核心逻辑:偶数层反转数组,实现锯齿形
            if (level % 2 == 0)
                reverse(tmp.begin(), tmp.end()); //反转临时数组
            
            ret.push_back(tmp);  //当前层处理完成,加入最终结果
            level++;             //层数+1,处理下一层
        }

        return ret; //返回最终结果
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>  
using namespace std;

//1.定义二叉树节点结构
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};

class Solution
{
public:
    vector<vector<int>> zigzagLevelOrder(TreeNode* root)
    {
        vector<vector<int>> ret;
        if (root == nullptr)
            return ret;
        queue<TreeNode*> q;
        q.push(root);
        int level = 1;
        while (q.size())
        {
            int sz = q.size();
            vector<int> tmp;
            for (int i = 0; i < sz; i++)
            {
                auto t = q.front();
                q.pop();
                tmp.push_back(t->val);
                if (t->left)
                    q.push(t->left);
                if (t->right)
                    q.push(t->right);
            }
            //偶数层反转,实现锯齿效果
            if (level % 2 == 0)
                reverse(tmp.begin(), tmp.end());
            ret.push_back(tmp);
            level++;
        }
        return ret;
    }
};

void printResult(vector<vector<int>>& res) {
    cout << "锯齿形层序遍历结果:[";
    for (int i = 0; i < res.size(); ++i) {
        cout << "[";
        for (int j = 0; j < res[i].size(); ++j) {
            cout << res[i][j];
            if (j != res[i].size() - 1) cout << ",";
        }
        cout << "]";
        if (i != res.size() - 1) cout << ",";
    }
    cout << "]" << endl << endl;
}

int main() {
    Solution sol;
    
    // 树结构:
    //      3
    //     / \
    //    9  20
    //       / \
    //      15  7
    TreeNode* root1 = new TreeNode(3);
    root1->left = new TreeNode(9);
    root1->right = new TreeNode(20);
    root1->right->left = new TreeNode(15);
    root1->right->right = new TreeNode(7);
    cout << "测试用例1:";
    vector<vector<int>> res1 = sol.zigzagLevelOrder(root1);
    printResult(res1);
    
    TreeNode* root2 = nullptr;
    cout << "测试用例2:";
    vector<vector<int>> res2 = sol.zigzagLevelOrder(root2);
    printResult(res2);
    
    TreeNode* root3 = new TreeNode(1);
    cout << "测试用例3:";
    vector<vector<int>> res3 = sol.zigzagLevelOrder(root3);
    printResult(res3);
    
    // 树结构:
    //      1
    //     / \
    //    2   3
    TreeNode* root4 = new TreeNode(1);
    root4->left = new TreeNode(2);
    root4->right = new TreeNode(3);
    cout << "测试用例4:";
    vector<vector<int>> res4 = sol.zigzagLevelOrder(root4);
    printResult(res4);

    return 0;
}

9.二叉树最大宽度--队列+宽搜(BFS)(OJ题)

算法思路:解法(层序遍历):

  1. 第一种思路(会超过内存限制):
    既然统计每一层的最大宽度,我们优先想到的就是利用层序遍历,把当前层的结点全部存在队列里面,利用队列的长度来计算每一层的宽度,统计出最大的宽度.
    但是,由于空节点也是需要计算在内的.因此,我们可以选择将空节点也存在队列里面.

这个思路是我们正常会想到的思路,但是极端境况下,最左边一条长链,最右边一条长链,我们需要存几亿个空节点,会超过最大内存限制.

  1. 第二种思路(利用二叉树的顺序存储 - 通过根节点的下标,计算左右孩子的下标):
    依旧是利用层序遍历,但是这一次队列里面不单单存结点信息,并且还存储当前结点如果在数组中存储所对应的下标(学习数据结构 - 堆的时候,计算左右孩子的方式).

这样我们计算每一层宽度的时候,无需考虑空节点,只需将当层结点的左右结点的下标相减再加1即可.

但是,这里有个细节问题:如果二叉树的层数非常恐怖的话,我们任何一种数据类型都不能存下下标的值.但是没有问题,因为

  • 我们数据的存储是一个环形的结构;
  • 并且题目说明,数据的范围在 int 这个类型的最大值的范围之内,因此不会超出一圈;
  • 因此,如果是求差值的话,我们无需考虑溢出的情况.

·核心功能·

计算二叉树的最大宽度 :

二叉树每一层的宽度 = 该层最左侧非空节点最右侧非空节点 之间的节点数量(包含中间的空节点);代码返回所有层中最大的宽度值.

完全二叉树编号规则(关键!)

根节点编号 = 1

任意节点编号 = y

→ 左孩子编号 = y * 2

→ 右孩子编号 = y * 2 + 1
宽度计算公式

当前层宽度 = 最右节点编号 - 最左节点编号 + 1

极简执行流程

测试用例二叉树:

复制代码
      1(1)
    /       \
  3(2)      2(3)
 /   \       \
5(4) 3(5)    9(7)

编号规则:根1,左2,右3 → 孙节点4、5、6、7

  1. 第一层:节点1(1) → 宽度 1-1+1=1
  2. 第二层:节点3(2)2(3) → 宽度 3-2+1=2
  3. 第三层:节点5(4)3(5)9(7) → 宽度 7-4+1=4
  4. 最大宽度 = 4

核心代码

cpp 复制代码
class Solution 
{
public:
    //函数功能:计算二叉树最大宽度
    //参数:root 二叉树根节点
    //返回值:最大宽度值
    int widthOfBinaryTree(TreeNode* root) 
    {
        //1.定义队列:用vector模拟队列
        //存储 节点指针 + 节点编号(unsigned int 防止编号溢出)
        vector<pair<TreeNode*, unsigned int>> q;
        //初始化队列:根节点入队,编号为1
        q.push_back({root, 1});

        //2.记录最终结果:最大宽度
        unsigned int ret = 0;

        //3.层序遍历循环:队列不为空,说明还有节点未处理
        while (q.size()) 
        {
            //步骤1:计算当前层的宽度
            //结构化绑定(C++17):取出当前层 第一个节点(最左) 和 最后一个节点(最右)
            auto& [x1, y1] = q[0];   // x1=节点,y1=最左节点编号
            auto& [x2, y2] = q.back();// x2=节点,y2=最右节点编号
            //更新最大宽度:用当前层宽度 和 历史最大值比较
            ret = max(ret, y2 - y1 + 1);

            //步骤2:构建下一层节点队列
            //临时队列:存储下一层的 节点+编号
            vector<pair<TreeNode*, unsigned int>> tmp;
            //遍历当前层所有节点
            for (auto& [x, y] : q) 
            {
                //如果左孩子非空,入队,编号=2*y
                if (x->left)
                    tmp.push_back({x->left, y * 2});
                //如果右孩子非空,入队,编号=2*y+1
                if (x->right)
                    tmp.push_back({x->right, y * 2 + 1});
            }

            //队列更新为下一层,继续循环
            q = tmp;
        }

        //遍历完成,返回最大宽度
        return ret;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>  
using namespace std;

//定义二叉树节点结构
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};

class Solution
{
public:
    int widthOfBinaryTree(TreeNode* root)
    {
        //数组模拟队列:存储节点 + 节点编号(无符号整型防止溢出)
        vector<pair<TreeNode*, unsigned int>> q;
        q.push_back({root, 1});
        unsigned int ret = 0;

        while (q.size())
        {
            //结构化绑定:获取当前层最左、最右节点的编号
            auto& [x1, y1] = q[0];
            auto& [x2, y2] = q.back();
            // 更新最大宽度:最右编号 - 最左编号 + 1
            ret = max(ret, y2 - y1 + 1);

            //构建下一层节点队列
            vector<pair<TreeNode*, unsigned int>> tmp;
            for (auto& [x, y] : q)
            {
                //左孩子编号:2*y
                if (x->left)
                    tmp.push_back({x->left, y * 2});
                //右孩子编号:2*y+1
                if (x->right)
                    tmp.push_back({x->right, y * 2 + 1});
            }
            //迭代到下一层
            q = tmp;
        }
        return ret;
    }
};

int main() {
    Solution sol;
    
    // 树结构:
    //      1
    //    /   \
    //   3     2
    //  / \     \
    // 5   3     9
    //最大宽度:4
    TreeNode* root1 = new TreeNode(1);
    root1->left = new TreeNode(3);
    root1->right = new TreeNode(2);
    root1->left->left = new TreeNode(5);
    root1->left->right = new TreeNode(3);
    root1->right->right = new TreeNode(9);
    cout << "测试用例1 最大宽度:" << sol.widthOfBinaryTree(root1) << endl;
    
    // 树结构:
    //      1
    //    /
    //   3
    //  / \
    // 5   3
    // 最大宽度:2
    TreeNode* root2 = new TreeNode(1);
    root2->left = new TreeNode(3);
    root2->left->left = new TreeNode(5);
    root2->left->right = new TreeNode(3);
    cout << "测试用例2 最大宽度:" << sol.widthOfBinaryTree(root2) << endl;
    
    TreeNode* root3 = new TreeNode(1);
    cout << "测试用例3 最大宽度:" << sol.widthOfBinaryTree(root3) << endl;

    return 0;
}

10.在每个树行中找最大值--队列+宽搜(BFS)(OJ题)

算法思路:解法(bfs):

层序遍历过程中,在执⾏让下⼀层节点⼊队的时候,我们是可以在循环中统计出当前层结点的最⼤值

的.因此,可以在bfs的过程中,统计出每⼀层结点的最⼤值.

核心功能

对二叉树进行层序遍历 ,找到每一层的最大节点值,最终返回一个数组,数组中的每个元素对应二叉树每一层的最大值.

极简执行流程

二叉树结构:

复制代码
      1
     / \
    3   2
   / \   \
  5   3   9

遍历过程:

  1. 第一层 :节点1 → 最大值 1
  2. 第二层 :节点3、2 → 最大值 3
  3. 第三层 :节点5、3、9 → 最大值 9
    最终结果:[1, 3, 9]

核心代码

cpp 复制代码
class Solution 
{
public:
    //函数功能:找到二叉树每一层的最大值
    //参数:root -> 二叉树根节点
    //返回值:vector<int> -> 存储每一层最大值的数组
    vector<int> largestValues(TreeNode* root) 
    {
        vector<int> ret;          //1.定义结果数组,存储每一层的最大值
        if (root == nullptr)      //2.边界条件:空树直接返回空数组
            return ret;
        
        queue<TreeNode*> q;       //3.定义队列,层序遍历核心工具(先进先出)
        q.push(root);             //4.根节点入队,开始遍历

        //5.外层循环:遍历每一层(队列不为空 = 还有节点未处理)
        while (q.size()) 
        {
            int sz = q.size();    //6.关键:固定当前层的节点总数
                                  //必须先保存,子节点入队会改变队列大小
            int tmp = INT_MIN;    //7.初始化当前层最大值为【整数最小值】
                                  //保证任何节点值都能比它大

            //8.内层循环:处理当前层的所有sz个节点
            for (int i = 0; i < sz; i++) 
            {
                auto t = q.front();//取出队首节点(当前要处理的节点)
                q.pop();           //节点出队

                //9.更新当前层最大值:比较已有最大值和当前节点值
                tmp = max(tmp, t->val);

                //10.子节点入队(为下一层遍历做准备)
                if (t->left)
                    q.push(t->left);
                if (t->right)
                    q.push(t->right);
            }

            //11.当前层遍历完成,把最大值加入结果数组
            ret.push_back(tmp);
        }

        //12.所有层处理完毕,返回结果
        return ret;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm> 
using namespace std;

//定义二叉树节点结构体
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};

class Solution
{
public:
    vector<int> largestValues(TreeNode* root)
    {
        vector<int> ret;
        if (root == nullptr)
            return ret;
        queue<TreeNode*> q;
        q.push(root);
        while (q.size())
        {
            int sz = q.size();
            int tmp = INT_MIN;
            for (int i = 0; i < sz; i++)
            {
                auto t = q.front();
                q.pop();
                tmp = max(tmp, t->val);
                if (t->left)
                    q.push(t->left);
                if (t->right)
                    q.push(t->right);
            }
            ret.push_back(tmp);
        }
        return ret;
    }
};

void printResult(vector<int>& res) {
    cout << "二叉树每层最大值:[";
    for (int i = 0; i < res.size(); ++i) {
        cout << res[i];
        if (i != res.size() - 1) cout << ", ";
    }
    cout << "]" << endl << endl;
}

int main() {
    Solution sol;
    
    // 树结构:
    //      1
    //     / \
    //    3   2
    //   / \   \
    //  5   3   9
    // 预期结果:[1, 3, 9]
    TreeNode* root1 = new TreeNode(1);
    root1->left = new TreeNode(3);
    root1->right = new TreeNode(2);
    root1->left->left = new TreeNode(5);
    root1->left->right = new TreeNode(3);
    root1->right->right = new TreeNode(9);
    cout << "测试用例1:";
    vector<int> res1 = sol.largestValues(root1);
    printResult(res1);
    
    TreeNode* root2 = nullptr;
    cout << "测试用例2:";
    vector<int> res2 = sol.largestValues(root2);
    printResult(res2);
    
    TreeNode* root3 = new TreeNode(10);
    cout << "测试用例3:";
    vector<int> res3 = sol.largestValues(root3);
    printResult(res3);
    
    // 树结构:1 -> 2 -> 3
    // 预期结果:[1, 2, 3]
    TreeNode* root4 = new TreeNode(1);
    root4->left = new TreeNode(2);
    root4->left->left = new TreeNode(3);
    cout << "测试用例4:";
    vector<int> res4 = sol.largestValues(root4);
    printResult(res4);

    return 0;
}

11.最后一块石头的重量--优先级队列(OJ题)

算法思路:解法(利用堆):

其实就是一个模拟的过程:

  • 每次从石堆中拿出最大的元素以及次大的元素,然后将它们粉碎;
  • 如果还有剩余,就将剩余的石头继续放在原始的石堆里面

重复上面的操作,直到石堆里面只剩下一个元素,或者没有元素(因为所有的石头可能全部抵消了)

那么主要的问题就是解决:

  • 如何顺利的拿出最大的石头以及次大的石头;
  • 并且将粉碎后的石头放入石堆中之后,也能快速找到下一轮粉碎的最大石头和次大石头;

这不正好可以利用堆的特性来实现嘛?

  • 我们可以创建一个大根堆;
  • 然后将所有的石头放入大根堆中;
  • 每次拿出前两个堆顶元素粉碎一下,如果还有剩余,就将剩余的石头继续放入堆中;
    这样就能快速的模拟出这个过程.

核心代码

cpp 复制代码
class Solution 
{
public:
    int lastStoneWeight(vector<int>& stones) 
    {
        //1.创建⼀个⼤根堆
        priority_queue<int> heap;
        //2.将所有元素丢进这个堆⾥⾯
        for (auto x : stones)
            heap.push(x);
        //3.模拟这个过程
        while (heap.size() > 1) 
        {
            int a = heap.top();
            heap.pop();
            int b = heap.top();
            heap.pop();
            if (a > b)
                heap.push(a - b);
        }
        return heap.size() ? heap.top() : 0;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>  
using namespace std;

class Solution
{
public:
    int lastStoneWeight(vector<int>& stones)
    {
        //1.创建⼀个⼤根堆(C++默认priority_queue就是大根堆)
        priority_queue<int> heap;
        //2.将所有元素丢进这个堆⾥⾯
        for (auto x : stones)
            heap.push(x);
        //3.模拟粉碎过程
        while (heap.size() > 1)
        {
            //取出最重的两块石头
            int a = heap.top();
            heap.pop();
            int b = heap.top();
            heap.pop();
            //重量不等,将剩余重量放回堆中
            if (a > b)
                heap.push(a - b);
        }
        //剩余石头重量,无剩余返回0
        return heap.size() ? heap.top() : 0;
    }
};

int main() {
    Solution sol;
    
    vector<int> stones1 = {2,7,4,1,8,1};
    cout << "测试用例1 结果:" << sol.lastStoneWeight(stones1) << endl;
    
    vector<int> stones2 = {2,2};
    cout << "测试用例2 结果:" << sol.lastStoneWeight(stones2) << endl;
    
    vector<int> stones3 = {5};
    cout << "测试用例3 结果:" << sol.lastStoneWeight(stones3) << endl;

    return 0;
}

12.数据流中的第k大元素--优先级队列(OJ题)


算法思路:解法(优先级队列):

我相信,看到 TopK 问题的时候,大家应该能⽴⻢想到堆,这应该是刻在⻣⼦⾥的记忆.
📌Top-K问题全面解析

Top-K问题是算法面试、数据处理中的经典高频题 ,核心需求是:从N个元素中,找出最大/最小的K个元素,或第K大/第K小的元素 .
核心解法1:堆(优先队列)法(面试首选,通用场景)

🔍 原理

  • 找前K大元素 :用大小为K的小根堆
    1. 遍历所有N个元素,依次入堆;
    2. 堆大小超过K时,弹出堆顶(当前堆中最小的元素);
    3. 最终堆中剩余的K个元素,就是全局最大的K个元素,堆顶为第K大元素.
  • 找前K小元素 :用大小为K的大根堆,逻辑完全对称.

⏱️ 复杂度

  • 时间复杂度:O(N log K)(每次堆操作O(log K),共N次)
  • 空间复杂度:O(K)(仅存K个元素,适合大数据量/流式数据)

核心解法2:快速选择(QuickSelect)法(静态数组最优,平均O(N))

🔍 原理

基于快速排序的分治思想,通过基准划分快速定位第K大元素:

  1. 随机选择基准pivot,将数组划分为大于pivot 等于pivot 小于pivot三部分;
  2. 根据大于pivot的元素个数cnt,递归缩小搜索范围:
    • cnt >= k:第K大在大于pivot区间;
    • cnt + 等于pivot的个数 >= k:pivot就是第K大;
    • 否则:在小于pivot区间搜索,更新k = k - cnt - 等于pivot的个数.

⏱️ 复杂度

  • 平均时间复杂度:O(N)(总操作≈2N)
  • 最坏时间复杂度:O(N²)(可通过随机基准优化)
  • 空间复杂度:O(log N)(递归栈)

其他解法(适合特定场景)

  1. 全排序法(极简,小数据量)
  • 原理:对数组全排序后取前K个
  • 时间复杂度:O(N log N),仅适合N极小的场景
  1. 桶排序/计数排序(数据范围小的整数)
  • 原理:用桶统计每个值的出现次数,从大到小遍历桶,累计到K个元素
  • 时间复杂度:O(N + M)(M为数据范围)
  • 适用场景:数据范围小,如年龄、分数等

解法对比表

解法 时间复杂度 空间复杂度 适用场景 核心优势 局限性
小根堆法 O(N log K) O(K) 大数据量、流式数据、动态数据 内存占用小、支持实时更新 略慢于快速选择的平均时间
快速选择法 平均O(N) O(log N) 静态数组、找第K大 平均时间最优 最坏O(N²)、不支持流式数据
全排序法 O(N log N) O(1) 小数据量 代码极简 大数据量效率极低
桶排序法 O(N + M) O(M) 数据范围小的整数 线性时间 数据范围大时不可用

核心代码

cpp 复制代码
class KthLargest 
{
    //创建⼀个⼤⼩为 k 的⼩跟堆
    priority_queue<int, vector<int>, greater<int>> heap;
    int _k;

public:
    KthLargest(int k, vector<int>& nums) 
    {
        _k = k;
        for (auto x : nums) 
        {
            heap.push(x);
            if (heap.size() > _k)
                heap.pop();
        }
    }
    int add(int val) 
    {
        heap.push(val);
        if (heap.size() > _k)
            heap.pop();
        return heap.top();
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>  
using namespace std;

class KthLargest
{
    //创建⼀个⼤⼩为 k 的小根堆
    priority_queue<int, vector<int>, greater<int>> heap;
    int _k;  // 保存k值

public:
    //构造函数:初始化堆
    KthLargest(int k, vector<int>& nums)
    {
        _k = k;
        for (auto x : nums)
        {
            heap.push(x);
            //堆大小超过k,弹出堆顶(最小元素)
            if (heap.size() > _k)
                heap.pop();
        }
    }

    //添加新元素,并返回当前第K大元素
    int add(int val)
    {
        heap.push(val);
        if (heap.size() > _k)
            heap.pop();
        // 堆顶即为第K大元素
        return heap.top();
    }
};

int main() {
    vector<int> nums = {4,5,8,2};
    KthLargest obj(3, nums);
    
    cout << "添加元素3,第3大元素:" << obj.add(3) << endl;    // 预期:4
    cout << "添加元素5,第3大元素:" << obj.add(5) << endl;    // 预期:5
    cout << "添加元素10,第3大元素:" << obj.add(10) << endl;  // 预期:5
    cout << "添加元素9,第3大元素:" << obj.add(9) << endl;    // 预期:8
    cout << "添加元素8,第3大元素:" << obj.add(8) << endl;    // 预期:8

    return 0;
}

13.前k个高频单词--优先级队列(OJ题)


算法思路:解法(堆):

  • 稍微处理一下原数组:

    (1)我们需要知道每一个单词出现的频次,因此可以先使用哈希表,统计出每一个单词出现的频次;

    (2)然后在哈希表中,选出前 k 大的单词(为什么不在原数组中选呢?因为原数组中存在重复的单词,哈希表里面没有重复单词,并且还有每一个单词出现的频次)

  • 如何使用堆,拿出前 k 大元素:

    (1)先定义一个自定义排序,我们需要的是前 k 大,因此需要一个小根堆.但是当两个字符串的频次相同的时候,我们需要的是字典序较小的,此时是一个大根堆的属性,在定义比较器的时候需要注意!当两个字符串出现的频次不同的时候:需要的是基于频次比较的小根堆;当两个字符串出现的频次相同的时候:需要的是基于字典序比较的大根堆

    (2)定义好比较器之后,依次将哈希表中的字符串插入到堆中,维持堆中的元素不超过 k 个;

    (3)遍历完整个哈希表后,堆中的剩余元素就是前 k 大的元素

代码核心功能

给定一个单词数组,返回出现频率前 K 高的单词:

  1. 频率越高,排名越靠前;
  2. 若频率相同 ,按字典序从小到大排列;
  3. 最终按要求返回前 K 个单词.

执行流程举例

输入:words = ["i","love","leetcode","i","love","coding"], k = 2

  1. 哈希统计频次:
    i:2, love:2, leetcode:1, coding:1
  2. 入堆,维护大小为2:
    堆中保留:love(2), i(2)
  3. 逆序取出:["i", "love"]

核心代码

cpp 复制代码
class Solution 
{
    //1.类型别名:简化 pair<string, int> 为 PSI
    //表示:<单词字符串, 出现频次>
    typedef pair<string, int> PSI;

    //2.自定义堆的比较规则
    struct cmp {
        //重载小括号运算符,给优先队列指定排序规则
        bool operator()(const PSI& a, const PSI& b) 
        {
            if (a.second == b.second) 
            {
                //情况1:频次相同 → 字典序**大**的排前面(大根堆)
                //保证最终输出时,字典序小的在结果数组前面
                return a.first < b.first;
            }
            //情况2:频次不同 → 频次**小**的排前面(小根堆)
            return a.second > b.second;
        }
    };

public:
    vector<string> topKFrequent(vector<string>& words, int k) 
    {
        //3.哈希表:统计每个单词出现的频次(去重+计数)
        unordered_map<string, int> hash;
        for (auto& s : words)
            hash[s]++;  //单词 s 出现次数 +1

        //4.创建优先队列(堆)
        //模板参数:元素类型PSI + 底层容器vector + 自定义比较器cmp
        priority_queue<PSI, vector<PSI>, cmp> heap;

        //5.Top-K 核心逻辑:维护大小为 k 的小根堆
        for (auto& psi : hash) 
        {
            heap.push(psi);          //元素入堆
            if (heap.size() > k)     //堆大小超过 k,弹出堆顶(最小的元素)
                heap.pop();
        }

        //6.提取结果:堆顶是第K大元素,需要逆序存入结果数组
        vector<string> ret(k);
        for (int i = k - 1; i >= 0; i--) 
        {
            ret[i] = heap.top().first;  //取出单词
            heap.pop();                 //弹出堆顶
        }

        return ret;  //返回最终结果
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <unordered_map>
#include <queue>
#include <string>
using namespace std;

class Solution
{
    typedef pair<string, int> PSI;
    //自定义堆的比较规则
    struct cmp {
        bool operator()(const PSI& a, const PSI& b)
        {
            if (a.second == b.second) //频次相同,字典序大的优先(大根堆)
            {
                return a.first < b.first;
            }
            return a.second > b.second; //频次不同,频次小的优先(小根堆)
        }
    };
public:
    vector<string> topKFrequent(vector<string>& words, int k)
    {
        //1.哈希表统计单词频次
        unordered_map<string, int> hash;
        for (auto& s : words)
            hash[s]++;
        //2.创建自定义规则的小根堆
        priority_queue<PSI, vector<PSI>, cmp> heap;
        //3.Top-K核心逻辑:维护堆大小为k
        for (auto& psi : hash)
        {
            heap.push(psi);
            if (heap.size() > k)
                heap.pop();
        }
        //4.逆序提取结果
        vector<string> ret(k);
        for (int i = k - 1; i >= 0; i--)
        {
            ret[i] = heap.top().first;
            heap.pop();
        }
        return ret;
    }
};

void printResult(const vector<string>& res) {
    cout << "前K个高频单词:[";
    for (int i = 0; i < res.size(); ++i) {
        cout << "\"" << res[i] << "\"";
        if (i != res.size() - 1) cout << ", ";
    }
    cout << "]" << endl << endl;
}

int main() {
    Solution sol;
    
    // 输入:words = ["i","love","leetcode","i","love","coding"], k = 2
    // 输出:["i","love"]
    vector<string> words1 = {"i","love","leetcode","i","love","coding"};
    int k1 = 2;
    cout << "测试用例1:";
    vector<string> res1 = sol.topKFrequent(words1, k1);
    printResult(res1);
    
    // 输入:words = ["the","day","is","sunny","the","the","the","sunny","is","is"], k = 4
    // 输出:["the","is","sunny","day"]
    vector<string> words2 = {"the","day","is","sunny","the","the","the","sunny","is","is"};
    int k2 = 4;
    cout << "测试用例2:";
    vector<string> res2 = sol.topKFrequent(words2, k2);
    printResult(res2);
    
    vector<string> words3 = {"hello"};
    int k3 = 1;
    cout << "测试用例3:";
    vector<string> res3 = sol.topKFrequent(words3, k3);
    printResult(res3);
    
    vector<string> words4 = {"a","b","c","a","b","c"};
    int k4 = 2;
    cout << "测试用例4:";
    vector<string> res4 = sol.topKFrequent(words4, k4);
    printResult(res4);

    return 0;
}

14.数据流的中位数--优先级队列(OJ题)

算法思路:解法(利用两个堆):

这是一道关于堆这种数据结构的一个经典应用.

我们可以将整个数组按照大小平分成两部分(如果不能平分,那就让较小部分的元素多一个),较小的部分称为左侧部分,较大的部分称为右侧部分:

  • 将左侧部分放入大根堆 中,然后将右侧元素放入小根堆中;
  • 这样就能在O(1) 的时间内拿到中间的一个数或者两个数,进而求得平均数.

如下图所示:

于是问题变成了如何将一个从数据流中过来的数据,动态调整到大根堆或者小根堆中,并且保证两个堆的元素一致,或者左侧堆的元素比右侧堆的元素多一个 .

为了方便叙述,将左侧的大根堆记为 left,右侧的小根堆记为 right,数据流中来的数据记为 x.

其实就是一个分类讨论的过程:

  1. 如果左右堆的数量相同,即 left.size() == right.size():

    • 如果两个堆都是空的,直接将数据 x 放入到 left 中;
    • 如果两个堆非空:
      • 如果元素要放入左侧,也就是 x <= left.top():那就直接放,因为不会影响我们制定的规则;
      • 如果要放入右侧:
        • 可以先将 x 放入 right 中,
        • 然后把 right 的堆顶元素放入 left 中;
  2. 如果左右堆的数量不相同,即 left.size() > right.size():

    • 这个时候我们关心的是 x 是否会放入 left 中,导致 left 变得过多:
      • 如果 x 放入 right 中,也就是 x >= right.top(),直接放;
      • 反之,就是需要放入 left 中:
        • 可以先将 x 放入 left 中,
        • 然后把 left 的堆顶元素放入 right 中;

只要每一个新来的元素按照上述规则 执行,就能保证 left 中放着整个数组排序后的左半部分,right 中放着整个数组排序后的右半部分,就能在O(1) 的时间内求出平均数.

核心代码

cpp 复制代码
class MedianFinder {
    // 成员变量:两个堆
    priority_queue<int> left;                             //大根堆(默认):存较小的一半数
    priority_queue<int, vector<int>, greater<int>> right; //小根堆:存较大的一半数
public:
    //构造函数:初始化对象,无需额外操作
    MedianFinder() {}

    //核心函数:添加数据流中的数字
    void addNum(int num) {
        //分类讨论:根据两个堆的大小关系,决定数字放哪里
        if (left.size() == right.size()) //情况1:左右堆元素个数相同
        {
            //堆为空 或 新数字 ≤ 左堆最大值 → 直接放左堆
            if (left.empty() || num <= left.top()) 
            {
                left.push(num);
            } 
            else //新数字更大,应该放右堆,但要保证左堆多1个
            {
                right.push(num);        //先放右堆
                left.push(right.top()); //把右堆最小值挪到左堆
                right.pop();            //右堆删除该元素
            }
        } 
        else //情况2:左堆比右堆多1个元素(唯一的不等情况)
        {
            if (num <= left.top()) // 新数字要放左堆,会导致失衡
            {
                left.push(num);        //先放左堆
                right.push(left.top());//把左堆最大值挪到右堆
                left.pop();            //左堆删除该元素
            } 
            else //新数字直接放右堆,保持平衡
            {
                right.push(num);
            }
        }
    }

    //获取中位数
    double findMedian() {
        if (left.size() == right.size()) //总元素个数:偶数
            return (left.top() + right.top()) / 2.0 ; //两个堆顶取平均
        else // 总元素个数:奇数,左堆多一个,堆顶就是中位数
            return left.top();
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <queue>
using namespace std;

class MedianFinder {
    priority_queue<int> left;                             //大根堆:存储较小的一半数据
    priority_queue<int, vector<int>, greater<int>> right; //小根堆:存储较大的一半数据
public:
    MedianFinder() {}

    void addNum(int num) {
        //分类讨论维护两个堆的平衡
        if (left.size() == right.size()) //左右堆元素个数相同
        {
            if (left.empty() || num <= left.top()) //直接放入左堆
            {
                left.push(num);
            } else { //先放右堆,再将右堆堆顶移到左堆
                right.push(num);
                left.push(right.top());
                right.pop();
            }
        } else { //左堆元素比右堆多1个
            if (num <= left.top()) { //先放左堆,再将左堆堆顶移到右堆
                left.push(num);
                right.push(left.top());
                left.pop();
            } else { //直接放入右堆
                right.push(num);
            }
        }
    }

    double findMedian() {
        //元素总数为偶数:两个堆顶取平均
        if (left.size() == right.size())
            return (left.top() + right.top()) / 2.0;
            //元素总数为奇数:左堆堆顶就是中位数
        else
            return left.top();
    }
};

int main() {
    cout << "===== 测试用例1 =====" << endl;
    MedianFinder mf1;
    mf1.addNum(1);
    cout << "添加元素1,当前中位数:" << mf1.findMedian() << endl;
    mf1.addNum(2);
    cout << "添加元素2,当前中位数:" << mf1.findMedian() << endl;
    mf1.addNum(3);
    cout << "添加元素3,当前中位数:" << mf1.findMedian() << endl;
    cout << endl;
    
    cout << "===== 测试用例2 =====" << endl;
    MedianFinder mf2;
    mf2.addNum(5);
    mf2.addNum(1);
    mf2.addNum(3);
    mf2.addNum(7);
    cout << "元素:5,1,3,7,中位数:" << mf2.findMedian() << endl;
    cout << endl;
    
    cout << "===== 测试用例3 =====" << endl;
    MedianFinder mf3;
    mf3.addNum(10);
    cout << "单个元素10,中位数:" << mf3.findMedian() << endl;
    cout << endl;
    
    cout << "===== 测试用例4 =====" << endl;
    MedianFinder mf4;
    mf4.addNum(0);
    mf4.addNum(1);
    mf4.addNum(2);
    mf4.addNum(4);
    mf4.addNum(5);
    cout << "元素:0,1,2,4,5,中位数:" << mf4.findMedian() << endl;

    return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容:【优选算法】(实战攻坚BFS之FloodFill、最短路径问题、多源BFS以及解决拓扑排序)


每日心灵鸡汤:相遇的意义在于彼此照亮,而不是"物是人非事事休,欲语泪先流!"
我们总以为相识一个人,就必须要有一个结果.直到后来才明白,有些人光是遇见,就已经是上上签.朋友也好,爱人也罢,他们都是上天赋予我们生命的珍贵礼物,陪伴一程,温暖一段,拥有过便已值得感念.所以,不必纠结他能否陪你走到最后.这世间,有些人的出现,本就是为了陪你走过一段短暂却美好的时光.即便日后形同陌路,也无需正式告别,更不必遗憾.因为"前世不欠,今生不见",我大大方方地为自己的选择买单,也要我的人生始终热烈,真诚而坦荡.山鸟与鱼不同路,从此山水不相逢,这才是常态.重要的不是结局,而是那些熠熠生辉的瞬间------那些被你改变的部分的我,将代替你,永远留在我生命里.不是每件事都要有结果,不是每段路都必须同行.只要在相遇时,我们曾真心相待,彼此照亮,那段回忆就足以在往后的黯淡时刻,兀自闪耀,成为前行的光.愿你相信:总有一场相遇,是隔着茫茫人海,为奔赴彼此而来,它不一定有结局,但一定有意义.物是人非事事休,欲语泪先流.我时常想,人与人的羁绊为何如此短暂,明明昨天还分享琐碎的日常,今天却默契的不说话,这或许就是人们常说的阶段性陪伴,我常常羡慕那些以年为单位的陪伴,每当有断联的旧人再次联系上我时,我总是庆幸,终于能够说话的人但在互不联系的日子里,或许对方已经忘记了过去的太多过往,长达一年甚至两年的断联重新呈现,人和人真是奇怪,明明那么多个夜晚,聊到凌晨时彼此袒露心迹但在某一天突然变成了不会联系的陌生人,抱怨人生路上不应该遇见的人,下雨坐在窗边回忆过去,其实好坏我都舍不得.所以珍惜眼前人,走好每一段旅途!

相关推荐
py有趣8 小时前
力扣热门100题之将有序数组转为二叉搜索树
算法·leetcode
盐焗西兰花8 小时前
鸿蒙学习实战之路-Share Kit系列(14/17)-手机间碰一碰分享实战
学习·智能手机·harmonyos
天若有情6738 小时前
Python精神折磨系列(完整11集·无断层版)
数据库·python·算法
凌波粒8 小时前
LeetCode--383.赎金信(哈希表)
java·算法·leetcode·散列表
liulilittle8 小时前
OPENPPP2 1.0.0.26145 正式版发布:内核态 SYSNAT 性能飞跃 + Windows 平台避坑指南
开发语言·网络·c++·windows·通信·vrrp
AIminminHu8 小时前
OpenGL渲染与几何内核那点事-项目实践理论补充(三-1-(2):当你的CAD代码变得“又大又乱”:从手动编译到CMake,从随性编码到单元测试))
c++·单元测试·cmake·cad·cad开发
arvin_xiaoting8 小时前
OpenClaw学习总结_II_频道系统_6:iMessage集成详解
学习
敲敲了个代码8 小时前
React 那么多状态管理库,到底选哪个?如果非要焊死一个呢?这篇文章解决你的选择困难症
前端·javascript·学习·react.js·前端框架
醇氧8 小时前
【学习】IP地址分类全解析
网络协议·学习·tcp/ip