本文共 7326 字,大约阅读时间需要 24 分钟。
线性表(list):零个或多个数据元素的有限序列
首先有序,其次是有限
线性表的抽象数据类型 就像排队一样 刚开始站的队有的高有的矮,置空后重新排列
ADT 线性表(List)
Data
Operation
InitList(*L);初始化操作,建立一个空的线性表L
ListEmpty(L);若线性表为空,返回true,否则返回false
ClearList(*L);将线性表清空
GetElem(L,i,*e);将线性表L中的第i个位置元素值返回给e
LocateElem(L,e);在线性表L中查找与给定值e相等的元素
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中 只需要遍历B表 然后找出来 插进A表 即可
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=1;i<=Lb_len;i++)
{
GetElem(Lb,i,e); 取Lb中第i个数据元素赋给e
if(!LocateElem(La,e,equal)) La中不存在的和e相同的数据元素
ListInsert(La,++La_len,e); 插入
}
}
一.物理存储结构:线性表的顺序存储结构:用一段地址连续的存储单元依次存储线性表的数据元素
#define MAXSIZE 20 存储空间初始分配量
typedef int ElemType; 定义int类型
typedef struct
{
ElemType data[MAXSIZE]; 数组存储数据元素,最大值
int length; 线性表当前长度
}SqList;
p->data=ai p->next->data=ai+1
84页
线性表的长度应该小于等于数组的长度
存储器中的每个存储单元都有自己的编号 这个编号称为地址。
假设占用的是c个存储单元
那么LOC(ai+1)=LOC(ai)+c
所以第i个数据元素ai的存储位置可以由a1推算出来:
LOC(ai)=LOC(a1)+(i-1)*c 所以不管你是第一个还是最后一个对于计算机来说时间都是相等的 不管你的存还是取。时间复杂度都是O(1) ,通常把具有这一特点的存储结构称为随机存取结构
1.获得元素操作
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0 操作结果:用e返回L中第i个数据元素的值
typedef int Status; Status是函数的类型,其值是函数结果状态代码
Status GetElem(SqList L,int i,ElemType *e)初始化条件:1<=i<=ListLength(L)
{
if(L.length==0 || i<1 || i>L.length)
return ERROR;
*e=L.data[i-1];
return OK;
}
2.插入操作
初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
操作结果:在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;
}
3.删除操作
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<L->length;k=i;k++) 将删除位置后继元素前移
L->data[k-1]=L->data[k];
}
L->length--;
return OK;
}
线性表 存和取 时间复杂度O(1),插入或删除,时间复杂度O(n),这就说明,适合元素个数不太变化,而更多是存取数据的应用
二。线性表的链式存储结构
为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域,指针域中存储的信息称作指针或链,这两部分信息组成数据元素ai的存储映像,称为结点
n个借点链结成一个链表,即为线性表(a1,a2,。。。。an)的链式存储结构。因为此链表的伟哥结点中只包含一个指针域
头指针是链表的必要元素,头结点不一定是链表必要元素
若线性表为空表,则头结点的指针域为空
1.单链表的读取
初始条件:顺序线性表L已存在 1<=i<=ListLength(L)
操作结果:用e返回L中第i个数据元素的值
Status GetElem(LinkList L,int i,ElemType *e)
{
int j;
LinkKist 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;
}
2.单链表的插入与删除
初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
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(Node)); 生成新结点(C标准函数)
s->data=e;
s->next=p->next; 将p的后继结点赋值给s的后继
p->next=s; 将s赋值给p的后继
return OK;
}
s->next=p->next; p->next=s;这是不能改顺序的
3.单链表的删除
初始条件:顺序线性表L已存在,1<=ListLength(L)
操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1
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;
}
如果我们希望从第i个位置,插入10个元素,对于顺序存储结构意味着,每一次插入都需要移动n-i个元素,每次都是O(n)。而单链表,我们只需要在第一次时找到第i个位置的指针,此时为O(n),接下来只是简单的通过赋值移动指针而已,时间复杂度都是O(1)。显然,对于插入或删除数据频繁的操作,单链表的效率优势越是明显。
4.单链表的整表创建
算法思路:
1、声明一结点p和计数器变量i
2.初始化一空链表L
3.让L的头结点的指针指向NULL,即建立一个带头结点的单链表
4.循环
生成一新结点赋值给p
随机生成一数字赋值给p的数据域p->data
将p插入到头结点与前一新结点之间
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;n++)
{
p=(LinkList)malloc(sizeof(Node)); 生成新结点
p->data=rand()%100+1; 随机生成100以内的数字
p->next=(*L)->next;
(*L)->next=p; 插入到表头
}
}
这段算法代码里,我们其实用的是插队的办法,就是始终让新结点的第一的位置。简称头插法
也可以把新结点都放到最后,所谓的先来后到,每次插入都放到终端结点的后面。这种算法称之为尾插法。
void CreateListTail(LinkList *L,int n)
{
LinkList p,r;
int i;
srand(time(0)); 初始化随机数种子
*L=(LinkList)malloc(sizeof(Node));为整个线性表
r=*L; r为指向尾部的结点
for(i=0;i<n;i++)
{
p=(Node *)malloc(sizeof(Node)); 生成新结点
p->data=rand()%100+1; 随机生成100以内的数字
r->next=p; 将表尾终端结点的指针指向新结点
r=p; 将当前的新结点定义为表尾终端结点
}
r->next=NULL; 表示当前链表结束
}
r->next=p 就是将表尾终端结点r的指针指向新结点p
r=p 因为r实在ai-1元素的结点,可现在它已经不是最后的结点了,现在最后的结点ai,所以应该要让将p结点这个最后的结点赋值给r
5.单链表的整表删除
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;
}
用数组描述的链表叫做静态链表
对数组的第一个和最后一个元素作为特殊元素处理,不存数据,我们通常把未被使用的数组元素称为备用链表。而数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标,而数组的最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点作用,当整个链表为空时,则为0^2
线性表的静态链表存储结构
#define MAXSIZE 1000
typedef struct
{
int cur; 游标 为0时表示无指向
}Component,StaticLinkList[MAXSIZE];
初始化
将一维数组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;
}
为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点
int Malloc_SLL(StaticLinList space)
{
int i=space[0].cur; 当前数组第一个元素的cur存的值,就是要返回的第一个备用空闲的下标
if(space[0].cur)
space[0].cur=space[i].cur; 由于要拿出一个分量来使用 就得把它的下一个分量用作备用
return i;
}
插入元素
在L中第i个元素之前插入新的数据元素e
Status ListInsert(StaticLinList L,int i,ElemType e)
{
int j,k,l;
k=MAX_SIZE-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;i++) 找到第i个元素之前的位置
k=L[k].cur; 把第i个元素之前的cur赋值给新元素的cur
L[j].cur=L[k].cur; 把新元素的下标赋值给第i个元素之前元素的cur
L[k].cur=j;
return OK;
}
}
静态链表的删除操作
删除在L中第i个数据元素e
Status ListDelete(StaticLinkList L,int i)
{
int j,k;
if(i<1 || i >ListLength(L))
return ERROR;
k=MAX_SIZE-1;
for(j=i;j<=i-1;j++)
k=L[k].cur;
j=L[k].cur;
L[k].cur=L[j].cur;
Free_SSL(L,j);
return OK;
}
将下标为k的空闲结点回收到备用链表
void Free_SSL(StaticLinkList space,int k)
{
space[k].cur=space[0].cur; 把第一个元素cur值赋给要删除的分量cur
space[0].cur=k; 把要删除的分量下标赋值给第一个元素的cur
}
静态链表也有相应的其他操作的相关实现,比如我们代码中的ListLength
初始化条件:静态链表L已存在,操作结果:返回L中数据元素个数
int ListLength(StaticLinkList L)
{
int j=0;
int i=L[MAXSIZE-1].cur;
while(i)
{
i=L[i].cur;
j++;
}
return k;
}
静态链表的优点:在插入和删除操作时只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点
循环链表
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在则是p->不等于头结点,则循环未结束。
两个循环链表,它们尾指针分别是rearA和rearB
要想把它们合并,只需要如下的操作即可
p=rearA->next; 保存A表的头结点
rearA->next = rearB->next->next;将本是指向B表的第一个结点(不是头结点)赋值给rearA->next
rearB->next=p; 将原A表的头结点赋值给rearB->next
free(p); 释放p
双向链表
双向链表是在单链表的每个结点中,在设置一个指向其前驱结点的指针域
双向链表的存储结构
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior; 直接前驱指针
struct DuLNode *next; 直接后继指针
}DulNode, *DuLinkList;
p->next->prior = p = p->prior->next
插入操作
我们现在假设存储元素e的结点为s,要实现将结点s插入到结点p和p->next之间需要下面几步
s->prior = p; 把p赋值给s的前驱
s->next = p->next;把p->next赋值给s的后继
p->next->prior=s;把s赋值给p->next的前驱
p->next=s;把s赋值给p的后继
删除操作
p->prior->next = p->next; 把p->next赋值给p->的后继
p->next->prior = p->prior; 把p->prior赋值给p->next的前驱
free(p);释放结点