您的位置:首页 > 理论基础 > 数据结构算法

第三部分 数据结构 第 10 章 基本数据结构

2017-06-26 15:52 351 查看
  数学中的集合是不变的,而由算法操作的集合却在整个过程中能增大、缩小或发生其他变化,称这样的集合是动态的。

  动态集合中的元素

  在动态集合的典型实现中,每个元素都由一个对象来表示,如果有一个指向对象的指针,就能对其各个属性进行检查和操作。一些类型的动态集合假定对象中的一个属性为标识关键字

  动态集合上的操作

  分为两类:简单返回有关集合信息的查询操作和改变集合的修改操作。下面列出一些标准操作:

  SEARCH(S,k):查询操作,给定一个集合S和关键字k,返回指向S中某个元素的指针x,使得x.key = k。若没有返回NULL。

  INSERT(S,x):修改操作。将由x指向的元素加入到集合S中。通常假定元素x中集合S所需要的每个属性都已经被初始化好了

  DELETE(S,x):修改操作。给定指针x指向集合S中的一个元素,从S中删除x。

  MINIMUN(S):查询操作,在全序集合S上返回一个指向S中的具有最小关键字元素的指针。

  MAXIMUM(S):查询操作,在全序集合S上返回一个指向S中的具有最大关键字元素的指针。

  SUCCESSOR(S,x):查询操作。给定关键字属于全序集S中一个元素x,返回S中x大的前一个元素的指针;若x为最大元素,返回NULL。

  PREDECESSOR(S,x):查询操作。给定关键字属于全序集S中一个元素x,返回S中x小的前一个元素的指针;若x为最小元素,返回NULL。

  

第 10 章 基本数据结构

10.1 栈和队列

  栈和队列都是动态集合,且在其上进行DELETE操作所移除的元素都是预先设定的。在中,被删除的是最近插入的元素:栈实现的是一种后进先出策略。类似地,在队列中,被删去的总是在集合中存在时间最长的那个元素:队列实现的是一种先进先出的策略。本章介绍如何利用一个简单的数组实现者两种结构。

  

  栈上的INSERT操作被称为压入(PUSH),而无元素参数的DELETE操作称为弹出(POP)。

  如下:可以用一个数组S[1..n]来实现一个最多可容纳n个元素的栈。该数组有一个属性S.top,指向最新插入的元素。栈中包含的元素为S[1..S.top],其中S[1]是栈底元素,而S[S.top]是栈顶元素。

  当S.top = 0是,栈中不包含任何元素,即栈是空的。如果试图对一个空栈执行弹出操作,则称栈下溢,如果S.top超过了n,则称栈上溢

  栈的操作:

STACK-EMPTY(S)

if S.top == 0
return true
else return FALSE


PUSH

S.top = S.top + 1
S[S.top] = x


POP

if STACH-EMPTY(S)
error "underflow"
else S.top = S.top - 1
return S[S.top + 1]


三种栈操作的执行时间都为O(1)。

  队列

  队列上的INSERT操作称为入队(ENQUEUE),DELETE操作称为出队(DEQUEUE);队列有队头队尾,当有一个元素入队时,它被放在队尾的位置,而出队的元素在总是在对头的那个。

  如下,利用数组Q[1..n]来实现一个最多容纳n-1元素的队列。该队列有一个属性Q.head指向队头元素。而属性Q.tail则指向下一个新元素将要插入的位置。队列中的元素存放在位置Q.head,Q.head+1,…,Q.tail-1,并在最后位置“环绕”,感觉好像位置1紧邻在位置n后面形成一个环序。当Q.heap = Q.tail时,队列为空。初始时,Q.head = Q.tail = 1.如果试图从空队列中删除一个元素,则队列发生下溢。当Q.head = Q.tail + 1时,队列是慢的,若试图插入元素,则发生上溢。

  在下列伪代码中,假设n = Q.length

ENQUEUE(Q,x)

Q[Q.tail] = x
if Q.tail == Q.length
Q.tail = 1
else Q.tail = Q.tail +1


DEQUEUE(Q)

x = Q[Q.head]
if Q.head = Q.length
Q.head = 1
else Q.head = Q.head + 1
return x


10.2 链表

  链表中的各对象按线性顺序排列。数组的线性顺序是由数组下标决定的,然而,链表的顺序是由各个对象里的指针决定的。

  双向链表 L 的的每个元素都是一个对象,每个对象有一个关键字key和两个指针:next和prev。设x为链表的一个元素,若x.prev=NULL,则元素没有前驱,则是链表的。如x.next = NULL,则x没有后继,则是链表的。属性L.head指向链表的第一个元素。若L.head = NULL,则链表为空。

  链表可以是多种形式。如是单链接的,则省略每个元素中的prev指针。若是已排序的,则链表的线性顺序与链表元素中关键字的线性顺序一致。在循环链表中,表头元素的prev指向表尾元素,而表尾元素的next指向表头元素。

  链表的搜索

  LIST-SEARCH(L,k)采用简单的线性搜索方法,用于查找L中第一个关键字为k的元素,并指向该元素的指针。如没有,则返回NULL。

  

x = L.head
while x != NULL and x.key != k
x = x.next
return x


过程LIST-SEATCH在最坏情况下的运行时间为θ(n)。

  链表的插入

  给定一个已设置好的关键字key的元素x,过程LIST-INSERT将x连接入到链表的前段

  

x.next = L.head
if L.head != NULL
L.head.prev = x
L.head = x
x.prev = NULL


