数据结构与算法(三):栈与队列

news2024/10/6 20:25:34

上一篇《数据结构与算法(二):线性表》中介绍了数据结构中线性表的两种不同实现——顺序表与链表。这一篇主要介绍线性表中比较特殊的两种数据结构——栈与队列。首先必须明确一点,栈和队列都是线性表,它们中的元素都具有线性关系,即前驱后继关系。

一、栈

1、基本概念

栈(也称下压栈,堆栈)是仅允许在表尾进行插入和删除操作的线性表。我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)。栈是一种后进先出(Last In First Out)的线性表,简称(LIFO)结构。栈的一个典型应用是在集合中保存元素的同时颠倒元素的相对顺序。

抽象数据类型:

栈同线性表一样,一般包括插入、删除等基本操作。其基于泛型的API接口代码如下:

public interface Stack<E> {

    //栈是否为空
    boolean isEmpty();
    //栈的大小
    int size();
    //入栈
    void push(E element);
    //出栈
    E pop();
    //返回栈顶元素
    E peek();
}

栈的实现通常有两种方式:

  • 基于数组的实现(顺序存储)
  • 基于链表的实现(链式存储)

2、栈的顺序存储结构

栈的顺序存储结构其实是线性表顺序存储结构的简化,我们可以简称它为「顺序栈」。其存储结构如下图:

在这里插入图片描述

实现代码如下:

import java.util.Iterator;
/**
 * 能动态调整数组大小的栈
 */
public class ArrayStack<E> implements Iterable<E>, Stack<E> {

    private E[] elements;
    private int size=0;
    
    @SuppressWarnings("unchecked")
    public ArrayStack() {
        elements = (E[])new Object[1]; //注意:java不允许创建泛型数组
    }
    
    @Override public int size() {return size;}
    
    @Override public boolean isEmpty() {return size == 0;}

    //返回栈顶元素
    @Override public E peek() {return elements[size-1];}

    //调整数组大小
    public void resizingArray(int num) {
        @SuppressWarnings("unchecked")
        E[] temp = (E[])new Object[num];
        for(int i=0;i<size;i++) {
            temp[i] = elements[i];
        }
        elements = temp;
    }

    //入栈
    @Override public void push(E element) {
        if(size == elements.length) {
            resizingArray(2*size);//若数组已满将长度加倍
        }
        elements[size++] = element;
    }

    //出栈
    @Override public E pop() {
        E element = elements[--size];
        elements[size] = null;     //注意:避免对象游离
        if(size > 0 && size == elements.length/4) {
            resizingArray(elements.length/2);//小于数组1/4,将数组减半
        }
        return element;
    }

    //实现迭代器, Iterable接口在java.lang中,但Iterator在java.util中
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            private int num = size;
            public boolean hasNext() {
                return num > 0;
            }
            public E next() {
                return elements[--num];
            }
        };
    }

    //测试
    public static void main(String[] args) {
        int[] a = {1,2,3,4,new Integer(5),6};//测试数组
        ArrayStack<Integer> stack = new ArrayStack<Integer>();
        System.out.print("入栈顺序:");
        for(int i=0;i<a.length;i++) {
            System.out.print(a[i]+" ");
            stack.push(a[i]);
        }
        System.out.println();
        System.out.print("出栈顺序数组实现:");
        //迭代
        for (Integer s : stack) {
            System.out.print(s+" ");
        }
    }
}

优点:

  • 每项操作的用时都与集合大小无关
  • 空间需求总是不超过集合大小乘以一个常数

缺点:

  • push()和pop()操作有时会调整数组大小,这项操作的耗时和栈的大小成正比

3、两栈共享空间

用一个数组来存储两个栈,让一个栈的栈底为数组的始端,即下标为0,另一个栈的栈底为数组的末端,即下标为 n-1。两个栈若增加元素,栈顶都向中间延伸。其结构如下:

在这里插入图片描述

这种结构适合两个栈有相同的数据类型并且空间需求具有相反的关系的情况,即一个栈增长时另一个栈缩短。如,买股票,有人买入,就一定有人卖出。

