当前位置: 首页 > news >正文

【数据结构与算法基础】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)操作

二、栈的基本操作

栈的基本操作主要包括:

  1. Push(压栈):将元素添加到栈顶。
  2. Pop(出栈):移除并返回栈顶元素。
  3. Peek/Top(查看栈顶):返回栈顶元素但不移除。
  4. IsEmpty(判断栈是否为空)
  5. Size(获取栈的大小)
操作类型方法名时间复杂度功能描述
插入操作push()O(1)将元素置于栈顶
删除操作pop()O(1)移除并返回栈顶元素
查看操作peek()O(1)返回栈顶元素但不移除

栈与队列的核心差异对比

特性栈(Stack)队列(Queue)
操作原则LIFOFIFO
主要操作push/popenqueue/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):在链表头部插入新节点,更新toppop():移除并返回栈顶节点的数据,释放节点内存,若栈空则抛出异常。 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整除。

提示:可以参考以下步骤实现:

  1. 遍历链表,每次处理k个节点。
  2. 翻转每组k个节点
  3. 将翻转后的组重新连接到链表中。

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 63个节点一组翻转后的链表: 3 2 1 6 5 4

总结

栈作为一种基础且强大的数据结构,遵循**后进先出(LIFO)**的原则,具有高效的操作性能和广泛的应用场景。通过本文的介绍,您应该对栈的基本概念、操作、实现方式以及实际应用有了深入的理解。栈不仅在理论上有重要地位,在实际编程中也扮演着不可或缺的角色,如函数调用管理、表达式求值、括号匹配等问题都可以通过栈优雅地解决。


(觉得有用请点赞收藏,你的支持是我持续更新的动力!)

http://www.dtcms.com/a/504973.html

相关文章:

  • 做网站要有什么团队雨发建设集团有限公司网站
  • seo建站优化asp.net+mvc+网站开发
  • 网站图片一般分辨率做多大赣州网站建设服务
  • 2017招远网站建设云兰装潢公司总部地址电话
  • 网站服务器查询工具wordpress 返回顶部插件
  • 做网站推广弊端青岛胶南做网站的
  • 2018年网站建设工作总结北京市建设工程信息网登录流程
  • 网站首页背景代码旅游类网站开发毕业设计
  • 9 垂直分片
  • 公司网站如何备案如何制作钓鱼网站
  • 做网站如何分类关于网站建设的外文翻译
  • 网站建设申请书搜了网推广
  • 昆明做网站建设有哪些网站推广优化联系方式
  • 免费注册自己的网站品牌建设卓有成效
  • 易语言和网站做交互中国赣州
  • 网站网页设计制作公司搬家网站建设案例说明
  • STM32Cubemx配置独立看门狗(IWDG)
  • 有做翻译英文网站山西网站开发二次开发
  • 外贸业务怎么利用网站开发客户网站建设模板网站
  • 免费的企业网站建设wordpress做漫画
  • 网站域名查询网网站推广的方法搜索引擎
  • 给外国小孩 做 英语题用的网站怎么做文学动漫网站
  • 网站制作网页设计室内设计软件3d
  • 北京专业制作网站公司哪家好贵南网站建设
  • 浙江人工智能建站系统软件网站页脚写什么
  • 公司网站怎么更新需要怎么做优化关键词哪家好
  • 简单的企业网站外贸网站广告宣传网站
  • 网站建设提供商wordpress next
  • 湖州北京网站建设定制手机网站建设
  • 深圳专门做网站WordPress如何快速排名