【数据结构】 线性表的顺序表

  线性表是一种最为常用的数据结构,包括了一个数据的集合以及集合中各个数据之间的顺序关系。线性表从数据结构的分类上来说是一种顺序结构。在Python中的tuple,list等类型都属于线性表的一种。

  从抽象数据类型的线性表来看,一个线性表应该具有以下这些操作(以伪代码的形式写出):

ADT List:
    List(self)    #表的构造操作,创建一个新表
    is_empty(self)    #判断一个表是不是空表
    len(self)    #返回表的长度
    prepend(self,elem)    #在表的开头加入一个元素elem作为新表头
    append(self,elem)    #在表的末尾加上一个elem
    insert(self,elem,i)    #在指定位置i加上一个元素elem
    del_first(self)    #删除表头元素
    del_last(self)    #删除表尾元素
    del(self,i)    #删除位置i的元素
    search(self,elem)    #查找元素elem在表中出现的位置,如果没找到返回-1
    forall(self,option)    #提供一个遍历接口,对表中每一个元素实行操作option

  另外还可以考虑一些如sort,reserve等等的操作。同时,以上所有操作都是对表本身做出变化,也可以设计一些非变化的操作,即改变并返回的是表的一个副本而原表只作为一个数据源,本身保持不变。

  考虑到计算机内存本身的特点和线性表的各种操作的效率,主要可以考虑线性表的两种基本模型或者说实现方式:

  1. 顺序表,将表元素连续地存放在一片计算机内存中,元素之间的顺序由它们的储存顺序自然表现出来。

  2. 链接表,通过每个元素的储存单元中额外指出下一个元素的方法来实现元素在逻辑上的连续排列。

  下面将按照这两种实现的方式分别说明

顺序表

■  顺序表的基本实现方式

  顺序表的大小通常在创建之时就确定下来(但是不一定一直都是这个大小不能改变,见后动态顺序表的实现方式)。

  顺序表的特点是计算表中任何元素位置的过程都非常简单,是O(1)操作。如果每个元素所需要的储存单元大小都相同为c,那么知道一个元素的下标之后想要计算它的物理地址访问它只需要L = L0+c*i,这是一步简单的单元操作,花费常量时间。如果表中每个元素所需要的储存单元大小不一样的话没有办法这么简单地算出物理地址,但是也不难处理。这牵扯到顺序表的布局方式,一共有两种布局:第一种“基本布局”是就如我们上面所说的,每个储存单元之间都互相相邻挨在一起,此时每个储存单元里都直接储存着元素。第二种布局称为“元素外置布局”,有点像linux中文件系统的架构,在顺序表中储存的都是储存元素单元的物理地址,因为地址大小都是一样的所以顺序表本身可以做到和第一类布局一样通过简单计算得到某个下标的元素,然后再进行一步O(1)的“通过物理地址取内容”的操作来获取到元素。虽然中间隔了一层,但是记录元素物理地址的 这个顺序表在时间上和空间上的开销都不大,所以可以认为这种实现形式也是实现了一般元素组成的顺序表。

  

  顺序表的基本实现形式确定了之后还要根据表需要的操作来进行优化和改造。比如,作为线性表的一个特点,顺序表必须要支持加入/删除元素的操作。因为顺序表的大小是确定的,这就带来一个问题,我该如何确定我加入新元素不会超出顺序表规定的空间。为了有效地解决这个问题和辅助其他很多的操作,在线性表里再加上两个储存单元分别用于存储表的长度和当前表内有多少元素非常有意义。所以,一个顺序表在内存中申请得到的一片区域进一步被分成了三部分,分别是记录表容量个数,记录当前元素个数以及数据储存块。

  进一步思考,内存中这个表一旦创建出来,其大小就确定了,其前后的内存空间也可能被其他数据占据导致我没有办法对当前的顺序表进行扩容或精简。这对于一开始就声明好表大小不再改变的数据结构比如Python中tuple这样的而言可能还好一点,因为它的数据储存部分不用改变,而对于list这样的就很麻烦了。一种很容易想到的解决方案就是在创建之时把表创建的大一点,可能初始化的时候不把表填满,留出一部分空间待以后有可能填进来的元素填充,当元素把这部分空间填满了之后我就申请一块更大的空间,把原来的数据复制过去然后继续填充。这样就可以让更多元素加入进表中。但是重新申请一块空间创建一个新表会使得原来旧表的地址失效,很多引用了旧表的操作也就失效了。这给人感觉是治标不治本的

  为了解决上面这个问题,另一种顺序表的结构被提出,即分离式结构。相对的我们之前说的,脑补的顺序表都是一体式结构。一体式结构中表信息(表长度和当前元素个数)与元素储存区紧邻在一起,这样虽然方便管理,但是当我需要换个更大的空间时会有问题。而分离式结构的顺序表将元素储存区的地址存在表信息后面(和上面元素外置布局方式很有一个意思的感觉),使得元素储存区成为一个可以替换的模块。当当前元素储存区存满,我就可以新申请一片更大的区域,把当前内容复制过去,然后直接把顺序表中指向原元素储存区的指针指向新元素储存区。这样就可以做到在不改变表本身地址的情况下使顺序表增容了。这实际上也是python中list的实现方式。这种储存区满了就换新的而不影响顺序表本身的表是动态顺序表。

  动态顺序表也需要思考策略,比如说每次扩大储存区的容量设置为多少比较合适。一种方案是每次扩大都是多扩大固定个元素,从复杂度的角度来看,每需要换一个储存区复制原有数据需要花时间O(m),m为当时满的储存区中元素个数,计算可以得出对于最终长度为n的一个线性表,复制的总次数大概要在kn**2这个量级,k是个常量参数由每次扩大储存区的大小决定。这看起来比较费时间的原因是更换储存区太过频繁了。有人提出了另一种策略,每次需要扩大储存区时扩大数量为上一次扩大的两倍,这样可以计算证明其从0个元素增加至n个元素所花的时间是O(1)的。后一个策略在操作复杂度上带来优化,但是同时它的空闲单元数最多的时候会占全部的一半,带来了空间上的浪费。这是一个以空间换时间的案例。

