堆栈详解及其python实现

wiki定义

堆栈(抽象数据类型)

有关在会计中使用术语LIFO,请参阅LIFO(会计)。对于力量训练中使用术语下推,请参阅下推(练习)。

有关其他用途,请参阅堆栈(消歧)。

 

使用推送弹出操作简单表示堆栈运行时。

在计算机科学中,堆栈是一种抽象数据类型,用作元素集合,具有两个主要操作:

  • push,它为集合添加了一个元素,以及
  • pop,删除最近添加的尚未删除的元素。

元素从堆栈中出现的顺序产生了它的替代名称LIFOlast in,first out)。另外,窥视操作可以在不修改堆栈的情况下访问顶部。[1]这种结构的名称“堆叠”来自于相互堆叠的一组物理项目的类比,这样可以轻松地将物品从堆叠顶部取出,同时获取物品堆叠深度可能需要先取下多个其他物品。[2]

被认为是线性数据结构,或者更抽象地是顺序集合,推送和弹出操作仅发生在结构的一端,称为堆栈的顶部。这使得可以将堆栈实现为单链表和指向顶部元素的指针。可以实现堆栈以具有有界容量。如果堆栈已满并且没有足够的空间来接受要推送的实体,则认为堆栈处于溢出状态。pop操作从堆栈顶部删除一个项目。

需要堆栈来实现深度优先搜索。

内容

  • 历史
  • 非必要的操作
  • 软件堆栈
    • 履行
      • 排列
      • 链接列表
    • 堆栈和编程语言
  • 硬件堆栈
    • 堆栈的基本架构
      • 堆叠在主内存中
      • 堆叠在寄存器或专用存储器中
  • 堆栈的应用
    • 表达式评估和语法分析
    • 回溯
    • 编译时间内存管理
    • 高效的算法
  • 安全
  • 也可以看看
  • 参考
  • 进一步阅读
  • 外部链接

历史

另见:JanŁukasiewicz§工作

Stacks于1946年进入计算机科学文献,当时Alan M. Turing使用术语“埋葬”和“unbury”作为从子程序调用和返回的手段。[3]子程序已于1945年在Konrad Zuse的Z4中实施。

克劳斯·萨梅尔森和弗里德里希·L·包尔的慕尼黑工业大学提出这个构想于1955年并于1957年申请了专利,[4]和1988年3月鲍尔收到的计算机先驱奖为堆原理发明。[5]同样的概念是由澳大利亚人Charles Leonard Hamblin在1954年上半年独立开发的。[6]

堆栈通常类似于自助餐厅中的弹簧加载的堆叠板来描述。[7] [2] [8] 清洁板放在堆叠的顶部,向下推动任何已经存在的板。当从堆叠中移除板时,其下方的板突然弹出以成为新的顶部。

非必要的操作

在许多实现中,堆栈具有比“推”和“弹出”更多的操作。一个例子是“堆栈顶部”或“peek”,它观察最顶层的元素而不将其从堆栈中移除。[9]由于这可以通过具有相同数据的“pop”和“push”来完成,因此不是必需的。如果堆栈为空,则在“堆栈顶部”操作中可能发生下溢情况,与“pop”相同。此外,实现通常具有仅返回堆栈是否为空的函数。

软件堆栈

履行

可以通过数组或链表轻松实现堆栈。在任何一种情况下,将数据结构标识为堆栈的不是实现,而是接口:用户只允许将项目弹出或推送到数组或链接列表上,而只需要很少的其他帮助操作。以下将使用伪代码演示这两种实现。

排列

数组可用于实现(有界)堆栈,如下所示。第一个元素(通常在零偏移处)是底部,导致array[0]第一个元素被压入堆栈并且最后一个元素弹出。程序必须跟踪堆栈的大小(长度),使用记录到目前为止推送的项目数的变量top,因此指向要插入下一个元素的数组中的位置(假设为零)基于指数的约定)。因此,堆栈本身可以有效地实现为三元素结构:

结构堆栈:
    maxsize:整数
    顶部:整数
    items:项目数组
procedure initialize(stk:stack,size:integer):
    stk.items←新的大小项目数组,最初为空
    stk.maxsize←大小
    stk.top←0

