引言
对于非空的线性表和线性结构,其特点如下:
- 存在唯一的被称作
第一个的数据元素 - 存在唯一的被称作
最后一个的数据元素 - 除了第一个之外,结构中的每个数据元素均有一个前驱
- 除了最后一个之外,结构中的每个数据元素均有一个后继
线性表的物理存储可以细分为两种:
- 顺序存储结构
- 链式存储结构
顺序存储
顺序表存储数据时,会提前申请一块足够大的屋里空间,然后将数据依次存储起来,存储时做到数据之间不留一丝缝隙。
{1,2,3,4,5}对应的存储状态如下图:
顺序表的操作
在初始化之前,我们先定义一些常量
#define ERROR 0
#define OK 1
#define MAXSIZE 20 /* 存储空间初始分配量 */
定义顺序表的结点
typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */
typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
//顺序表结构设计
typedef struct {
ElemType *data;
int length;
}Sqlist;
-
顺序表的初始化
/*
初始条件:顺序线性表L已存在,1≤i≤ListLength(L);
操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
*/
Status InitList(Sqlist *L){
//为顺序表分配一个大小为MAXSIZE 的数组空间
L->data = malloc(sizeof(ElemType) * MAXSIZE);
//存储分配失败退出
if(!L->data) exit(ERROR);
//空表长度为0
L->length = 0;
return OK;
}
此处传入指针Sqlist *是因为c语言中函数分为值传递和指针传递,值传递并不会影响外部变量的数值。当前采用的此种初始化方法需要同过改变函数的参数来对顺序表进行初始化,因此需要进行指针传递。
-
顺序表的插入
约定L的位置从1开始,但是实际L->data的索引从0开始,即位置1对应索引0。
/*
初始条件:顺序线性表L已存在,1≤i≤ListLength(L);
操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
*/
Status ListInsert(Sqlist *L,int i,ElemType e){
//i值不合法判断
if(i<1 || i>L->length+1) return ERROR;
//表已满
if(L->length == MAXSIZE) return ERROR;
//从i-L->lenght位置的全部元素后移一位
if(i <= L->length) {
for(int j = L->length - 1; j >= i - 1; j--) {
L->data[j+1] = L->data[j];
}
}
//i位置放入e
L->data[i-1] = e;
//L长度加1
++L->length;
return OK;
}
-
顺序表的取值
Status GetElem(Sqlist L,int i, ElemType *e){
if(i < 1 || i > L.length) return ERROR;
*e = L.data[i - 1];
return OK;
}
此处Sqlist使用的是指传递,因为只需要读取数值而不需要进行操作,而ElemType使用指针传递是因为这样可以改变函数外部变量e的值。
-
顺序表的删除
/*
初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
操作结果: 删除L的第i个数据元素,L的长度减1
*/
Status ListDelete(Sqlist *L,int i){
//非法i
if(i<1 || i>L->length) return ERROR;
//L为空
if(L->length == 0) return ERROR;
//i位置后面的向前移动
for(int j = i; j <= L->length-1; j++) {
L->data[j-1] = L->data[j];
}
//长度减1
L->length--;
return OK;
}
-
清空顺序表
/* 初始条件:顺序线性表L已存在。操作结果:将L重置为空表 */
Status ClearList(Sqlist *L)
{
L->length=0;
return OK;
}
-
判断是否为空
/* 初始条件:顺序线性表L已存在。操作结果:若L为空表,则返回TRUE,否则返回FALSE */
Status ListEmpty(Sqlist L)
{
if(L.length==0)
return TRUE;
else
return FALSE;
}
-
获取长度
/*获取顺序表长度ListEmpty元素个数 */
int ListLength(Sqlist L)
{
return L.length;
}
-
顺序表查找元素并返回位置
/* 初始条件:顺序线性表L已存在 */
/* 操作结果:返回L中第1个与e满足关系的数据元素的位序。 */
/* 若这样的数据元素不存在,则返回值为0 */
/* 插入了哨兵e,位置位于L.lenght+1处
int LocateElem(Sqlist L,ElemType e)
{
ListInsert(&L, L.length+1, e);
int i = 1;
while(L.data[i - 1] != e) {
++i;
}
if(i == L.length) return 0;
return i;
}
链式存储
与顺序表不同,链表不限制数据的物理存储状态,换句话说,使用链表存储的数据元素,其物理位置是随机的。
{1,2,3}对应的存储状态如下图:
- 数据域:保存结点的数据
- 指针域:指向后继结点的位置
可以看出链表比顺序表要更耗费内存,因为它需要存储额外的指针域。
typedef struct Node{
ElemType data; //数据域
struct Node* next; //指针域
}Node;
typedef struct Node* LinkList;
单向循环链表的操作
由于单向链表比较简单,直接看单向循环链表的操作吧
-
初始化
循环链表的初始化有两种情况:
- 链表还没有创建
首节点,将*L指向首节点,并且首节点的next指针指向自己。同时我们需要将指针变量tail指向首节点,以便后续使用。 tail的作用为标记尾结点。
- 链表已经创建,需要添加新结点
目标结点,将tail指针的next指向目标结点,目标结点的next指针指向首结点*L。因为有tail指针的存在,我们不需要遍历链表来寻找最后一个结点
Status CreateList(LinkList *L){
int item;
LinkList temp = NULL;
LinkList tail = NULL;
printf("输入节点的值,输入0结束\n");
while (1) {
scanf("%d", &item);
if(item == 0) break;
if(*L == NULL) {
//链表为空,需要创建首节点
*L = (LinkList)malloc(sizeof(struct Node));
if(*L == NULL) exit(ERROR);
(*L)->data = item;
(*L)->next = *L;
//tail记录当前链表的为节点
tail = (*L);
}else {
temp = (LinkList)malloc(sizeof(struct Node));
if(temp == NULL) return ERROR;
temp->data = item;
temp->next = tail->next;
tail->next = temp;
tail = temp;
}
}
return OK;
}
-
循环链表的插入
循环链表的插入也有两种情况:
- 插入位置为首结点
- 创建
目标结点。 - 遍历链表,找到
尾结点。 目标结点的next指针指向首结点*L。首结点指向目标结点。尾结点的next指针指向首结点。
- 插入位置为其他结点
- 创建
目标结点。 - 遍历链表,找到目标位置的前一个结点
target。 目标结点的next指针指向target的next结点。target的next指针指向目标结点。
Status ListInsert(LinkList *L, int place, int num){
LinkList temp,target;
int j = 1;
//创建新结点
temp = (LinkList)malloc(sizeof(struct Node));
if(temp == NULL) return ERROR;
temp->data = num;
if(place == 1) {
//插入位置为第一位,需要更改尾部结点的next指针
for(target = *L; target->next != *L; target = target->next);
target->next = temp;
temp->next = *L;
*L = temp;
}else {
for(target = *L,j = 1; target->next != *L && j != place-1; target = target->next,j++);
//如果循环结束,j!=place-1,说明place超过了链表的总长度
if(j != place - 1) return ERROR;
temp->next = target->next;
target->next = temp;
}
return OK;
}
-
循环链表的删除
循环链表的删除同样分为两种情况
- 删除的位置为首结点
- 如果
首结点的next指向首结点,即链表只剩一个结点,释放首结点,指针置NULL。 - 遍历链表,找到
尾结点。 临时变量temp记录*L。*L指向*L的next。尾结点next指向*L。- 释放
temp。
- 删除的位置为其他结点
- 遍历链表,找到目标结点的
前一个结点target。 临时变量temp记录目标结点。target的next指向目标结点next。- 释放
temp。
Status LinkListDelete(LinkList *L,int place){
//如果删除的是第一个元素,需要找到尾结点,更改尾结点的next指针。
LinkList target = *L;
LinkList temp;
int j;
if(place == 1) {
if((*L)->next == *L) {
free(*L);
*L = NULL;
return OK;
}
while(target->next != *L) {
target = target->next;
}
temp = *L;
*L = (*L)->next;
target->next = *L;
free(temp);
}else {
for(j = 1; target->next != *L && j != place-1; target = target->next, j++);
//循环结束,还没有找到要删除结点的前一个位置,说明place超过了链表的长度
if(j != place-1) return ERROR;
temp = target->next;
target->next = temp->next;
free(temp);
}
return OK;
}
扩展
约瑟夫环问题的链表解法
约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3,1。
//约瑟夫环,num 位置
void CircleProblem(LinkList *L, int num) {
LinkList pre = *L;
//首先找到尾结点
while(pre->next != *L) {
pre = pre->next;
}
LinkList cur = *L;
//如果链表只剩一个结点,循环结束
while(cur != cur->next) {
int k = num;
while(--k) {
//遍历链表,找到需要删除的结点以及前一个结点
cur = cur->next;
pre = pre->next;
}
//删除目标结点
pre->next = cur->next;
printf("%d\n", cur->data);
free(cur);
cur = pre->next;
}
printf("%d\n", cur->data);
free(cur);
*L = NULL;
}