数据结构与算法---栈与ADT

来源:互联网 发布:java实现xml文件解析 编辑:程序博客网 时间:2024/05/14 22:08

1、数据结构的基本概念

在计算机学科中数据结构表示数据在计算机中的存储和组织形式。主要描述数据元素之间和位置关系等。一般来说,选择适当的数据结构可以提高计算机程序的运行效率(时间复杂度)和存储效率(空间复杂度)。

2、数据结构的三种层次

(1)逻辑结构--抽象层

主要描述的是数据元素之间的逻辑关系

(2)物理结构--结构层

主要描述的是数据元素之间的位置关系

(3)运算结构--实现层

主要描述的是如何实现数据结构

3、逻辑结构的分类 (抽象数据类型 ADT)

(1)集合结构(集)

主要描述所有的元素都属于一个总体,除了同属于一个集合外没有其他关系。集合结构不强调元素之间的任何关联性。

(2)线性结构(表)

主要描述元素之间具有一对一的前后关系。结构中必须存在唯一的首元素和唯一的尾元素。除了首元素之外结构中的每一个元素有且仅有一个前趋元素,除了尾元素之外结构中的每一个元素有且仅有一个后继元素。

(3)树形结构(树)

主要描述元素之间存在一对一个关系。树形结构中必须存在唯一跟关系,顶端的元素叫做叶元素。除了根元素之外,结构中每一个元素有且仅有一个前趋元素,除了叶元素之外,结构中每一个元素拥有一个到多个后继元素。如:树,家谱。

(4)网状结构(图)

主要描述数据元素之间存在多对多的交叉映射关系,也叫做图形结构。结构中的每个元素都可以拥有多个任意数量,前驱和后继元素,结构中的任意两个元素都可以建立关联。如:蜘蛛网 网球拍。

4、物理结构的分类

(1)顺序结构

顺序结构就是使用一组连续的存储单元依次存储逻辑上相邻的各个元素,顺序结构可以借助计算机程序设计语言(如C/C++)提供的数组类型加以描述。

优点:

只需要申请存放数据本身的内存空间即可,不需要额外的内存来表达数据元素之间的逻辑关系。

支持下标访问,也可以实现随机访问。

缺点:

连续的存储空间导致内存空间的利用率比较低

向顺序存储结构中插入/删除元素时,可能需要移动大量元素,效率比较低

(2)链式结构

表示在计算机中使用一组任意的存储单元来存储所有的元素(这组存储单元可以是连续的,也可以是不连续的)。不要求逻辑上相邻的元素在物理位置上也相邻。

链式存储结构不使用连续的存储空间存放结构的元素,而是为每一个元素构造一个节点。节点中除了存放数据本身以外,还需要存放指向下一个节点的指针。绝大多数程序设计语言(如C/C++)都没有提供用于描述链式结构的数据类型,需要编写额外的代码去实现。

优点:

不采用连续的存储空间导致内存空间利用率比较高,克服顺序存储结构中预知元素个数的缺点

插入或删除元素时,不需要移动大量的元素,比较当便。

缺点:

除了申请存放数据本身的内存空间外,还需要额外的空间来表达数据之间的逻辑关系

不支持下标访问和随机访问。

5、逻辑结构和物理结构的关系

每种逻辑结构采用何种物理结构来实现,并没有具体的规定。通过根据实现的难易程度,以及在时间复杂程度和空间复杂程度方面的要求,来选择合适的物理结构。

6、运算结构

(1)分配资源,建立结构,释放资源

如:int arr[5]; ->栈区,系统自动分配内存和回收内存。

(2)插入和删除

增加和减少元素

(3)获取和遍历

查看具体的元素值,以及遍历结构中所有的元素值

(4)修改和排序

修改元素的值,采用排序算法进行排序

二、栈 ADT

栈(stack)是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫做栈顶(top)。它是后进先出(LIFO)的。对栈的基本操作只有push(进栈)和pop(出栈)两种,前者相当于插入,后者相当于删除最后的元素。上篇文章讲递归的有讲到,程序在运行时,如果调用一个函数,则将该函数压栈(push)调用完一个函数则出栈(pop)。递归过程中,不断的压栈(因为该函数并没有执行完毕,因为还没有执行到函数的最后一个大括号 } )。因为内存是有限的,因此会造成栈溢出