检查溢出后,push操作会添加一个元素并递增顶部索引:

procedure push(stk:stack,x:item):
     如果 stk.top = stk.maxsize:
        报告溢出错误
    否则:
        stk.items [stk.top]←x
        stk.top←stk.top + 1

类似地,pop在检查下溢后递减顶部索引,并返回先前最顶层的项:

procedure pop(stk:stack):
     如果 stk.top = 0:
        报告下溢错误
    否则:
        stk.top←stk.top  -  1
        r←stk.items [stk.top]
        返回

使用动态数组,可以实现可以根据需要增长或缩小的堆栈。堆栈的大小就是动态数组的大小,这是一个非常有效的堆栈实现,因为向动态数组末尾添加项目或从中移除项目需要分摊O(1)时间。

链接列表

实现堆栈的另一个选择是使用单链表。然后堆栈是指向列表“头部”的指针,可能还有一个计数器来跟踪列表的大小:

结构框架:
    数据项
    下一个:框架或零
结构堆栈:
    头:框架或零
    大小:整数
procedure initialize(stk:stack):
    stk.head←无
    stk.size←0

推送和弹出项目发生在列表的头部; 在此实现中无法溢出(除非内存耗尽):

程序推送(stk:stack,x:item):
    newhead←新框架
    newhead.data←x
    newhead.next←stk.head
    stk.head←newhead
    stk.size←stk.size + 1
procedure pop(stk:stack):
     如果 stk.head = nil:
        报告下溢错误
    r←stk.head.data
    stk.head←stk.head.next
    stk.size←stk.size  -  1
    返回

堆栈和编程语言

某些语言(如Perl,LISP,JavaScript和Python)使堆栈操作在其标准列表/数组类型上可以推送和弹出。有些语言,特别是Forth系列中的语言(包括PostScript),是围绕语言定义的堆栈设计的,这些堆栈直接由程序员可见并由程序员操纵。

以下是在Common Lisp中操作堆栈的示例(“ > ”是Lisp解释器的提示;不以“ > ” 开头的行是解释器对表达式的响应):

        
        > 
        (
        SETF 
        堆
        (
        列表
        '一个
        ' B 
        “C 
        ))
        ;; 设置变量“stack” 
        (
        A 
        B 
        C 
        )
        > 
        (
        pop 
        stack 
        )
        ;; 得到顶部(最左边)的元素,应该修改堆栈
        A 
        > 
        stack 
        ;; 检查堆栈的值
        (
        B 
        C 
        )
        > 
        (
        推
        '新
        堆栈
        )
        ;; 将新顶部推入堆栈
        (
        NEW 
        B 
        C 
        )
      

一些C ++标准库容器类型具有带LIFO语义的push_back和pop_back操作; 此外,堆栈模板类调整现有容器以提供仅具有推/弹操作的受限API。PHP有一个SplStack类。Java的库包含一个专门化的类。以下是使用该类的Java语言示例程序。 StackVector

        
        import 
        java.util。* 
        ; 
        class 
        StackDemo 
        { 
        public 
        static 
        void 
        main 
        (
        String 
        [] 
        args 
        )
        { 
        Stack 
        < 
        String 
        > 
        stack 
        = 
        new 
        Stack 
        < 
        String 
        >(); 
        堆栈
        。
        推
        (
        “A” 
        ); 
        //在堆栈
        堆栈中
        插入“A” 。
        推
        (
        “B” 
        ); 
        //在堆栈
        堆栈中
        插入“B” 。
        推
        (
        “C” 
        ); 
        //插入“C”
        堆栈
        。
        推
        (
        “D” 
        ); 
        //在堆栈
        系统中
        插入“D” 。
        出
        。
        的println 
        (
        堆
        。
        偷看
        ()); 
        //打印堆栈顶部(“D”)
        堆栈
        。
        pop 
        (); 
        //删除顶部(“D”)
        堆栈
        。
        pop 
        (); 
        //删除下一个顶部(“C”)
        } 
        }
      

硬件堆栈

架构级别的堆栈的常见用途是作为分配和访问存储器的手段。

堆栈的基本架构

 

