【数据结构与算法基础】05. 栈详解(C++ 实战)

【数据结构与算法基础】05. 栈详解(C++ 实战)

栈是一种特殊的线性表,它遵循**后进先出(LIFO, Last In First Out)**的原则。本文将详细介绍栈的基本概念、操作、实现方式以及实际应用案例,并通过C++代码进行实战演练。

(关注不迷路哈!!!)

文章目录

  • [【数据结构与算法基础】05. 栈详解(C++ 实战)](#【数据结构与算法基础】05. 栈详解(C++ 实战))

一、栈的基本概念

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)
操作原则 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++实现

cpp 复制代码
#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; // 输出 30
    std::cout << "栈的大小: " << stack.size() << std::endl; // 输出 3

    std::cout << "出栈元素: " << stack.pop() << std::endl; // 输出 30
    std::cout << "出栈元素: " << stack.pop() << std::endl; // 输出 20

    std::cout << "栈顶元素: " << stack.peek() << std::endl; // 输出 10
    std::cout << "栈的大小: " << stack.size() << std::endl; // 输出 1

    return 0;
}

代码说明

  • ArrayStack类 :实现了基于数组的顺序栈。 成员变量data[MAX_SIZE]:用于存储栈元素的数组。 top:指示栈顶位置的指针,初始为-1成员函数push(int value):将元素压入栈顶,若栈满则抛出异常。 pop():移除并返回栈顶元素,若栈空则抛出异常。 peek():返回栈顶元素,若栈空则抛出异常。 isEmpty():判断栈是否为空。 size():返回栈中元素的数量。
  • main函数:演示了栈的基本操作,包括压栈、查看栈顶、出栈等。

4.2 链式栈(基于链表)的C++实现

cpp 复制代码
#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; // 输出 300
    std::cout << "栈的大小: " << stack.size() << std::endl; // 输出 3

    std::cout << "出栈元素: " << stack.pop() << std::endl; // 输出 300
    std::cout << "出栈元素: " << stack.pop() << std::endl; // 输出 200

    std::cout << "栈顶元素: " << stack.peek() << std::endl; // 输出 100
    std::cout << "栈的大小: " << stack.size() << std::endl; // 输出 1

    return 0;
}

代码说明

  • Node结构体:定义了链表节点,包含数据和指向下一个节点的指针。
  • LinkedStack类 :实现了基于链表的链式栈。 成员变量top:指向栈顶节点的指针,初始为nullptr成员函数push(int value):在链表头部插入新节点,更新toppop():移除并返回栈顶节点的数据,释放节点内存,若栈空则抛出异常。 peek():返回栈顶节点的数据,若栈空则抛出异常。 isEmpty():判断栈是否为空。 size():遍历链表计算节点数量。 析构函数:释放所有节点的内存,防止内存泄漏。
  • main函数:演示了链式栈的基本操作,包括压栈、查看栈顶、出栈等。

五、栈的经典应用案例

5.1 括号匹配问题

问题描述 :给定一个只包括 '('')''{''}''['']'的字符串,判断字符串是否有效。有效字符串需满足:左括号必须与相同类型的右括号匹配,且左括号必须以正确的顺序匹配。

解决方案:使用栈来匹配括号。遍历字符串,遇到左括号时压栈,遇到右括号时出栈并与当前右括号匹配,若不匹配则字符串无效。遍历结束后,若栈为空,则字符串有效。

C++代码实现

cpp 复制代码
#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++代码实现

cpp 复制代码
#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(); // 后退到 4
    browser.back(); // 后退到 3

    cout << "当前页面: " << browser.getCurrentPage() << endl;

    browser.forward(); // 前进到 4
    browser.forward(); // 前进到 5

    cout << "当前页面: " << 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++代码实现示例

cpp 复制代码
#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个节点一组翻转,并打印结果。

输出结果

cpp 复制代码
原链表: 1 2 3 4 5 6 
每3个节点一组翻转后的链表: 3 2 1 6 5 4

总结

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


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

相关推荐
lingran__4 小时前
算法沉淀第七天(AtCoder Beginner Contest 428 和 小训练赛)
c++·算法
前端小刘哥4 小时前
新版视频直播点播平台EasyDSS,打通远程教研与教师培训新通路
算法
2401_840105204 小时前
P1049 装箱问题 题解(四种方法)附DP和DFS的对比
c++·算法·深度优先·动态规划
老K的Java兵器库4 小时前
Collections 工具类 15 个常用方法源码:sort、binarySearch、reverse、shuffle、unmodifiableXxx
java·开发语言·哈希算法
武子康4 小时前
Java-153 深入浅出 MongoDB 全面的适用场景分析与选型指南 场景应用指南
java·开发语言·数据库·mongodb·性能优化·系统架构·nosql
kobe_t4 小时前
数据安全系列7:常用的非对称算法浅析
算法
靠近彗星4 小时前
3.4特殊矩阵的压缩存储
数据结构·人工智能·算法
rit84324994 小时前
ES6 箭头函数:告别 `this` 的困扰
开发语言·javascript·es6
嵌入式-老费4 小时前
Easyx图形库应用(用lua开发图形界面)
开发语言·lua