运行时间是O(1)。

  链表的删除

  过程LIST-DELETE将一个元素x从链表L中移除。该过程要求给定一个指向x的指针,然后通过修改一些指针,将x删除出该链表。如要删除具有给定关键字的元素,则必须先调用LIST-SEATCH找到该元素。

  

if x.prev != NULL
x.prev.next = x.next
else L.head = x.next
if x.next != NULL
x.next.prev = x.prev


运行时间为O(1)。但删除给定关键字元素的最坏情况为θ(n)。

  哨兵

  如忽视表头和表尾的边界条件,则LIST-DELETE更简单:

  

x.prev.next = x.next
x.next.prev = x.prev


  哨兵是一个哑对象,其作用是简化边界条件的处理。例如:在链表 L 中设置一个对象L.NULL,该对象代表 NULL,但也具有和其他对象相同的各个属性。对于链表代码中出现的每一处对NULL的引用,都代之以对哨兵L.NULL的引用。这样调整的链表变为有哨兵的双向循环链表。哨兵L.NULL位与表头和表尾之间。属性L.NULL.next指向表头,L.NULL.prev指向表尾。如下,一个空的链表只由一个哨兵组成,L.NULL.next和L.NULL.prev同时指向L.NULL。

  代码调整

  LIST-SEARCH’(L,k)

  

x = L.NULL.next
while x != L.NULL and x.key != k
x = x.next
return x


LIST-INSERT’(L,x)

x.next = L.NULL.next
L.NULL.next.prev = x
L.NULL.next = x
x.prev = L.NULL


 哨兵不能降低数据结构的渐近时间界,但是可以降低常数因子。

 我们应当慎用哨兵。假如有很多歌很短的链表,它们的哨兵所用的额外存储空间会造成严重的存储浪费。

 

10.3 指针和对象的实现

  本节介绍在没有显式的指针数据类型的情况下实现链式数据结构的两种方法。我们将利用数组和数组下标来构造对象的指针。

  对象的多数组表示

  对每个属性使用同一个数组表示,可以来表示一组用相同属性的对象。数组key存放该动态集合中现有的关键字,指针则分别存放在数组next和prev中。对于一个给定的数组下标x,三个数组项key[x]、next[x]、prev[x]一起表示链表中的一个对象。

  尽管常数NULL出现在表尾的next属性和表头的prev属性中,但通常用一个不能代表数组中任何实际位置的整数(例如0或-1)来表示。此外,变量L存放表头元素的下标。

  对象的单数组表示

  计算机内存中的字往往从整数 0 到M-1进行编址,其中 M 是一个足够大的整数。在许多程序设计语言中,一个对象在计算机内存中占据一组连续的内存单元。指针仅仅是该对象所在的第一个存储单元的地址,要访问对象内其他存储单元可以在指针上加上一个偏移量。

  在不支持显式的指针数据类型的编程环境下,可以采用同样的策略来实现对象。一个对象占用一段连续的子数组A[j..k],对象中的每个属性对应于从0到k-j之间的一个偏移量,指向该对象的指针就是下标 j。

  这种单数组的表示法比较灵活,因为它允许不同长度的对象存储于同一数组中。由于我们考虑的结构大多是有同构的元素构成,因此采用对象的多数组表示法足够满足我们的需求。

  对象的分配与释放

  假设多数组表示法中的各数组长度为m,且在某一时刻该动态集合含有n<=m个元素,则n个对象代表现存于该动态集合中的元素,而余下的m-n个对象是自由的

  把自由对象保存在一个单链表中,称为自由表。自由表只使用next数组,该数组只存储链表中的next指针。自由表的头保存在全局变量free中。当有链表L表示的动态集合非空时,自由表可能会和链表L相互交错.

  

10.4 有根树的表示

  本节中,专门讨论用链式数据结构表示有根数的问题。输的节点用对象表示。与链表类似,假设每个节点都含有一个关键字key。其余的属性包括指向其他节点的指针,它们随树的种类不同会有所变化。

  二叉树

  在二叉树中利用属性p、left和right存放指向父节点、左孩子和右孩子的指针。如果x.p=NULL,则x是根节点。如果x没有左孩子,则x.left=NULL,类似,右孩子的情况类似。属性T.root指向整棵树T的根节点。若T.root=NULL,则概述为空。

  分支无限制的有根树

  二叉树的表示方法可以推广到每个节点的孩子数至多为常数k的任意类型的数:只需要将left和right属性用child1,child2,…,childk来代替。当孩子的节点数无限制时,这种方法就失效了,因为不知道应当预先分配多少个属性(在数组表示法中就是多少个数组)。此外,即使孩子数k限制咋一个大的常数以内,但若多数节点只有少量的孩子,则会浪费大量存储空间。

  所幸的是,有一个巧妙的方法可以用来表示孩子数任意的数。该方法优势在于,对任意 n 个节点的有根树,只需要O(n)的存储空间。这种左孩子右兄弟的表示法,每个节点都包含一个父节点指针p,且T.root指向数T的根节点。然而,每个节点中不是包含指向每个孩子的指针,而是只有两个指针:

  1.x.left-child指向节点x最左边的孩子节点;

  2.x.right-siblin指向x右侧相邻的兄弟节点;

  如果节点x没有孩子节点,则x.left-child=NULL如果节点x是其父节点的最右孩子,则x.right-siblin=NULL。

  树的其他表示方法

  堆用一个单数组加上堆的最末节点的下标表示。还有很多方法。

  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  数据结构