典型的堆栈,用于存储嵌套过程调用的本地数据和调用信息(不一定是嵌套过程)。这个堆栈从它的起源向下增长。堆栈指针指向堆栈上当前最顶层的数据。推送操作递减指针并将数据复制到堆栈; 弹出操作从堆栈中复制数据,然后递增指针。程序中调用的每个过程通过将过程信息推送到堆栈中来存储过程返回信息(黄色)和本地数据(其他颜色)。这种类型的堆栈实现非常常见,但它容易受到缓冲区溢出攻击(参见文本)。

典型的堆栈是具有固定原点和可变大小的计算机存储器区域。最初堆栈的大小为零。甲堆栈指针,通常以硬件寄存器的形式,指向堆栈上最近引用的位置; 当堆栈的大小为零时,堆栈指针指向堆栈的原点。

适用于所有堆栈的两个操作是:

  • 一个操作,其中一个数据项被放置在该位置所指向堆栈指针,并且在堆栈指针的地址由数据项的大小调节;
  • 一个弹出操作:在当前位置的数据项指向堆栈指针被除去,堆栈指针由数据项的大小进行调整。

堆栈操作的基本原理有很多变化。每个堆栈在内存中都有一个固定的位置。随着数据项被添加到堆栈中,堆栈指针被移位以指示堆栈的当前范围,该范围从原点扩展。

堆栈指针可以指向堆栈的原点或者指向原点上方或下方的有限范围的地址(取决于堆栈增长的方向); 但是,堆栈指针不能跨越堆栈的原点。换句话说,如果堆栈的原点位于地址1000并且堆栈向下增长(朝向地址999,998等),则堆栈指针绝不能增加超过1000(到1001,1002等)。如果堆栈上的弹出操作导致堆栈指针移过堆栈的原点,则会发生堆栈下溢。如果推送操作导致堆栈指针递增或递减超出堆栈的最大范围,则发生堆栈溢出

某些严重依赖堆栈的环境可能会提供其他操作,例如:

  • 重复:弹出顶部项目,然后再次按下(两次),以便前一个顶部项目的附加副本现在位于顶部,原始项目位于其下方。
  • Peek:检查(或返回)最顶层的项目,但堆栈指针和堆栈大小不会改变(意味着该项目保留在堆栈中)。这在许多文章中也称为顶级操作。
  • 交换交换:堆栈交换位置上的两个最顶层项目。
  • 旋转(或滚动)n个最顶部的项目以旋转方式在堆栈上移动。例如,如果n = 3,则堆栈上的项目1,2和3分别移动到堆栈上的位置2,3和1。此操作的许多变体都是可能的,最常见的是左旋转右旋。

堆栈通常是从底部向上可视化的(如真实世界的堆栈)。它们也可以从左到右可视化,使“最顶层”变得“最右边”,甚至从上到下增长。重要的特征是堆栈的底部处于固定位置。本节中的插图是从上到下增长可视化的示例:顶部(28)是堆栈“底部”,因为堆栈“顶部”(9)是项目被推送或弹出的位置。

右旋转将第一元素移动到该第三位置时,第二到第一和第三至第二。以下是此过程的两个等效可视化:

苹果香蕉
香蕉===右旋==>黄瓜
黄瓜苹果
黄瓜苹果
香蕉===左转==>黄瓜
苹果香蕉

堆栈通常由计算机中的一块存储器单元表示,其中“底部”位于固定位置,堆栈指针保持堆栈中当前“顶部”单元的地址。无论堆栈是实际朝向较低的存储器地址还是朝向较高的存储器地址增长,都使用顶部和底部术语。

将项目推入堆栈会根据项目的大小调整堆栈指针(递减或递增,具体取决于堆栈在内存中的增长方向),将其指向下一个单元格,并将新的顶部项目复制到堆栈区域。再次取决于确切的实现,在推送操作结束时,堆栈指针可以指向堆栈中的下一个未使用的位置,或者它可以指向堆栈中的最顶层的项目。如果堆栈指向当前最顶层的项,则在将新项目推入堆栈之前,将更新堆栈指针; 如果它指向堆栈中的下一个可用位置,则将新项目推入堆栈后将更新它。

