单向队列、双端队列、栈的模型实现

引言

自己实现简单的队列、栈的逻辑结构。

队列都包含头和尾两个指针,简单的单向队列只能在一端(如:head端)入列,在另一端(如:tail 端)出列;双端队列可以在 head 进出,也可以在 tail 进出。

栈的模型更加简单,它分为 top 和 bottom 两个指针,只能在 top 端进出。

经典的模型结构包含几个主要方法,进、出、isEmpty、size 等。

一、队列的实现

不论是单向队列还是双端队列,始终都是在操作 head 和 tail 两个指针。它们的实现没有什么本质区别,实际上,掌握了双端队列,单向队列就易如反掌。

在手动实现队列的时候,需要在脑海中模拟出队列的构造,左侧为 head ,右侧为 tail:

因此,队列中一定需要两个特殊的成员变量:head 和 tail。

其次,由于队列和栈都属于逻辑结构,因此,我们必须考虑使用哪种底层数据结构来实现,一般有两个最典型的结构可供使用:双向链表、数组。

1.1 双端队列的实现

首先,我们需要敲定队列中的节点的数据结构,双向链表是个不错的选择,它不受空间的约束,这是相对于数组实现的一个明显的有点。

经典的双向链表结构

    private static class Node<T> {public T value;private Node<T> prev;private Node<T> next;public Node(T value) {this.value = value;}@Overridepublic String toString() {return value.toString();}}

建议把Node设计为私有静态内部类,作为队列内部的存储对象 T 的容器,它不需要被外界感知。

双端队列需要四个进出方法:头进,头出,尾进,尾出。

头进为例,当接收到一个入列对象时,我们需要先封装为 Node 类型的 cur ,然后判断队列是否为空,即是否 head == null,如果为空,那么当前元素即是 head 也是 tail:

if (head == null) {// 如果是第一个元素,则头尾都指向curhead = cur;tail = cur;
}

如果不为空,那么,需要建立 cur 与 head 之间的链表关系,即 cur.next = head,head.prev = cur。然后head角色变为 cur,就完成了入列操作:

else {// 如果不是第一个元素// cur在前,head在后,建立链表关系cur.next = head;head.prev = cur;// head更新head = cur;
}

再以尾出为例,返回值为泛型 T,首先同样是要判断队列是否为空(head == null?),如果空,则直接返回null。若存在元素,首先我们要拿到 tail 元素,将它保存下来。

然后关键要判断是否队列中仅存在一个元素,即 head == tail,如果只有一个元素,那么取出后需要将 head 和 tail 都变为 null:

if (head == tail) {head = null;tail = null;
}

若不是最后一个,那么和入列相反,我们需要斩断 Node 之间的引用关系,刚刚我们已经将原来的 tail 并保存到了一个新的 Node 局部变量里,此时已经可以将其成功返回,但除此之外,还需要释放一些指针空间,并改变 tail 的指向:

else {// tail 前移tail = tail.prev;// 释放空间cur.prev = null;tail.next = null;
}
return cur.value;

剩下的方法逻辑大同小异,一定不要搞混 Node 节点的 prev 和 next,与 head 和 tail,明确并牢记这些概念的含义,以及链表左头右尾的方向,入列时要建立两个节点之间的对应关系,出列时要切断两个节点之间的关系。以下是完整代码示例

