数据结构,算法与应用(1)

第3章 数据描述
最常见的数据描述方法有:公式化描述、链接描述、间接寻址和模拟指针。
公式化描述借助数学公式来确定元素表中的每个元素分别存储在何处(如存储器地址)。
最简单的情形就是把所有元素依次连续存储在一片连续的存储空间中,这就是通常所说的连续线性表。
在链接描述中,元素表中的每个元素可以存储在存储器的不同区域中,每个元素都包含一个指向下一个元素的指针。同样,在间接寻址方式中,元素表中的每个元素也可以存储在存储器的不同区域中,不同的是,此时必须保存一张表,该表的第 i项指向元素表中的第 i个元素,所以这张表是一个用来存储元素地址的表。
在公式化描述中,元素地址是由数学公式来确定的;在链接描述中,元素地址分布在每一个表元素中;而在间接寻址方式下,元素地址则被收集在一张表中。
模拟指针非常类似于链接描述,区别在于它用整数代替了 C++指针,整数所扮演的角色与指针所扮演的角色完全相同。
1. 公式化描述的评价
这种描述方法的一个缺点是空间的低效利用。考察如下的情形:我们需要维持三个表,而且已经知道在任何时候这三个表所拥有的元素总数都不会超过 5000个。然而,很有可能在某个时刻一个表就需要5000个元素,而在另一时刻另一个表也需要 5000 个元素。若采用类LinearList,这三个表中的每一个表都需要有 5000个元素的容量。因此,即使我们在任何时刻都不会使用5000以上的元素,也必须为此保留总共 15000个元素的空间。
为了避免这种情形,必须把所有的线性表都放在一个数组 list中进行描述,并使用两个附加的数组first和last对这个数组进行索引。图 3-3给出了在一个数组 list中描述的三个线性表。我们采用大家很习惯的约定,即如果有 m个表,则每个表从1到m进行编号,且 first[i]为第i个表中的第一个元素。有关 first[i]的约定使我们更容易地采用公式化描述方式。 last[i]是表i的最后一个元素。注意,根据这些约定,每当第 i个表不为空时,有 last[i]>first[i],而当第i个表为空时,有last[i]=first[i]。所以在图3-3的例子中,表 2是空表。在数组中,各线性表从左至右按表的编号次序1,2,3,...,m进行排列。

 

 

 

公式化描述代码如下:

View Code

为了避免第一个表和最后一个表的处理方法与其他的表不同,定义了两个边界表:表 0和表m+1,其中first[0]=last[0]=-1, first[m+1]=last[m+1]=MaxSize-1。为了在第 i 个表的第 k 个元素之后插入一个元素,首先需要为新元素创建空间。如果last[i]=first[i+1],则在第 i 个表和第 i + 1个表之间没有空间,因此不能把第 k + 1至最后一个元素向后移动一个位置。在这种情况下,通过检查关系式 last[i-1]<first[i]是否成立,可以确定是否有可能把第i 个表的1至k-1元素向前移一个位置;如果这个关系式不成立,要么需要把表 1至表i-1的元素向前移一个位置,要么把表 i+1至表m 向后移一个位置,然后为表 i创建需要增长的空间。当表中所有的元素总数少于 MaxSize时,这种移位的方法是可行的。图3-4是一个伪 C++函数,它向表 i 中插入一个新的元素,可以把该函数细化为兼容的 C++代码。

 

 

 

尽管在一个数组中描述几个线性表比每个表用一个数组来描述空间的利用率更高,但在最坏的情况下,插入操作将耗费更多的时间。事实上,一次插入操作可能需要移动 MaxSize-1个元素。

2. 链表
令L=(e1 , e2 , ..., en )是一个线性表。在针对该表的一个可能的链表描述中,每个元素 ei 都放在不同的节点中加以描述。每个节点都包含一个链接域,用以指向表中的下一个元素。所以节点ei 的指针将指向ei +1,其中1≤i<n。节点en 没有下一个节点,所以它的链接域为 NULL(或0)。指针变量first 指向描述中的第一个节点。图 3-5给出了表L=(e1 , e2 , ..., en )的链表描述。
由于图3 - 5中的每个链表节点都正好有一个链接域,所以该图的链表结构被称之为单向链表(singly linked list)。并且,由于第一个节点 e1 的指针指向第二个节点 e 2,e2 的指针指向 e3 ,. . .,最后一个节点链接域为 N U L L (或0 ),故这种结构也被称作链( c h a i n)。

 

删除操作

 

 

 

为了从图3-6所示的链中删除第四个元素,需进行如下操作:
1) 找到第三和第四个节点。
2) 使第三个节点指向第五个节点。
3) 释放第四个节点所占空间,以便于重用。
程序3-14中给出了删除操作的代码。有三种情形需要考虑。第一种情形是: k小于1或链表为空;第二种情形是:第一个元素将被删除且链表不为空;最后一种情形是:从一个非空的链表中删除首元素之外的其他元素。