代码:

public class SqDoubleStack<E> {

    private static final int MAXSIZE = 20;
    private E[] elements;
    private int leftSize=0;
    private int rightSize= MAXSIZE - 1;
    
    //标记是哪个栈
    enum EnumStack {LEFT, RIGHT}

    @SuppressWarnings("unchecked")
    public SqDoubleStack() {
        elements = (E[])new Object[MAXSIZE]; //注意:java不允许创建泛型数组
    }
    

    //入栈
    public void push(E element, EnumStack es) {

        if(leftSize - 1 == rightSize)
            throw new RuntimeException("栈已满,无法添加"); 
        if(es == EnumStack.LEFT) {
            elements[leftSize++] = element;
        } else {
            elements[rightSize--] = element;
        }
    }

    //出栈
    public E pop(EnumStack es ) {

        if(es == EnumStack.LEFT) {
            if(leftSize <= 0)
                throw new RuntimeException("栈为空,无法删除"); 
            E element = elements[--leftSize];
            elements[leftSize] = null;     //注意:避免对象游离
            return element;
        } else {
            if(rightSize >= MAXSIZE - 1)
                throw new RuntimeException("栈为空,无法删除"); 
            E element = elements[++rightSize];
            elements[rightSize] = null;     //注意:避免对象游离
            return element;
        }
    }

    //测试
    public static void main(String[] args) {
        int[] a = {1,2,3,4,new Integer(5),6};//测试数组
        SqDoubleStack<Integer> stack = new SqDoubleStack<Integer>();
        System.out.print("入栈顺序:");
        for(int i=0;i<a.length;i++) {
            System.out.print(a[i]+" ");
            stack.push(a[i], EnumStack.RIGHT);
        }
        System.out.println();
        System.out.print("出栈顺序数组实现:");
        //迭代
        for(int i=0;i<a.length;i++) {
            System.out.print(stack.pop(EnumStack.RIGHT)+" ");
        }
    }
}

4、栈的链式存储结构

栈的链式存储结构,简称链栈。为了操作方便,一般将栈顶放在单链表的头部。通常对于链栈来说,不需要头结点。

其存储结构如下图:

在这里插入图片描述

代码实现如下:

import java.util.Iterator;
public class LinkedStack<E> implements Stack<E>, Iterable<E> {
    private int size = 0;
    private Node head = null;//栈顶

    private class Node {
        E element;
        Node next;
        Node(E element, Node next) {
            this.element = element;
            this.next = next;
        }
    }

    @Override public int size() {return size;}

    @Override public boolean isEmpty() {return size == 0;}

    @Override public E peek() {return head.element;}

    @Override public void push(E element) {
        Node node = new Node(element, head);
        head = node;
        size++;
    }

    @Override public E pop() {
        E element = head.element;
        head = head.next;
        size--;
        return element;
    }
    //迭代器
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            private Node current = head;

        public boolean hasNext() {
                return current != null;
            }

            public E next() {
                E element = current.element;
                current = current.next;
                return element;
            }
        };
    }

    public static void main(String[] args) {
        int[] a = {1,2,3,4,new Integer(5),6};//测试数组
        LinkedStack<Integer> stack = new LinkedStack<Integer>();
        System.out.print("入栈顺序:");
        for(int i=0;i<a.length;i++) {
            System.out.print(a[i]+" ");
            stack.push(a[i]);
        }
        System.out.println();
        System.out.print("出栈顺序链表实现:");
        for (Integer s : stack) {
            System.out.print(s+" ");
        }
    }
}

注意:私有嵌套类(内部类Node)的一个特点是只有包含他的类能够直接访问他的实例变量,无需将他的实例变量(element)声明为public或private。即使将变量element声明为private,外部类依然可以通过Node.element的形式调用。

优点:

  • 所需空间和集合的大小成正比
  • 操作时间和集合的大小无关
  • 链栈的push和pop操作的时间复杂度都为 O(1)。

