栈和队列比较简单,而且实用性非常广泛,这里主要介绍一下他们的概念和实现,在很多算法中,栈和队列的运用很重要,因此,虽然简单确是最重要的数据结构之一,必须重视。

栈是保证元素后进先出(后存入者先使用,Last In First Out,LIFO)的关系结构。

队列是保证元素先进先出(先存入者先使用,First In First Out,FIFO)的关系结构。

栈的顺序表实现

class  

顺序表操作的后端插入和删除操作都是o(1)操作。

栈的链接表实现:

顺序表的操作时间复杂度为o(1),那么为什么还要考虑链接表?因为顺序表扩大存储区需要做一次高代价的操作,另外顺序表需要完整的大块存储区。采用链接表实现可以缓解这两个缺陷,但是链接表也有自身的缺点:更多依赖于解释器的存储管理,每个结点的链接开销以及链接结点在实际计算机内存中的任意散布可能带来操作开销。

采用链接表技术,自然是表头一端作为栈顶,表尾作为栈底。

class  

栈和递归

这里要介绍一下栈和递归的关系,我们平时用的递归算法,其内部原理就是运用了栈。

递归:在一个函数定义中引用了被定义的对象(被定义的函数本身)。

在递归的结构中,递归的部分必须比原来的整体简单,这样才有可能达到某种终点(递归定义的出口),显然,终点不能是递归的。递归结构中必须包含非递归的基本结构构成的部分,否则就会出现无限递归。

例如阶乘函数n!,其递归的代码实现:

def 

现在来解析这一递归结构,表述其于栈之间的关系:

  • 为了得到factor(n)的结果,必须先计算出factor(n-1)。
  • 递归调用计算出factor(n-1)时还需要乘n,以得到factor(n)的结果,说明在递归调用factor(n-1)时,参数n的值需要被记录,同样计算factor(n-1)调用factor(n-2)时,也需要记录这个调用之前参数n-1的记录,向下继续...
  • 显然需要记录的数据的量与递归的层数成正比,一般而言没有数量上的上限,不能用几个整形变量来保存。
  • 在这一系列调用中保存的数据,如n,n-1..较后保存的将先被使用,因为函数返回的顺序与调用顺序相反,后进入的层次先返回。

这种后进先出的使用方式和数据项数的无明确限制,就说明需要用一个栈来支持递归函数的实际运行,这个栈称为程序运行栈

下图表示的是阶乘函数factor(3)的是如何递归计算的。

第一个图表示函数调用factor(3)开始执行的状态,参数3压入栈。随后执行调用factor(2),参数2压入栈,直到factor(0)压入栈。然后开始逐一返回,其中factor(0)直接返回1(递归出口),factor(1)同样返回1*factor(0),计算得到factor值为1,factor(2)返回2factor(1)=2,factor(3)返回3*factor(2)=6。

递归的实现依赖于运行栈,对递归函数的每次调用都在栈上开辟一块区域,称为函数帧,函数的执行总以栈顶的帧作为当前帧。所有局部变量都在这里体现。当函数从下一层递归调用中返回时,函数的上一层执行取得下层函数调用得到的结果,执行系统弹出已经结束的调用对应的帧,然后回到调用前的那一层执行时的状态。

函数的嵌套调用秉持着“先调用后返回”的规则,函数调用时的内部动作分为两个部分:在进入新的函数调用前需要保存一些信息,退出一次函数调用时需要恢复调用前的状态。这被称为函数调用的前序动作和后序动作。

函数调用的前序动作包括:

  • 为被调用函数的局部变量和形式参数分配存储区(称为函数帧/活动记录/数据区)
  • 将所有实参和函数的返回地址存入函数帧(实参和形参的结合/传值)
  • 将控制转到被调用函数入口

函数调用的后序动作包括:

  • 将被调用函数的计算结果存入指定位置
  • 释放被调用函数的存储区
  • 按以前保存的返回地址将控制转回调用函数