插入操作
插入和删除的过程很相似。为了在链表的第 k个元素之后插入一个新元素,需要首先找到第k 个元素,然后在该节点的后面插入新节点。图 3-7给出了k=0 和k≠0 两种情况下链表指针的变化。插入之前的实线指针,在插入之后被“打断”。程序3-15给出了相应的 C++ 代码,它的复杂性为O (k)。
链表遍历器(迭代器)
链表遍历器(见程序3-18)有两个共享成员Initialize和Next。Initialize返回一个指针,该指针指向第一个链表节点中所包含的数据,同时把私有变量location设置为指向链表的第一个节点,该变量用来跟踪我们在链表中所处的位置。成员 Next用来调整 location,使其指向链表中的下一个节点,并返回指向该节点数据域的指针。由于 ChainIterator类访问了Chain类的私有成员first,所以应把它定义为Chain的友类。

链表描述如下:

View Code

下面是一个扩展的链表,包括了尾指针:

View Code

对应的测试代码如下: 

View Code
循环列表
采纳下面的一条或两条措施,使用链表的应用代码可以更简洁、更高效: 1)把线性表描述成一个单向循环链表(singly linked circular list),或简称循环链表(circular list),而不是一个单向链表;2) 在链表的前部增加一个附加的节点,称之为头节点( head node)。通过把单向链表最后一个节点的链接指针改为指向第一个节点,就可以把一个单向链表改造成循环链表,如图3-8a 所示。图 3-8b 给出了一个带有头指针的非空的循环链表,图 3-8c 给出了一个带有头指针的空的循环链表。
在使用链表时,头指针的使用非常普遍,因为利用头指针,通常可以使程序更简洁、运行速度更快。CircularList类的定义与Chain类的定义很类似。尽管链表搜索的复杂性仍然保持为O(n),但代码本身要稍微简单一些。由于与程序3-12相比,程序3-20在for循环的每次循环中执行的比较次数 较少,因此程序3-20将比程序3-12运行得更快一些,除非要查找的元素紧靠链表的左部。

与公式化描述方法的比较
采用公式化描述方法的线性表仅需要能够保存所有元素的空间以及保存表长所需要的空间,而链表和循环链表描述还需要额外的空间,用来保存链接指针(线性表中 的每个元素都需要一个相应的链接指针)。采用链表描述所实现的插入和删除操作要比采用公式化描述时执行得更快。当每个元素都很长时(字节数多),尤其如此。
还可以使用链接模式来描述很多表,这样做并不会降低空间利用率,也不会降低执行效率。对于公式化描述,为了提高空间利用率,不得不把所有的表都放在一个数 组中加以描述,并使用了另外两个数组来对这个数组进行索引,更有甚者,与一个表对应一个数组的情形相比,插入和删除操作变得更为复杂,而且存在一个很显著的最坏运行时间。
采用公式化描述,可以在O(1)的时间内访问第k 个元素。而在链表中,这种操作所需要的时间为O(k)。

双向链表
对于线性表的大多数应用来说,采用链表和 /或循环链表已经足够了。然而,对于有些应用,如果每个链表元素既有指向下一个元素的指针,又有指向前一个元素的指针,那么在设计应用代码时将更为方便。双向链表( doubly linked list)即是这样一个有序的节点序列,其中每个节点都有两个指针:left和right。left指针指向左边节点(如果有) ight指针指向右边节点,r(如果有)。图3 - 9给出了线性表 ( 1 , 2 , 3 , 4 )的双向链表表示。

小结
本节引入了以下重要概念:
• 单向链表 令x 是一个单向链表。当且仅当x.first=0时x 为空。如果x 不为空,则x.first指向链表的第一个节点。第一个节点指向第二个节点;第二个节点指向第三个节点,如此进行下去。最后一个节点的链指针为 0。
• 单向循环链表 它与单向链表的唯一区别是最后一个节点又反过来指向了第一个节点。当循环链表x为空时,x.first=0。
• 头指针 这是在链表中引入的附加节点。利用该节点通常可以使程序设计更简洁,因为这样可以避免把空表作为一种特殊情况来对待。使用头指针时,每个链表(包括空表)都至少包含一个节点(即头指针)。
• 双向链表 双向链表由从左至右按序排列的节点构成。 right 指针用于把节点从左至右链接在一起,最右边节点的 right指针为0。left指针用于把节点从右至左链接在一起,最左边节点的left指针为0。
• 双向循环链表 双向循环链表与双向链表的唯一区别在于,最左边节点的 left指针指向最右边的节点,而最右边节点的 right指针指向最左边的节点。

间接寻址
间接寻址(indirect addressing)是公式化描述和链表描述的组合。采用这种描述方法,可以保留公式化描述方法的许多优点——可以根据索引在
( 1 )的时间内访问每个元素、可采用二叉搜索方法在对数时间内对一个有序表进行搜索等等。与此同时,也可以获得链表描述方法的重要特色——在诸如插入和删除操 作期间不必对元素进行实际的移动。因此,大多数间接寻址链表操作的时间复杂性都与元素的总数无关。
在间接寻址方式中,使用一个指针表来跟踪每个元素。可采用一个公式(如公式( 3 - 1))来定位每个指针的位置,以便找到所需要的元素。
元素本身可能存储在动态分配的节点或节点数组之中。图3-10给出了一个采用间接寻址表 table 描述的5元素线性表。其中 table[i]是一个指针,它指向表中的第i+1个元素,length 是表的长度。
尽管可以使用公式(3-1)来定位指向表中第 i个元素的指针,但这个公式本身并不能直接定位第 i个元素。在对表元素的寻址模式中table提供了一级“间接”引用。
间接寻址对应的代码如下:
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值