由于栈在本质上是一种受限制的表,所以可以使用任何一种表的形式来实现它,我们最常用的一般有两种:

(1)顺序栈:数组

(2)链式栈:链表

它们在复杂度上的优缺点对比如下:

1、新增和删除元素时的时间复杂度

链表:在动态申请内存(new或者malloc)上的花销非常昂贵。

数组:几乎没有花销,以常数 O(1) 时间运行,在带有自增和自减寻址功能的寄存器上操作时,编译器会把整数的 push 和 pop 操作编译成一条机器指令。

2、空间复杂度

链表:由于空间是动态申请、释放的,因此不会浪费空间,而且只要物理存储器允许,理论上能够满足最大范围未知的情况。

数组:必须在初始化时执行栈的大小,有可能会浪费空间,也有可能不够空间用。

结论:

(1)如果对运动时的效率要求非常高,并且能够在初始化时预知栈的大小,那么应该首选数组形式,否则应该选用链表形式。

(2)由于对栈的操作永远都是针对栈顶(top)进行的,因此数组的随机存取的优点就没有了,而且数组必须预先分配空间,空间大小也受到限制,所以一般情况下(对运行时效的要求不是太高)链表应该是首选。

三、顺序栈

其中又分为静态数组和动态数组两类。

静态数组:特点是要求结构的长度固定,而且长度在编译时候就得确定。其优点是结构简单,实现起来方便而不容易出错。而缺点就是不够灵活以及固定长度不容易控制,适用于知道明确长度的场合。

动态数组:特点是长度可以在运行时候才确定以及可以更改原来数组的长度。优点是灵活,缺点是由此会增加程序的复杂性。

1、静态数组

在静态数组堆栈中,栈的长度以一个 #define 定义的形式出现,在模块被编译之间用户必须对数组长度进行设置。变量top_element 保存栈顶元素的下标值。它的初始值为 -1,提示栈为空。push 函数在存储新元素前先增加这个变量的值,这样 top_elelment 始终包含栈顶元素的下标志。如果把它的初始值设为 0,top_element 将指向数组的下一个可用位置。这种方法当然也可以,但它的效率稍差一些,因为它需要执行一次减法运算才能访问栈顶元素。执行 pop 函数,top_element 在元素被复制出数组之后才减 1,这和 push 相反,后者是在被元素复制到数组之前先加 1。pop 函数不需要从数组中删除元素,只需减少栈顶指针的值就足矣,因为用户此时已不能再访问这个旧值了。

使用断言,相对来说比较简单。但是也会有困扰就是入栈出栈时检测到栈为满或栈为空 程序会终止。但如果用户希望确保程序不会终止,那么程序压栈一个新值之前必须检测堆栈是否仍有空间。因此,断言必须只能够对那些用户自己也能进行检查的内容进行检查。后面会再写一个例子,讲到这种情况。

难懂的数据结构与算法----栈与ADT

再有注意:所有不属于外部接口的内容都被声明为 static,这可以防止用户预定义接口之外的任何方式访问堆栈中的值。

难懂的数据结构与算法----栈与ADT

难懂的数据结构与算法----栈与ADT

难懂的数据结构与算法----栈与ADT

难懂的数据结构与算法----栈与ADT

2、动态数组

和静态数组主要区别在于在接口中定义了两个新函数:创建堆栈、销毁堆栈。

create_stack 函数首先检查堆栈是否已经创建。然后分配所需要数量的内存并检查分配是否成功。

destroy_stack 函数在释放内存之后把长度设置为 0、指针变量重新设置为 NULL,这样它们可以用于创建另一个堆栈。

在is_full he is_empty 函数中都增加了一条断言。这条断言可以防止任何堆栈函数在堆栈被创建前就被调用。其余的堆栈函数并不需要这条断言,因为它们都调用了这两个函数之一。

警告:在内存有限的环境中,使用 assert 检查内存分配是否成功并不合适,因为它很可能导致程序终止。这未必是你希望的结果。