/*** 使用链表实现的双端队列*/
public class MyDoubleEndsQueue<T> {private Node<T> head;private Node<T> tail;private int size = 0;private static class Node<T> {T value;Node<T> prev;Node<T> next;public Node(T value) {this.value = value;}@Overridepublic String toString() {return value.toString();}}public void addFromHead(T value) {size++;Node<T> cur = new Node<T>(value);if (head == null) {// 如果是第一个元素,则头尾都指向curhead = cur;tail = cur;} else {// 如果不是第一个元素// cur在前,head在后,建立链接关系cur.next = head;head.prev = cur;// head更新head = cur;}}public void addFromTail(T value) {size++;Node<T> cur = new Node<>(value);if (tail == null) {head = cur;tail = cur;} else {tail.next = cur;cur.prev = tail;tail = cur;}}public T popFromHead() {if (head == null) return null;size--;Node<T> cur = head;if (head == tail) {// 只有一个元素head = null;tail = null;} else {head = head.next;cur.next = null;head.prev = null;}return cur.value;}public T popFromTail() {if (tail == null) return null;size--;Node<T> cur = tail;if (head == tail) {head = null;tail = null;} else {// tail 前移tail = tail.prev;// 释放空间cur.prev = null;tail.next = null;}return cur.value;}public boolean isEmpty() {return head == null;}public int size() {return size;}
}

1.2 单向队列

1.2.1 双向链表实现

单向队列的实现可以建立在双端队列的基础之上,直接调用其方法,或使用相同的逻辑来完成实现,完整代码如下:

/*** 使用链表实现的单向队列*/
public class MyQueue<T> {private Node<T> head;private Node<T> tail;private int size = 0;private static class Node<T> {Node<T> prev;T value;Node<T> next;public Node(T value) {this.value = value;}@Overridepublic String toString() {return value.toString();}}/*** 头进** @param t*/public void push(T t) {Node<T> cur = new Node<>(t);size++;if (isEmpty()) {head = cur;tail = cur;} else {head.prev = cur;cur.next = head;head = cur;}}/**** 尾出* @return*/public T pop() {if (isEmpty()) return null;size--;Node<T> cur = tail;if (head == tail) {head = null;tail = null;} else {tail = tail.prev;cur.prev = null;tail.next = null;}return cur.value;}public boolean isEmpty() {return head == null;}public int size() {return size;}
}

1.2.2 数组实现队列

数组实现队列相对复杂一些,由于数组是定长结构,因此,需要在创建之初设定大小,且这一大小如果不涉及扩容,在使用过程中是不可变更的。

另一方面,由于数组的大小被限定,因此,必须循环使用数组空间,如果数组满了再添加元素,以及数组空了再取元素,这两种情况都需要报错。

为了不陷入头尾追赶的怪圈逻辑,在使用数组实现队列时,需要特别小心,不要去考虑收尾追击的问题,只需要通过 size 来控制,首尾自然不会出现“交叉”的情况。

/*** 环形数组队列*/
public class RingArrayQueue<T> {private T[] arr;private int pushIdx;private int popIdx;private int size;private final int limit;public RingArrayQueue(Class<T> type, int limit) {this.arr = (T[]) Array.newInstance(type, limit);this.pushIdx = 0;this.popIdx = 0;this.size = 0;this.limit = limit;}public void push(T t) {if (size == limit)throw new RuntimeException("队列满了,不能再加了");size++;arr[pushIdx] = t;pushIdx = nextIndex(pushIdx);}public T pop() {if (size == 0)throw new RuntimeException("队列空了!");size--;T t = (T) arr[popIdx];popIdx = nextIndex(popIdx);return t;}public boolean isEmpty() {return size == 0;}public int size() {return size;}// 如果现在的下标是i,返回下一个位置private int nextIndex(int i) {return i < limit - 1 ? i + 1 : 0;}
}

二、栈的实现

有了双端队列的逻辑铺垫,栈的实现完全可以照搬,完整代码如下:

/*** 使用链表实现的栈*/
public class MyStack<T> {private Node<T> top;private Node<T> bottom;private int size = 0;private static class Node<T> {Node<T> prev;T value;Node<T> next;public Node(T value) {this.value = value;}@Overridepublic String toString() {return value.toString();}}public void push(T t) {Node<T> cur = new Node<>(t);if (isEmpty()) {top = cur;bottom = cur;} else {top.next = cur;cur.prev = top;top = cur;}}public T pop() {if (isEmpty()) {return null;}size--;Node<T> cur = top;if (top == bottom) {top = null;bottom = null;} else {top = top.prev;top.next = null;cur.prev = null;}return cur.value;}public boolean isEmpty() {return top == null;}public int size() {return size;}
}

三、测试

使用jdk自带的数据结构来对照测试自定义的队列和栈,完整代码如下:

import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;public class Checker {public static void main(String[] args) {int oneTestDataNum = 100;int value = 10000;int testTimes = 100000;for (int i = 0; i < testTimes; i++) {MyStack<Integer> myStack = new MyStack<>();MyQueue<Integer> myQueue = new MyQueue<>();// 对照组Stack<Integer> stack = new Stack<>();Queue<Integer> queue = new LinkedList<>();for (int j = 0; j < oneTestDataNum; j++) {int nums = (int) (Math.random() * value);if (stack.isEmpty()) {myStack.push(nums);stack.push(nums);} else {if (Math.random() < 0.5) {myStack.push(nums);stack.push(nums);} else {if (!isEqual(myStack.pop(), stack.pop())) {System.out.println("oops!");}}}int numq = (int) (Math.random() * value);if (stack.isEmpty()) {myQueue.push(numq);queue.offer(numq);} else {if (Math.random() < 0.5) {myQueue.push(numq);queue.offer(numq);} else {if (!isEqual(myQueue.pop(), queue.poll())) {System.out.println("oops!");}}}}}System.out.println("finish!");}public static boolean isEqual(Object o1, Object o2) {if (o1 == null) {return o1 == o2;}return o1.equals(o2);}
}

四、实现一个查找最小值的时间复杂度是 O(1) 的栈

设计思路:使用一个单独的栈,用于存储数据栈的最小值,在每次push 的时候,判断是否为最小值,如果是,则如 minStack,如果不是,则继续压入上一次的最小值。完整代码如下:

import java.util.Stack;public class GetMinStack {public static class MyMinStack {private Stack<Integer> dataStack;private Stack<Integer> minStack;public MyMinStack() {this.dataStack = new Stack<>();this.minStack = new Stack<>();}public void push(Integer num) {if (minStack.isEmpty()) {minStack.push(num);} else if (getMin() >= num) {minStack.push(num);} else {minStack.push(getMin());}dataStack.push(num);}public Integer pop() {if (dataStack.isEmpty()) {return null;}minStack.pop();return dataStack.pop();}public Integer getMin() {if (minStack.isEmpty())return null;return minStack.peek();}}public static void main(String[] args) {MyMinStack stack1 = new MyMinStack();stack1.push(3);System.out.println(stack1.getMin());stack1.push(4);System.out.println(stack1.getMin());stack1.push(1);System.out.println(stack1.getMin());System.out.println(stack1.pop());System.out.println(stack1.getMin());}}

五、只使用栈实现队列

只使用栈实现队列和只使用队列实现栈在面试中还比较常见,例如,用栈来实现一个图的宽度优先遍历,或用队列来实现图的深度优先遍历,但图的宽度优先遍历一定需要用队列来实现,而深度优先遍历也一定要用栈来实现,因此,我们可以通过栈与队列的转化来完成类似这样的面试题。

使用栈来实现队列的思路是,用两个栈,分别存储入栈的顺序,和出栈的顺序,因为栈的出栈顺序是入栈时的反向,因此,只要两个栈互倒就可以实现队列的结构。

互倒是一种思路,还有一种思路是,只从push 栈往 pop 栈倒,这样的方式可以减少一定的性能开销,但需要保证两个原则:1、如果决定要倒数据,必须一定倒完;2、只要pop为空时,才可以倒入。

以下是两种实现代码,第一个是互倒的方式,第二个是单向倒入的方式:

import java.util.Stack;/*** 使用两个栈来实现队列*/
public class TwoStackQueue<T> {private Stack<T> pushStack;private Stack<T> popStack;public TwoStackQueue() {this.pushStack = new Stack<>();this.popStack = new Stack<>();}private synchronized void reverseStack(Stack<T> from, Stack<T> to) {while (!from.isEmpty()) {to.push(from.pop());}}public void push(T t) {if (!popStack.isEmpty()) {reverseStack(popStack, pushStack);}pushStack.push(t);}public T pop() {if (!pushStack.isEmpty())reverseStack(pushStack, popStack);if (!popStack.isEmpty())return popStack.pop();elsereturn null;}
}
import java.util.Stack;/*** 单向倒入双栈队列*/
public class SingleDirTwoStackQueue<T> {private Stack<T> pushStack;private Stack<T> popStack;public SingleDirTwoStackQueue() {pushStack = new Stack<>();popStack = new Stack<>();}private void pushToPop() {// 两点原则:// 1、pop栈必须为空时才倒入if (popStack.empty()) {// 2、要倒就一次性全倒完while (!pushStack.empty()) {popStack.push(pushStack.pop());}}}public void push(T t) {pushStack.push(t);pushToPop();}public T pop() {if (popStack.empty() && pushStack.empty()) {return null;}pushToPop();return popStack.pop();}public T peek() {if (popStack.empty() && pushStack.empty()) {return null;}pushToPop();return popStack.peek();}public static void main(String[] args) {SingleDirTwoStackQueue<Integer> queue = new SingleDirTwoStackQueue<>();for (int i = 0; i < 100; i++) {// 多存少取if (Math.random() < 0.7) {queue.push(i);} else {System.out.print(queue.pop() + "\t");}}System.out.println("\n==================");while (true) {Integer tail = queue.pop();if (tail == null) break;System.out.print(tail + "\t");}}
}

六、只使用队列实现栈

使用经典的单向队列实现栈结构。

思路是,使用两个队列,masterQueue和slaveQueue,放入的时候,正常放入,当要取出的时候,循环将元素从master放入到slave中,最后只留 1 个元素,并返回,然后将 slave 与 master 的引用互换。

完整代码如下:

import java.util.LinkedList;
import java.util.Queue;/*** 双队列栈*/
public class TwoQueueStack<T> {private Queue<T> masterQueue;private Queue<T> slaveQueue;public TwoQueueStack() {this.masterQueue = new LinkedList<>();this.slaveQueue = new LinkedList<>();}public void push(T t) {masterQueue.offer(t);}public T poll() {while (masterQueue.size() > 1)slaveQueue.offer(masterQueue.poll());T ans = masterQueue.poll();Queue<T> tmp = masterQueue;masterQueue = slaveQueue;slaveQueue = tmp;return ans;}public T peek() {while (masterQueue.size() > 1)slaveQueue.offer(masterQueue.poll());T ans = masterQueue.poll();// peek 与 poll的区别就是取出后,再次放回slaveQueue.offer(ans);Queue<T> tmp = masterQueue;masterQueue = slaveQueue;slaveQueue = tmp;return ans;}}

 

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

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

相关文章

递归算法及其时间复杂度分析

引言 “递归” 一词是比较专业的计算机术语&#xff0c;在现实生活中&#xff0c;有一个更可爱的词——“套娃”。如果把“递归算法”叫做“套娃算法”&#xff0c;或许可以减少一些恐惧程度。 套娃是有限的&#xff0c;同样&#xff0c;递归也是有限的&#xff0c;这和我们经…

算法设计中的基础常用代码

引言 本篇博客旨在记录一些基础算法知识的常见组合用法&#xff0c;以及何时使用&#xff0c;需要注意的问题等&#xff0c;长期更新。 为什么要这样总结呢&#xff1f;难道掌握了位运算、常用算法工具API的定义还不够吗&#xff1f; 这是因为某些知识比如 &、 |、 ~、 …

Redis —— 常用命令一览

引言 参考《菜鸟教程 Redis 常用命令》&#xff0c;其中红色为极其重要&#xff0c;蓝色为重要。 一、总览 二、key相关命令 三、String 相关命令 四、Hash 相关命令 五、List 相关命令 六、Set 相关命令 七、ZSet 相关命令

Redis 实用技术——消息发布和订阅

引言 发布订阅模型是redis的重要功能&#xff0c;它可以像网站动态一样&#xff0c;将消息发送到多个订阅者的主页里。 一、常用命令 二、消息格式 消息是一个有三个元素的多块响应&#xff1a; 如上图&#xff0c;发布者向 mysub 频道发送了一条消息&#xff0c;redis会返回…

Redis 实用技术——事务

引言 redis的事务不像关系型数据库的事务那样完整。 “快”是redis的特征&#xff0c;在事务管理的过程中&#xff0c;使用muti命令开启事务块&#xff0c;当输入多条命令后&#xff0c;再使用exec命令执行事务块中的全部命令。 Redis事务可以保证两件事&#xff1a; 1、隔…

排序算法——归并排序的相关问题

一、小和问题 问题描述&#xff0c;给定一个数组&#xff0c;如[1, 3, 2, 6, 5]&#xff0c;计算每个数左边小于自己的所有数的和&#xff0c;并累加。例如&#xff1a; 1左边没有数 3左边有一个小于自己的数 1 2左边有一个小于自己的数 1 6左边有三个小于自己的数 1 3 2 6…

经典数据结构——堆的实现

一、完全二叉树 堆是一种完全二叉树&#xff0c;什么是完全二叉树&#xff1f; 简单的说&#xff0c;一棵满二叉树表示的是所有节点全部饱和&#xff0c;最后一层全部占满&#xff1a; 而完全二叉树指的是满二叉树的最后一层&#xff0c;所有叶子节点都从左往顺序排满&#x…

排序算法 —— 堆排序

引言 此文基于《经典数据结构——堆的实现》中堆结构&#xff0c;实现一个以堆处理排序的算法。 一、算法思想 基于堆结构的堆排序的算法思想非常简单&#xff0c;循环获取大根堆中的最大值&#xff08;0位置的根节点&#xff09;放到堆的末尾&#xff0c;直到将堆拿空。 由…

经典数据结构——前缀树

引言 前缀树——trie /ˈtraɪ//树&#xff0c;也叫作“单词查找树”、“字典树”。 它属于多叉树结构&#xff0c;典型应用场景是统计、保存大量的字符串&#xff0c;经常被搜索引擎系统用于文本词频统计。它的优点是利用字符串的公共前缀来减少查找时间&#xff0c;最大限度…

排序算法 —— 计数排序

引言 计数排序是桶排序思想的一种具体实现&#xff0c;针对一些具有特殊限制的样本数据&#xff0c;如公司员工年龄&#xff0c;那么样本数据本身就一定在0~200之间&#xff0c;针对这样的数据&#xff0c;使用从0到200 的桶数组&#xff0c;桶的位置已经是有序的&#xff0c;…

Java多线程 —— 线程状态迁移

引言 线程状态迁移&#xff0c;又常被称作线程的生命周期&#xff0c;指的是线程从创建到终结需要经历哪些状态&#xff0c;什么情况下会出现哪些状态。 线程的状态直接关系着并发编程的各种问题&#xff0c;本文就线程的状态迁移做一初步探讨&#xff0c;并总结在何种情况下…

Java中的Unsafe

Java和C语言的一个重要区别就是Java中我们无法直接操作一块内存区域&#xff0c;不能像C中那样可以自己申请内存和释放内存。Java中的Unsafe类为我们提供了类似C手动管理内存的能力。 Unsafe类&#xff0c;全限定名是sun.misc.Unsafe&#xff0c;从名字中我们可以看出来这个类对…

arm中断保护和恢复_浅谈ARM处理器的七种异常处理

昨天的文章&#xff0c;我们谈了ARM处理器的七种运行模式&#xff0c;分别是&#xff1a;用户模式User(usr)&#xff0c;系统模式System(sys)&#xff0c;快速中断模式(fiq)&#xff0c;管理模式Supervisor(svc)&#xff0c;外部中断模式(irq)&#xff0c;数据访问中止模式Abor…

Queue —— JUC 的豪华队列组件

目录引言一、Queue 的继承关系1.1 Queue 定义基础操作1.2 AbstractQueue 为子类减负1.3 BlockingQueue 阻塞式Queue1.4 Deque 两头进出二、Queue 的重要实现三、BlockingQueue 的实现原理四、Queue 在生产者消费者模式中的应用五、Queue 在线程池中的应用六、ConcurrentLinkedQ…

daad转换器实验数据_箔芯片电阻在高温应用A/D转换器中的应用

工业/应用领域高温&#xff1a;地震数据采集系统、石油勘探监测、高精度检测仪产品采用&#xff1a;V5X5 Bulk Metal (R) Foil芯片电阻案例介绍TX424是一个完整的4通道24位模数转换器&#xff0c;采用40脚封装。该设计采用最先进设计方案&#xff0c;两个双通道24位调节器和一个…

excel分段排序_学会这个神操作,报表填报不再五花八门,效率远超Excel

在报表工作人员的的日常工作中&#xff0c;常常要面临统计混乱的终端用户输入的问题。由于无法准确限制用户的输入内容&#xff0c;所以在最终进行数据统计时&#xff0c;常常会出现数据不合法的情况。为此需要花费大量的人力和时间核对校验数据。举个简单的例子&#xff0c;某…

IDEA——必备插件指南

目录一、Free-Mybatis-Plugin二、Lombok三、jclasslib Bytecode Viewer一、Free-Mybatis-Plugin 二、Lombok 三、jclasslib Bytecode Viewer 学习 class 文件的必备插件。 使用简单&#xff0c;安装后可以在菜单 View 中看到 show bytecode with jclasslib&#xff1a; 效果…

jitter 如何优化网络_如何做好关键词优化网络?

越来越多的传统企业开始建立自己的网站&#xff0c;进而不断的推广自己的产品。为了能够让自己的企业网站出现在搜索引擎的首页&#xff0c;现在最常用的手段就是竞价排名和关键词优化网络。往往很多企业会选择关键词优化网络这种方式来推广自己的网站&#xff0c;对于新手seoe…

python学生名片系统_Python入门教程完整版400集(懂中文就能学会)快来带走

如何入门Python&#xff1f;权威Python大型400集视频&#xff0c;学了Python可以做什么&#xff1f;小编今天给大家分享一套高老师的python400集视频教程&#xff0c;里面包含入门进阶&#xff0c;源码&#xff0c;实战项目等等&#xff0c;&#xff0c;不管你是正在学习中&…

JVM——详解类加载过程

导航一、过程概述二、Loading2.1 类加载器2.2 双亲委派机制2.3 类在内存中的结构三、Linking四、Initializing一、过程概述 java 源文件编译后会生成一个 .class文件存储在硬盘上。 在程序运行时&#xff0c;会将用到的类文件加载到 JVM 内存中。从磁盘到内存的过程总共分为三…