顺序表


顺序表

在程序中,经常需要将一组(通常是同为某个类型的)数据元素作为整体管理和使用,需要创建这种元素组,用变量记录它们,传进传出函数等。一组数据中包含的元素个数可能发生变化(可以增加或删除元素)。

对于这种需求,最简单的解决方案便是将这样一组元素看成一个序列,用元素在序列里的位置和顺序,表示实际应用中的某种有意义的信息,或者表示数据之间的某种关系。

这样的一组序列元素的组织形式,我们可以将其抽象为线性表。一个线性表是某类元素的一个集合,还记录着元素之间的一种顺序关系。线性表是最基本的数据结构之一,在实际程序中应用非常广泛,它还经常被用作更复杂的数据结构的实现基础。

根据线性表的实际存储方式,分为两种实现模型:

  • 顺序表,将元素顺序地存放在一块连续的存储区里,元素间的顺序关系由它们的存储顺序自然表示。
  • 链表,将元素存放在通过链接构造起来的一系列存储块中。

 

顺序表的基本形式

int 1,2,3,4,5

现在我们要存储这几个连续的整型;
列表,元组,字典等属于python中的高级数据结构,现在我们研究的是存储的本质,所以不用这些高级结构;
这里我们要使用的是内存存储,计算机中的内存时用来存储数据以及和cpu打交道的;

位,比特bit,字节Byte,字之间的关系

字节、字、位、比特之间的关系是:

1位=1bit比特;1Byte字节=8位;1字=2Byte字节;1字=16位。

bit、byte、位、字节、汉字的关系

1 bit = 1  二进制数据
1 byte  = 8  bit
1 字母 = 1  byte = 8 bit
1 汉字 = 2  byte = 16 bit

1. bit:位

一个二进制数据0或1,是1bit;

2. byte:字节

存储空间的基本计量单位,如:MySQL中定义 VARCHAR(45) 即是指 45个字节;
1 byte = 8 bit

3. 一个英文字符占一个字节;

1 字母 = 1 byte = 8 bit

4. 一个汉字占2个字节;

1 汉字 = 2 byte = 16 bit
中文字符和编码有关系的,utf-8编码下,是3个字节;

计算机中的存储单位中最小的单位是——位 bit (比特)(Binary Digits),一个位存放一位二进制数,即 0 或 1,它是计算机存储中最小的存储单位。

顺序表的基本形式

内存的基本单位是1Byte;
内存是一个连续的存储空间;

int a = 1 

在内存中存储时,占了四个存储单元,即占了4*8bit;
一个字符即一个char占一个字节存储单元;
基础数据类型不同,所占用的存储单元也不同;
在存储的时候,如果存储的是数字,那么在取连续的四个存储单元是会当做一个整体取出然后转换为一个数字;如果存储的是一个字符,那么取的时候会当做四个字符取出;
存储数字1的时候,会在内存中存储 00000000 00000000 00000000 00000001;

0x01 00000000
0x02 00000000
0x03 00000000
0x04 00000001

在存储一组数据 1,2,3 时,我们要将这三个数字在内存中连续的存储,以方便取出;
那么就是

0x01 1
0x05 2
0x09 3

这样存储的;
我们可以这样理解,当我们存储一个纯数字列表的时候,存储了列表第一个元素在内存中的初始位置,当我们要按照下标去取数据的时候,因为数据在内存中是连续存储的,假设存储的第一个数据是0x01,那么第二个数据就是 0x0(1+4),第三个数据是 0x0(1+4*2),即按照数据的下标进行数据偏移即可;
所以当一组数据全部是相同数据类型时,我们按照一组数据在内存中紧靠在一起存放,那么这种数据存储形式就叫做顺序表;

 

编程语言中的索引从0开始的原因是存储数据时,变量指向的是第一个元素的内存位置,它的偏移量为0,即索引的0代表的就是偏移量为0;

数据存储的基本布局

当我们要存储一个包含4个整型的列表时,想操作系统申请4个连续的内存,往内存中放置数据;因为是整型,所以一个数据占用四个存储单元;而我们变量中存储的就是第一个元素的内存地址,第一个元素偏移量为0,当我们根据索引获取数据时,就是通过使用第一个元素的内存地址加上索引的偏移量来计算要获取数据的内存地址的。但应该注意,这是基本布局,存储的数据类型应该是一样的,否则因为每个数据类型占用的内存大小不同无法实现这种获取方式;


元素外置的顺序表
因为列表中要存储不同的数据类型,而基本布局是不能实现的,所以要使用元素外置;
元素外置就是,假设一个列表中存储的是4个类型不同的数据,我们还是去申请4个连续的内存,但内存中存储的不再是数据了,而是四个数据的存储地址;存储地址占用的大小为4个字节;
然后取数据就和基本布局取数据的方式差不多了;

 

数据表的结构与实现

数据表的结构

顺序表的结构分为表头区和数据区;
当我们要存储数据时,要向操作系统申请连续的内存,因为要连续,但操作系统并不知道到我们要使用多少的内存大小,所以我们在申请时要先预估存储多少数据并指定申请的数据数量,这样操作系统才能分配给我们连续的内存空间;顺序表的表头区存储的就是 存储限制数量和已存储数量;

一个顺序表的完整信息包括两部分,一部分是表中的元素集合,另一部分是为实现正确操作而需记录的信息,即有关表的整体情况的信息,这部分信息主要包括元素存储区的容量和当前表中已有的元素个数两项。

 

数据表的基本实现方式

怎么把表头区和数据区组合在一起?有两种方式,一体式和分离式;

