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

数据结构之线性结构(一,表结构)

2011-05-17 22:46 363 查看
原文:
http://lotusroots.bokee.com/5787315.html




数据结构之线性结构(一,表结构)

作者:冲出宇宙

时间:2006-10-24

修改:2006-11-3

转载请注明作者。

  作者主要参考了www.answers.com
上面的资料(因为wikipedia上不去)和部分较新学术论文(一般来自于acm, IEEE和springer),如果有什么疑问,您可以参考以上资料,我会努力的把重要的论文罗列在文章里面。

  本文主要介绍了线性数据结构部分,线性数据的分类来自于wikipedia,见页面: http://www.answers.com/topic/list-of-data-structures


1 线性数据结构的分类


  线性数据结构的分类如下:

.表(List)


.数组(Array)

.位图(Bitmaps)

  .图片(Images)

  .数字高程模型(Heightfield)

.动态数组(Dynamic array)

.平行数组(Parallel array)

.向量(Vector)

.集合(Set)

.链表(Linked list)

  .松散链表(Unrolled linked list)

  .Xor链表(Xor linked list)

.可变表(VList)

.联合数组(Associative array)

.散列表(Hash table)

.跳跃表(Skip list)

.栈(Stack)

.队列(Queue)

.优先队列(Priority queue)

.双向队列(Deque)

.间隙缓冲(Gap buffer)

2 表(List)


  


  表是一个抽象数据结构(ADT, abstract data type,它表示的是一种接口,而不是实现),它表示的是一个有序实体集合。但是,表并没有一个确定的接口。比如,可变的表会包含4种操作:

  1,建立空表;

  2,测试表十分为空;

  3,在前端插入一个数据到表里面去;

  4,返回一个新表,包含了除了第一个元素(也可能是最后一个元素)以外的其它所有元素。

  在现实中,表通常由数组或者链表实现,因为它们都和表具有一些共同点。通常人们会把表当成是链表的别名。序列也是表的另外一个名字,它突出了实体之间的顺序关系。

2.1 数组(Array)

  


  和数组相关的有向量、表(一维数组实现)、
矩阵(二维数组实现),数组是一种最简单的数据结构。数组包含了一系列的数据元素,而且这些元素一般都是同样的大小和类型。数组里面的元素通过索引来访
问,一般的说,索引是一段连续的整数范围,但是,它也可以为任何有序的数值。数组可以是多维的,比如,2维数组。3维或者以上的数组很少被采用。

时间复杂度:

 
 通过索引访问数组极快(o(1)),但是,从数组里面插入或者删除一个数据的代价很高(o(n)),特别是删除数据可能会造成数组空闲太多,和插入数据
造成数组空间不够。这些可以利用动态数组来解决。链表虽然插入或者删除数据较快,可是访问其中的元素十分慢(o(n))。

空间复杂度:

  数组是最不浪费内存的数据结构,比较起来散列表是十分浪费内存的。数组不占用任何额外的空间(最多增加一个保存数组的大小,4字节)。

程序:

  大部分程序都内置数组类型。

2.1.1 位图(Bitmap)

  


  位图其实是一个数组,数组的每个元素都是布
尔值(0/1)。常常使用字节数组来表示位图,因为每个字节可以表示8个位图的元素。位图最常用在空间处理上,比如,磁盘分配。根据位图的个性,所有用来
表示是和否的地方都可以使用它。因为一个字节的位图可以表示8个是和否,所以,它也常常用来压缩数据。不过,访问一位比访问一个字节会慢很多,访问一个字
节比访问一个int会慢很多(如果32位机器)。

2.1.1.1 图片(Images)

 
 图片也叫数字图片。图片是一个2维结构,每个元素对应于图片上的某点。每个元素的大小根据其要显示的效果而变化,主要有1位,8位,16位,24
位,32位几种。根据显示色彩的不同,一般可以分为黑白、灰度、彩色(8位,16位,24位,32位)、抖动色这几种。一般的图片都由很多像素组成,所
以,一个图片占用的空间十分大。一般情况下,压缩是必须的。最常见的几种压缩格式为:gif(lzw压缩)、png(未知)、jpg(DCT压缩)、
jpg2000(小波压缩)。

2.1.1.2 数字高程模型(Heightfield 也叫:Digital Elevation Model, or DEM)

  
DEM也是一个位图,只是,它每个点表示的意思是高度。



  上图是根据dem图还原出来的火星地图。下图是一个加上了色彩的DEM图。