弹出堆栈只是推动的反过来。删除堆栈中最顶层的项目,并按照与推送操作中使用的相反的顺序更新堆栈指针。

堆叠在主内存中

许多CISC类型的CPU设计,包括x86,Z80和6502,都有一个专用寄存器,用作调用堆栈栈指针,带有专用的call,return,push和pop指令,可以隐式更新专用寄存器,从而提高代码密度。一些CISC处理器,如PDP-11和68000,也具有用于实现堆栈的特殊寻址模式,通常还具有半专用堆栈指针(例如68000中的A7)。相比之下,大多数RISC CPU设计没有专用的堆栈指令,因此大多数(如果不是全部)寄存器可以根据需要用作堆栈指针。

堆叠在寄存器或专用存储器中

主要文章:堆叠机器

所述 的x87 浮点架构是一组组织成堆叠,其中直接访问各个寄存器(相对当前顶部)也是可能的寄存器的一个例子。与基于堆栈的计算机一样,将堆栈顶部作为隐式参数允许小的机器代码占用空间,同时充分利用总线 带宽和代码高速缓存,但它也阻止了处理器允许的某些类型的优化随机访问所有(两个或三个)操作数的寄存器文件。堆栈结构也使用寄存器重命名进行超标量实现(for推测执行)稍微更复杂的实施,尽管它仍然是可行的,如举例说明由现代的x87实现。

Sun SPARC,AMD Am29000和Intel i960都是在寄存器堆栈中使用寄存器窗口的架构示例,作为避免将慢速主存储器用于函数参数和返回值的另一种策略。

还有许多小型微处理器直接在硬件中实现堆栈,而一些微控制器具有不能直接访问的固定深度堆栈。例如PIC微控制器,Computer Cowboys MuP21,Harris RTX系列和Novix NC4016。许多基于堆栈的微处理器用于在微码级实现编程语言Forth。堆栈也被用作许多大型机和迷你计算机的基础。这种机器被称为堆叠机,最着名的是Burroughs B5000。

堆栈的应用

表达式评估和语法分析

采用反向波兰表示法的计算器使用堆栈结构来保存值。表达式可以用前缀,后缀或中缀表示法表示,并且可以使用栈来完成从一种形式到另一种形式的转换。许多编译器在转换为低级代码之前使用堆栈来解析表达式,程序块等的语法。大多数编程语言都是无上下文的语言,允许使用基于堆栈的机器解析它们。

回溯

主要文章:回溯

堆栈的另一个重要应用是回溯。考虑一个在迷宫中找到正确路径的简单示例。从起点到目的地有一系列要点。我们从一点开始。要到达最终目的地,有几条路径。假设我们选择一个随机路径。在遵循某条路径之后,我们意识到我们选择的路径是错误的。因此,我们需要找到一种方法,以便我们可以返回到该路径的开头。这可以通过使用堆栈来完成。在堆栈的帮助下,我们记住了我们达到的目标。这是通过将该点推入堆栈来完成的。如果我们最终走错了路径,我们可以从堆栈中弹出最后一个点,从而返回到最后一个点并继续寻找正确的路径。这称为回溯。

回溯算法的典型示例是深度优先搜索,其查找可以从指定的起始顶点到达的图的所有顶点。回溯的其他应用涉及搜索代表优化问题的潜在解决方案的空间。分支和绑定是一种用于执行这种回溯搜索的技术,而无需在这样的空间中穷举搜索所有潜在的解决方案。

编译时间内存管理

主要文章:基于堆栈的内存分配和堆栈机器

许多编程语言都是面向堆栈的,这意味着它们定义了大多数基本操作(添加两个数字,打印一个字符),从堆栈中获取参数,并将任何返回值放回堆栈。例如,PostScript具有返回堆栈和操作数堆栈,还具有图形状态堆栈和字典堆栈。许多虚拟机也是面向堆栈的,包括p代码机和Java虚拟机。

