线性数据结构之栈结构

栈(Stack)是一种线性数据结构,遵循“后进先出”(Last In, First Out,LIFO)的原则。栈主要有两种基本操作:push(入栈)和 pop(出栈)。在栈中,最新添加的元素最先被移除,类似于一摞盘子:放在最上面的盘子是最后放上去的,但却是最先拿走的。

栈的实现可以基于数组和链表,各有优缺点,适合不同的应用场景。下面将详细介绍栈的定义、特点、优缺点、应用场景以及用数组和链表实现栈的方式。

栈的基本概念

1. 特点

  • LIFO:栈遵循“后进先出”原则。
  • 基本操作
    • Push:将元素放入栈顶。
    • Pop:移除栈顶元素并返回其值。
    • Peek(Top):返回栈顶元素但不移除它。
    • isEmpty:检查栈是否为空。
    • Size:返回栈中元素的数量。

2. 优点

  • 操作简单:只允许在栈顶进行添加或删除操作。
  • 快速:只需要在栈顶操作,时间复杂度为 O(1)。

3. 缺点

  • 容量限制:基于数组的栈实现通常有固定容量,需要判断是否溢出。
  • 仅支持栈顶操作:无法直接访问中间元素。

4. 应用场景

  • 函数调用:程序执行时的调用栈,用来管理方法的调用和返回。
  • 表达式求值:如计算器中的中缀表达式转后缀表达式、表达式求值。
  • 撤销操作:用于实现文本编辑器、图片编辑器的撤销和重做功能。

一、数组实现栈

定义与实现原理

在数组实现的栈中,数组的最后一个位置作为栈顶。入栈时将元素添加到数组末尾,出栈时从数组末尾移除元素。数组实现的栈通常具有固定的大小,且在栈满时可能会引发溢出错误。

1. 优点

  • 访问速度快:数组在内存中是连续存储的,因此访问栈顶元素非常快。
  • 实现简单:操作简单,入栈和出栈仅涉及数组末尾元素。

2. 缺点

  • 固定大小:数组的大小在初始化时确定,不灵活。
  • 栈满溢出:当栈达到数组容量时,将无法继续入栈操作。

3. 适用场景

  • 静态数据栈:如数据量固定的递归栈、调用栈等。

4. 示例代码(Java)

class ArrayStack {
    private int[] stack;
    private int top;       // 栈顶指针
    private int capacity;  // 栈的最大容量

    // 构造函数,初始化栈
    public ArrayStack(int size) {
        stack = new int[size];
        top = -1;  // 栈为空时,top 指向 -1
        capacity = size;
    }

    // 入栈操作
    public void push(int value) {
        if (isFull()) {
            throw new *Error("栈已满,无法入栈");
        }
        stack[++top] = value;
    }

    // 出栈操作
    public int pop() {
        if (isEmpty()) {
            throw new RuntimeException("栈为空,无法出栈");
        }
        return stack[top--];
    }

    // 返回栈顶元素,但不删除
    public int peek() {
        if (isEmpty()) {
            throw new RuntimeException("栈为空,无法查看栈顶");
        }
        return stack[top];
    }

    // 判断栈是否为空
    public boolean isEmpty() {
        return top == -1;
    }

    // 判断栈是否已满
    public boolean isFull() {
        return top == capacity - 1;
    }

    // 返回栈中元素个数
    public int size() {
        return top + 1;
    }
    
    // 打印栈元素
    public void display() {
        for (int i = 0; i <= top; i++) {
            System.out.print(stack[i] + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        ArrayStack stack = new ArrayStack(5);
        stack.push(10);
        stack.push(20);
        stack.push(30);
        stack.display(); // 输出:10 20 30

        System.out.println("栈顶元素: " + stack.peek()); // 输出:栈顶元素: 30
        stack.pop();
        stack.display(); // 输出:10 20
    }
}

二、链表实现栈

定义与实现原理

链表实现的栈使用链表的头节点作为栈顶。每次入栈时在链表头部插入一个新节点,每次出栈时删除链表的头节点。这种实现方式可以动态扩展栈的大小。

1. 优点

  • 动态大小:链表实现的栈大小可以动态增长,不受固定容量限制。
  • 内存管理灵活:在需要的时候才分配内存,适合大型数据或频繁变化的数据。

2. 缺点

  • 内存开销大:链表节点需要存储数据和指针,内存消耗较大。
  • 访问速度慢:链表节点在内存中不连续,访问时间相对较长。

3. 适用场景

  • 动态数据栈:如大量动态数据的进出,或内存受限且数据量不确定的场景。

4. 示例代码(Java)

class LinkedListNode {
    int data;
    LinkedListNode next;

    LinkedListNode(int data) {
        this.data = data;
        this.next = null;
    }
}

class LinkedListStack {
    private LinkedListNode top; // 栈顶指针

    // 构造函数,初始化栈
    public LinkedListStack() {
        top = null;
    }

    // 入栈操作
    public void push(int value) {
        LinkedListNode newNode = new LinkedListNode(value);
        newNode.next = top; // 新节点指向当前的栈顶
        top = newNode;      // 栈顶指针指向新节点
    }

    // 出栈操作
    public int pop() {
        if (isEmpty()) {
            throw new RuntimeException("栈为空,无法出栈");
        }
        int value = top.data;
        top = top.next; // 栈顶指针指向下一个节点
        return value;
    }

    // 返回栈顶元素,但不删除
    public int peek() {
        if (isEmpty()) {
            throw new RuntimeException("栈为空,无法查看栈顶");
        }
        return top.data;
    }

    // 判断栈是否为空
    public boolean isEmpty() {
        return top == null;
    }

    // 返回栈中元素个数
    public int size() {
        int count = 0;
        LinkedListNode current = top;
        while (current != null) {
            count++;
            current = current.next;
        }
        return count;
    }

    // 打印栈元素
    public void display() {
        LinkedListNode current = top;
        while (current != null) {
            System.out.print(current.data + " ");
            current = current.next;
        }
        System.out.println();
    }

    public static void main(String[] args) {
        LinkedListStack stack = new LinkedListStack();
        stack.push(10);
        stack.push(20);
        stack.push(30);
        stack.display(); // 输出:30 20 10

        System.out.println("栈顶元素: " + stack.peek()); // 输出:栈顶元素: 30
        stack.pop();
        stack.display(); // 输出:20 10
    }
}

总结对比

实现方式 优点 缺点 适用场景
数组实现栈 访问速度快,适合小规模、静态数据的栈 固定大小,可能导致溢出 数据量固定、性能要求高的场景,如递归栈
链表实现栈 动态大小,适合大量动态数据 内存消耗较大,访问速度稍慢 数据量不确定且变化频繁的场景,如动态堆栈
  • 数组实现的栈:适合数据量固定或可以预估的情况,比如递归函数调用栈。
  • 链表实现的栈:适合大量数据动态进出,或不确定数据量的情况,比如模拟浏览器前进后退操作栈。
上一篇:协程1 --- 发展历史


下一篇:商业数据库 - oracle -表空间