数据结构基础 - 双端队列生成排列数计算

题目:将 4 个不同元素按顺序输入到双端队列(deque)中,可以得到多少种不同的输出排列? ✅ 问题理解 我们有一个 双端队列(deque),可以在 队头 和 队尾 进行插入和删除。 有 4 个不同的元素,按固定顺序输入(比如 1, 2, 3, 4),但每个元素在入队时可以选择从前端插入或后端插入。 所有元素入队后,我们再考虑出队的顺序。由于双端队列允许两端删除,出队顺序会有很多种可能。 ❓ 分析思路 第一个元素从左或右入队没有区别,以后每个元素都有从左和从右两种入队方式,即有 $2^{x-1}$ 种方法。 这个思路是正确的,它描述了入队策略的数量。对于 $x=4$ 个元素,确实有 $2^{4-1} = 8$ 种不同的入队方式。 但是,这 不是 本题要求的答案。题目问的是:最终可以得到多少种不同的输出排列? 也就是说,我们需要计算:在所有可能的入队策略下,通过后续的出队操作,总共能产生多少种不同的元素排列(顺序)。 🔍 正确解题思路 这是一个经典的组合数学问题,与 双栈排序(Two Stack Sortable Permutations) 或 双端队列生成的排列(Deque-generated Permutations) 有关。 对于 $n$ 个元素按顺序入双端队列,最终能得到的不同排列数,是一个已知的数列,称为 大施罗德数(Large Schröder Numbers) 的一种变形。 对于 $n=4$,我们可以通过程序来模拟所有可能的入队策略,并计算所有可能的出队序列,从而得到最终的不同排列数。 🧠 程序思路 我们用一个 std::deque<int> 来模拟双端队列的行为。 对于 4 个元素 (1, 2, 3, 4),我们按顺序处理每个元素。 对于每个元素,它有两种入队方式:push_front 或 push_back。 这构成了一个二叉树的搜索空间(每个节点代表一种入队选择)。 在所有元素都入队后,我们模拟所有可能的出队方式(即,从 front 或 back 弹出元素),并记录所有可能产生的排列。 最后,我们统计所有不同排列的数量。 为了高效实现,我们可以使用 递归 + 回溯 的方法来遍历所有可能的状态。 ...

2025-9-22 · 1 分钟 · Atom.X

数据结构基础 - 后缀表达式