几乎所有调用约定 - 子例程接收其参数和返回结果的方式 - 使用特殊堆栈(“ 调用堆栈 ”)来保存有关过程/函数调用和嵌套的信息,以便切换到被调用函数的上下文并在调用结束时恢复到调用者函数。这些函数遵循调用者和被调用者之间的运行时协议来保存参数并在堆栈上返回值。堆栈是支持嵌套或递归函数调用的重要方法。编译器隐式使用这种类型的堆栈来支持CALL和RETURN语句(或它们的等价物),并且不由程序员直接操作。

一些编程语言使用堆栈来存储过程本地的数据。输入过程时,从堆栈中分配本地数据项的空间,并在过程退出时释放。在C编程语言以这种方式通常执行。对数据和过程调用使用相同的堆栈具有重要的安全隐患(见下文),程序员必须注意这一点,以避免在程序中引入严重的安全漏洞。

高效的算法

一些算法使用堆栈(与大多数编程语言的通常函数调用堆栈分开)作为用于组织其信息的主要数据结构。这些包括:

  • 格雷厄姆扫描,一种二维点系统的凸包算法。输入子集的凸包保持在堆栈中,当将新点添加到船体时,该堆栈用于查找和移除边界中的凹陷。[10]
  • 用于查找单调矩阵的行最小值的SMAWK算法的一部分使用与Graham扫描类似的方式的堆栈。[11]
  • 所有最接近的较小值,即对于数组中的每个数字,找到最接近的前一个数字的问题。针对该问题的一种算法使用堆栈来维护候选者的集合以获得最接近的较小值。对于数组中的每个位置,将弹出堆栈,直到在其顶部找到较小的值,然后将新位置中的值压入堆栈。[12]
  • 在最近邻算法链,对于方法凝聚层次聚类基于维持堆叠簇,其中的每一个是其在栈上前身最近邻。当此方法找到一对相互最近邻居的聚类时,会弹出并合并它们。[13]

安全

某些计算环境使用堆栈的方式可能使它们容易受到安全漏洞和攻击。在这种环境中工作的程序员必须特别小心,以避免这些实现的陷阱。

例如,一些编程语言使用公共堆栈来存储被调用过程本地的数据和允许过程返回其调用者的链接信息。这意味着程序将数据移入和移出包含过程调用的关键返回地址的同一堆栈。如果将数据移动到堆栈上的错误位置,或者将超大数据项移动到不足以容纳它的堆栈位置,则过程调用的返回信息可能已损坏,从而导致程序失败。

恶意方可以通过向不检查输入长度的程序提供过大的数据输入来尝试利用此类实现的堆栈粉碎攻击。这样的程序可以将数据完整地复制到堆栈上的位置,并且这样做可以改变已经调用它的过程的返回地址。攻击者可以尝试查找可以提供给此类程序的特定类型的数据,以便重置当前过程的返回地址以指向堆栈本身内的区域(以及攻击者提供的数据内),其中包含执行未授权操作的指令。

这种类型的攻击是缓冲区溢出攻击的一种变体,是软件中非常频繁的安全漏洞来源,主要是因为一些最流行的编译器使用共享堆栈进行数据和过程调用,并且不验证长度数据项。程序员经常不编写代码来验证数据项的大小,当超大或小型数据项被复制到堆栈时,可能会发生安全漏洞。

python实现

# 后进先出
class Stack():
    def __init__(self,size):
        self.size=size
        self.stack=[]
        self.top=-1

    def push(self,x):# 入栈之前检查栈是否已满
        if self.isfull():
            print("stack is full")
        else:
            self.stack.append(x)
            self.top=self.top+1

    def pop(self):# 出栈之前检查栈是否为空
        if self.isempty():
            print("stack is empty")
        else:
            self.top=self.top-1
            self.stack.pop()

    def isfull(self):
        return self.top+1 == self.size
    def isempty(self):
        return self.top == '-1'
    def showStack(self):
        print(self.stack)

s=Stack(10)
for i in range(6):
    s.push(i)
s.showStack()
for i in range(3):
    s.pop()
s.showStack()

"""
类中有top属性,用来指示栈的存储情况,初始值为1,一旦插入一个元素,其值加1,利用top的值乐意判定栈是空还是满。
执行时先将0,1,2,3,4,5依次入栈,然后删除栈顶的前三个元素
"""

 

你可能感兴趣的:(基础知识)