函数的调用是有代价的,在得到程序代码的模块化和语义清晰性等优势的同时,可能会付出执行时间的代价。

递归与非递归:

将一个递归定义的函数变成非递归的形式,可以自己建立一个栈来模拟程序运行栈:

def  

任何一个递归定义的函数都可以通过引入一个栈保存中间结果的方式,翻译为一个非递归的过程。与此对应的是,任何一个包含循环的程序都可以翻译为一个不包括循环的递归函数。

栈的应用:简单背包问题(动态规划思想)

其求解算法用递归的方式描述很简单,但是通过自己管理一个栈来存储中间信息定义非递归算法,则比较复杂。

问题描述:一个背包里可放入重量为weight的物品,现有n件物品的集合S,其中物品的重量分别为w0,w1...wn-1。问题是能否从中选出若干件物品,其重量之和正好等于weight。如果存在就说这一背包有解,否则无解。

问题的求解:假设weight>=0,n>=0。用记法knap(weight,n)表示n件物品相对于总重量weight的背包问题,在考虑它是否有解时,通过考虑一件物品的选或者不选,可以把问题分为两种情况

  • 如果不选最后一件物品(wn-1),那么knap(weight,n-1)的解也就是knap(weight,n)的解,如果找到前者的解也就找到了后者的解。
  • 如果选择最后一件物品,那么knap(weight-wn-1,n-1)有解,其解加上最后一件物品就是knap(weight,n)的解,即前者有解后者也有解。

定义递归出口:

  • 重量weight已经等于0,说明问题有解
  • 重量weight已经小于0,由于不断归结中所需的重量递减,有可能出现这种情况,这说明按照已做的安排不能得到一个解。
  • 重量大于0但已经没有物品可用,说明按照这种安排无解。
def  

函数三个参数分别是总重量weight,各物体重量表wlist和物品数量n,前两个if处理三种简单情况,返回True或False(递归出口),后两种情况通过递归调用得到结果。

队列

队列的链接表实现:

带有首指针的单链表其插入操作enqueue()为o(1)操作,但是在另一端的去除操作dequeue()确实o(n)操作。那么为了实现首尾两端的插入和删除操作都为o(1)操作,则需要首尾指针都有的单链表。

class  

队列的list实现:

基于顺序表实现队列的困难:出队操作如果在首端操作,取出当时的元素后,需要将后面的元素全部前移需要o(n)时间。如果是从尾端弹出元素,时间是o(1)操作,但是尾端插入时间为o(n),因此无论何种方式进行插入和删除,都避免不了o(n)的操作。另一种可能是队首的元素弹出后,后面的元素不前移,但记住新表头元素的位置,这样做会带来另一个问题,队首的空位会越变越多,造成空间的浪费。

有一种想法:如果入队时队尾已经达到存储区的末尾,应该考虑转到存储区开始的位置去入队新元素。

循环顺序表:

  • 在队列使用中,顺序表的开始位置并不改变,上图是一个包含8个位置的表,例如变量q.elems始终指向表元素区开始。
  • 队头变量q.head记录当前队列里第一个元素的位置(图中位置4),队尾变量q.rear记录当前队列里最后元素之后的第一个空位(图中位置1)。
  • 队列元素保存在顺序表的一段连续单元,用python的写法是[q.head:q.rear],左闭右开区间里。图中有5个元素,从位置4到位置0的一段。两个变量的值之差(取模存储区长度)就是队列里的元素个数。

初始时队列为空,q.head和q.rear取相同值,表示顺序表里的为空,出队和入队操作时需要更新q.head和q.rear

q

如下图所示,如果队列再存一个元素,q.rear和q.head就会相同,这与判断队列为空时一样,产生冲突。事实上这种状态就看成了队列满,即定义其条件为(q.rear+1)%q.len=q.head。当队列满时又需要继续插入元素的话,就需要更换存储区更大的空间。

