首页 > 其他 > 详细

数据结构-02 |栈 |队列| 双端队列| 优先队列

时间:2020-06-14 16:52:34      阅读:78      评论:0      收藏:0      [点我收藏+]

 

 栈Stack |队列Queue| 双端队列Deque| 优先队列PriorityQueue

堆栈和队列特点:
1. Stack - First In Last Out(FILO) 先入后出,先进来的被压入栈底
    .Array or Linked List
2. Queue - First In First Out(FIFO) 排队时先来先出
    .Array or Doubly Linked List

1. 栈Stack 

 Stack中文名可叫堆栈,不能叫堆,堆是heap

手写栈堆比较少了,很多语言在标准库都实现了。

1.1 概念与特性 

  栈 Stack -先入后出的容器结构

  技术分享图片

 

栈:查询  O(n),平均情况,要看它栈中栈底的元素,要清空了才能看到。

       插入和删除它的栈顶元素只需要一次性操作,时间复杂度是O(1)。

1.2 如何实现一个“栈”?

            从刚才栈的定义里,我们可以看出,栈主要包含两个操作,入栈和出栈,也就是在栈顶插入一个数据和从栈顶删除一个数据。理解了栈的定义之后,我们来看一看如何用代码实现一个栈。

实际上,栈既可以用数组来实现,也可以用链表来实现。

用数组实现的栈,叫作顺序栈,用链表实现的栈,叫作链式栈

// 基于数组实现的顺序栈
public class ArrayStack {
    private String[] items;  // 数组
    private int count;       // 栈中元素个数
    private int n;           // 栈的大小
    // 初始化数组,申请一个大小为 n 的数组空间
    public ArrayStack(int n) {
        this.items = new String[n];
        this.n = n;
        this.count = 0;
    }
    // 入栈操作
    public boolean push(String item) {
        // 数组空间不够了,直接返回 false,入栈失败。
        if (count == n) return false;
        // 将 item 放到下标为 count 的位置,并且 count 加一
        items[count] = item;
        ++count;
        return true;
    }
    // 出栈操作
    public String pop() {
        // 栈为空,则直接返回 null
        if (count == 0) return null;
        // 返回下标为 count-1 的数组元素,并且栈中元素个数 count 减一
        String tmp = items[count - 1];
        --count;
        return tmp;
    }
}

不管是顺序栈还是链式栈,我们存储数据只需要一个大小为 n 的数组就够了。在入栈和出栈过程中,只需要一两个临时变量存储空间,所以空间复杂度是 O(1)。

    注意,这里存储数据需要一个大小为 n 的数组,并不是说空间复杂度就是 O(n)。因为,这 n 个空间是必须的,无法省掉。所以我们说空间复杂度的时候,是指除了原本的数据存储空间外,算法运行还需要额外的存储空间。

空间复杂度分析是不是很简单?时间复杂度也不难。不管是顺序栈还是链式栈,入栈、出栈只涉及栈顶个别数据的操作,所以时间复杂度都是 O(1)。

1.3 栈的应用

如何基于栈实现浏览器的前进和后退功能?

         当你依次访问完一串页面 a-b-c 之后,点击浏览器的后退按钮,就可以查看之前浏览过的页面 b 和 a。当你后退到页面 a,点击前进按钮,就可以重新查看页面 b 和 c。但是,如果你后退到页面 b 后,点击了新的页面 d,那就无法再通过前进、后退功能查看页面 c 了。

如何实现浏览器的前进、后退功能?其实,用两个栈就可以非常完美地解决这个问题。

我们使用两个栈,X 和 Y,我们把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。当我们点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。

后进者先出,先进者后出,这就是典型的“栈”结构。

        从栈的操作特性上来看,栈是一种“操作受限”的线性表,只允许在一端插入和删除数据。

事实上,从功能上来说,数组或链表确实可以替代栈,但你要知道,特定的数据结构是对特定场景的抽象,而且,数组或链表暴露了太多的操作接口,操作上的确灵活自由,但使用时就比较不可控,自然也就更容易出错。

当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,我们就应该首选“栈”这种数据结构

 

① 栈在函数调用中的应用,经典的一个应用场景就是函数调用栈

           操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。

② 栈在表达式求值中的应用

我们再来看栈的另一个常见的应用场景,编译器如何利用栈来实现表达式求值

为了方便解释,我将算术表达式简化为只包含加减乘除四则运算,比如:34+13*9+44-12/3。

  技术分享图片

③ 栈在括号匹配中的应用

