1. 简介
数据结构是指数据元素的集合以及元素间的相互关系和构造方法。元素之间的相互关系是数据的逻辑结构,数据元素以及元素之间的存储称为存储结构(或物理结构)。
数据结构按照逻辑关系的不同分为现行结构和非线性结构两大类,其中非线性结构可分为树结构和图结构。
算法与数据结构密切相关,数据结构是算法的基础,设计合理的数据结构可以使得算法简单而高效。
2. 线性结构
线性结构是最简单,最基本也是最常用的一种线性结构。常常采用顺序存储和链式存储,主要的基本操作是插入,删除和查找等。
2.1 线性表的定义
一个线性表是n(n≧0)个元素的有限序列(序列表明是有序的),通常表示为(a1,a2,···,an)。非空线性表的特点如下。
- 存在唯一的一个被称为"第一个"的元素。
- 存在唯一的一个被称为"最后一个"的元素。
- 除了第一个元素外,序列中的每个元素均只有一个直接前驱。
- 除了最后一个元素外,序列中的每个元素均只有一个直接后继。
除了上述的特点外线性表还有如下的特点:
一、 线性表中不包含任何元素时称为空表。
二、当前存储的元素数目称为标的长度。
三、 线性表中开始结点称为表头,结束结点称为表尾。
2. 线性表的存储结构
线性表的存储结构分为顺序存储和链式存储。
一、线性表的顺序存储
线性表的顺序存储是指用一组连续的存储单元依次存储线性表的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻,如下图所示。
在这种存储模式下,元素之间的逻辑关系无须占用额外的空间来存储。
一般地,以LOC(a1)表示线性表中的第一个元素的存储位置,在顺序存储结构中,第i个元素的ai的存储位置为
LOC(ai) = LOC(a1)+(i-1)*L
其中,L是表中每个元素所占空间的字节数。根据该计算关系,可随机存取表中任一个元素。
线性表采用顺序存储结构的优点是可以随机存取表中的元素,缺点是插入和删除操作需要移动元素。在插入前要移动元素以挪出空的存储单元,然后再插入元素;删除时同样需要移动元素,以填充被删除的元素空出来的存储单元。
在表长为n的线性表中插入新元素时,共有n+1个插入位置,在位置为1(元素a1所在位置)插入新元素,表中原有的n个元素都需要移动,在位置n+1(元素an所在位置之后)插入新元素时不需要移动任何元素,因此,在等概率下(即新元素在n+1个位置插入的概率相同时)插入一个新元素需要需要移动的元素个数期望值E为n/2。
在表长为n的线性表中的删除元素时,共有n个可删除的元素,删除元素a1时需要移动n-1个元素,删除元素an时不需要移动元素,因此,在等概率下删除元素时需要移动元素的个数期望值E为(n-1)/2。
二、线性表的链式存储
线性表的链式存储是通过指针链接起来的结点来存储数据元素,基本的结点结构如下所示:
其中,数据域用于存储数据元素的值,指针域则存储当前元素的直接前驱或者直接后继的位置信息,指针域中信息称为指针(或链)。
存储各数据元素的结点的地址并不要求是连续的,因此存储数据元素的同时必须存储元素之间的逻辑关系。另外,结点空间只有在需要的时候才申请,无须事先分配。
结点之间通过指针域构成一个链表,若结点中只有一个指针域,则称为线程链表(或单链表),如下图:
设线性表中的元素是整型,则单链表的结点类型的定义为:
typedef struct node{
int data; /*结点的数据域,此处假设为整型*/
struct node *next; /*结点指针域*/
}NODE,*LinkList;
在链式存储结构中,只需要一个指针(称为头指针,如上图中的head)指向第一个结点,就可以顺序地访问到表中的任意一个元素。
在链式存储结构下进行插入和删除,其实质都是对相关指针的修改。在单链表中,若在p所指结点后插入新元素的结点(s所结点,已经生成),如图3-3(a)所示,其基本步骤如下。
(1) s->next = p->next;
(2) p->next = s;
即先将p所指结点的后继结点指针赋给s所指结点的指针域,然后将p所指结点的指针域修改为s所指结点。
同理,在单链表中删除p所指结点的后继结点时(如图3-3(b)所示),步骤如下。
(1) q = p->next;
(2) p->next = p->next->next;
(3) free(q);
即先令临时指针q指向待删除的结点,然后修改p所指结点的指针域为p所指结点的后继的后继结点,从而将元素b所在的结点从链表中删除,最后释放q所指结点的空间。
在实际的应用中,为了简化对链表状态的判断和处理,特别引入一个不存储数据元素的结点,称为头结点,将其作为链表的第一个结点并令头指针指向该结点。
下面给出单链表的查找,插入和删除运算的实现过程。
- 单链表的查找运算
/*L为带头结点单链表的头指针*/
LinkList Fink_List(LinkList L,int k)
/*在表中查找第k个元素,若找到,返回该元素结点的指针;否则,返回空指针NULL*/
{
/*初始时,令P指向第一个元素结点,i为计数器*/
LinkList p; int i; i=1; p=L->next;
/*顺时针链向后查找,直到p指向第k个元素结点或p为空指针*/
while(p&&i<k){
p=p->next;i++;
}
/*存在第k个元素且指针p指向该元素结点*/
if(p&&i==k){
return p;
}
/*第k个元素不存在,返回空指针*/
return NULL;
}/*Find_List*/
- 单链表的插入运算
int Insert_List(LinkList L, int k, int newElem) /*L为带头结点单链表的头指针*/
/*将元素newElem插入表中的第k个元素之前,若成功则返回0,否则返回-1*/
/*该插入操作等同于将元素newElem插入第k-1元素之后*/
{
LinkList p,s; /*p,s为临时指针*/
if(k==1)p==L; /*元素newElem要插入到第1个元素之前*/
else p=Find_List(L,k-1); /*查找表中的第k-1个元素并令p指向该元素结点*/
if(!p) return -1; /*表中不存在第k-1个元素,不满足运算要求*/
s=(NODE*)malloc(sizeof(NODE));/*创建新元素的节点空间*/
if(!s)return -1;
s->data = newElem;
s->next = p->next;p->next = s; /*将元素newElem插入第k-1个元素之后*/
return 0;
} /*Insert_List*/
- 单链表的删除运算
int Delete_List(LinkList L,int k);/*L为带头结点单链表的头指针*/
/*删除表中的第k个元素结点,若成功则返回0,否则返回-1*/
/*删除第k个元素相当于令第k-1个元素结点的指针域指向第k+1个元素所在结点*/
{
LinkList p,q; /*p,q为临时指针*/
if(k==1)p=L; /*删除的是第一个元素结点*/
else p=Find_List(L,K-1);/*查找表中的第k-1个元素并令p指向该元素结点*/
if(!p||!p->next) return -1; /*表中不存在第k个元素*/
q=p->next;/*令q指向第k个元素结点*/
p->next=q->next;
free(q); /*删除结点*/
return 0;
}/*Delete_List*/
当线性表采用链表作为存储结构时,不能对数据元素进行随机访问,但是具有插入和删除操作不需要移动元素的有点。
根据节点中指针域的设置方式,还有其他链表结构:
- 双向链表。每个结点包含两个指针,分别指出当前元素的直接前驱和直接后继。其特点是可以从表中任意的结点出发,从两个方向上遍历链表。
- 循环链表。在单向链表(或双向链表)的基础上令表尾结点的指针指向链表的第一个结点,构成循环表。其特点是可以从表中任意结点开始遍历整个链表。
- 静态链表。借助数组来描述线性表的链式存储结构,用数组元素的下标来表示元素所在的结点指针。
若双向链表中结点的front和next指针域分别表示当前结点的直接前驱和直接后继,则在双向链表中插入结点*s时的指针变化情况3-4所示,其操作过程可表示为:
(1)s->front = p->front;
(2)p->front->next =s; //或者表示为s->front->next=s;
(3)s->next=p;
(4)p->front=s;
在双向链表中删除结点时指针的变化情况如图3-4(b)所示,其操作过程可表示为:
(1)p->front->next = p->next;
(2)p->next->front = p->front;free§;