2.1.2 动态数组(Dynamic Array)

  它同时的名字还有:可增数组(growable array), 可变长数组(resizable array), 动态表(dynamic table), 数组表(array list)。它是一种能够自动调整自己大小的数组。

 
 当加入一个新数据时,如果动态数组没有空间了,它会申请一个新的空间,然后把旧数据全部拷贝过去,释放旧空间(有些动态数组的实现并不会拷贝旧数据过
去,也不会释放旧空间)。一般的时候,新分配的空间大小都是原来空间大小的一个倍数,保证有一部分空间是空闲的。简单的计算一下,就能发现加入一个数据的
平均花销是o(1)。同样的道理,删除一个数据的时候,如果空闲的空间太多了,动态数组也可能申请一个新空间,然后删除旧空间。

申请新空间时,申请多大的空间是一个值得考虑的问题。目前来说,一般认为申请的新空间为旧空间的1.4-4倍之间都是合适的,最常见的是2倍。

在浪费空间上面,有人证明了至少需要浪费o(n^1/2)这么多空间才能保证插入和删除都在常数时间内完成。

Andrej
Brodnik, Svante Carlsson, Erik D. Demaine, J. Ian Munro, and Robert
Sedgewick. Resizable Arrays in Optimal Time and Space (1999). Workshop
on Algorithms and Data Structures, pp.37–48

  动态数组还有很多变形,比如,为了加快删除的速度,可以把开始的数据放到中间(如图):

  


  这里,第一个数据放到数组的中间,第i个数据放到(i+n/2)%n的位置。当删除数据的时候,
最多只需要移动n/2次就行了。当然了,需要保存一个开始的位置和实际的数组长度。比如,删除掉3,得到:5 6 7 1 2 4 *,删除掉7得到:*
5 6 1 2 4 *。

2.1.3 平行数组(Parallel Array)

  平行数组最初是为了在不支持记录(对象)的环境下面使用的。它把一个记录切分成多个基本数组,每个数组的长度一样。比如,

  struct Info{

    int   age;

    char*  name;

  }

  Info person[2];

  我们可以使用平行数组表示为:

  int ages[] = {1,2};

  char *names[] = {"good","zzp"};

 
 平行数组拥有的结构简单,访问速度快速,同时,还能够节省结构可能需要使用的指针(某些语言需要指针,某些不需要。比如,c语言不需要指针,而java
语言需要指针)。但是,它的最大缺点是当记录含有很多域的时候,平行数组将变得极难维护,同时,对它的顺序访问并不会实际问顺序的位置,对基于访问局部性
的缓冲策略是一大妨碍。

2.1.4 向量(Vector)

  向量是一个十分基本的动态数组,它具有和动态数组一样的性质。

2.1.5 集合(Set)

 
 集合是包含了一系列的数据,这些数据没有经过排序而且也没有重复的数据。它比较严格的对应于数学上的集合的概念。集合必须是有限的,这点和数学上的集合
不同。集合一般来说必须支持以下几种操作:1) 判断一个元素是否在集合里面;2) 对集合的所有元素进行遍历;3)
2个集合之间的交和并操作。虽然联合数组是通常的建立集合的数据结构,但是,它并不能很好的支持集合之间的交并操作。

2.1.6 链表(Linked list)

 
 链表是计算机科学里面最基础的数据结构之一。它包含一系列的节点,每个节点含有任意多个的域和一个或者两个的指针。指针可能指向前面一个节点也可能指向
后面一个节点。增加或者删除一个指定节点都是常数时间,但是随机访问一个节点的代价是o(n)。常用的3种链表为:单链表、双向链表和循环链表。见图。



  单链表是最简单的链表形式,每个节点有一个指针,指向后一个节点。最后的那个节点的指针指向null。它访问任何节点都必须从头开始,一步一步的走到待访问的节点。

  双向链表是一个复杂一点的结构,它的每个节点包含2个指针,一个指向前一个节点,一个指向后一个节点。同样,第一个节点的前向指针指向null,最后那个节点的后向指针指向null。它可以从头遍历也可以从尾遍历。

  循环链表把第一个节点和最后一个节点链接了起来。它可以在单向链表或者双向链表的基础上构建。第一个节点的前向指针指向最后的那个节点,而最后那个节点的后向指针指向第一个节点。

  哨兵节点(Sentinel Nodes)是一个额外的未使用的节点,它常常在链表的开头或者结尾。它用来加快或者简化某些计算的过程。



2.1.6.1 松散链表(unrolled linked list)

  链表的最大问题是访问非聚集(即连续的访问并不是访问连续的内存空间),松散链表的主要目的是为了解决这个问题(从而显著的提高缓冲性能)。

松散链表改进了普通链表的结构,现在每个节点可以包含多个数据。每个节点包含多个元素。其基本结构为:

  struct Node{

    Node  next;     //下一个节点

    int   maxElement; // 节点包含的最大元素个数

    Array  Elements;  // 本节点包含的元素个数

}

 
 链表里面的每个节点对应一个元素,而松散链表每个节点可以包含最多maxElements个元素。从其定义可以看出,节点可以包含少于
