线性表
线性表(list):由零个或多个数据元素组成的有限序列
注意:
—线性表是一个序列,也就是说元素之间有先来后到的
—如元素存在多个,则第一个元素无前驱,最后一个无后继,其他元素有且只有一个前驱或后继
—线性表是有限的
线性表记 a[0,1,2,…i,i+1,…n]
抽象数据类型
数据类型:是指一组性质相同的值的集合及定义在次集合上的一些操作的总称。例如很多编程语言的整型,浮点型,字符型,
抽象:是指抽取出事物具有的普遍性的本质。它要求抽出问题的特征而忽略非本质的细节,是对具体事物的概括。抽象是一种思考的方式,他隐含了繁杂的细节。
集合 A=BUC,将B,C不同的元素放入A,相同的元素不重复;
void unionL(List *La,list Lb){
int La_len, Lb_len, i;
ElemType e;
for(i=1;i<=Lb_len;i++){
GetElem(Lb, i, &e);
if(!LocateElem(*La, e))
ListInsert(La, ++La_len, e);
}
}
线性表的存储结构
数组存储结构
#define MAXSIZE 20
typedef int ElemType;
typedef struct{
ElemType data[MAXSIZE];
int length;
}SqList;
假如ai地址为 1200,该数据是int (4字节),则ai+1地址为 1203
如下:
可以清晰地得出地址公式 c 为数据类型的字节
#define OK 1
#define ERROR
#define TURE 1
#define FALSE 0
typedef int Status;
//Status 是数据的类型
//初始条件 线性表存在 大于1 小于总长
//操作结果:用e返回L中第i个数据元素的值
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;
}
插入操作
插入思路
- 如果插入的位置不合理,抛出异常
- 如果线性表长度大于等于数组长度,抛出异常或增加数组容量
- 从最后一个元素开始向前遍历到第i个位置,分别将他们都向后移动一个位置
- 将插入元素填入位置i处
- 表长加1
/* 初始条件:顺序表L存在 */
/*操作结果:在L中第i个位置插入新的元素,长度加一*/
Status ListInsert(SqList *L, int i, ElemType e){
int k;
if(L->length == MAXSIZE) //顺序表是满了
return ERROR;
if(i<1 || i>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++; //长度加一
}
删除元素
删除思路与添加类似
/*初始条件:顺序线性表已存在*/
/*操作结果:删除的第i个元素,并用e放回其值,长度减一*/
Status ListDelete(SqList *L, int i, ElemType *e){
int k;
if(L->length == 0) //顺序表是空
return ERROR;
if(i<1 || i>length) //i输入错误,不在范围
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]; //i之后的元素,都向前移一位
}
L->length--; //长度加一
}
插入和删除的时间复杂度
最好的情况:最后一个元素O(1)
最坏的情况:第一个的元素O(n)
结果:线性结构在读取数据时不管是哪个位置都是O(1),插入删除都是O(n)
线性表顺序存储的结构优缺点
优点:无需为表示表中元素之间的逻辑关系增加额外的存储空间;可以快速存取表中任意元素
缺点:插入删除操作需要移动较大量的元素; 当表大时,难以确定存储空间的容量; 容易造成存储空间的碎片
单链表
定义
我们把存储的数据元素信息的域称为数据域,把存储直接后继位置的域称指针域,指针域中存储的信息称为指针或链,节点包括数据域和指针域。n各节点链接成一个链表,也叫做单链表。
图例
这里的头结点的数据域是为空的,可以根据自己的需要设置值,如链表长度,…
单链表结构体
typedef struct Node
{
ElemType data; //数据域
struct Node* Next; //指针域 该指针是struct Node类型,指向struct Node数据类型的节点(这个指针放左边也是可以的)
}Node;
typedef struct Node* LinkList; // 取别名 出现LinkList则定义为node的节点指针
如果 p_>data = a[i],那么 p->next->data = a[i+1],相当于下一个元素。
单链表的读取
- 申明一个节点指向链表的第一个节点,初始化j从1开始
- 当 j < i 时就遍历列表,让指针向后移动到下一个节点
- 如链表末尾为空,则说明 i 不存在
- 查找成功,返回数据
/*初始条件:链表已经存在*/
/*操作:用 e返回第i个元素的值*/
Status GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p;
p = L->next;
j = 1;
while(p && j<i){
p = p->next;
++j;
}
if(!p || j>i)
return ERROR;
*e = p->data;
return OK;
}
最后一个元素读取的复杂度为O(n)
单链表的插入
过程
s 节点为插入元素 ,步骤不能倒置,否则出现死循环。
s->next = p->next;
p->next = s
图例
代码
/*插入操作:寻找的该节点不为空,且输入i长度小于链表长*/
Status LinkList(LinkList *L, int i, ElemType e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
while(p && j<i){ //若p不为空,找到第i个元素
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;
}
删除
过程
p->next = p->next->next
删除之后记得也需要 free 内存空间
图例
代码
/*删除操作:寻找的该节点不为空,用 e 返回其值,释放q*/
Status LinkDelete(LinkList *L, int i, ElemType *e)
{
int j;
LinkList p,q;
p = *L;
j = 1;
while(p->next && j<i){ //若p不为空,找到第i个元素
p = p->next;
++j;
}
if(!(p->next) || j>i)
return ERROR;
q = p->next;
p->next = q->next;
*e = q->data;
free(q);
return OK;
}
复杂度分析;对于顺序存储每次插入都要移动后面的所有元素,链表只需要移动指正,链表复杂度为O(i),对于插入,链表比较适合。
表头建立
头插法
过程
每一次插入都是在表的第一个元素插入,虽然简单,但是输入的数据时原始数据的倒置
代码
/*从头节点插入,创建n个节点*/
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; //新节点作为头结点
}
}
尾插法
过程
每次都是插入尾节点,顺序与插入顺序相同
/*尾插法,将指针不是指向头节点,而是指向尾节点*/
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 = (Node*)malloc(sizeof(Node)); //生成新节点
p->data = rand()%100 + 1;
r->next = p; // 这句是为了使链表连接,使两个 node进行连接
r = p; //该指针节点指向尾结点
}
}
删除节点
过程
创建两个新的节点p,q,将该节点q指向当前p的下一个节点,释放当前节点p空间,将p相等于q,并判断是否为空。注意不能直接释放节点,还需要记录下一个节点的位置。
代码
/*删除指针*/
Status ClearList(LinkList *L)
{
LinkList p, q;
p = (*L)->next;
while(p){
q = p->next; //先记录下一个节点的地址
free(p); //再把该节点删去
p = q; // 使 p q 节点相等,
}
(*L)->next = NULL;
return OK;
}
顺序存储与单链表存储比较
存储:顺序一般采用连续的存储单元依次存储线性数据元素,单链表采用链式结构,用一组任意的存储单元存储。
时间:查找: 顺序存储结构O(1) 单链表O(n)。 插入删除 : 顺序 O(n) 单链表 O(1)
空间:顺序还需要预先分配元素个数, 链表可以不需要
应用:比如在游戏开发中,对于用户注册的个人信息,除了用户注册数据外,其他时候都是读取,可以考虑使用顺序存储。在玩家的武器装备列表中,玩家插入,删除的比较多的话单链表就适合了,当单链表元素较大时,或不知道有多少元素时应该使用链表结构。
静态链表
对于线性链表,也可用一维数组来进行描述。这种描述方法便于在没有指针类型的高级程序设计语言中使用链表结构。
结构
这里的下标类似节点(node),游标(cur)类似next。数组的第一个元素cur存放第一个备用元素(未被占用的元素)下标,而数组的最后一个元素cur存放第一个有值的元素下标,相当于头结点作用
存储结构
#define MAXSIZE 100
typedef struct {
ElemType data;
int cur;
}Component, StaticLinkList[MAXSIZE];
初始化
Status InitList(StaticLinkList space){
int i;
for(i = 0; i<MAXSIZE-1, i++){
space[i].cur = i+1;
space[MAXSIZE-1].cur = 0;
return 0;
}
}
获取空闲分量下标
/*获取空闲分量的下标*/
int Malloc_sLL(StaticLinkList space){
int i = space[0].cur;
if(space[0].cur)
space[0].cur= space[i].cur;
return i;
}
插入元素
/*静态链表中第i个元素之前插入新的数据元素 e */
Status LimkInsert(StaticLinkList L, int i, ElemType e)
{
int j, k, l;
k = MAXSIZE-1;
if(i<1 || i>ListLength(L)+1)
return ERROR;
j = Malloc_sLL(l); //获取空闲分量的下标(node)
if(j){ //j是否是空闲
L[j].date = e; //将空闲的数据域存放插入数据
for(l=1; l<= i-1;l++)
k = L[k].cur; // k为最后一个元素(最后一个元素存放了第一个节点的下标)
L[j].cur = L[k].cur; //更换到他的游标
L[k].cur = j;
return OK;
}
return ERROR;
}