数据结构与算法(线性表)

在程序中,经常需要将一组(通常是同为某个类型的)数据元素作为整体管理和使用,需要创建这种元素组,用变量记录它们,传入传出函数等。线性表就是这样一组元素(的序列)的抽象。一个线性表是某类元素的一个集合,还记录着元素之间的一种顺序关系

线性表是最基本的数据结构之一,在实际程序中应用非常广泛,它还经常被用作更复杂的数据结构的实现基础。Python语言的内置类型list和tuple都可以看做是线性表的实现。

线性表数据结构的实现,需要从两个方面考虑:

1)计算机内存的特点,以及保存元素和元素顺序信息的需要;

2)各种重要操作的效率。例如创建操作、访问、加入和删除元素等。

同时还需要格外考虑的是元素遍历,元素的遍历是依次访问表里的所有(或一批)元素,操作效率与访问元素的个数有关。

基于以上的考虑,人们提出了两种基本的实现模型:

1)将表中元素顺序地放在一大块连续的存储区中,这样实现的表也被称作顺序表。在这种实现中,元素间的顺序关系由它们的存储自然顺序表示。

2)将表中元素存放在通过链接构造起来的一系列存储块中,这样实现的表称为链接表,记为链表。


(一)顺序表

顺序表的基本实现方式很简单:表中元素顺序存放在一片足够大的连续存储区中,首元素(第一个元素)存入存储区的开始位置,其余元素依次顺序存放。元素之间的逻辑顺序关系通过元素在存储区里的物理位置表示(隐式表示元素间的关系)。

总体而言,针对顺序表的存储特点(表中元素--->存储区域),我们需要从两个大的方面考虑问题:

(1)顺序表中元素大小(元素类型)

1、最常见的情况是一个表里保存的元素类型相同,也即表中每个元素所需的存储量相同,可以在表里等距离安排同样大小的存储位置。这种安排可以直接映射到计算机的内存和单元,表中任何元素的位置计算非常简单,存取操作可以在O(1)时间内完成。

2、如果表中的元素大小不统一,按照上面的方案无法通过统一公式计算元素位置。这个时候可以采用另外一种布局方案,将实际元素另外存储,在顺序表里各单元保存对应元素的引用信息(链接,即地址信息)由于每个链接所需的存储量相同,则可以通过公式在常量时间内找到元素链接的存储位置,而又按照链接做一次间接访问,就能得到实际元素。可以对比联想一下引用语义的概念。

(2)顺序表的存储区域大小

线性表的重要性质就是加入/删除元素。也即在表的存续期间,其长度(元素的个数)可能发生变化,但是表元素存储块需要安排在计算机内存里,一旦分配就占据了内存里的一块区域,有了固定的大小(并因此确定了存储,即确定了元素个数的上限)。而且,该块的前后都有可能被其他对象占据,存储块的大小不能随便改变,特别是无法扩充。

一种方案是在建立时,就按确定的元素个数分配存储。显然这种做法适合创建不变的顺序表,例如Python中的tuple。第二种合理的方案是分配一块足以容纳当前需要记录的元素的存储块,还应该保留一些空位,以满足元素的需要,为了保证正确操作,还需要记录元素存储区的大小和当前的元素个数,并且当前元素的个数需要与表中实际元素保持一致。显然这种做法的缺点也是明显的,一是容易浪费存储空间二是存储块依旧可能无法满足逐渐变多的元素。

下面以第二种方案来分析各种实际操作的时(间)空(间)复杂度。

1、在不修改表结构的操作中,一是直接访问,例如访问给定下标i的元素等,都是O(1)的时间复杂度;二是基于一个整形变量,按下标循环并检查和处理,例如遍历操作等,由于这类操作与访问的元素个数有关,复杂度为O(n),n是指表中元素的个数。

2、加入元素的操作分为在表尾添加元素和在表中添加元素。在表尾添加元素时,当表未满时,这时的时间复杂度为O(1)操作,当表满时,操作就会失败(后面会介绍处理这种情况的其他技术);在表中添加元素时,又分为保序操作和非保序操作,如果是非保序,是O(1)操作,如果是保序的就是O(n)操作,由于要求保序时,需要把插入位置i之后的所有元素逐一后移,这种操作的开销与移动元素的个数成正比,一般而言受限于表中元素个数,最坏和平均情况都是O(n)