■  顺序表的基本操作

  ●  创建空表

  向内存申请一块空间,在开头记录下表的容量max并将元素个数的计数num设置为0。

  ● 简单判断操作

  当num=max是判断表满,当num=0时判断表空

  ●  访问给定下标的元素

  访问下标为i的元素时,先检查i是否在[0,num)中,如果不是的话表明访问越界。如果i合法的话,就计算出相关元素的地址(这个过程可能根据布局方式和结构的不同而不同)。这个操作显然跟表一共有多少个元素没多大关系,所以是个O(1)的操作。

  ●  遍历操作

  按顺序访问表中元素,另外维护一个当前访问元素的下标值,每访问一次该值+1。因为每一次访问都是通过计算地址得到目标元素的,所以每一次访问都是O(1)的,一共n次访问所以最终,遍历操作是O(n)的。

  ●  查找给定元素的位置

  在没有其他信息的情况下,检索一个元素只能通过遍历表来检索。这称为线性检索。遍历每一个元素,判断元素的值是否和给出的值相同,否则继续遍历。

  ●  加入新元素

  加入元素分成好几种情况。每成功加入一个新元素之后,表头维护的num信息应该+1

  1,在尾部加入新元素,先判断当前num是否等于max,如果是的话那就表明我们需要更换数据储存区了。否则就在尾部加上相关元素。这个操作显然是O(1)的,跟表当前有多少元素在没有关系。

  2,把新数据存进储存区的第i个单元,此时在判断完表是否满了之后还要考虑,把新数据写入相关位置之后,原本在那个位置如果有老数据该怎么办。如果不要求插入后的表保持原来的顺序(不保序)的话,那么可以直接把原先那个位置的元素给放到整个表的最后面,这样操作仍然是O(1)。如果要求保序,那么在插入相关位置之后,从 此位置的原元素开始,之后每个元素都要向后移动一位,这么一来使得操作和原表中一共有多少元素就挂钩了。事实上,不论是平均还是最坏情况,保序的插入操作是O(n)的。

  ●  删除元素

  删除其实和插入是类似的,分成尾端删除和定位删除两种情况。和插入也类似的,尾端删除是一个O(1)操作,而定位删除因为要考虑保序和不保序两种情况,也分成了O(1)和O(n)两种情形。

  除此之外,删除还有一种比较特殊的情况,条件删除。即看准某个符合条件的元素进行删除。一般来说进行条件删除也是先要遍历表的,在遍历中加上一个条件判断,总体而言条件删除仍然是一个O(n)的操作。

  总的来说,用顺序表操作有优点也有缺点。一般而言其优点在于它直接按位置访问元素,元素在表中储存得十分紧凑,除了一些占O(1)的辅助信息以外其他的都是有效的元素信息。但是顺序表的缺点也很明显,为了能够放下足够多的元素,通常需要进行储存区更换,而在更换之后又会有大量的空闲单元的出现。