中缀表达式 (Infix Notation) 这是我们人类最习惯的数学表达式写法。操作符(如 +, -, *, /)放在两个操作数(数字)之间。例如:3 + 4, (10 - 2) * 5。 优点:符合我们的阅读和书写习惯。 缺点:计算机在解析时需要处理运算符优先级和括号,比较复杂。 后缀表达式 (Postfix Notation / Reverse Polish Notation - RPN) 在这种表示法中,操作符放在它的操作数之后。例如:3 4 + 表示 3 + 4。(10 - 2) * 5 会写成 10 2 - 5 *。 优点: 没有括号:运算顺序完全由操作符和操作数的位置决定。 无歧义:表达式只有一个明确的含义。 易于计算机求值:只需要一个栈即可轻松计算结果。算法如下: 从左到右扫描表达式。 如果是数字,则压入栈。 如果是操作符,则从栈中弹出所需数量的操作数(例如,对于二元操作符-,弹出两个),执行运算,然后将结果压回栈中。 最终,栈中剩下的唯一一个数就是表达式的值。 中缀转后缀 (调度场算法 - Shunting-yard algorithm 的简化版) 将中缀表达式转换为后缀表达式的基本思路是: 使用一个栈来存放操作符。 从左到右扫描中缀表达式。 如果遇到操作数(数字),直接输出。 如果遇到左括号 (,将其压入操作符栈。 如果遇到右括号 ),则依次弹出操作符栈中的操作符并输出,直到遇到左括号 ( 为止,并将左括号 ( 弹出(但不输出)。 如果遇到操作符(如 +, -, *, /): 当操作符栈顶的操作符优先级大于或等于当前操作符的优先级时,将栈顶操作符弹出并输出。 将当前操作符压入操作符栈。 当整个表达式扫描完毕后,将操作符栈中剩余的所有操作符依次弹出并输出。 这种写法(指将数字(操作数)和操作符(如 +, - 等)都用空格分隔开,形成一个由token(词法单元)组成的序列)非常符合 C++ 数据结构和算法课程中对于表达式处理的要求和实践。 ...

2025-9-21 · 1 分钟 · Atom.X

数据结构基础 - 链表操作

单元2 链表操作详解:插入、删除、读取及指针修改次数 以下针对四种链表类型(单链表、双链表、循环单链表、循环双链表),详细说明插入、删除、读取操作步骤及指针修改次数(仅统计已有节点的指针修改,不包括新节点初始化或删除节点释放)。 1. 单链表(Singly Linked List) 节点结构:数据 + next指针(指向后继) 插入操作(在节点A和B之间插入C): 步骤: 创建新节点C,设置 C.next = B(新节点初始化,不计入修改)。 修改A的 next 指针指向C:A.next = C。 指针修改次数:1次(仅修改前驱节点A的指针)。 边界情况: 头部插入:修改头指针指向C(头指针修改不计入节点指针修改)。 尾部插入:同一般情况(修改原尾节点的 next)。 删除操作(删除节点B,A为其前驱): 步骤: 修改A的 next 指针指向B的后继:A.next = B.next。 释放B(不计入指针修改)。 指针修改次数:1次(仅修改前驱节点A的指针)。 边界情况: 删除头节点:修改头指针指向原头节点的后继(头指针修改不计入节点指针修改)。 删除尾节点:修改倒数第二节点的 next 为 null(1次修改)。 读取操作: 仅支持顺序访问:从头节点遍历,时间复杂度 O(n)。 不支持随机访问(无索引)。 2. 双链表(Doubly Linked List) 节点结构:数据 + next指针(指向后继) + prev指针(指向前驱) 插入操作(在节点A和B之间插入C): 步骤: 创建新节点C,设置 C.prev = A, C.next = B(新节点初始化)。 修改A的 next 指针指向C:A.next = C。 修改B的 prev 指针指向C:B.prev = C。 指针修改次数:2次(修改前驱A的 next 和后继B的 prev)。 边界情况: 头部插入:需修改原头节点的 prev 指向C(1次修改)。 尾部插入:需修改原尾节点的 next 指向C(1次修改)。 删除操作(删除节点B): ...

2025-7-22 · 2 分钟 · Atom.X

数据结构基础 - 栈与队列

栈和队列有哪些特殊的操作?栈和队列能解决什么样的问题?用栈和队列这种数据结构,来解决与“先进先出”、“先进后出”有关的实际问题了,如宽度优先搜索、表达式求值等。 类比 - 日常生活普遍规律 如果桌上有一叠盘子,大家都只会拿最上面的那一个。食堂排队的时候,你总是先找到队尾加入,而排在队首的同学打完饭之后就会离开。 类比计算机,也许只需要在线性序列的一端或两端进行操作,对应的就是栈和队列这两种受限的线性表,他们是最简单的基础数据结构,应用也最广泛。 重点:栈的 LIFO(后进先出) 特性,深度优先搜索,理解递归中栈的作用;队列的 FIFO(先进先出)特性,宽度优先搜索。 难点:机械的递归转非递归,简单理解就可以了,不需要掌握;顺序队列的实现假溢出处理。 栈 在C++程序中,栈(Stack) 是一种非常重要的线性数据结构,它遵循 LIFO(Last In, First Out) 原则,即 后进先出。 一、栈的核心特点 后进先出(LIFO):最后被添加到栈中的元素,会最先被移除。 只能在一端操作:所有的插入(入栈/Push)和删除(出栈/Pop)操作都发生在栈的同一端,这一端被称为 栈顶(Top)。 另一端是固定的:栈的另一端称为 栈底(Bottom)。 📌 类比: 想象一个放羽毛球的筒: 你只能从筒的开口(栈顶)放入或取出羽毛球。 最后放进去的球,会最先被拿出来。 二、栈的基本操作 操作 描述 push(x) 将元素 x 压入栈顶 pop() 移除并返回栈顶元素 top() / peek() 返回栈顶元素,但不移除 empty() 判断栈是否为空 size() 返回栈中元素的个数 三、C++ 中如何实现栈? C++ 提供了两种主要方式来使用栈: 1. 使用 STL 容器适配器 std::stack 这是最常用、最推荐的方式。std::stack 是一个容器适配器,默认基于 std::deque 实现。 #include <iostream> #include <stack> int main() { std::stack<int> s; // 创建一个存储 int 类型的栈 s.push(10); // 入栈 s.push(20); s.push(30); std::cout << "栈顶元素: " << s.top() << std::endl; // 输出: 30 s.pop(); // 出栈 std::cout << "新的栈顶元素: " << s.top() << std::endl; // 输出: 20 std::cout << "栈的大小: " << s.size() << std::endl; // 输出: 2 if (!s.empty()) { std::cout << "栈非空" << std::endl; } return 0; } std::stack 的特点: 使用简单,接口清晰。 默认底层容器是 std::deque,也可以指定为 std::vector 或 std::list: std::stack<int, std::vector<int>> s_vec; // 基于 vector std::stack<int, std::list<int>> s_list; // 基于 list 2. 自己用数组或链表实现 虽然不常用,但理解其原理很重要。 ...

2025-7-22 · 3 分钟 · Atom.X

数据结构基础 - 线性表

单元2 下面是一个简洁的 C++ 程序示例,展示了**顺序表(使用 vector)和链表(使用自定义单向链表)**的基本操作对比,包括插入、删除和访问元素: 🧪 顺序表 vs 链表:C++ 示例对比 ✅ 顺序表(使用 std::vector) #include <iostream> #include <vector> using namespace std; int main() { vector<int> seqList; // 插入元素 seqList.push_back(10); seqList.push_back(20); seqList.push_back(30); // 删除元素(删除第二个) seqList.erase(seqList.begin() + 1); // 访问元素 for (int i = 0; i < seqList.size(); ++i) { cout << "顺序表元素[" << i << "] = " << seqList[i] << endl; } return 0; } ✅ 链表(自定义单向链表) #include <iostream> using namespace std; struct Node { int data; Node* next; Node(int val) : data(val), next(nullptr) {} }; void insert(Node*& head, int val) { Node* newNode = new Node(val); newNode->next = head; head = newNode; } void deleteNode(Node*& head, int val) { Node* curr = head; Node* prev = nullptr; while (curr) { if (curr->data == val) { if (prev) prev->next = curr->next; else head = curr->next; delete curr; return; } prev = curr; curr = curr->next; } } void printList(Node* head) { int index = 0; while (head) { cout << "链表元素[" << index++ << "] = " << head->data << endl; head = head->next; } } int main() { Node* head = nullptr; // 插入元素(头插法) insert(head, 30); insert(head, 20); insert(head, 10); // 删除元素(删除值为 20 的节点) deleteNode(head, 20); // 打印链表 printList(head); return 0; } 📊 对比总结 特性 顺序表(vector) 链表(自定义) 插入效率 中间插入需移动元素,较慢 修改指针即可,效率高 删除效率 删除需移动元素 修改指针即可 随机访问 支持 O(1) 下标访问 需遍历,O(n) 内存结构 连续内存,缓存友好 分散内存,灵活扩展 实现复杂度 简单(STL支持) 需手动管理指针 单链表中,做插入数据的操作时,是否需要同时做两处数据的操作? 也就是修改前一个数据的后置链接指向,并增加新数据,例如在数据A和B之间插入C,首先要生成新的数据C,其后驱指针指向B,再修改前一个数据A的后驱指针,删除其与B的链接,建立与C的链接。 此外删除操作也是类似的的需要修改两处,该规则适用于双链表和循环链表。 ...

2025-7-21 · 2 分钟 · Atom.X