【数据结构与算法基础】05. 栈详解(C++ 实战)
【数据结构与算法基础】05. 栈详解(C++ 实战)
栈是一种特殊的线性表,它遵循**后进先出(LIFO, Last In First Out)**的原则。本文将详细介绍栈的基本概念、操作、实现方式以及实际应用案例,并通过C++代码进行实战演练。
(关注不迷路哈!!!)
文章目录
- 【数据结构与算法基础】05. 栈详解(C++ 实战)
- 一、栈的基本概念
- 1.1 栈的定义
- 1.2 栈的特点
- 1.3 栈的用途
- 二、栈的基本操作
- 三、栈的实现方式
- 3.1 顺序栈(基于数组)
- 3.2 链式栈(基于链表)
- 四、栈的C++实现
- 4.1 顺序栈(基于数组)的C++实现
- 4.2 链式栈(基于链表)的C++实现
- 五、栈的经典应用案例
- 5.1 括号匹配问题
- 5.2 浏览器的前进和后退功能
- 六、栈的性能对比
- 七、栈的总结
- 7.1 栈的特性
- 7.2 栈的优势
- 7.3 栈的劣势
- 八、练习题
- 总结
一、栈的基本概念
1.1 栈的定义
栈是一种只能在一端进行插入和删除操作的线性数据结构。允许进行插入和删除操作的一端称为栈顶(Top),另一端称为栈底(Bottom)。
1.2 栈的特点
- 后进先出(LIFO):最后压入栈的元素最先被弹出。
- 操作受限:只能在栈顶进行插入和删除操作,限制了数据的访问方式,提高了特定场景下的效率。
1.3 栈的用途
栈在计算机科学中有广泛的应用,包括但不限于:
- 函数调用和递归:管理函数调用栈。
- 表达式求值和语法分析:如括号匹配、中缀转后缀表达式。
- 回溯算法:如深度优先搜索(DFS)。
- 浏览器的前进和后退功能。
- 撤销(Undo)操作。
二、栈的基本操作
栈的基本操作主要包括:
- Push(压栈):将元素添加到栈顶。
- Pop(出栈):移除并返回栈顶元素。
- Peek/Top(查看栈顶):返回栈顶元素但不移除。
- IsEmpty(判断栈是否为空)
- Size(获取栈的大小)
操作类型 | 方法名 | 时间复杂度 | 功能描述 |
---|---|---|---|
插入操作 | push() | O(1) | 将元素置于栈顶 |
删除操作 | pop() | O(1) | 移除并返回栈顶元素 |
查看操作 | peek() | O(1) | 返回栈顶元素但不移除 |
栈与队列的核心差异对比
特性 | 栈(Stack) | 队列(Queue) |
---|---|---|
操作原则 | LIFO | FIFO |
主要操作 | push/pop | enqueue/dequeue |
典型应用 | 函数调用/表达式求值 | 任务调度/消息队列 |
实现复杂度 | 简单 | 相对复杂 |
三、栈的实现方式
栈可以通过两种主要方式实现:顺序栈(基于数组) 和 链式栈(基于链表)
实现方式 | 底层结构 | 优势 | 劣势 | 适用场景 |
---|---|---|---|---|
顺序栈 | 数组 | 内存连续,访问速度快 | 大小固定,可能浪费空间 | 栈大小可预估的场景 |
链式栈 | 链表 | 动态扩容,灵活度高 | 需要额外指针空间 | 栈大小变化频繁的场景 |
3.1 顺序栈(基于数组)
顺序栈使用数组来存储栈中的元素,通过一个指针(通常称为top
)来指示栈顶的位置。
实现原理
- 数组:用于存储栈中的元素。
- Top指针:指向栈顶元素的位置。初始时,
top = -1
表示栈为空。
操作实现
- Push:将元素添加到
top + 1
的位置,并更新top
。 - Pop:返回
top
位置的元素,并将top
减一。 - Peek/Top:返回
top
位置的元素。 - IsEmpty:检查
top
是否为-1
。 - Size:返回
top + 1
。
优缺点
- 优点: 实现简单,访问速度快。 内存连续,缓存友好。
- 缺点: 栈的大小固定,扩展性差(除非动态扩容)。 可能浪费空间,如果栈的使用量波动较大。
3.2 链式栈(基于链表)
链式栈使用链表来存储栈中的元素,每个节点包含数据和指向下一个节点的指针。栈顶即为链表的头节点。
实现原理
- 链表节点:包含数据和指向下一个节点的指针。
- Top指针:指向链表的头节点,即栈顶。
操作实现
- Push:在链表头部插入新节点,并更新
top
。 - Pop:移除链表头节点,并更新
top
。 - Peek/Top:返回
top
节点的数据。 - IsEmpty:检查
top
是否为nullptr
。 - Size:遍历链表计算节点数量(通常不常用,可以维护一个计数器)。
优缺点
- 优点: 动态大小,无需预先分配固定空间。 插入和删除操作高效,时间复杂度为O(1)。
- 缺点: 每个节点需要额外的指针空间,内存开销略大。 访问速度可能略低于顺序栈,尤其是在缓存不友好的情况下。
四、栈的C++实现
下面将分别展示顺序栈和链式栈的C++实现。
4.1 顺序栈(基于数组)的C++实现
#include <iostream>
#include <stdexcept>class ArrayStack
{
private:static const int MAX_SIZE = 1000; // 栈的最大容量int data[MAX_SIZE];int top;public:ArrayStack() : top(-1) {}// 压栈操作void push(int value){if (top >= MAX_SIZE - 1){throw std::overflow_error("Stack Overflow");}data[++top] = value;}// 出栈操作int pop(){if (isEmpty()){throw std::underflow_error("Stack Underflow");}return data[top--];}// 查看栈顶元素int peek() const{if (isEmpty()){throw std::underflow_error("Stack is Empty");}return data[top];}// 判断栈是否为空bool isEmpty() const { return top == -1; }// 获取栈的大小int size() const { return top + 1; }
};int main()
{ArrayStack stack;stack.push(10);stack.push(20);stack.push(30);std::cout << "栈顶元素: " << stack.peek() << std::endl; // 输出 30std::cout << "栈的大小: " << stack.size() << std::endl; // 输出 3std::cout << "出栈元素: " << stack.pop() << std::endl; // 输出 30std::cout << "出栈元素: " << stack.pop() << std::endl; // 输出 20std::cout << "栈顶元素: " << stack.peek() << std::endl; // 输出 10std::cout << "栈的大小: " << stack.size() << std::endl; // 输出 1return 0;
}
代码说明
- ArrayStack类:实现了基于数组的顺序栈。 成员变量:
data[MAX_SIZE]
:用于存储栈元素的数组。top
:指示栈顶位置的指针,初始为-1
。 成员函数:push(int value)
:将元素压入栈顶,若栈满则抛出异常。pop()
:移除并返回栈顶元素,若栈空则抛出异常。peek()
:返回栈顶元素,若栈空则抛出异常。isEmpty()
:判断栈是否为空。size()
:返回栈中元素的数量。 - main函数:演示了栈的基本操作,包括压栈、查看栈顶、出栈等。
4.2 链式栈(基于链表)的C++实现
#include <iostream>
#include <stdexcept>// 定义链表节点
struct Node
{int data;Node* next;Node(int val) : data(val), next(nullptr) {}
};class LinkedStack
{
private:Node* top;public:LinkedStack() : top(nullptr) {}// 压栈操作void push(int value){Node* newNode = new Node(value);newNode->next = top;top = newNode;}// 出栈操作int pop(){if (isEmpty()){throw std::underflow_error("Stack Underflow");}Node* temp = top;int poppedValue = temp->data;top = top->next;delete temp;return poppedValue;}// 查看栈顶元素int peek() const{if (isEmpty()){throw std::underflow_error("Stack is Empty");}return top->data;}// 判断栈是否为空bool isEmpty() const { return top == nullptr; }// 获取栈的大小int size() const{int count = 0;Node* current = top;while (current != nullptr){++count;current = current->next;}return count;}// 析构函数,释放内存~LinkedStack(){while (!isEmpty()){pop();}}
};int main()
{LinkedStack stack;stack.push(100);stack.push(200);stack.push(300);std::cout << "栈顶元素: " << stack.peek() << std::endl; // 输出 300std::cout << "栈的大小: " << stack.size() << std::endl; // 输出 3std::cout << "出栈元素: " << stack.pop() << std::endl; // 输出 300std::cout << "出栈元素: " << stack.pop() << std::endl; // 输出 200std::cout << "栈顶元素: " << stack.peek() << std::endl; // 输出 100std::cout << "栈的大小: " << stack.size() << std::endl; // 输出 1return 0;
}
代码说明
- Node结构体:定义了链表节点,包含数据和指向下一个节点的指针。
- LinkedStack类:实现了基于链表的链式栈。 成员变量:
top
:指向栈顶节点的指针,初始为nullptr
。 成员函数:push(int value)
:在链表头部插入新节点,更新top
。pop()
:移除并返回栈顶节点的数据,释放节点内存,若栈空则抛出异常。peek()
:返回栈顶节点的数据,若栈空则抛出异常。isEmpty()
:判断栈是否为空。size()
:遍历链表计算节点数量。 析构函数:释放所有节点的内存,防止内存泄漏。 - main函数:演示了链式栈的基本操作,包括压栈、查看栈顶、出栈等。
五、栈的经典应用案例
5.1 括号匹配问题
问题描述:给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串,判断字符串是否有效。有效字符串需满足:左括号必须与相同类型的右括号匹配,且左括号必须以正确的顺序匹配。
解决方案:使用栈来匹配括号。遍历字符串,遇到左括号时压栈,遇到右括号时出栈并与当前右括号匹配,若不匹配则字符串无效。遍历结束后,若栈为空,则字符串有效。
C++代码实现
#include <iostream>
#include <stack>
#include <string>
#include <unordered_map>using namespace std;bool isLegal(const string& s)
{stack<char> st;unordered_map<char, char> matching = {{')', '('}, {'}', '{'}, {']', '['}};for (char c : s){if (c == '(' || c == '{' || c == '['){st.push(c);} else{if (st.empty() || st.top() != matching[c]){return false;}st.pop();}}return st.empty();
}int main()
{string s1 = "{[()()]}";string s2 = "{( [ ) ] }";cout << "字符串 \"" << s1 << "\" 是否合法: " << (isLegal(s1) ? "合法" : "非法") << endl; // 合法cout << "字符串 \"" << s2 << "\" 是否合法: " << (isLegal(s2) ? "合法" : "非法") << endl; // 非法return 0;
}
代码说明
- isLegal函数:判断字符串中的括号是否匹配。 栈st:用于存储左括号。 matching映射:定义右括号与对应左括号的匹配关系。 遍历字符串: 遇到左括号,压栈。 遇到右括号,检查栈顶是否为对应的左括号,若匹配则出栈,否则返回非法。 最终检查:若栈为空,则所有括号匹配,返回合法;否则返回非法。
- main函数:测试两个示例字符串的合法性。
5.2 浏览器的前进和后退功能
问题描述:利用栈实现浏览器的后退和前进功能。用户访问新页面时,将其压入后退栈;当用户后退时,将页面从后退栈弹出并压入前进栈;当用户前进时,将页面从前进栈弹出并压入后退栈。
实现思路
- 两个栈: 后退栈(Back Stack):存储用户访问过的页面,用于后退操作。 前进栈(Forward Stack):存储用户后退后可以前进的页面,用于前进操作。
- 操作流程: 访问新页面:将页面压入后退栈,清空前进栈。 后退操作:从后退栈弹出页面,压入前进栈,显示弹出的页面。 前进操作:从前进栈弹出页面,压入后退栈,显示弹出的页面。
C++代码实现
#include <iostream>
#include <stack>
#include <string>using namespace std;class Browser
{
private:stack<string> backStack;stack<string> forwardStack;string currentPage;public:Browser() : currentPage("") {}// 访问新页面void visit(const string& url){if (!currentPage.empty()){backStack.push(currentPage);}currentPage = url;// 清空前进栈while (!forwardStack.empty()){forwardStack.pop();}cout << "访问页面: " << currentPage << endl;}// 后退操作void back(){if (backStack.empty()){cout << "无法后退" << endl;return;}forwardStack.push(currentPage);currentPage = backStack.top();backStack.pop();cout << "后退到页面: " << currentPage << endl;}// 前进操作void forward(){if (forwardStack.empty()){cout << "无法前进" << endl;return;}backStack.push(currentPage);currentPage = forwardStack.top();forwardStack.pop();cout << "前进到页面: " << currentPage << endl;}// 获取当前页面string getCurrentPage() const { return currentPage; }
};int main()
{Browser browser;browser.visit("1");browser.visit("2");browser.visit("3");browser.visit("4");browser.visit("5");cout << "当前页面: " << browser.getCurrentPage() << endl;browser.back(); // 后退到 4browser.back(); // 后退到 3cout << "当前页面: " << browser.getCurrentPage() << endl;browser.forward(); // 前进到 4browser.forward(); // 前进到 5cout << "当前页面: " << browser.getCurrentPage() << endl;return 0;
}
代码说明
- Browser类:模拟浏览器的后退和前进功能。 成员变量:
backStack
:存储后退页面的栈。forwardStack
:存储前进页面的栈。currentPage
:当前显示的页面。 成员函数:visit(const string& url)
:访问新页面,将当前页面压入后退栈,清空前进栈。back()
:执行后退操作,将当前页面压入前进栈,从后退栈弹出页面作为当前页面。forward()
:执行前进操作,将当前页面压入后退栈,从前进栈弹出页面作为当前页面。getCurrentPage()
:返回当前页面。 - main函数:演示了访问多个页面后的后退和前进操作。
六、栈的性能对比
栈作为一种基础数据结构,其性能在不同实现方式下有所差异。以下是顺序栈和链式栈的简要对比:
特性 | 顺序栈(基于数组) | 链式栈(基于链表) |
---|---|---|
空间复杂度 | 固定大小,可能浪费空间 | 动态大小,每个节点额外指针 |
时间复杂度 | 所有操作 O(1) | 所有操作 O(1) |
扩展性 | 受限于预分配的数组大小 | 动态扩展,无需预先分配 |
实现复杂度 | 简单 | 稍复杂,需管理节点内存 |
缓存友好性 | 高,内存连续 | 低,节点分散 |
总结:
- 顺序栈适用于栈大小固定或可预估的场景,实现简单且访问速度快。
- 链式栈适用于栈大小动态变化或不可预估的场景,具有更好的扩展性和灵活性。
七、栈的总结
7.1 栈的特性
- 后进先出(LIFO):最后压入栈的元素最先被弹出,这种特性使得栈在处理需要逆序操作的问题时非常有效。
- 操作受限:只能在栈顶进行插入和删除操作,这种限制提高了特定场景下的效率,减少了不必要的操作。
7.2 栈的优势
- 高效的操作:压栈和出栈操作的时间复杂度为O(1),适用于需要频繁进行插入和删除操作的场景。
- 简单易用:栈的基本操作直观,易于理解和实现。
- 广泛应用:栈在各种算法和系统设计中有着广泛的应用,如函数调用、表达式求值、括号匹配等。
7.3 栈的劣势
- 功能受限:由于只能在栈顶进行操作,栈在需要随机访问或中间插入/删除操作的场景下不适用。
- 空间限制:顺序栈的固定大小可能限制其应用,尽管可以通过动态扩容解决,但会增加复杂性。
八、练习题
每k个节点一组翻转链表
问题描述:给定一个包含n个元素的链表,要求每k个节点一组进行翻转,打印翻转后的链表结果。其中,k是一个正整数,且n可被k整除。
提示:可以参考以下步骤实现:
- 遍历链表,每次处理k个节点。
- 翻转每组k个节点。
- 将翻转后的组重新连接到链表中。
C++代码实现示例:
#include <iostream>using namespace std;// 定义链表节点
struct ListNode
{int val;ListNode* next;ListNode(int x) : val(x), next(nullptr) {}
};// 翻转从head开始的k个节点
ListNode* reverseKGroup(ListNode* head, int k)
{ListNode* curr = head;int count = 0;// 检查是否有k个节点while (curr != nullptr && count < k){curr = curr->next;count++;}if (count < k){return head; // 不足k个,不翻转}// 翻转k个节点ListNode* prev = nullptr;curr = head;ListNode* next = nullptr;for (int i = 0; i < k; ++i){next = curr->next;curr->next = prev;prev = curr;curr = next;}// 递归翻转后续组,并连接if (curr != nullptr){head->next = reverseKGroup(curr, k);}return prev;
}// 辅助函数:打印链表
void printList(ListNode* head)
{ListNode* curr = head;while (curr != nullptr){cout << curr->val << " ";curr = curr->next;}cout << endl;
}// 辅助函数:创建链表
ListNode* createList(int arr[], int n)
{if (n == 0)return nullptr;ListNode* head = new ListNode(arr[0]);ListNode* curr = head;for (int i = 1; i < n; ++i){curr->next = new ListNode(arr[i]);curr = curr->next;}return head;
}int main()
{int arr[] = {1, 2, 3, 4, 5, 6};int n = sizeof(arr) / sizeof(arr[0]);int k = 3;ListNode* head = createList(arr, n);cout << "原链表: ";printList(head);ListNode* newHead = reverseKGroup(head, k);cout << "每" << k << "个节点一组翻转后的链表: ";printList(newHead);return 0;
}
代码说明:
- reverseKGroup函数:递归地翻转每k个节点一组。 检查是否有k个节点:遍历k个节点,若不足则返回原链表头。 翻转k个节点:使用三个指针(prev, curr, next)翻转当前组的k个节点。 递归处理后续组:将翻转后的组的尾节点连接到后续翻转组的头节点。
- createList和printList函数:辅助函数,用于创建链表和打印链表内容。
- main函数:创建一个示例链表,调用
reverseKGroup
函数进行每k个节点一组翻转,并打印结果。
输出结果:
原链表: 1 2 3 4 5 6
每3个节点一组翻转后的链表: 3 2 1 6 5 4
总结
栈作为一种基础且强大的数据结构,遵循**后进先出(LIFO)**的原则,具有高效的操作性能和广泛的应用场景。通过本文的介绍,您应该对栈的基本概念、操作、实现方式以及实际应用有了深入的理解。栈不仅在理论上有重要地位,在实际编程中也扮演着不可或缺的角色,如函数调用管理、表达式求值、括号匹配等问题都可以通过栈优雅地解决。
(觉得有用请点赞收藏,你的支持是我持续更新的动力!)