■  Python中的List类型

  前面已经说过了,python中的tuple和list都是顺序表的实现。此外list还是个采用了分离式结构的动态顺序表,保证了在不断加入元素的过程中,表对象的标识(id)不会改变。在python的官方实现中,为了让list能够更有效率地更换储存区,采用了下面这样的储存区更换策略:建立空表或者很小的表的时候,系统默认给出一块可以容纳8个元素的储存区,在元素增加的过程中,如果区满了就换一块4倍大的储存区,以此类推直到表的大小达到5万个元素左右的时候,当区再满的时候就换一块两倍大的储存区。之所以要在5万这个节点上做出这种变化是为了避免出现过多的空闲储存单元。

  对于list的一些主要的操作而言,有下面这几点可以提一下:

  len函数的操作是一个O(1)的操作。因为它只是取了表头信息中的num这个参数做了一些加工而已。

  元素访问和赋值,尾端加入以及尾端删除(包括尾端切片删除)都是O(1)操作

  指定位置的元素加入,切片替换,切片删除,表拼接(extend)都是O(n)操作

  reverse()操作是O(n),而sort()操作,python封装的是最好的排序算法,其平均和最坏的时间复杂度都是O(nlogn)。

  另外,值得指出的一点是,python没有提供接口让程序员可以管理list中每个储存单元的大小,虽然这样设计的初衷是为了减轻编程负担,避免人为的操作引起的错误,但这也无疑在一定程度上限制了使用表的自由。

由于篇幅过长,链接表的内容放在了另一篇笔记中。

时间: 2024-10-01 20:41:03

【数据结构】 线性表的顺序表的相关文章

《数据结构》复习之线性表(顺序表和链表)

线性表的概念 线性表的比较 线性表的数据结构 顺序表的算法操作 双链表的补充 总结 1.线性表的概念 线性表的存储结构有顺序存储结构和链式存储结构两种.前者成为顺序表,后者称为链表. 顺序表: 顺序表就是把线性表中的所有元素按照其逻辑顺序,一次存储到从指定的存储 位置开始的一块连续的存储空间中,如下图所示. 链表 在链表的存储中,每一个节点不仅包含所存元素本身的信息,还包含元素之间的逻辑关系的信息,即前驱节点包含后继节点的地址信息,这样就可以通过前驱节点中的地址信息方便地找到后继节点的位置,如下

[C++] 数据结构(C):线性表之顺序表

1 顺序表 ADT + Status InitList(SeqList &L) 初始化顺序表 + Status GetElement(SeqList L, int i, ElementType &e) (按位)取值  + int LocateElement(SeqList L, ElementType e) (按值)查找  + Status ListInsert(SeqList &L, int i, ElementType e) (按位)插入  + Status ListDelet

数据结构-线性表_顺序表

进入大学一年了,今日终于有勇气写写随笔并展示出来了. 如有不足之处,请大家指正. 今日我想写的就是我对数据结构-线性表_顺序表的理解. 不BB了,进入正题!!!!! 数据结构中的逻辑结构分为线性结构和非线性结构,而线性表就属于线性结构. 线性结构是 n 个数据元素的有序(次序)集合,它有下列几个特征: 集合中必存在唯一的一个 "第一个元素": 集合中必存在唯一的一个 "最后的元素": 除最后元素之外,其它数据元素均有唯一的 "后继": 除第一元素