maxElement个元素。那么,我们需要一个规则来决定如何包含元素到节点里面,同时尽量保证节点的elements数组空间利用率高。基本的松散链
表使用的是1-2规则,也就是说,如果节点里面包含的元素个数将大于可以包含的元素个数,那么,就把这个节点分裂成2个节点,而如果这个节点包含的元素数
将小于maxElement/2,就从邻近的节点借一些元素过来,如果邻近的节点在借给它之后,拥有的节点数小于maxElement/2的话,就把这2
个节点合并成一个节点。总之,1-2规则就是保证(只有一个节点例外)每个节点至少空间利用在1/2。按照这个规律,最常使用的还有2-3规则和3-4规
则。再大一些的规则就十分难以控制了,因为笔者曾经写过3-4规则的B*树,代码的复杂程度让我不敢重新再看一遍了。

  至于空间性能方面,每个
节点至少有1/2的空间利用率,在平衡的情况下,每个节点的利用率应该是(1+1/2)/2=3/4。但是,如果我们的插入和删除仅仅发生在表头和表尾的
话,那每个节点就几乎都是满的了。假设每个节点都是满的,链表总共表示了N个元素,每个元素的空间占用为e,同时,每个节点的指针和计算开销为r,松散链
表里每个节点最大可以包含的元素为M个,那么,基本链表占用的空间为:(e+r)*N,而松散链表的空间占用为:(e*M+r)*(N/M)。

  在时间性能方面,显然查找一个元素,基本的链表需要花费o(n)的时间,而松散链表为o(n/M);

2.1.6.2 XOR链表(XOR linked list 或者zip链表,拉链式链表)

 
 双向链表虽然每个节点只包含了2个指针,可是在内存紧张的地方(比如手机或者单片机)还是占用了太多空间。于是,Xor链表就被设计出来减少空间的占
用,它使用一个值(前向指针 xor 后向指针)来同时保存前向指针和后向指针。传统的双向链表中,每个节点需要2个指针来指向前一个节点和后一个节点:

   A   B   C   D

  <--|   <--|  <--|   <--|

  |-->   |-->  |-->   | -->

  xor链表试图把节点的前后指针合并起来(利用xor操作):

 .    A      B     C     D   *

   <- . xor B -> <- A xor C -> <- B xor D -> <- C xor * ->

  xor满足如下性质: X xor ( X xor Y) = Y;

  这样,只要知道了"."的值,就能够从前往后遍历整个链表,只要知道了"*",就能够从后往前遍历节点。这种方式类似于拉拉链,所以,有人把它称为zip链表。



  为什么这种操作可行呢?因为xor的性质。xor有如下几个性质:1,m xor 0 = m;
2, m xor m = 0; 3, m xor n = n xor m; 于是,就有了m xor (m xor n) =
n。在给定一个后向指针的初始值m的时候,我们就能够根据上面的这个式子一步一步的得到下一个节点的指针。

xor的性质的另外一个运用就是swap函数。swap函数的版本有很多,但是,最简单的还是下面的这种方式:

  swap(x,y)

  {

   x = x xor y;

   y = x xor y;

   x = x xor y;

  }

  建议不使用xor链表,因为它浪费计算时间及影响缓存性能。如果真的要节省空间的话,推荐使用松散链表。

2.2 可变表(VList)


  


  VList的显然是基于松散表的,它的每个节点都包含多个元素。下面是一个VList的例子:

 *  ---->  *

  200    1000

  130

  这里有3个元素,为1000,130,200。共2个节点,第一个节点可以最多包含2个元素,第2个最多可以包含一个元素。那么,在删除200的时候,VList会变成:

   *   --->  *

   .       1000

  130

  这里,表示第一个节点的第一个元素为空。

 
 简单的说,VList包含多个节点,除了第一个节点以外,其他第i个节点包含的空间是第i+1个节点的r倍。r是固定的,一般可以为2。每个节点包含的
还有下一个节点的指针及其有效数据的开始位置。同时,它还包含本节点的数组长度,和当前数据的最大的使用位置。VList的填充方向是从下到上。



  VList具有如下性质:

1,查找List的第i个元素,花销是o(1);

2,计算List的长度,花销是o(logN);

3,在List头加入数据的花销是o(1);

  其中,在List的头加入数据是这样操作的:

1,如果第一个节点还有位置,直接放进去;

2,如果第一个节点没有位置了,建立一个新节点,空间大小为某个值,然后加入新数据。

3,当下次有更新的数据加入的时候,如果第一个节点还有位置的花,就加入,否则,看看第一个节点的空间是否已经达到指定的最大值,如果达到了就建立新节点,不然就分配新的空间给第一个节点。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: