数据结构与算法分析学习笔记(四) 栈

简介: 软件世界是现实世界的投影。

引言

软件世界的一些概念大多都不是凭空创造,这些概念大多都是从现实世界抽象而来。就像我们本文所讨论的"栈"一样,日常生活中也有这样的模型,比如叠成一摞的碗,最先放置的反而放在最下面,像下面这样:image.png这与排队相反。排队对应的一种数据结构,我们称之为队列。事实上栈这种数据结构在日常使用中还是非常常用的,就比如说撤销操作,这是软件内普遍内置的操作,就像我们在日常编码中做了一些修改,想回到上一步,我们一般就是使用ctrl + z。软件按时间记录下了你的操作或者是输入,像下面这样:image.png时间最靠前的反而在最下面,这样我们就能吃后悔药了。在比如我们写代码中的括号匹配,如果少写了一个,编译器是如何知道的呢?我们来分析一下这个问题:image.png编号为括号的进入顺序, 每个右括号将与最近遇到的那个未匹配的左括号相匹配,即"后进的先出, 或者先进的后出"。image.png体会一下上面的话,每个右括号将与最近遇到的那个未匹配的左括号相匹配。我们用p记录左括号的最新位置,用q指向当前新进入的括号,将p与q相比对,如果相匹配,则删除左括号,p后退一位。如果匹配到最后还是有剩余的左括号那么这个括号就是不匹配的。

再比如我们常用的函数互相调用,Java总是从一个main函数开始,如果我们的代码是main方法调用a方法,a方法里面又调用c方法,c方法里面又调用d方法,那么显而易见的是d方法执行完毕之后再执行c方法,c方法执行完才执行a方法。这种执行模型也很像栈:image.png仔细体会上面的用例,这些问题的共同的特点就是处理过程中都具有"后进先出"的特性,这是一类典型的问题,我们将其抽象出来也就是数据结构中的"栈"模型。下面开始讲解一些栈的概念和一些基本应用。

概述

下面是栈的一种示意图:image.png你可以将示意图中的长方体理解为一个竹筒,最上面开口,最下面封闭,里面放的是写有编号的小球,放入顺序依据小球顺序,从小到大。如果我们想从这个竹筒中取球,可以发现一种规律: 先放进去的只能后拿出来; 反之,后放进去的小球能够先拿出来,也就是先进后出,这是栈的典型特点。

当我们把上面结构中的小球抽象为数据结构中的结点时,因为结点间的关系是线性的,因此它也属于线性表,又由于它的操作只能在同一端进行,因此它是一个运算受限的线性表,我们将这种类型线性表称之为栈。

现在我们给出栈的定义: 栈是一种特殊的线性表,它所有的插入和删除都限制在表的同一端进行。栈中允许允许进行插入、删除操作的一段叫栈顶(top),另一端则叫栈底(bottom)。当栈中没有元素时,称之为空栈

栈中的插入运算我们通常称之为压栈、进栈或入栈(PUSH),栈的删除运算通常被称为弹栈(push)出栈(pop). 第一个进栈的元素在栈底,最后一个进栈的元素在栈顶,第一个出栈的元素为栈顶元素,最后一个出栈的元素为栈底元素。

可能会有人觉得为什么要引入栈,或者直接用数组和链表实现栈不就行了吗?为什么要专门划出一种数据结构来?  原则上我们后面讲的队列、树、图也都是用数组和链表来实现,每一种数据结构都对应了一类专门的数据处理问题,像栈专门处理"先进后出",同时也简化了程序设计思路,缩小了我们的思考范围。就算是用数组来实现栈,我们也得让外部调用者无需担心下标越界以及增减的问题,那么这就是一种新的数组,只不过我们习惯上用栈来称呼而已。

同线性表基本类似,栈的基本运算如下:

  • 栈初始化操作:  建立一个空栈表
  • 栈判空:  判断栈是否为空
  • 取栈顶元素: 取栈顶元素的值
  • 压栈:  在栈S中插入元素e, 使其成为新的栈顶元素
  • 弹栈:  删除栈S的元素。