数据结构-02 _用顺序表解决线性表的编程问题

看到这个标题,相必最先应该只到什么是顺序表,什么是线性表. 线性表(linear list):由n(n>=0)个相同的数据类型的数据元素(结点)a0,a1,a2,...an-1 组成的有限序列. 顺序表:把线性表的结构按照逻辑顺序存放在一组地址连续的存储单元里,用这种方式存储的线性表简称顺序表. 线性表的基本操作: 1.初始化操作 2.插入操作:InsertNode(T a,int i) 在线性表的第i个位置插入一个值为a的新元素,使得原序号为i,i+1,...,n 的数据元素的序号变成i+1,

【算法和数据结构】_17_小算法_线性结构:顺序表

/* 本程序用来测试数据结构中的线性结构:顺序表 */ #include <stdio.h> #include <stdlib.h> #define LINEAR_MAX_SIZE 64 struct LinearList { int* List; //顺序表指针 unsigned short int ListLen; //顺序表最大的元素个数 unsigned short int CurrentLen; //顺序表当前元素的个数 }; typedef struct LinearL

数据结构Java实现02----线性表与顺序表

[正文] 本节内容: 线性结构 线性表抽象数据类型 顺序表 顺序表应用 一.线性结构: 如果一个数据元素序列满足: (1)除第一个和最后一个数据元素外,每个数据元素只有一个前驱数据元素和一个后继数据元素: (2)第一个数据元素没有前驱数据元素: (3)最后一个数据元素没有后继数据元素. 则称这样的数据结构为线性结构. 二.线性表抽象数据类型: 1.线性表抽象数据类型的概念: 线性表抽象数据类型主要包括两个方面:既数据集合和该数据集合上的操作集合. 数据集合: 可以表示为a0,a1,a2,...a

线性链表之顺序表

顺序表中数据元素的存储地址是其序号的线性函数,只要确定了存储顺序表的起始地址(即 基地址),计算任意一个元素的存储地址的时间是相等的,具有这一特点的存储结构称为[随机存储]. 使用的基本数据结构:数组 特点:顺序存取,随机访问. /* Name: SeqList Copyright: 1.0 Author: Johnny Zen Date: 04/06/17 21:51 Description: 线性链表之顺序表 *//* Chinese:顺序(线性)表 English:SeqList*/#in

java数据结构与算法之顺序表与链表深入分析

转载请注明出处(万分感谢!): http://blog.csdn.net/javazejian/article/details/52953190 出自[zejian的博客] 关联文章: java数据结构与算法之顺序表与链表设计与实现分析 java数据结构与算法之双链表设计与实现 ??数据结构与算法这门学科虽然在大学期间就已学习过了,但是到现在确实也忘了不少,因此最近又重新看了本书-<数据结构与算法分析>加上之前看的<java数据结构>也算是对数据结构的进一步深入学习了,于是也就打算

数据结构C#版笔记--顺序表(SeqList)

线性结构(Linear Stucture)是数据结构(Data Structure)中最基本的结构,其特征用图形表示如下: 即:每个元素前面有且只有一个元素(称为"前驱"),同样后面有且只有一个元素(称为"后继")--注:起始元素的前驱认为是空,末尾元素的后继认为也是空,这样在概念上就不冲突了. 线性表(List)是线性结构的一种典型实现,它又可以分为:顺序表(SeqList)和链表(LinkList)二大类. 顺序表(SeqList)的基本特征为:元素在内部存储时

C#线性表之顺序表

线性表是最简单.最基本.最常用的数据结构.线性表是线性结构的抽象(Abstract), 线性结构的特点是结构中的数据元素之间存在一对一的线性关系. 这种一对一的关系指的是数据元素之间的位置关系,即: (1)除第一个位置的数据元素外,其它数据元素位置的前面都只有一个数据元素: (2)除最后一个位置的数据元素外,其它数据元素位置的后面都只有一个元素.也就是说,数据元素是一个接一个的排列.因此,可以把线性表想象为一种数据元素序列的数据结构. 线性表的接口如下所示. public interface I