线性表定义:
零个或者多个数据元素的有限序列。元素之间是有顺序的,如果元素存在多个,则第一个元素无前驱,最后一个元素无后继。其他每个元素都有且只有一个前驱和后继。并且数据元素的类型要相同。
线性表的抽象数据类型:
ADT 线性表(List) Data 线性表的数据对象集合为{a1,a2,...,an},每个元素的类型均为DataType。 其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素。 数据元素之间的关系是一对一的关系。 Operation InitList(*L): 初始化操作,建立一个空的线性表L。 ListEmpty(L): 若线性表为空,返回true,否则返回false。 ClearList(*L): 将线性表清空。 GetElem(L,i,*e): 将线性表L中的第i个位置元素值返回给e。 LocateElem(L,e): 在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号,表示成功;否则,返回0表示失败。 ListInsert(*L,i,e): 在线性表L中的第i个位置插入新元素e。 ListDelete(*L,i,*e): 删除线性表L中第i个位置元素,并用e返回其值。 ListLength(L): 返回线性表L的元素个数。 endADT
对于不同的应用,线性表的基本操作是不同的,上面的操作是最基本的,对于实际问题中涉及的线性表的更复杂的操作,完全可以用这些基本操作的组合来实现。
比如要实现两个线性表的A和B的并集操作A=A并B,也就是说把存在B中但不存在A中的数据元素插入到A中。
/*将所有在线性表Lb中但是不在线性表La中的数据元素插入到L中*/ void union(List *La, List Lb) { int La_len, Lb_len, i; ElemType e;//声明与La和Lb相同的数据元素e La_len = ListLength(La);//求线性表的长度 Lb_len = ListLength(Lb); for(i = 0; i < Lb_len; i++) { GetElem(Lb, i, e);//取Lb中下标为i的元素赋值给e if(!LoacateElem(La, e))//La中不存在和e相同的数据元素 { ListInset(La, ++La_len, e);//插入 }//if }//for }//union()
对于union操作,用到了前面线性表的基本操作ListLength、GetElem、LocateElem、ListInsert等。所以对于复杂的个性化操作,其实就是把基本操作组合起来实现。
线性表的顺序存储结构:
前面说的线性表,它是一种逻辑结构,但是如果要存储到计算机上,几必须了解其物理结构,先看顺序存储结构。
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
线性表的顺序存储的结构代码:
#define MAXSIZE 20 //存储空间初始分配量
typedef int ElemType; //ElemType类型根据实际情况而定,这里假设为int
typedef struct
{
ElemType data[MAXSIZE]; //数组存储数据元素,最大值为MAXSIZE
int length; //线性表当前长度
}SqList;
存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
线性表的最大存储量:数组长度MaxSize。
线性表的当前长度:length。
线性表中的每个元素,都具有一定的数据类型,都需要占用一定的存储单元空间,假设占用的是C个存储单元。那么线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置满足下面关系:
所以,对于第i个数据元素ai的存储位置可以由a1推算出来:
这里可以看出线性表中的任意位置的存取时间都是O(1),通常把具有这一特点的存储结构成为随机存储结构。
顺序存储结构的插入和删除:
首先来实现GetElem获取元素操作:
#define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 typedef int Status;//Status是函数的类型,其值是函数结果状态代码,如OK等 //初始条件:顺序线性表L已存在,0<=i<=ListLength(L) //操作结果:用e返回L中下标为index的数据元素的值 Status GetElem(SqList L, int index, ElemType *e) { if(L.length == 0 || index < 0 || i > L.length) { return ERROR; } *e = L.data[index]; return OK; }
上面已经提到了,获取元素的时间复杂度为O(1)。
下面看线性表的插入和删除操作:
/初始条件:顺序线性表L已存在,0<=index<=ListLength(L) //操作结果:在L中下标为index的位置插入新元素e,L的长度增加1 Status ListInsert(SqList *L, int index, ElemType e) { int k; if(L->length == MAXSIZE)//线性表已经满 { return ERROR; } if(index < 0 || index >= L->length)//当index不在范围之内 { return ERROR; } for(k = L->length - 1; k >= index; k--) { L->data[k+1] = L->data[k]; }//for L->data[index] = e; L->length++; return OK; } //初始条件:顺序线性表已存在,0<= index <=ListLength(L) //操作结果:删除L的下标为index的数据元素,并用e返回其值,L的长度减1 Status ListDelete(SqList *L, int index, ElemType *e) { int k; if(L->length == 0) { return ERROR; } if(index < 0 || index >= L->length) { return ERROR; } *e = L->data[index]; for(k = index; k < L->length - 1; k++) { L->data[k] = L->data[k + 1]; } L->length--; return ERROR; }
线性表的顺序存储结构能快速的访问其中的元素,但是插入和删除时要移动大量的数据,解决这个问题的办法就是线性表的链式存储结构。
线性表的链式存储结构:
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据可以存在内存未被占用的任意位置。
我们把存储数据元素信息的域成为数据域,把存储直接后继位置的域成为指针域。
因为此链表的每个结点中只包含一个指针域,所以叫做单链表。对链表的一个重要的结论就是:永远不要把任何的排序算法用在链表上,这是很傻的行为。
我们把链表中第一个结点的存储位置叫做头指针。那么整个链表的存取就必须是从头指针开始进行了。同时规定线性表的最后一个结点指针为空NULL。
有时,为了更方便的对链表进行操作,会在单链表的第一个结点前增加一个结点,成为头结点。有了头结点,对在第一个元素结点前插入和删除第一结点,其操作和其他结点就统一了。
线性表的单链表存储结构代码:
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList;
单链表的元素结点的读取:
Status GetElem(LinkList L, int index, ElemType *e) { int j; LinkList p; p = L->next;//让p指向链表L的第一个结点,因为有头结点 j = 1; while(p && j < index) { p = p->next; j++; } if(!p || j > index) { return ERROR; } *e = p->data; return OK; }
再来看单链表的插入和删除代码:
//初始条件:顺序线性表L已知,1<=i<=ListLength(L) //操作结果:在L中第i个位置之前插入新的数据元素e,L的长度增加1 Status ListInset(LinkList L, int i, ElemType e) { int j; LinkList p,s; p = L; j = 1; while(p && j < i)//注意这里遍历寻找第i个元素,为了防止线性表短,要判断p { p = p->next; j++; } if(!p || j > i) { return ERROR; } s = (LinkList)malloc(sizeof(Node));//为新的结点分配内存空间 s->data = e; s->next = p->next; p->next = s; return OK; } //初始条件:顺序线性表L存在,1<=i<=ListLength(L) //操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1 Status ListDelete(LinkList L, int i, ElemType *e) { int j; LinkList p,s; j = 1; p = L; while(p->next && j < i)//注意这里遍历寻找第i个元素,为了防止线性表短,要判断p->next { p = p->next; j++; } if(!(p->next) || j > i) { return ERROR; } s = p->next; p->next = s->next; *e = s->data; free(s); return OK; }
那么从上面的分析,显然看出获取结点元素、插入结点、删除结点的时间复杂度都是O(n)。
单链表的创建:
单链表的创建过程就是一个动态生成链表的过程,也就是从空表的初始状态起,依次建立各元素结点,并逐个插入的过程。
//随机产生n个元素的值,建立带表头结构的单链线性表L(头插法) void CreateListHead(LinkList L, int n) { LinkList p; int i; srand(time(0)); L = (LinkList)malloc(sizeof(Node)); L->next = NULL; //建立一个带头结点的链表 for(i = 0; i < n; i++) { p = (LinkList)malloc(sizeof(Node)); p->data = rand() % 100 + 1; p->next = L->next; L->next = p; } } //随机产生n哥元素的值,建立带头结点的单链线性表L(尾插法) void CreateListTail(LinkList L, int n) { LinkList p, r; int i; srand(time(0)); L = (LinkList)malloc(sizeof(Node)); r = L; for(i = 0; i < n; i++) { p = (LinkList)malloc(sizeof(Node));//生成新结点 p->data = rand() % 100 + 1; r->next = p; r = p; } r->next = NULL; }
我们不打算继续使用单链表的时候,要删除它:
//初始条件:顺序线性表L已存在 //操作结果:将L重置为空 Status ClearList(LinkList L) { LinkList p, q; p = L->next; //p指向第一个结点 while(p) { q = p->next; free(p); p = q; } L->next = NULL;//头结点指针域为空 return OK; }