假设表达式中只包含三种括号,圆括号 ()、方括号 [] 和花括号{},并且它们可以任意嵌套。比如,{[{}]}或 [{()}([])] 等都为合法格式,而{[}()] 或 [({)] 为不合法的格式。那我现在给你一个包含三种括号的表达式字符串,如何检查它是否合法呢?

这里也可以用栈来解决。我们用栈来保存未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号。如果能够匹配,比如“(”跟“)”匹配,“[”跟“]”匹配,“{”跟“}”匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。

当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明有未匹配的左括号,为非法格式。

  思考

内存中的堆栈和数据结构堆栈不是一个概念,可以说内存中的堆栈是真实存在的物理区数据结构中的堆栈是抽象的数据存储结构
        内存空间在逻辑上分为三部分:代码区、静态数据区和动态数据区,动态数据区又分为栈区和堆区。
代码区:存储方法体的二进制代码。高级调度(作业调度)、中级调度(内存调度)、低级调度(进程调度)控制代码区执行代码的切换。
静态数据区:存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。系统自动分配和回收。
栈区:存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。
堆区:new一个对象的引用或地址存储在栈区,指向该对象存储在堆区中的真实数据。

栈的应用场景

1)  子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。      

2)   处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。

3)   表达式的转换与求值(实际解决)。

4)  二叉树的遍历。

5)  图形的深度优先(depth一first)搜索法。

 

  1) 用数组模拟栈的使用,由于栈是一种有序列表,当然可以使用数组的结构来储存栈的数据内容,
    下面我们就用数组模拟栈的出栈,入栈等操作。

  2) 实现思路分析,并画出示意图

代码实现:

技术分享图片
import scala.io.StdIn

object ArrayStackDemo {
  def main(args: Array[String]): Unit = {
    //测试栈
    val stack = new ArrayStack(4)
    var key = ""

    while (true) {
      println("list: 显示栈")
      println("push: 入栈")
      println("pop: 出栈")
      println("peek: 查看栈顶")
      key = StdIn.readLine()
      key match {
        case "list" => stack.list()
        case "push" =>
          println("请输入一个数")
          val value = StdIn.readInt()
          stack.push(value)
        case "pop" =>
          val res = stack.pop()
          if(res.isInstanceOf[Exception]) {
            println(res.asInstanceOf[Exception].getMessage)
          }else{
            printf("取出栈顶的元素是%d", res)
          }
        case "peek" =>
          val res = stack.peek()
          if(res.isInstanceOf[Exception]) {
            println(res.asInstanceOf[Exception].getMessage)
          }else{
            printf("栈顶的元素是%d", res)
          }
          //默认处理

      }
    }


  }
}

//ArrayStack 表示栈
class ArrayStack(arrMaxSize:Int) {
  var top = -1
  val maxSize = arrMaxSize
  val arr = new Array[Int](maxSize)

  //判断栈空
  def isEmpty(): Boolean = {
    top == -1
  }
  //栈满
  def isFull(): Boolean = {
    top == maxSize - 1
  }

  //入栈操作
  def push(num:Int): Unit = {
    if(isFull()) {
      println("栈满,不能加入")
      return
    }
    top += 1
    arr(top) = num
  }

  //出栈操作
  def pop(): Any = {
    if(isEmpty()) {
      return new Exception("栈空,没有数据")
    }
    val res = arr(top)
    top -= 1
    return res
  }

  //遍历栈
  def list(): Unit = {
    if(isEmpty()) {
      println("栈空")
      return
    }
    //使用for
    for(i <- 0 to top reverse) { //逆序打印
      printf("arr(%d)=%d\n", i, arr(i))
    }
    println()
  }

  //查看栈的元素是什么,但是不会真的把栈顶的数据弹出
  def peek(): Any = {
    if(isEmpty()) {
      return  new Exception("栈空")
    }
    return arr(top)
  }

}
View Code

2. 队列

2.1 概念和特性 

队列,排队,先来先出,依次排队。技术分享图片

 栈:查询  O(n),平均情况,要看它栈中栈底的元素,要清空了才能看到。

       插入和删除它的栈顶元素只需要一次性操作,时间复杂度是O(1)。

队列:与栈类似, 查询是O(n),插入和删除是O(1)。

  查询操作O(n),因为它是元素无序的 ,就必须把这个数据结构遍历一遍

把它想象成排队买票,先来的先买,后来的人只能站末尾,不允许插队。先进者先出,这就是典型的“队列

我们知道,栈只支持两个基本操作:入栈 push()出栈 pop()

队列跟栈非常相似,支持的操作也很有限,最基本的操作也是两个:

  入队 enqueue(),放一个数据到队列尾部;出队 dequeue(),从队列头部取一个元素。

                       技术分享图片

所以,队列跟栈一样,也是一种操作受限的线性表数据结构

作为一种非常基础的数据结构,队列的应用也非常广泛,特别是一些具有某些额外特性的队列,比如循环队列、阻塞队列、并发队列。它们在很多偏底层系统、框架、中间件的开发中,起着关键性的作用。比如高性能队列 Disruptor、Linux 环形缓存,都用到了循环并发队列;Java concurrent 并发包利用 ArrayBlockingQueue 来实现公平锁等。

2.2 顺序队列和链式队列

队列跟栈一样,也是一种抽象的数据结构。它具有先进先出的特性,支持在队尾插入元素,在队头删除元素,那究竟该如何实现一个队列呢?

跟栈一样,队列可以用数组来实现,也可以用链表来实现。用数组实现的栈叫作顺序栈,用链表实现的栈叫作链式栈。

同样,用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列

  1)  队列是一个有序列表,可以用数组或是链表来实现。数据结构就是研究数据组织形式,并且为算法打基础。

  2)  遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出

  3) 示意图:(使用数组模拟队列示意图)

  4)  线性结构,一对一关系

  • 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下 其中 maxSize 是该队列的最大容量。
  • 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front及 rear分别记录队列前后端的下标,front 会随着数据输出而改变,而 rear则是随着数据输入而改变,如图所示:

          技术分享图片

数组模拟队列

rear 表示队列尾,rear 指向队列的最后一个元素

front 表示队列的头,表示指向队列的第一个元素的前一个位置

  • 当我们将数据存入队列时称为”addQueue”,addQueue 的处理需要有两个步骤:思路分析

1) 将尾指针往后移:rear+1 , front == rear 【队列空】

2) 若尾指引 rear 小于队列的最大下标 MaxSize-1,则将数据存入 rear所指的数组元素中,否则无法存入数据。 rear  == MaxSize - 1[队列满]

 代码实现:

技术分享图片
object ArrayQueueDemo {
  def main(args: Array[String]): Unit = {
    //创建一个队列对象
    val queue = new ArrayQueue(3)
    var key = "" //接收用户的输入
    //简单写个菜单
    while (true) {
      println("show : 显示队列")
      println("add: 添加数据到队列")
      println("get: 从队列头取出元素")
      println("peek: 查看队列头元素")
      key = StdIn.readLine()
      key match {
        case "show" => queue.showQueue()
        case "add" =>
          println("请输入一个数吧")
          val value = StdIn.readInt()
          queue.addQueue(value)
        case "get" =>
          val res = queue.getQueue()//取出
          //判断res 的类型
          if(res.isInstanceOf[Exception]) {
            //输出异常信息
            println(res.asInstanceOf[Exception].getMessage)
          }else {
            printf("取出的队列头元素是%d", res)
          }
        case "peek" =>
          val res = queue.peek()//查看
          //判断res 的类型
          if(res.isInstanceOf[Exception]) {
            //输出异常信息
            println(res.asInstanceOf[Exception].getMessage)
          }else {
            printf("队列头元素是%d", res)
          }
      }
    }
  }
}

//定义一个类ArrayQueue 表队列
//该类会实现队列的相关方法
//数据结构 创建-遍历-测试-修改-删除
class ArrayQueue(arrMaxSize: Int) {
  val maxSize = arrMaxSize
  val arr = new Array[Int](maxSize) //队列的数据存放的数组
  var front = -1 // 表示指向队列的第一个元素的前一个位置
  var rear = -1 //表示指向队列的最后个元素

  //查看队列的头元素,但是不取出
  def peek(): Any = {
    if(isEmpty()) {
      return new Exception("队列空,没有数据返回");
    }
    return arr(front + 1)
  }

  //判断队列是否满
  def isFull(): Boolean = {
    rear == maxSize - 1
  }

  //判断队列是空
  def isEmpty(): Boolean = {
    rear == front
  }

  //从队列中取出数据
  //异常时可以加入业务逻辑
  def getQueue(): Any = {
    if(isEmpty()) {
      return new Exception("队列空,没有数据返回");
    }
    //将front 后移一位
    front += 1
    return arr(front)
  }

  //给队列添加数据
  def addQueue(num: Int): Unit = {
    if (isFull()) {
      println("队列满,不能加入!!")
      return
    }
    //添加时调整rear
    //1. 先将rear 后移
    rear += 1
    arr(rear) = num
  }

  //遍历队列
  def showQueue(): Unit = {
    if (isEmpty()) {
      println("队列空!!")
      return
    }

    for (i <- (front + 1) to rear) {
      printf("arr(%d)=%d\t", i, arr(i))
    }
    println()
  }

}
View Code

数组模拟环形队列

  • 对前面的数组模拟队列的优化,充分利用数组. 因此将数组看做是一个环形的。(通过取模的方
    式来实现即可)
  • 分析说明(思路):

  1) 尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定,这个在做判断队列满的时候需要注意 (rear + 1) % maxSize == front 满] rear == front [空]

  2) 测试示意图:

  3) 代码实现

技术分享图片
object CircleArrayQueueDemo {
  def main(args: Array[String]): Unit = {

    //创建一个队列对象
    val queue = new CircleArrayQueue(4)
    var key = "" //接收用户的输入
    //简单写个菜单
    while (true) {
      println("show : 显示队列")
      println("add: 添加数据到队列")
      println("get: 从队列头取出元素")
      println("peek: 查看队列头元素")
      key = StdIn.readLine()
      key match {
        case "show" => queue.showQueue()
        case "add" =>
          println("请输入一个数吧")
          val value = StdIn.readInt()
          queue.addQueue(value)
        case "get" =>
          val res = queue.getQueue()//取出
          //判断res 的类型
          if(res.isInstanceOf[Exception]) {
            //输出异常信息
            println(res.asInstanceOf[Exception].getMessage)
          }else {
            printf("取出的队列头元素是%d", res)
          }
        case "peek" =>
          val res = queue.peek()//查看
          //判断res 的类型
          if(res.isInstanceOf[Exception]) {
            //输出异常信息
            println(res.asInstanceOf[Exception].getMessage)
          }else {
            printf("队列头元素是%d", res)
          }
      }
    }


  }
}

//定义一个类CircleArrayQueue 表环形队列
//该类会实现队列的相关方法
//数据结构 创建-遍历-测试--删除
class CircleArrayQueue(arrMaxSize: Int) {
  val maxSize = arrMaxSize
  val arr = new Array[Int](maxSize) //队列的数据存放的数组
  var front = 0 // front 初始化 = 0 , front 约定 指向队列的头元素
  var rear = 0 // rear 初始化为 = 0, rear 指向队列的 最后元素的后一个位置, 因为需要空出一个位置做约定

  //判断队列是否满
  def isFull(): Boolean = {
    (rear + 1) % maxSize == front
  }
  //判断队列是空,和前面一样
  def isEmpty(): Boolean = {
    rear == front
  }


  //从队列中取出数据
  //异常时可以加入业务逻辑
  def getQueue(): Any = {
    if(isEmpty()) {
      return new Exception("队列空,没有数据返回");
    }
    //返回front指向的值
    //1. 先把  arr(front) 保存到一个临时变量
    //2. front += 1
    //3. 返回临时变量
    var temp = arr(front)
    front = (front + 1) % maxSize
    return temp
  }

  //查看队列的头元素,但是不取出
  def peek(): Any = {
    if(isEmpty()) {
      return new Exception("队列空,没有数据返回");
    }
    return arr(front)
  }

  //给队列添加数据
  def addQueue(num: Int): Unit = {
    if (isFull()) {
      println("队列满,不能加入!!")
      return
    }
    //先把数据放入到arr(rear) 位置,然后后移rear
    arr(rear) = num
    rear = (rear + 1) % maxSize
  }

  //遍历队列
  def showQueue(): Unit = {
    if (isEmpty()) {
      println("队列空!!")
      return
    }
    //动脑筋
    //1. 从front 开始打印 , 打印几个元素
    for (i <- front until (front + size()) ) {
      printf("arr(%d)=%d\t", (i % maxSize) , arr(i % maxSize))
    }
    println()
  }

  //求出当前队列共有多少个数据
  def size(): Int = {
    (rear + maxSize - front) % maxSize
  }

}
View Code

2.3 队列的应用

队列在线程池等有限资源池中的应用

     CPU 资源是有限的,任务的处理速度与线程个数并不是线性正相关。相反,过多的线程反而会导致 CPU 频繁切换,处理性能下降。所以,线程池的大小一般都是综合考虑要处理任务的特点和硬件环境,来事先设置的。

当我们向固定大小的线程池中请求一个线程时,如果线程池中没有空闲资源了,这个时候线程池如何处理这个请求?是拒绝请求还是排队请求?各种处理策略又是怎么实现的呢?

3. 双端队列 Deque: Double -End- Queue

 可理解为一个queue和stack的结合体,一个简单的队列,但它的头 和 尾都可以进行元素的出和进

  技术分享图片

   插入和删除都是 O(1) 操作,查询是O(n) ;

4. 优先队列 PriorityQueue 

  插入O(1)

  取出 O(logN) -- 按照元素的优先级取出;(取出相比栈和队列变慢了,好处是它的顺序不再是FIFO或者FILO,而是按照元素的优先级取出,VIP先行)

  底层具体实现的数据结构较为多样和复杂:堆heap、 二叉搜索树bst、 treap

 

数据结构-02 |栈 |队列| 双端队列| 优先队列

原文:https://www.cnblogs.com/shengyang17/p/13124538.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!