缺点:

  • 每个元素都有指针域,增加了内存的开销。

顺序栈与链栈的选择和线性表一样,若栈的元素变化不可预料,有时很大,有时很小,那最好使用链栈。反之,若它的变化在可控范围内,使用顺序栈会好一些。

5、栈的应用——递归

栈的一个最重要的应用就是递归。那么什么是递归呢?借用《哥德尔、艾舍尔、巴赫——集异璧之大成》中的话:

递归从狭义上来讲,指的是计算机科学中(也就是像各位程序猿都熟悉的那样),一个模块的程序在其内部调用自身的技巧。如果我们把这个效果视觉化就成为了「德罗斯特效应」,即图片的一部分包涵了图片本身。

如下面这张图,「先有书还是先有封面 ?」
在这里插入图片描述

我们把一个直接调用自身或通过一系列语句间接调用自身的函数,称为递归函数。每个递归函数必须至少有一个结束条件,即不在引用自身而是返回值退出。否则程序将陷入无穷递归中。

一个递归的例子:斐波那契数列(Fibonacci)

在这里插入图片描述

递归实现:

public int fibonacci(int num) {
    if(num < 2)
        return num == 0 ? 0 : 1;
    return fibonacci(num - 1) + fibonacci(num - 2);
}

迭代实现:

public int fibonacci(int num) {
    if(num < 2)
        return num == 0 ? 0 : 1;
    int temp1 = 0;
    int temp2 = 1;
    int result = 0;
    for(int i=2; i < num; i++) {
        result = temp1 + temp2;
        temp1 = temp2;
        temp2 = result;
    }
    return result;
}

迭代与递归的区别:

  • 迭代使用的是循环结构,递归使用的是选择结构。
  • 递归能使程序的结构更清晰、简洁,更容易使人理解。但是大量的递归将消耗大量的内存和时间。

编译器使用栈来实现递归。在前行阶段,每一次递归,函数的局部变量、参数值及返回地址都被压入栈中;退回阶段,这些元素被弹出,以恢复调用的状态。

二、队列

1、基本概念

队列是只允许在一端进行插入操作,在另一端进行删除操作的线性表。它是一种基于先进先出(First In First Out,简称FIFO)策略的集合类型。允许插入的一端称为队尾,允许删除的一端称为队头。

抽象数据类型:

队列作为一种特殊的线性表,它一样包括插入、删除等基本操作。其基于泛型的API接口代码如下:

public interface Queue<E> {

    //队列是否为空
    boolean isEmpty();

    //队列的大小
    int size();

    //入队
    void enQueue(E element);

    //出队
    E deQueue();
}

同样的,队列具有两种存储方式:顺序存储和链式存储。

2、队列的顺序存储结构

其存储结构如下图:

在这里插入图片描述

与栈不同的是,队列元素的出列是在队头,即下表为0的位置。为保证队头不为空,每次出队后队列中的所有元素都得向前移动,此时时间复杂度为 O(n)。此时队列的实现和线性表的顺序存储结构完全相同,不在详述。

若不限制队列的元素必须存储在数组的前n个单元,出队的性能就能大大提高。但这种结构可能产生「假溢出」现象,即数组末尾元素已被占用,如果继续向后就会产生下标越界,而前面为空。如下图:

在这里插入图片描述

解决「假溢出」的办法就是若数组未满,但后面满了,就从头开始入队。我们把这种逻辑上首尾相连的顺序存储结构称为循环队列。

数组实现队列的过程:

在这里插入图片描述

假设开始时数组长度为5,如图,当f入队时,此时数组末尾元素已被占用,如果继续向后就会产生下标越界,但此时数组未满,将从头开始入队。当数组满(h入队)时,将数组的长度加倍。

代码如下:

import java.util.Iterator;
/**
 * 能动态调整数组大小的循环队列
 */
public class CycleArrayQueue<E> implements Queue<E>, Iterable<E> {
    private int size; //记录队列大小
    
    private int first; //first表示头元素的索引
    private int last; //last表示尾元素后一个的索引
    private E[] elements;