一体式就是表头区和数据区的地址是连续的;
分离式是指表头区中除了有 存储限制数量和已存储数量之外,还存储有一个指向存储区地址的内存地址;

两种基本实现方式

 

 

图a为一体式结构,存储表信息的单元与元素存储区以连续的方式安排在一块存储区里,两部分数据的整体形成一个完整的顺序表对象。
一体式结构整体性强,易于管理。但是由于数据元素存储区域是表对象的一部分,顺序表创建后,元素存储区就固定了。

图b为分离式结构,表对象里只保存与整个表有关的信息(即容量和元素个数),实际数据元素存放在另一个独立的元素存储区里,通过链接与基本表对象关联。

 

如果采用的是一体式,那么当我们要往存储限制已满的列表中再添加数据时,因为要采用一体式,并且要扩充数据,如果原来只能存储4个数据,那么只能再向操作空间申请一个可以存储2个数据的表头区和大于4个数据的数据区的连续内存了,然后再将原来的4个数据依次移到新内存中,再添加第5个数据,然后表头区也要搬移过去,即整体搬迁,可以看出,这是很不方便的;
而分离式结构,虽然也要重新申请连续内存,但只需要申请数据区的内存,然后将表头区中数据区地址重新指向新的数据区地址即可;
他们之间的主要区别在于,变量中指向的表头区起始地址是否会发生变化,一体式会重定向,而分离式则不需要;
所以我们一般使用的分离式结构比较多;

 

顺序表数据区替换与扩充

替换

一体式结构由于顺序表信息区与数据区连续存储在一起,所以若想更换数据区,则只能整体搬迁,即整个顺序表对象(指存储顺序表的结构信息的区域)改变了。
分离式结构若想更换数据区,只需将表信息区中的数据区链接地址更新即可,而该顺序表对象不变。

扩充

采用分离式结构的顺序表,若将数据区更换为存储空间更大的区域,则可以在不改变表对象的前提下对其数据存储区进行了扩充,所有使用这个表的地方都不必修改。只要程序的运行环境(计算机系统)还有空闲存储,这种表结构就不会因为满了而导致操作无法进行。人们把采用这种技术实现的顺序表称为动态顺序表,因为其容量可以在使用中动态变化。

申请数据内存时需要预估数据量;
当存满了后要扩充数据时要为了减少申请次数,要再申请一部分预留内存,而不是只再额外申请一个数据内存;

扩充的两种策略

  • 每次扩充增加固定数目的存储位置,如每次扩充增加10个元素位置,这种策略可称为线性增长。
  • 特点:节省空间,但是扩充操作频繁,操作次数多。
  • 每次扩充容量加倍,如每次扩充增加一倍存储空间。
  • 特点:减少了扩充操作的执行次数,但可能会浪费空间资源。以空间换时间,推荐的方式。

推荐使用加倍扩充,用空间换时间;

 

 

顺序表的操作-python列表的实现

数据插入分为三种方式

  • 尾部加入数据;直接在尾部加入数据,很简单;
  • 非保序的元素插入,比如能存储8个数据,现有5个数据,我们要插入到索引3,就是将原来第三位的数据放入第6位,再将新数据放入第三位数据;基本不用;
  • 保序的元素插入:比如能存储8个数据,现有5个数据,我们要插入到索引3,就是将索引3及往后的数据全都往后挪一位,再将新数据插入第3位;插入一个数据后,保持原有的数据顺序不同;

删除数据同理

 

增加元素

如图所示,为顺序表增加新元素111的三种方式

 

 

a. 尾端加入元素,时间复杂度为O(1)
b. 非保序的加入元素(不常见),时间复杂度为O(1)
c. 保序的元素加入,时间复杂度为O(n)

 

删除元素

 

 

a. 删除表尾元素,时间复杂度为O(1)
b. 非保序的元素删除(不常见),时间复杂度为O(1)
c. 保序的元素删除,时间复杂度为O(n)

 

Python中的顺序表

  • Python中的list和tuple两种类型采用了顺序表的实现技术,具有前面讨论的顺序表的所有性质。
  • tuple是不可变类型,即不变的顺序表,因此不支持改变其内部状态的任何操作,而其他方面,则与list的性质类似。

 

list的基本实现技术

Python中list采用的是分离式顺序表和元素外置的存储结构;动态顺序表;前期采用四倍扩充,超过5000采用一倍扩充;

Python标准类型list就是一种元素个数可变的线性表,可以加入和删除元素,并在各种操作中维持已有元素的顺序(即保序),而且还具有以下行为特征:

  • 基于下标(位置)的高效元素访问和更新,时间复杂度应该是O(1);

为满足该特征,应该采用顺序表技术,表中元素保存在一块连续的存储区中。

  • 允许任意加入元素,而且在不断加入元素的过程中,表对象的标识(函数id得到的值)不变。

为满足该特征,就必须能更换元素存储区,并且为保证更换存储区时list对象的标识id不变,只能采用分离式实现技术。

在Python的官方实现中,list就是一种采用分离式技术实现的动态顺序表。这就是为什么用list.append(x) (或 list.insert(len(list), x),即尾部插入)比在指定位置插入元素效率高的原因。
在Python的官方实现中,list实现采用了如下的策略:在建立空表(或者很小的表)时,系统分配一块能容纳8个元素的存储区;在执行插入操作(insert或append)时,如果元素存储区满就换一块4倍大的存储区。但如果此时的表已经很大(目前的阀值为50000),则改变策略,采用加一倍的方法。引入这种改变策略的方式,是为了避免出现过多空闲的存储位置。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM