线性表(Linear List)
类型定义
线性表是最常用且最简单的一种数据结构。
简单来说,一个线性表就是n个数据元素的有限序列(n≥0 定义为线性表的长度,n=0时为空表)。至于每个数据元素的具体含义,在不同情况下各不相同,它们可以是一个数或一个符号,也可以是一页书,甚至是其他更复杂的信息。
在稍复杂的线性表中,一个数据元素可以由若干个数据项(item)组成。这种情况下,常把数据元素称作记录(record),含有大量记录的线性表又叫文件。
线性表的操作:访问、查找、插入、删除元素等。
线性表的顺序表示及实现(顺序存储结构)
线性表的顺序表示是指用一组地址连续的存储单元依次存储线性表的数据元素。
假设线性表的每个元素需占用m个存储单元,并以所占的第一个单元的存储地址作为数据元素的存储位置。则线性表中第i+1个数据元素的存储位置LOC(a(i+1))和第i个数据元素的存储位置LOC(a(i))之间满足:
LOC(a(i+1))=LOC(a(i))+m
一般来说,线性表的第i个数据元素a(i)的存储位置为 LOC(a(i))=LOC(a(1))+(i-1)×m
其中LOC(a(1))表示线性表的第一个数据元素a(1)的存储位置,通常叫做线性表的起始位置或基地址。
//线性表动态分配顺序存储结构
#define LIST_INIT_SIZE 100
#define LISTINCREMENT 10
typedef struct{
ElemType *elem;
int length;
int listsize;
}SqList;
Status InitList_Sq(SqList &L){
//构造空表
L.elem = (ElemType *)malloc(LIST_INIT_SIZE*typeof(ElemType));
if(!L.elem) exit(OVERFLOW); //存储分配失败
L.length = 0; //空表长度为0
L.listsize = LIST_INIT_SIZE; //初始存储容量
return OK;
}//InitList_Sq
在顺序存储结构的线性表中的某个位置上插入或删除一个数据元素时,其时间主要耗费在移动元素上,对于不同的插入和删除元素的位置,时间也不同。平均来说,在顺序存储结构的线性表中插入或删除一个数据元素,约移动表中的一般元素,时间复杂度为O(n)
线性表的链式表示及实现(链式存储结构、链表)
线性链表
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,即可以连续也可以不连续。因此,为了表示每个数据元素和其后继数据元素之间的逻辑关系,对每个数据元素来说,除了存储其本身的信息之外,还需存储一个指示器直接后继的信息(存储位置)。这两部分组成一个数据元素的存储映像,称为结点(node)。它包含两个域:存放数据元素信息的数据域,和存储直接后继存储位置的指针域。指针域中存储的信息叫做指针或链。n个结点链结成一个链表,即为线性表的链式存储结构。每个结点只包含一个指针域,故又叫线性链表或单链表。
对于线性链表,整个表的存取必须从头指针开始进行(有时我们在单链表的第一个结点之前附设一个结点,称之为头结点。其数据域可以不存储任何信息,也可以存储如线性表长度等类的附加信息,而其指针域存储指向第一个结点的指针。),头指针指示链表中第一个结点的存储位置。同时,由于最后一个数据元素没有直接后继,线性链表最后一个结点的指针为“空”(NULL)。
//线性表的单链表存储结构
typedef struct{
ElemType data;
struct LNode *next;
};LNode, *LinkList;
在顺序表中,任意一个元素的存储可以由线性表的起始位置计算得到。而在单链表中,任何两个元素的存储位置没有固定的联系,取得第i个数据元素的存取位置只能从头指针出发寻找,因此,单链表是非随机存取的存储结构。下面是获得单链表中第i个元素值的代码示例:
Status GetElem_L(LinkList L, int i, ElemType &e){
//L为带头结点 单链表的头指针
//当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR
p = L->next;
j=1;
while (p&&j<i){
p = p->next;
++j;
}
if (!p||j>i) return ERROR;
e = p->data;
return OK:
}//GetElem_L
该算法基本操作是比较j和i并后移指针p,时间复杂度为O(n)。
而对于链表的插入和删除操作,只需要修改对应位置前项结点的指针即可。故其只需找到前一个结点的存储位置,其时间复杂度为O(n)。
单链表和顺序存储结构不同,它是一种动态结构。每个链表占用的空间不需预先分配划定,而是可以由需求即时生成。因此,建立线性表链式存储结构的过程实际就是一个动态生成链表的过程,即从“空表”,依次建立各元素的结点,并逐个插入链表中。下面是一个从表尾到表头逆向建立单链表的算法,时间复杂度为O(n)。
void CreateList_L(LinkList &L, int n){
//逆序输入n个元素的值,建立带表头结点的单链表L
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL; //建立一个带头结点的单链表
for (i = n; i > 0; --i){
p = (LinkList)malloc(sizeof(LNode)); //生成新结点
scanf(&p->data); //输入元素值
p->next = L->next;
L->next = p; //插入到表头
}
}//CreateList_L
静态链表
//线性表的静态单链表存储结构
#define MAXSIZE 1000 //链表的最大长度
typedef struct{
ElemType data;
int cur;
}component, SLinkList[MAXSIZE];
这种静态链表的方法便于在不设“指针”类型的高级程序设计语言中使用链表结构。在如上描述的链表中,数组的一个分量表示一个节点,同时用游标(指示器cur)代替执政指示结点在数组中的相对位置。数组的第零分量可以看成头结点,其指针域指示链表的第一个结点。
这种存储结构仍需要预先分配一个较大的空间,但在做线性表的插入和删除操作时不需移动元素,仅需修改指针,仍有链式存储结构的主要优点。
静态链表的查找与动态链表相似,仅以整型游标i代替动态指针。对于插入和删除操作,需要由用户自己实现malloc和free这两个函数。为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过及被删除的分量用游标链成一个备用的链表,每当进行插入时便可以从备用链表上取得第一个结点作为待插入的新结点;反之,删除时便将从链表上删除下来的结点链接到备用链表上。
循环链表
循环链表(circular linked list)是另一种形式的链式存储结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。由此,从表中任一结点出发均可找到表中的其他结点。
循环链表的操作与线性链表基本一致,差别仅在于算法中的循环条件不是p或p->next是否为空,而是它们是否等于头指针。
双向链表
在上述存储结构中,从某个结点出发只能顺序往后查找其他结点,若要寻查结点的直接前驱,则需从表头出发。换句话说,在单链表中,NextElem的执行时间为O(1),为PriorElem的执行时间为O(n)。
为克服单链表的这种单向性的缺点,可以利用双链表(double linked list)。
//线性表的双向链表存储结构
typedef struct{
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
}DuLNode, *DuLinkList;
双向链表中一些操作如查询,确定表长等仅需涉及一个方向的指针,它们的算法描述和线性链表的操作相同。但在插入,删除时则需要同时修改两个方向上的指针。
后注
上述代码采用类C语言精选C语言的一个核心子集,有修改,为增强可读性。
预定义常量和类型*
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
//Status 是函数的类型,其值是函数结果状态代码
tyoedef int Status;
*数据的存储结构用类型定义(typedef)描述。数据元素类型约定为ElemType。