现在的许多高级语言,比如说Java、C#都内置了对栈数据结构的封装,我们可以不用关注栈的实现细节,而直接使用对栈的push和Pop的操作。同时也可以借鉴设计思路。

用数组实现的栈我们称之为顺序栈,用链表实现的栈我们一般就称之为链栈。关于顺序栈其实也可以再进行细分,一种是一开始就给定栈的大小,入栈超出大小的时候我们可以选择将栈底的上一个元素称为栈底元素。第二种我们称之为动态栈,栈的大小是动态的,也就是动态扩容,超出栈的上限的时候,自动扩容,栈的大小基本取决于内存的大小。

顺序栈

我们尝试用数组来实现以下顺序栈,其实顺序栈相关的设计也可以参考Java中的相关的设计思路,我们来看下Java中的Stack:image.png操作也与我们上面讨论的一致。那peek和pop有什么区别:

  • peek  取栈顶的元素不删除
  • pop    取栈顶的元素删除并返回栈顶元素 Java中Stack的设计思路:
  • 栈判空 看数组的实际容量
  • push操作   将元素添加到数组的末尾
  • peek 操作  返回数组最后一个元素
  • pop 操作    返回数组的最后一个元素, 同时栈顶元素向后移动一个位置 我们也可以让我们建的顺序栈继承我们在数据结构与算法分析(三) 线性表SequenceList,这样扩容操作我们就不用在重写一遍,只用实现栈的方法就可以了。关于方法的设计我们可以完全借鉴Java中的Stack:
public class SequenceStack extends SequenceList {
    public SequenceStack() {
    }
    public int push(int data){
        add(data);
        return data;
    }
    /**
     * 返回数组的最后一个元素, 同时栈顶元素向后移动一个位置
     * @return
     */
    public int pop(){
        if (size() == 0){
            // 抛出异常
        }
        int data = peek();
        remove(size() - 1);
        return data;
    }
    public int peek(){
        if (size() == 0){
            // 抛出异常
        }
        return  get(size()  - 1);
    }
    public boolean empty(){
        return size() == 0;
    }
}

链式栈

采用链式存储方式的栈我们称之为链栈或链式栈。链栈有一个个结点构成,每个结点包含数据数据域和指针域两部分。在链式栈中,利用每一个结点的存储域存储栈中的每一个元素,利用指针域来表示元素之间的关系。像下面这样:image.png

public class LinkedStack {
    private Node top; //指向栈顶元素
    private class Node{
        private int data;
        private Node next;
        public Node(int data , Node next){
            this.data = data;
            this.next = next;
        }
        public int getData() {
            return data;
        }
        public Node getNext() {
            return next;
        }
    }
    public LinkedStack() {
    }
    /**
     * @param data
     * @return
     */
    public int push(int data){
        Node node = new Node(data,top);
        top = node;
        return data;
    }
    public int pop(){
        return peek();
    }
    public int peek(){
        int data = top.data;
        top = top.next;
        return data;
    }
    public boolean empty() {
        return top == null;
    }
}

栈的应用举例

数制转换

十进制数到R进制数的转换,我们一般采用的方法是十进制数和进制取余,最高位往往是最后才出现,刚好符合后入先出的栈。例如1024 转8进制:image.png最高位最后出现, 但栈刚好是后进先出。进制转换示例:

/**
     * 进制转换
     * @param input
     * @param decimal
     */
    private static void binaryConversion(int input, int decimal) {
        if(decimal == 0){
            return;
        }
        Stack<Integer> stack = new Stack<>();
        while (input != 0){
            stack.push(input % decimal);
            input = input /  decimal;
        }
        String result =  "";
        while (!stack.empty()){
            result = result + stack.pop();
        }
        System.out.println(result);
    }
相关文章
|
27天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
123 9
|
18天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
23 1
|
5天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
26 5
|
21天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
72 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
24天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
26天前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
47 4
|
2月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
34 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
1月前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
2月前
初步认识栈和队列
初步认识栈和队列
61 10