    @SuppressWarnings("unchecked")
    public CycleArrayQueue() {
        elements = (E[])new Object[1];
    }
    
    @Override public int size() {return size;}
    @Override public boolean isEmpty(){return size == 0;}

    //调整数组大小
    public void resizingArray(int num) {
        @SuppressWarnings("unchecked")
        E[] temp = (E[])new Object[num];
        for(int i=0; i<size; i++) {
            temp[i] = elements[(first+i) % elements.length];
        }
        elements = temp;
        first = 0;//数组调整后first,last位置
        last =  size;
    }

    @Override public void enQueue(E element){
        //当队列满时,数组长度加倍
        if(size == elements.length) 
            resizingArray(2*size);
        elements[last] = element;
        last = (last+1) % elements.length;//【关键】
        size++;
    }
    
    @Override public E deQueue() {
        if(isEmpty()) 
            return null;
        E element = elements[first];
        first = (first+1) % elements.length;//【关键】
        size--;
        //当队列长度小于数组1/4时,数组长度减半
        if(size > 0 && size < elements.length/4) 
            resizingArray(2*size);
        return element;
    }

    //实现迭代器
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            private int num = size;
            private int current = first;
            public boolean hasNext() {
                return num > 0;
            }
            public E next() {
                E element = elements[current++];
                num--;
                return element;
            }                
        };
    }

    public static void main(String[] args) {
        int[] a = {1,2,4,6,new Integer(10),5};
        CycleArrayQueue<Integer> queue = new CycleArrayQueue<Integer>();

        for(int i=0;i<a.length;i++) {
            queue.enQueue(a[i]);
            System.out.print(a[i]+" ");
        }    
        System.out.println("入队");

        for (Integer s : queue) {
            System.out.print(s+" ");
        }
        System.out.println("出队");
    }
}

3、队列的链式存储结构
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们简称为「链队列」。

存储结构如下图:

在这里插入图片描述

代码如下:

import java.util.Iterator;
/**
 * 队列的链式存储结构,不带头结点的实现
 */
public class LinkedQueue<E> implements Queue<E>, Iterable<E> {
    private Node first; //头结点
    private Node last; //尾结点
    private int size = 0;

    private class Node {
        E element;
        Node next;
        Node(E element) {
            this.element = element;
        }
    }
    
    @Override public int size() {return size;}
    @Override public boolean isEmpty(){return size == 0;}

    
    //入队
    @Override public void enQueue(E element) {
        Node oldLast = last;
        last = new Node(element);
        if(isEmpty()) {
            first = last;//【要点】
        }else {
            oldLast.next = last;
        }
        size++;
    }
    //出队
    @Override public E deQueue() {
        E element = first.element;
        first = first.next;
        size--;
        if(isEmpty()) 
            last = null;//【要点】
        return element;
    }
    //实现迭代器
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            private Node current = first;

            public boolean hasNext() {
                return current != null;
            }

            public E next(){
                E element = current.element;
                current = current.next;
                return element;
            }
        };
    }

    public static void main(String[] args) {
        int[] a = {1,2,4,6,new Integer(10),5};
        LinkedQueue<Integer> queue = new LinkedQueue<Integer>();

        for(int i=0;i<a.length;i++) {
            queue.enQueue(a[i]);
            System.out.print(a[i]+" ");
        }    
        System.out.println("入队");

        for (Integer s : queue) {
            System.out.print(s+" ");
        }
        System.out.println("出队");
    }
}

循环队列与链队列,它们的基本操作的时间复杂度都为 O(1)。和线性表相同,在可以确定队列长度的情况下,建议使用循环队列;无法确定队列长度时使用链队列。

三、总结

栈与队列,它们都是特殊的线性表,只是对插入和删除操作做了限制。栈限定仅能在栈顶进行插入和删除操作,而队列限定只能在队尾插入,在队头删除。它们都可以使用顺序存储结构和链式存储结构两种方式来实现。

