一、线性表的定义
1、线性表:零个或多个具有相同类型的数据元素的有限序列。元素之间是有顺序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继;且线性表强调是有限的。
2、线性表数学定义:若将线性表记为(a1,...,ai-1,ai,ai+1,...,an),则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。当i=1,2,...,n-1时,ai有且仅有一个直接后继,当i=2,3,...,n时,ai有且仅有一个直接前驱,如下图。所以线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表。
3、在非空表中的每个数据元素都有一个确定的位置,比如ai是第i个数据元素,称i为数据元素ai在线性表中的位序。
4、在较复杂的线性表中,一个数据元素可以由若干个数据项组成。
5、线性表的每个数据元素的类型都相同。
二、线性表的顺序存储结构
1、线性表的顺序存储结构指的是用一段地址连续的存储单元依次存储线性表的数据元素。如下图。所以可以用C语言的一维数组来实现顺序存储结构。
2、若采用数组来实现线性表的顺序存储结构,则数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的;线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。在任意时刻,线性表的长度应该小于等于数组的长度。
3、地址计算方法:线性表不像数组那样起始下标是0,而是1。如下图。
假设线性表每个数据元素占用的是c个存储单元,所以对于第i个数据元素ai的存储位置可以由a1推算得出,如下图,其中LOC表示获得存储位置的函数。通过这个公式,可以算出线性表中任意位置的地址,不管是第一个还是最后一个都是相同的时间,所以对每个线性表位置的存入或取出数据,对于计算机来说都是相等的时间,也就是一个常数,所以用算法的时间复杂度来说,它的存取时间性能为O(1)。通常把具有这一特点的存储结构称为随机存取结构。
4、采用C语言的一维数组来实现线性表的顺序存储结构,定义结构代码如:
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int Status; /* Status是函数的返回类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType; /* ElemType类型根据实际情况而定,这里假设为int */
typedef struct
{
ElemType data[MAXSIZE]; /* 数组,存储数据元素 */
int length; /* 线性表当前长度 */
}SqList;
5、获得元素操作:获取线性表的第i个位置的元素,就是把数组第i-1下标的值返回即可。实现代码如下:
/* 初始条件:顺序线性表L已存在,1≤i≤当前线性表的长度 */
/* 操作结果:用e返回L中第i个数据元素的值,注意i是指位置,第1个位置在数组中是从0开始 */
Status GetElem(SqList L, int i, ElemType *e)
{
if(L.length == 0 || i < 1 || i > L.length)
{
return ERROR;
}
*e = L.data[i-1];
return OK;
}
6、插入操作:线性表的顺序存储结构在插入数据时的实现过程如下图:
插入算法的思路:
1)如果插入位置不合理,抛出异常;
2)如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
3)如果插入位置不在表尾,从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
4)将要插入元素填入位置i处;
5)表长加1。
实现代码如下:
/* 初始条件:顺序线性表L已存在,1≤i≤当前线性表的长度+1*/
/* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */
Status ListInsert(SqList *L, int i, ElemType e)
{
int k;
if (L->length == MAXSIZE) /* 顺序线性表已经满 */
{
return ERROR;
}
if (i < 1 || i > L->length + 1)/* 当i比第一位置小或者比最后一位置后一位置还要大时 */
{
return ERROR;
}
if (i <= L->length) /* 若插入数据位置不在表尾 */
{
for(k = L->length-1; k >= i-1; k--) /* 将要插入位置之后的数据元素向后移动一位 */
{
L->data[k+1] = L->data[k];
}
}
L->data[i-1] = e; /* 将新元素插入 */
L->length++;
return OK;
}
7、删除操作:线性表的顺序存储结构删除元素过程如下图
删除算法的思路:
1)如果删除位置不合理,抛出异常;
2)取出删除元素;
3)如果删除元素不在表尾,从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
4)表长减1。
实现代码如下:
/* 初始条件:顺序线性表L已存在,1≤i≤当前线性表的长度 */
/* 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1 */
Status ListDelete(SqList *L, int i, ElemType *e)
{
int k;
if (L->length == 0) /* 线性表为空 */
{
return ERROR;
}
if (i < 1 || i > L->length) /* 删除位置不正确 */
{
return ERROR;
}
*e = L->data[i-1];
if (i < L->length) /* 如果删除不是最后位置 */
{
for(k = i; k < L->length; k++)/* 将删除位置后继元素前移 */
{
L->data[k-1] = L->data[k];
}
}
L->length--;
return OK;
}
8、插入和删除的时间复杂度:最好情况是若元素插入到表尾,或删除最后一个元素,则时间复杂度为O(1),因为不需要移动元素。最坏情况就是元素要插入到第一个位置或删除第一个元素,则意味着要移动所有的元素向后或向前,所以时间复杂度为O(n)。至于平均的情况,由于元素插入到第i个位置,或删除第i个元素,需要移动n-i个元素,根据概率原理,每个位置插入或删除元素的可能性是一样的,也就是说位置靠前,移动元素多,位置靠后,移动元素少,但是最终平均移动次数和最中间的那个元素的移动次数相等,为(n-1)/2,所以,平均时间复杂度还是O(n)。所以:线性表的顺序存储结构,在读数据时,不管是哪个位置,时间复杂度都是O(1);而插入或删除时,时间复杂度都是O(n)。
9、线性表的顺序存储结构优缺点如下图。
三、线性表的链式存储结构
1、线性表链式存储结构定义:用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着这些数据元素可以存在内存未被占用的任意位置。除了要存数据元素信息外,还要存储它的后继元素的存储地址。
2、为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息,把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称为指针或链。这两部分信息组成数据元素ai的存储映像,称为结点。
3、n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,...an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起。如下图。
4、把链表中第一个结点的存储位置叫做头指针,整个链表的存取就必须是从头指针开始进行。
5、由于最后一个元素没有后继,所以线性链表的最后一个结点指针为空。
6、为了操作方便,可以在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息,其指针域存储指向第一个结点的指针。如下图。
7、头指针与头结点的异同,如下图。
8、单链表的存储示意图。
带有头结点:
空链表:
9、用C语言的结构指针来描述单链表,如。
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */
struct Node
{
ElemType data;
struct Node *next;
};
typedef struct Node *LinkList; /* 定义LinkList */
10、单链表的读取:获取链表第i个数据元素的算法思路:
1)声明一个结点p指向链表第一个结点,初始化j从1开始;
2)当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
3)若到链表末尾p为空,则说明第i个元素不存在(链表为空或者当链表不为空时i大于链表长度);或j>i,说明第i个元素不存在(i小于1);
4)否则查找成功,返回结点p的数据。
实现算法代码如下:
/* 初始条件:链式线性表L已存在,1≤i≤链式线性表的长度 */
/* 操作结果:用e返回L中第i个数据元素的值 */
Status GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p; /* 声明一结点p */
p = L->next; /* 让p指向链表L的第一个结点 */
j = 1; /* j为计数器 */
while (p && j < i) /* p不为空或者计数器j还没有等于i时,循环继续 */
{
p = p->next; /* 让p指向下一个结点 */
++j;
}
if ( !p || j > i )
{
return ERROR; /* 第i个元素不存在 */
}
*e = p->data; /* 取第i个元素的数据 */
return OK;
}
11、单链表的读取时间复杂度:此算法的时间复杂度取决于i的位置,当i=1时,不需要遍历,第一个就取出数据了,而当i=n时则遍历n-1次才可以。所以最坏情况的时间复杂度为O(n)。
12、单链表的插入:插入示意图如下:
只需执行s->next=p->next;p->next=s;即可,但是这两句代码的执行次序可不能改变。 对于带有头节点的的单链表的表头和表尾插入的情况如下图:
单链表第i个数据插入节点的算法思路:
1)声明一结点p指向链表头结点,初始化j从1开始;
2)当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
3)若到链表末尾p为空,则说明第i个元素不存在(i 大于当前单链表长度+1,包括第i-1个元素也不存在);
4)否则查找成功,在系统中生成一个空结点s;
5)将数据元素e赋值给s->data;
6)单链表的插入标准语句s->next=p->next;p->next=s;
7)返回成功。
实现算法代码如下:
/* 初始条件:链式线性表L已存在,1≤i≤链式线性表的长度+1*/
/* 操作结果:在L中第i个位置之前插入新的数据元素e*/
Status ListInsert(LinkList *L, int i, ElemType e)
{
int j;
LinkList p, s;
p = *L;
j = 1;
while (p && j < i) /* 寻找第i个结点 */
{
p = p->next;
++j;
}
if (!p || j > i)
{
return ERROR; /* 第i个元素不存在 */
}
s = (LinkList)malloc(sizeof(struct Node)); /* 生成新结点(C语言标准函数) */
s->data = e;
s->next = p->next; /* 将p的后继结点赋值给s的后继 */
p->next = s; /* 将s赋值给p的后继 */
return OK;
}
13、单链表的删除:删除示意图如下:
删除q节点需要执行的操作就是p->next=p->next->next,即:q=p->next;p->next=q->next;
单链表删除第i个数据结点的算法思路:
1)声明一结点p指向链表头结点,初始化j从1开始;
2)当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
3)若到链表末尾p->next为空,则说明第i个元素不存在;
4)否则查找成功,将欲删除的结点p->next赋值给q;
5)单链表的删除标准语句p->next=q-next;
6)将q结点中的数据赋值给e;
7)释放q结点;
8)返回成功。
实现算法代码如下:
/* 初始条件:链式线性表L已存在,1≤i≤链式线性表的长度 */
/* 操作结果:删除L的第i个数据元素,并用e返回其值*/
Status ListDelete(LinkList *L, int i, ElemType *e)
{
int j;
LinkList p,q;
p = *L;
j = 1;
while (p->next && j < i) /* 遍历寻找第i个元素 */
{
p = p->next;
++j;
}
if (!(p->next) || j > i)
{
return ERROR; /* 第i个元素不存在 */
}
q = p->next;
p->next = q->next; /* 将q的后继赋值给p的后继 */
*e = q->data; /* 将q结点中的数据给e */
free(q); /* 让系统回收此结点,释放内存 */
return OK;
}
14、单链表插入和删除的时间复杂度:都是由两部分组成,第一是遍历查找第i个元素,第二是插入或删除元素;它们的时间复杂度都是O(n)。若从第i个位置插入10个元素,对于顺序存储结构的线性表意味着每一次插入都需要移动n-i个元素,每次都是O(n),但是对于单链表而言只需要在第一次找到第i个位置的指针,仅此为O(n),接下来只是简单的通过赋值移动指针,时间复杂度都是O(1),所以对于插入和删除数据越频繁的操作,单链表的效率优势就越明显。
15、单链表的整表创建:对于单链表而言它所占用空间的大小和位置不像顺序线性表,是不需要预先分配划定的,所以创建单链表的过程就是一个动态生成链表的过程,其整表创建的算法思路如下:
1)声明一结点p和计数器变量i;
2)初始化一空链表L;
3)让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
4)循环:生成一新结点赋值给p;随机生成一数字赋值给p的数据域p->data;将p插入到头结点与前一新结点之间;
实现算法代码如下:
/* 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0)); /* 初始化随机数种子 */
*L = (LinkList)malloc(sizeof(struct Node));
(*L)->next = NULL; /* 先建立一个带头结点的单链表 */
for (i=0; i<n; i++)
{
p = (LinkList)malloc(sizeof(struct Node)); /* 生成新结点 */
p->data = rand()%100+1; /* 随机生成100以内的数字 */
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(struct Node)); /* L为整个线性表 */
r = *L; /* r为指向尾部的结点 */
for (i = 0; i < n; i++)
{
p = (LinkList)malloc(sizeof(struct Node)); /* 生成新结点 */
p->data = rand()%100+1; /* 随机生成100以内的数字 */
r->next = p; /* 将表尾终端结点的指针指向新结点 */
r = p; /* 将当前的新结点定义为表尾终端结点 */
}
r->next = NULL; /* 表示当前链表结束 */
}
16、单链表整表的删除:算法思路如下:
1)声明一结点p和q;
2)将第一个结点赋值给p;
3)如果p不为空,将下一结点赋值给q;释放p;将q赋值给p;循环
实现算法代码如下:
/* 初始条件:链式线性表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;
}
17、单链表结构与顺序存储结构优缺点:如下图
四、静态链表
1、用数组来代替指针来描述单链表,让数组的元素都是由两个数据域组成,data和cur,其中数据域data用来存放数据元素,数据域cur相当于单链表中的next指针,存放该元素的后继在数组中的下标,把这种用数组描述的链表叫做静态链表,这种描述方法还有起名叫游标实现法。
2、线性表的静态链表存储结构定义如下:
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 1000 /* 存储空间初始分配量 */
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef char ElemType; /* ElemType类型根据实际情况而定,这里假设为char */
/* 线性表的静态链表存储结构 */
struct Node
{
ElemType data;
int cur; /* 游标(Cursor) ,为0时表示无指向 */
};
typedef struct Node StaticLinkList[MAXSIZE];
3、另外,对数组第一个和最后一个元素作为特殊元素处理,数据域不存数据。通常把未被使用的数组元素称为备用链表。而数组第一个元素的cur存放备用链表的第一个结点在数组中的下标;而数组的最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点的作用,当整个链表为空时,则为0。如下图即为初始化的数组状态。
4、初始化数组,代码如下:
/* 将一维数组space中各分量链成一个备用链表,space[0].cur为头指针,"0"表示空指针 */
Status InitList(StaticLinkList space)
{
int i;
for (i = 0; i < MAXSIZE-1; i++)
{
space[i].cur = i+1;
}
space[MAXSIZE-1].cur = 0; /* 目前静态链表为空,最后一个元素的cur为0 */
return OK;
}
5、如果静态链表中已有数据了,其图示图下图。
6、静态链表要解决的问题:如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放。在动态链表中,结点的申请和释放使用的是malloc()和free()函数来实现,而在静态链表中操作的是数组,不存在像动态链表的结点申请和释放问题,所以需要手动实现这两个函数。首先是申请,为了辨别数组中哪些分量未被使用,解决的办法是将所有未被使用过的以及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点,实现代码如下:
/* 若备用空间链表非空,则返回分配的结点下标,否则返回0 */
int Malloc_SSL(StaticLinkList space)
{
int i = space[0].cur; /* 当前数组第一个元素的cur存的值 */
/* 就是要返回的第一个备用空闲的下标 */
if (space[0]. cur)
space[0]. cur = space[i].cur; /* 由于要拿出一个分量来使用了, */
/* 所以我们就得把它的下一个 */
/* 分量用来做备用 */
return i;
}
释放:
/* 将下标为k的空闲结点回收到备用链表 */
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur; /* 把第一个元素的cur值赋给要删除的分量cur */
space[0].cur = k; /* 把要删除的分量下标赋值给第一个元素的cur */
}
7、静态链表的插入操作:实现代码如下:
首先是求静态链表的数据元素个数函数:
/* 初始条件:静态链表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(StaticLinkList L)
{
int j = 0;
int i = L[MAXSIZE-1].cur;
while(i)
{
i = L[i].cur;
j++;
}
return j;
}
然后是插入操作实现函数:
/* 在L中第i个元素之前插入新的数据元素e */
Status ListInsert(StaticLinkList L, int i, ElemType e)
{
int j, k, l;
k = MAXSIZE - 1; /* 注意k首先是最后一个元素的下标 */
if (i < 1 || i > ListLength(L) + 1)
{
return ERROR;
}
j = Malloc_SSL(L); /* 获得空闲分量的下标 */
if (j)
{
L[j].data = e; /* 将数据赋值给此分量的data */
for(l = 1; l <= i - 1; l++) /* 找到第i个元素之前的位置 */
{
k = L[k].cur;
}
L[j].cur = L[k].cur; /* 把第i个元素之前的cur赋值给新元素的cur */
L[k].cur = j; /* 把新元素的下标赋值给第i个元素之前元素的cur */
return OK;
}
return ERROR;
}
比如在上图的乙后面插入丙,结果如下图。
8、静态链表的删除操作:代码如下
/* 删除在L中第i个数据元素 */
Status ListDelete(StaticLinkList L, int i)
{
int j, k;
if (i < 1 || i > ListLength(L))
{
return ERROR;
}
k = MAXSIZE - 1;
for (j = 1; j <= i - 1; j++)
{
k = L[k].cur;
}
j = L[k].cur;
L[k].cur = L[j].cur;
Free_SSL(L, j);
return OK;
}
比如继续在上图的结果中删除甲,结果如下:
9、静态链表的优缺点:总的来说,静态链表其实是为了给没有指针的高级语言设计的一种实现单链表能力的方法。其优缺点如下图。
五、循环链表
1、循环链表定义:将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
2、为了使空链表与非空链表处理一致,通常设一个头结点,但这头结点并不是必要的,循环链表带有头结点的空链表和非空链表如下图。
3、循环链表和单链表的主要差异在于循环的判断条件上,单链表是判断p->next是否为空,循环链表是判断p->next不等于头结点,则循环未结束。
4、在单链表中采用头结点时,可以用O(1)的时间访问第一个结点,用O(n)的时间访问到最后一个结点;采用指向终端结点的尾指针来表示循环链表时可以用O(1)的时间由链表指针访问到最后一个结点,如下图。
5、尾指针应用举例:将两个循环链表合并成一个表,要合并的两个循环链表如下图。
要合并它们只需要执行以下程序代码:
p=rearA->next; /*保存A表的头结点*/
rearA->next=rearB->next->next;/*将本是指向B表的第一个结点(不是头结点)赋值给rearA->next*/
rearB->next=p; /*将原A表的头结点赋值给rearB->next*/
free(p);
示意图如下:
六、双向链表
1、双向链表定义:双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
2、双向链表也可以是循环表,如下图是双向链表的循环带头结点的空链表和非空链表。
3、双向链表比单链表多了如可以反向遍历查找等操作,但是在插入和删除时,需要更改两个指针变量。对于插入操作重要的就是操作顺序很重要,如下图。
对于删除操作如下图。
4、双向链表由于保存了两份指针,所以在空间上要占用略多一些,但是对于结点的前后结点的操作带来了方便,可以有效提高算法的时间性能,也就是用空间来换时间。