循环队列无法直接利用python的list里的自动存储扩充机制:队列元素的存储方式与list元素的默认存储方式不一致,list元素总是在存储区的最前面一段,而队列的元素可能是表里的任意一段,有时还分为头尾两段,如果list自动扩充,其中的队列元素就有可能失控,另一方面list没提供检查存储区容量的机制,队列操作中无法判断系统何时扩容。

考虑其基本设计:

  • 定义队列名SQueue对象里有一个list类型的成分_elems存放队列元素。
  • 分别考虑两个属性head_num表示队首元素所在位置以及表中元素的个数。
  • 用python的list作为队列存储区。需要检查当前的表是否已满,必要时换一个存储表,因此要记录当前表的长度,下面用属性_len。

数据不变式:实现一种数据结构时,最基本的问题是这些操作需要维护对象属性之间的正确关系。

  • 所有构造对象的操作,都必须把对象成分设置为满足数据不变式的状态,也就是说对象的初始状态要满足数据不变式。
  • 每个对象操作都应该保证不破坏数据不变式,也就是说如果对一个状态完好的对象应用一个操作,该操作完成时,还必须保证对象处于完好的状态。

下面队列实现中,考虑数据不变式:

  • elems属性引用着队列的元素存储区,它是一个list对象,_len属性记录存储区的有效容量。
  • _head是队列的首元素的下标,_num始终记录着队列中元素的个数。
  • 当时队列里的元素总保存在elems里从head开始的连续位置中,新入队的元素存入由_head+_num算出的位置,但如果需要把元素存入下标_len的位置时,改为在下标0位置存入该元素。
  • 在_num==_len的情况下出现入队操作,就扩大存储区。
class  

双端队列:

允许两端插入和删除元素,因此功能覆盖以上所有结构,作为效率要求很高的结构,这里要求两端插入和删除操作时间复杂度均为o(1)。双链表结构可以实现两端的常量时间插入和删除操作,可以实现双端队列。python中的collections库中定义了一种deque类型双端队列,支持元素两端的插入和删除。deque采用一种双链表技术实现,每个链接结点里顺序存储一组元素。

顺序表与链表的计算机内存分配问题:

对于现在计算机内存机制,如果连续进行的一批内存访问是局部的,操作速度就会块得多。人们在考虑效率的同时,一个重要线索就是尽可能使计算机内存的使用局部化python中顺序表是局部化的代表,在可能的情况下应该尽量使用。链接结构的一个特点是,其中的结点可能在内存中任意分配即使程序顺着链接逐个访问结点,在内存层面的表现通常也是在许多位置随机的单元之间跳来跳去因此链接表再带来灵活性的同时,效率上可能有明显的付出这些情况就是需要考虑顺序表实现的最重要原因。另外在其它的一些语言中,建立顺序表结构还可以避免复杂的存储管理。


参考书籍:《数据结构与算法—python语言描述》—裘宗燕