对于栈来说,若两个栈数据类型相同,空间需求相反,则可以使用共享数组空间的方法来实现,以提高空间利用率。对于队列来说,为避免插入删除操作时数据的移动,同时避免「假溢出」现象,引入了循环队列,使得队列的基本操作的时间复杂度降为 O(1)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/363107.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

4、Redis安装

前言&#xff1a;工具下载地址阿里云盘&#xff1a;Redis&#xff1a;https://www.aliyundrive.com/s/qSYxRyXAxQy提取码: ne27一、Redis下载下载地址&#xff1a;https://github.com/tporadowski/redis/releases此处下载mis版本&#xff0c;安装版mis版直接安装成功就启动服务…

CCNP350-401学习笔记(451-500题)

451、what is the function of the LISP map resolver? A. to send traffic to non-LISP sites when connected to a service provider that does not accept nonroutable EIDs as packet sources B. to connect a site to the LISP-capabie part of a core network, publish …

数据挖掘,计算机网络、操作系统刷题笔记53

数据挖掘&#xff0c;计算机网络、操作系统刷题笔记53 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xff0c;orac…

2023年AI语音会议汇总

2023年&#xff0c;AI语音领域学术会议精彩纷呈&#xff0c;语音之家汇总了国内外重要的会议呈现给大家&#xff0c;大家可根据时间统筹安排好2023年的学术活动交流行程。如果信息有误&#xff0c;欢迎指正。 ICASSP 2023 2023 IEEE International Conference on Acoustics, S…

【操作系统】概述

操作系统 操作系统的定义与目标 定义&#xff1a;操作系统是控制管理计算机系统的硬软件&#xff0c;分配调度资源的系统软件。 目标&#xff1a;方便性&#xff0c;有效性&#xff08;提高系统资源的利用率、提高系统的吞吐量&#xff09;&#xff0c;可扩充性&#xff0c;…

node.js降低版本的方式(解决sass和node.js冲突)

一、概述 在使用vue的时候&#xff0c;会遇到项目的依赖&#xff08;package.json可以查看依赖信息&#xff09;下载失败的原因。因为项目用的依赖和要运行环境的node.js版本起了冲突&#xff0c;这个时候就要么更改项目的依赖版本&#xff0c;要么更改node.js的版本。本文示范…

11种主流生成式AI应用【2023】

生成式 AI 允许在几分钟内生成新颖逼真的视觉、文本和动画内容。 据 Gartner 称&#xff0c;到 2025 年&#xff0c;生成式 AI 生成的数据将占所有生成数据的 10%。 作为 2022 年最重要的战略技术趋势之一&#xff0c;人工智能 (AI) 的这一分支具有广泛的应用&#xff0c;适用…

提高转化率的 3 个客户引导最佳实践

如果您的试用客户没有转化为付费客户&#xff0c;或者您总体上正在努力解决试用到付费转化率&#xff0c;那么您来对地方了。本文的最终目标是向您展示一些可用于提高自己的激活率和整体试用到付费转化的最佳客户引导实践。SaaS公司目前生活在一个以产品为主导的增长时代。换句…

Tencent OS下逻辑卷(LVM)增加硬盘扩容

上一篇文章写了逻辑卷创建以及使用剩余空间为已经创建的逻辑卷扩容。 本篇是针对卷组空间已经用尽时的扩容方法。那就是增加硬盘。 首先我们为虚拟机增加硬盘/dev/sdd 使用fdisk为/dev/sdd分区,方法在上一篇文章已经描述,在此不再赘述。 新增的硬盘使用如下命令添加到卷组…

【日志框架-笔记】JUL日志框架的介绍及其使用

日志框架JUL日志框架入门程序Logger直接的父子继承关系Formatters独立的配置文件进行编写日志使用日志打印异常学习日志无非就是为了四大目的&#xff1a; 问题诊断&#xff0c;调试程序&#xff1b;错误定位&#xff0c;定位数据问题、网络问题、内存问题等错误位置&#xff…

【进阶教程】Appium自动化测试,太详细了!

