数据结构之线性结构

转载 2007年09月18日 11:52:00
                                   

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


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

.表(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,当下次有更新的数据加入的时候,如果第一个节点还有位置的花,就加入,否则,看看第一个节点的空间是否已经达到指定的最大值,如果达到了就建立新节点,不然就分配新的空间给第一个节点。

相关文章推荐

数据结构C语言实现——顺序线性表SqList

数据结构与算法是语言学习中的重点与难点。之前没学好,现在特地将严蔚敏的《数据结构》C语言版中的伪代码,予以实现,算作是自己再学习一次。同时希望能帮助到那些刚接触数据结构,对书中内容还不甚理解不知如何动...
  • codebat
  • codebat
  • 2014年08月12日 22:48
  • 2160

数据结构与算法 线性表

  • 2015年03月19日 22:59
  • 2.37MB
  • 下载

2-7-集合运算(A-B)∪(B-A)-线性表-第2章-《数据结构》课本源码-严蔚敏吴伟民版

严蔚敏吴伟民版《数据结构》课本源码第2章线性表第7节求集合(A-B)∪(B-A)。

数据结构 线性表

  • 2015年07月17日 22:21
  • 21KB
  • 下载

数据结构线性表操作

  • 2015年05月22日 14:19
  • 3KB
  • 下载

Java数据结构-线性表之顺序表ArrayList

线性表的顺序存储结构,也称为顺序表,指用一段连续的存储单元依次存储线性表中的数据元素。根据顺序表的特性,我们用数组来实现顺序表,下面是我通过数组实现的Java版本的顺序表。package com.ph...

线性表_数据结构

  • 2015年04月05日 13:33
  • 6.15MB
  • 下载
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:数据结构之线性结构
举报原因:
原因补充:

(最多只允许输入30个字)