2.2基本算法之递归和自调用函数_数据结构与算法之5——队列和栈相关推荐

  1. 2.2基本算法之递归和自调用函数_你为什么学不会递归?读完这篇文章轻松理解递归算法...

    对于很多编程初学者来说,递归算法是学习语言的最大障碍之一.很多人也是半懂不懂,结果学到很深的境地也会因为自己基础不好,导致发展太慢. 可能也有一大部分人知道递归,也能看的懂递归,但在实际做题过程中,却 ...

  2. 2.2基本算法之递归和自调用函数_用栈算法递归解决汉诺塔问题

    今天博主收一下线性表的尾,最近我们要学习的内容是栈和队列板块,栈和队列板块分为两讲,第一讲也就是今天我们主要学习栈的相关知识,包括栈的定义.栈的顺序表示及实现,栈的链式表示,栈的应用举例,以及栈递归实 ...

  3. 2.2基本算法之递归和自调用函数_一文学会递归解题

    前言 递归是算法中一种非常重要的思想,应用也很广,小到阶乘,再在工作中用到的比如统计文件夹大小,大到 Google 的 PageRank 算法都能看到,也是面试官很喜欢的考点 最近看了不少递归的文章, ...

  4. 2.2 基本算法之递归和自调用函数 1751 分解因数 python

    http://noi.openjudge.cn/ch0202/1751/ """ 2.2 基本算法之递归和自调用函数 1751 分解因数 http://noi.openj ...

  5. 2.2 基本算法之递归和自调用函数 8758 2的幂次方表示 python

    http://noi.openjudge.cn/ch0202/8758/ """ 2.2 基本算法之递归和自调用函数 8758 2的幂次方表示 python http:/ ...

  6. 2.2基本算法之递归和自调用函数_7592 求最大公约数问题

    http://noi.openjudge.cn/ch0202/7592/ /* 2.2基本算法之递归和自调用函数_7592 求最大公约数问题 http://noi.openjudge.cn/ch020 ...

  7. NOI题库答案 2.2基本算法之递归和自调用函数

    1696:逆波兰表达式 总时间限制: 1000ms 内存限制: 65536kB 描述 逆波兰表达式是一种把运算符前置的算术表达式,例如普通的表达式2 + 3的逆波兰表示法为+ 2 3.逆波兰表达式的优 ...

  8. 获取用户列表为空_数据结构和算法(Golang实现)(15)常见数据结构-列表

    列表 一.列表 List 我们又经常听到 列表 List 数据结构,其实这只是更宏观的统称,表示存放数据的队列. 列表 List:存放数据,数据按顺序排列,可以依次入队和出队,有序号关系,可以取出某序 ...

  9. vrp 节约算法 c++_数据结构和算法(Golang实现)(8.1)基础知识-前言

    基础知识 学习数据结构和算法.我们要知道一些基础的知识. 一.什么是算法 算法(英文algorithm)这个词在中文里面博大精深,表示算账的方法,也可以表示运筹帷幄的计谋等.在计算机科技里,它表示什么 ...

最新文章

  1. mysql整理类型_Mysql 时间类型整理
  2. 国内高校简称撞车史:南大、西大、东大都在争,唯独北大没人抢
  3. 盘点|应用落地,构建城市“大脑”
  4. Ubuntu上nfs的安装配置
  5. js bom window对象
  6. AndroidWear开发之HelloWorld篇
  7. win7 php mysql扩展名_win7下MySQL 5.1.73安装过程(图解)并在php.ini中启用相关扩展。...
  8. 软件工程学习笔记《四》需求分析
  9. 直通BAT必考题系列:JVM性能调优的6大步骤,及关键调优参数详解
  10. 天池-街景字符编码识别5-模型训练与验证
  11. 港中文开源 | 融合视频目标检测与单目标、多目标跟踪
  12. [原]sencha touch之表单(login demo)
  13. (52)FPGA条件选择(casex)
  14. pwm一个时间单位_通过PWM进行数模转换的滤波电路分析计算
  15. Eclipse javax.servlet.jsp.PageContext cannot be resolved to a type 错误解决办法
  16. Spring中的Bean是线程安全的么?
  17. mvc html 辅助方法,MVC HTML辅助类常用方法记录
  18. jdbc preparestatement 执行多条语句_MyBatis执行器
  19. tensorflow之视频质量诊断
  20. 【Spring-tx】AutoProxyRegistrar类

热门文章

  1. 接口(定义,使用方法)
  2. 做简单的android 软件推荐,Android_适用于Android开发的简单聊天软件,适用于android 开发。是一个简 - phpStudy...
  3. Linux 安装 lanmp
  4. FFmpeg开发实战(三):FFmpeg 打印音视频Meta信息
  5. 【旧文章搬运】无Device的驱动如何通信
  6. android旋转动画的两种实现方式
  7. 16-1平衡树源代码
  8. HttpContext
  9. php 把一个数组分成有n个元素的二维数组的算法
  10. iOS开发技巧,细节(二)