目录Appium环境搭建Mac iOS环境搭建Appium基础Appium进阶Monkey的使用Copyadb shell monkey -p com.lqr.wechat -v 500 > monkey.logadb shell monkey -p com.lqr.wechat -vvv 5000 > monkey2.logadb shell monkey -p com.lqr.wechat -vvv 5000--send1556166765229 --thr…

pdb文件删除杂原子 HETATM;保留或去除水分子

删除杂原子 HETATM&#xff1b;去除水分子 with open(r"C:*****\StructA.pdb", "r") as inputFile,open(r"C*****8\StructA_remove_HETATM.pdb","w") as outFile:for line in inputFile:if not line.startswith("HETATM"):…

C++复习笔记13

多态&#xff1a;具体点就是去完成某个行为&#xff0c;当不同的对象去完成时会产生出不同的状态。&#xff08;同一个接口根据调用对象不同产生不同的行为&#xff09;。两个条件&#xff1a; 1. 必须通过基类的指针或者引用调用虚函数。 2. 被调用的函数必须是虚函数&#x…

ESP32 Arduino FAT文件系统详细使用教程

ESP32 Arduino FAT文件系统详细使用教程&#x1f4cc;参考1&#xff08;在 Linux环境下创建二进制文件&#xff09;&#xff1a;https://github.com/marcmerlin/esp32_fatfsimage&#x1f4cc;参考2 http://marc.merlins.org/perso/arduino/post_2019-03-30_Using-FatFS-FFat-o…

Scala函数式编程(第五章:函数基础、函数高级详解)

文章目录第 5 章 函数式编程5.1 函数基础5.1.1 函数基本语法5.1.2 函数和方法的区别5.1.3 函数定义5.1.4 函数参数5.1.5 函数至简原则&#xff08;重点&#xff09;5.2 函数高级5.2.1 高阶函数5.2.2 匿名函数5.2.3 高阶函数案例5.2.4 函数柯里化&闭包5.2.5 递归5.2.6 控制抽…

GooglePlay SSL Error Handler

应用上架GooglePlay 收到邮件提示 出现这个原因是因为我在app中使用webview加载Https的H5界面&#xff0c;在onReceivedSslError()中处理SslErrorHandler时&#xff0c;出现白屏现象&#xff0c;原因是webview默认在加载有证书验证的url时&#xff0c;会默认使用handler.cancel…

SPDK vhost target

SPDK vhost target主流的I/O设备虚拟化的方案1.virtio2.vhost加速1&#xff09;QEMU virtio-scsiQemu 架构2&#xff09;Kernel vhost-scsi3&#xff09;SPDK vhost-user-scsi3.SPDK vhost-scsi加速4.SPDK vhost-NVMe加速主流的I/O设备虚拟化的方案 纯软件模拟&#xff1a;完全…

vTESTstudio - VT System CAPL Functions - VT7001(续1)

vtsSetInterconnectionMode - 设置VT7001的电源输出模式功能&#xff1a;设置电源模块VT7001的三个可能电源和两个电源输出的互连模式。注意&#xff1a;此函数不能在任何CAPL处理程序例程或ECU节点中调用。它只能在测试模块的MainTest方法上下文中调用&#xff1b;使用在测量开…

企业为什么需要数据可视化报表

数据可视化报表是在商业环境、市场环境已经改变之后&#xff0c;发展出来为当前企业提供替代解决办法的重要方案。而且信息化、数字化时代&#xff0c;很多企业已经进行了初步的信息化建设&#xff0c;沉淀了大量业务数据&#xff0c;这些数据作为企业的资产&#xff0c;是需要…

Logstash:在 Logstash 管道中的定制的 Elasticsearch update by query

我们知道 Elasticsearch output plugin 为我们在 Logstash 的 pipeline 中向 Elasticsearch 的写入提供了可能。我们可以使用如下的格式向 Elasticsearch 写入数据&#xff1a; elasticsearch {hosts > ["https://localhost:9200"]index > "data-%{YYYY.M…