线性表
首先线性表的定义就是:零个或多个数据元素的有限序列。
列如高考前每个班级排队照准考证,这个队伍就可以看做一个线性表,大家都井然有序的排着队,是一个有限的序列,一个班就那么几十个人,而且每个人之间都是有顺序的,站错了顺序就不好核对信息。相对于在广场上一群人在跳广场舞、打拳、散步,这就不是线性表,首先它不是有序的序列,而是散乱的一堆,而且相对来说不是有限的。计算机处理的对象都是有限的,无限的数列一般都存在于数学的概念中。
在线性表中,中间的元素会有一个直接前驱元素和一个直接后继元素,除了线性表的第一个元素,其他都有且仅有一个前驱元素;除了最后一个元素,其他都有且仅有一个后继元素;线性表个数n(n >= 0)定义为线性表的长度,当n = 0时,称为空表。
线性表中的元素也不一定就是一个值,也可以是一个结构体,包含着很多数据组。着也就显得线性表在学习计算机技术中显得比较重要。这就牵扯出了我们的顺序表和链表,写一个简单的新生管理系统一班就会用到线性表,就比较好管理班级中每个人的信息。顺序表和链表的差别就是顺序表没有一个专门的指针来表示自己的位置,而在链表中每个结点元素里都会有一个指针域来表示自己在这个线性表中的位置。接下来我就分开来叙述一下顺序表和链表。
顺序表
顺序表的顺序储存结构,指的是用一段地址连续的储存单位依次存储线性表的数据元素。
其实就是和C语言中的一维数组差不多,每个元素都是按顺序放好,并且元素后面会有一个“尾巴”,用“[]”标记着它的位置。因为这个和数组一样,在刚开始定义的时候就会定义好储存量。随着放置元素进去,线性表的长度就会变长,但是不能超过刚开始定义的储存量。这也算是顺序链表的一个不便之处。
我们先开始顺序表的结构代码。
#define MAXSIZE 20 //储存空间初始分配量
typedef int ElemType; //ElemType类型根据实际情况而定,这里假设为int
typedef struct
{
ElemType date[MAXSIZE];//数组存储数据结构,最大值为MAXSIZE
int length;//线性表当前的长度
} SqList;
这里就能看到顺序储存结构的三个属性:
- 线性表的最大储存量:MAXSIZE。
- 储存空间的起始位置:数组date,它的储存位置就是储存空间的储存位置。
- 线性表的当前长度:length。
获得i元素的操作:
Status GetElem(SqList L,int i,ElemType *e)
{
if(L.length==0 || i<1 || i>L.length)
return 0;
*e=L.date [i-1];
return 1;
}
在线性储存i处中插入元素:
Status ListInsert(SqList *L,int i,ElemType e)
{
int k;
if(L->length == MAXSIZE)//顺序线性表已满
return 0;
if(i<1 || i>L->length+1)//当i不在范围内时
return 0;
if(i<=L->length)
{
for(k = L->length-1;k >= i-1;k--)//将要插入位置后数据元素向后移动一位
L->date[k+1] = L->date[k]
}
L->date[i-1]=e;//将新元素插入
L->length++;//要记得把线性表当前长度增加1
return 1;
}
删除在线性表的第i处元素:
Status ListDelete(SqList *L,int i,ElemType *e)
{
int k;
if(L->length == 0)//线性表为空
return 0;
if(i<1 || i>L->length)//删除位置不正确
return 0;
*e = L->date[i-1];
if(i < L->length)
{
for(k = i;k <L->length; k++)//将删除位置后继元素向前移
L->date[k-1] = L->date[k];
}
L->length--;//将当前线性表的长度减一
return 1;
}
线性表的顺序储存结构,存、读数据时,不管在哪个位置。时间复杂度都是O[1],但是在插入和删除时,时间复杂度都是O[n]。所以顺序储存的优点是可以快速便捷得存取表中任意元素;缺点是插入和删除都需要移动大量元素,而且长度变化比较大,难以确定储存空间容量,造成储存空间的“碎片”。
链表
链表的每个元素除了本身信息以外,还需要储存一个指示它直接后继的信息,一般把储存元素信息和数据的域称为数据域,把储存后继位置的域称为指针域。指针域中储存的信息称为指针或者链。这两部分结合起来称为结点。
n个结点链结成一个链表,即为线性表的链式储存结构,因为每个结点中只有一个指针域,所以称为单链表。
一般把链表的第一个结点的储存位置叫做头指针,最后一个结点指针为“空”(一般用NULL表示)。有时候为了方便对链表的操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点可以不储存任何信息也可以储存线性表的长度等公共信息。这里我觉得需要区别一下头指针和头结点,因为我也总是有点儿搞不清楚。
头指针 |
头结点 |
|
|
单链表的结构体定义:
typedef struct Node
{
ElemType data;
struct Node *next;
} Node;
typedef struct Node *LinkList;//定义LinkList
对单链表中第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;
j++;
}
if(!p || j>i)
return 0;//第i个结点不存在
*e = p->data;//取第i个结点数据
return 1;
}
在单链表中第i个结点位置之前插入新结点:
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
while(p && j<i)//寻找第i-1个结点
{
p = p->next;
j++;
}
if(!p || j>i)
return 0;
s = (LinkList)malloc(sizeof(Node));//生成新结点
s->data = e;
s->next = p->next;//将p的后继结点赋值给s的后继
p->next = s; //将s赋值给p的后继
return 1;
}
删除单链表中的第i个结点:
Status ListDelete (LinkList *L, int i, ElemType *e)
{
int j;
LinkList p,q;
p = *L;
j = 1;
while(p->next && j < i)//遍历寻找第i-1个结点
{
p = p->next;
j++;
}
if(!(p->next) || j >i)
return 0;//第i个结点不存在
q = p->next;
p->next = q->next; //将q的后继赋值给p的后继
*e = q->next;//将q结点中的数据给e
free(q);
return 1;
}
单链表的整表是一种动态结构,单链表的创建有两种方法,一种是头插法一种是尾插法。在插入新结点的时候让新结点始终在第一的位置,这种就是头插法:
/*随机产生n个元素的值,建立带表头结点的单链表线性表L(头插法)*/
void CreateListHead(LinkList *L,int n)
{
LinkList p;//先声明一个指针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;//随机生成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;//表示当前链表结束
}
将单链表整表删除的代码:
Status ClearList(LinkList *L)
{
LinkList p,q;
p = (*L)->next;//p指向第一个结点
while(P)
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;//头结点指针域为空
return 1;
}
单链表在查找数据的时候时间是O[n],但是在知道了位置删除和插入数据时间复杂度仅为O[1]。所以当不知道数据元素数量的时候和需要频繁删除插入数据,用单链表就会方便很多,不会牵一发而动全身。
还有一种链表,它用数组代替了指针,来叙述单链表,元素由两个数据域组成,data和cur。cur就相当于链表中的next指针,把cur叫做游标。这种把数组描述的链表叫做静态链表。
静态链表的储存结构定义:
#define MAXSIZE 1000 //假设链表的最大长度是1000
typedef struct
{
ElemType date;
int cur; //游标,为0时表无指向
} 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;//目前静态链表为空,最后一个元素的cur为0
return 1;
}
为了辨明数组中哪些分量未被使用,就将所有没有使用过的及已被删除的分量用游标链成一个备用的链表,需要插入,就从备用链表上取第一个结点作为待插入的新结点,就像一个数组一样,先定义好了储存空间大小,再取来用。所以一般都会把链表定义得比较大,这样子就不容易溢出。
若备用链表为非空,则返回分配的结点下标,否则返回0:
int Malloc_SLL (StaticLinkList space)
{
int i = space[0].cur ;//当前数组第一个元素的cur存的值,就是要返回的第一个备用空闲的下标
if(space[0].cur)
space[0].cur = space[i].cur;//由于要拿出一个分量来使用了,
//所以我们就要把它下一个分量用来备用
return i;
}
在i元素之前插入新的元素:
Status ListInsert (StaticLinkList L,int i,ElemType e)
{
int j,k,l;
k = MAX_SIZE - 1; //注意k首先是最后一个元素的下标
if (i < 1 || i > ListLength(L) + 1)
return 0;
j = Malloc_SSL(L); //获得空闲分量下标
if(j)
{
L[j].date = 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 1;
}
return 0;
}
删除静态链表中第i个数据元素:
Status ListDelete (StaticLinkList L,int i)
{
int j,k;
if (i < 1 || i > ListLength(L))
return 0;
k = MAX_SIZE - 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 1;
}
将下标为k的空闲结点回收到备用链表:
void Free_SSL (StaticLinkList space, int k)
{
space[k].cur = space[0].cur;//把第一个元素的cur赋值给要删除的分量cur
space[0].cur = k; //把要删除的分量下标赋值给第一个元素的cur
}
查找静态链表中数据元素个数:
int ListLength (StaticLinkList L)
{
int j = 0;
int i = L[MAXSIZE-1].cur;
while(i)
{
i = L[i].cur;
j++;
}
return j;
}
静态链表在插入和删除的时候只用改动游标就行了,看起来很方便,但是静态链表还是没有解决表长难以确定,因为事先定义好了,而且失去了顺序储存的随机存取的特性。
将单链表中终端结点的指针端由空指针改为指向头指针,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
我这里就简单讲一下循环链表的定义和建造:
typedef struct person
{
int number;
struct person *next;
}person;
/*输入一个n,使循环链表里的数据都存入由1到n的数*/
person *str (int n)//创建循环链表
{
int i;
person *head = (person*)malloc(sizeof(person));//设置头指针并分配内存
head->number = 1;
head->next = NULL;
person *p = head;
for(i = 2; i <= n; i++)
{
person *body = (person*)malloc(sizeof(person));//定义一个中间指针
body->number = i;
body->next = NULL;
p->next = body;
p = p->next; //使p指针一直是下一个
}
p->next = head;//收尾相接成为循环链表
return head;
}
这里我再顺便串讲一下快慢指针叭,这是一种判断链表是单链表还是循环链表的一种方法,慢指针一次走一步,快指针一次走两步,如果是单链表那快指针就会先遇到NULL,如果是循环链表,那快指针就会追上慢指针。可以用这种办法来判断,而且还可以用快慢指针来计算循环链表的中位,快指针走了一圈,慢指针就刚好到链表的中间,这里我就不串开讲啦。太多啦,我就先把快慢指针的判断函数代码发出来看看:
bool is_cir(person *head)//判断是否是循环链表
{
if(head->next == NULL)
return false;
person *slow = head, *fast = head;
while(fast != NULL && fast->next != NULL)//如果快指针先遇到了NULL就是线性链表
{
slow = slow->next;//慢指针走一步
fast = fast->next->next;//快指针走两步
if(fast == slow)//相遇就是循环
return true;
}
return false;
}
在链表中还有一种双向链表,就是在单链表的每个结点中,多设置了一个指向前驱结点的指针域。挺好理解的,就两个方向嘛,一个指针指向前面,另一个指向后面,双重保险嘛,我觉得挺好理解的。
双向链表的储存结构:
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior;//直接前驱指针
struct DuLNode *next;//直接后驱指针
} DulNode, *DuLinkList;
双向链表中插入s结点:(我这里写得比较简单,其实挺好理解的我就不赘述了)
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:
p->prior->next = p->next;//把p的前驱指向的后驱改成p的后驱,这样子就把p取出来了
p->next->piror = p->next;
free(p);
终于完成了。线性表应该差不多了,这也是我第一次写博客,如果有错误或者不好的地方欢迎您提出来,我一定会加以改正。写这个主要是完成自己对这章知识的归纳和加深的理解,也希望我的这篇文章能给您提供帮助。本文一些代码和理论参考了程杰老师的《大话数据结构》。谢谢观看。