3、删除元素的操作也分为在表尾删除元素和在表中删除元素。显然,对于尾端删除和非保序定位删除的时间复杂度都是O(1)操作,而保存定位删除的复杂度是O(n),因为可能需要移动一系列的元素。还有一种是基于条件的删除,这种操作也是需要通过循环实现,在循环中逐个检查元素,查找到要删除的元素再删除,因此时间复杂度也为O(1)。

从上面讨论的顺序表结构中可以看到一个顺序表的完整信息包括两个方面:一部分是表中的元素集合另一个是为了实现正确操作而需要记录的信息,即那些与有关表整体情况的信息,而具体的实现时我们主要会考虑元素存储区域的容量和当前表中元素个数两项。

由于表的全局信息只需要常量存储,对于不同的表,这部分的信息规模相同,根据计算机内存的特点,这里可以分为一体式结构和分离式结构

一体式结构是指:将存储表信息的单元与元素存储区以连续的方式安排在一块存储区里,表信息和表元素共同构成完整表信息。

分离式结构是指:其中表对象里只存放整个表有关的信息(容量和元素个数),实际元素存放在另一个独立的元素存储区域里,通过链接(地址)与基本表对象关联,这样表对象的大小统一,不同表对象可以关联不同大小的元素存储区。这样实现的缺点是一个表通过多个(两个)独立的对象实现,创建和管理工作复杂一点。

分离式实现的最大优点是带了一种可能性:可以在标识不变的情况下,为表对象换一块元素存储区,从而实现改变顺序表的存储区域大小。如果采用分离式技术实现,这时可以在不改变对象的情况下换一块更大的元素存储区,使加入元素操作可以正常完成。操作过程如下:

1)另外申请一块更大的元素存储区;

2)把表中已有的元素复制到新的存储区;

3)用新的元素存储区替换原来的元素存储区(改变表对象的元素区链接);

4)实际加入新元素。

经过这几步操作,还是原来的那个表对象,但其元素存储区可以容纳更多元素,而所有使用这个表的地方都不必修改。这样就做出一种可以扩充容量的表,只要程序的运行环境(计算机系统)还有空闲存储,这种表就不会因为满了而导致操作无法进行。人们也把这种技术实现的顺序表称为动态顺序表

在创建一个顺序表时,通常都是从空表或者很小的表出发,通过不断增加元素而构造起来的。如果创建表的时候不能确定最终大小,又需要保证操作的正常进行,采用动态顺序表技术就是最合理的选择。

在Python的官方实现中,list和tuple就采用了顺序表的实现技术,而list就是采用分离式技术实现的动态顺序表。Python的list就是一种元素个数可变的线性表,可以删除和加入元素,在各种操作中维持已有元素的顺序(即保序操作)。

在Python的官方实现中,list实现采用了如下的实际策略:在建立空表(或者很小的表)时,系统分配一块能够容纳8个元素的存储区;在执行插入操作时(insert或append等)时,如果元素区满就换一块四倍大的存储区。但如果当时的表已经很大,系统将改变策略,换存储区时容量加倍。这里的‘很大’是一个实际确定的参数,目前大小为50000。引入后一个策略是避免出现过多空闲的存储位置。

list结构的其他特点也有其顺序表实习方式决定,下面分析list操作的性质:

1、由于其中一般元素的插入和删除操作都是保序的,要移动一些表元素,因此时间复杂度为O(n)时间;

2、len(.)是O(1)操作,因为表中必须记录元素个数,自然可以简单的取用;

3、元素访问和赋值,尾端加入和删除元素(包括尾端切片删除)都是O(1)操作,但是这里需要注意一个问题:动态顺序表后端插入的代价不统一,大多数可以在O(1)时间完成,但也会因为替换存储区域而出现高代价操作。当然,高代价操作的出现很偶然,并随着表的增大而变得稀疏

4、一般位置的元素插入、切片替换、切片删除、表拼接(extend)等都是O(n)操作,pop操作默认删除表尾元素并将其返回,时间复杂度为O(1),一般情况下的pop操作(指定非尾端位置)为O(n)时间复杂度。

5、list的特殊操作是sort,它将对表中的元素进行排序。操作中需要移动表存储区里的元素,有关算法将在以后讨论。最好的排序算法的平均复杂度和最坏情况的时间复杂度都是O(nlogn)。

Python的一个问题是没有提供检查一个list对象的当前存储块容量的操作,也没有设置容量的操作,一切与容量有关的处理都是由Python解释器完成的。这样做的优点是降低编程负担,避免了人为操作可能引入的错误。但这种设计也限制了表的使用方式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值