写在前面:本篇文章与其他数据结构文章不同,本文通过在代码之间添加注释的方式来帮助大家理解思路,目前我所了解的很多数据结构类型的文章中思路和代码是分开的,尽管这种方式没有问题并且可能适合很多人,但对于我来说陷入了"大脑会了手却不会"的尴尬局面,因此我采用了将思路和代码相结合的方式,这种方式可以让我清楚理解"为什么代码是这样写的",也希望可以帮助到那些和我一样的初学者。
PS:本文为作者刚学习数据结构所作,对于数据结构的理解才疏学浅,尽管再三检查但仍可能有纰漏,如果各位发现了错误或表述不准确的地方,以及对于文章的建议都可以在评论区或私信发表看法。感谢大家的支持。
一、线性表是什么
首先是我的理解:顾名思义,线性表是用"线"将"表格"连接起来。在C语言中,我们可以创建一个结构体来代表一个"表格";创建一个指针指向这个"表格"。而这个"表格"也被称为节点。
以下是官方的定义:
线性表(List):零个或多个数据元素的有限序列。线性表的数据集合为{a1,a2,…,an},假设每个元素的类型均为DataType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
二、线性表的顺序存储结构
线性表的顺序存储结构:指的是用一段地址连续的存储单元依次存储线性表的数据元素
1.定义顺序表存储结构
#define MAXSIZE 20//假定顺序表最大长度为20
#define OK 1//代表操作成功
#define ERROR 0//代表操作失败
//1.定义顺序表存储结构
typedef struct
{
int* elem;//指针域(个人认为顺序表的指针主要作用是方便动态分配内存和回收内存)
int length;//数据域(存放顺序表元素个数/元素的值)
}SqList;
2.创建一个新的空顺序表
int Init_SqList(SqList* L)
{
//(1)分配内存,其中*p是什么类型就强制转换成什么类型
L->elem = (int*)malloc(MAXSIZE * sizeof(SqList));
//(2)判断是否成功(万一出错了及时止损(手动doge))
if (!L->elem)//正常分配成功之后L->elem不会是0,因此根据这个来判断是否创建成功
{
return ERROR;
}
//(3)记录表长
L->length = 0;//初始顺序表长度为0
return OK;
}
3.插入元素
int Insert_List(SqList* L, int i, int e)//第二个参数表示插入到第i个位置前,第三个参数表示传入的值
{
//(1)检查顺序表是否已满
if (L->length==MAXSIZE)
{
return ERROR;
}
//(2)检查插入位置是否合理
if (i<1 || i>L->length + 1)
{
return ERROR;
}
//(3)当插入位置不是表尾时,需要将插入位置之后的元素后移
if (i <= L->length)
{
for (int j = L->length-1; j >= i-1; j--)
{
L->elem[j + 1] = L->elem[j];
}
}
L->elem[i - 1] = e;
L->length++;
return OK;
}
4.删除第i个元素(删除成功则用e存放其值)
int Delate_List(SqList* L, int i, int *e)
{
//(1)检查顺序表是否为空
if (L->length == 0)
{
printf("删除失败\n");
return ERROR;
}
//(2)检查删除的位置是否合理
if (i<1 || i>L->length)
{
printf("删除失败\n");
return ERROR;
}
//删除之前要保存存放的值
*e=L->elem[i-1];
//当删除位置不为表尾时,则需将元素前移
if (i < L->length)
{
for (int j = i; j < L->length; j++)
{
L->elem[j - 1] = L->elem[j];
}
}
L->length--;
return OK;
}
5.获取顺序表中第i个元素,并用e存放其值
int Get_SqList(SqList L, int i, int* e)//注意这里将L传进去而不是指针
{
//(1)判断顺序表是否为空和i的值是否合理
if (L.length == 0 || i<1 || i>L.length)
{
return ERROR;
}
//(2)将第i个元素用e存放
*e = L.elem[i-1];
return OK;
}
6.输出顺序表所有元素
void Put_SqList(SqList L)//这里传入的也不是指针
{
printf("当前顺序表的长度:%d\n", L.length);
for (int i = 0; i < L.length; i++)
{
printf("%d ", L.elem[i]);
}
printf("\n");
}
7.测试代码
int main()
{
SqList L;
printf("------构造一个空的线性表L------\n");
Init_SqList(&L);
Put_SqList(L); //打印结果
printf("------测试插入10个数------\n");
for (int i = 1; i <= 10; i++)
{
Insert_List(&L, i, i);
}
Put_SqList(L); //打印结果
printf("------在第三位之前插入0------\n");
Insert_List(&L, 3, 0);
Put_SqList(L); //打印结果
printf("------删除第6位的数据------\n");
int e;
Delate_List(&L, 6, &e);
printf("删除的数据为:%d\n", e);
Put_SqList(L); //打印结果
printf("------获取元素操作------\n");
Get_SqList(L, 5, &e);
printf("得到第5个元素:%d", e);
return 0;
}
8.运行结果
总结
1.对于传入SqList还是SqList*我的理解是:如果对单个元素操作,如插入,删除,仅需指针即可;如果涉及到遍历顺序表,或者获取顺序表元素,则需要传入整个SqList
2.顺序表时间复杂度:线性表的顺序存储结果在读、存数据是的时间复杂度是O(1),插入,删除操作的时间复杂度是O(n)。
3.顺序表的优缺点:
优点:无须为表中元素之间的逻辑关系而增加额外的存储空间;可以快速的存取表中任一位置的元素。
缺点:插入和删除操作需要移动大量元素;当线性表长度较大时,难以确定存储空间的容量,造成存储空间的“碎片”。
由于顺序表在插入和删除操作需要移动大量元素,元素很多时会浪费大量时间,那该怎么解决呢?
既然每个元素紧挨着不便于插入和删除,那为什么不把元素分开放呢?只要让某个元素存放一个指针指向下一个元素即可,因此人们想到了链式存储结构。
三、线性表的链式存储结构
首先是我的理解:和顺序结构相比,每个节点多了一个(或两个)指针指向下一个(或分别指向前后两个)节点。为什么需要指针呢?在顺序存储结构中,只需要知道第一个节点的地址就可以推算出剩下的节点地址(类似于数组)。而链表节点的地址上可以在内存的任何空闲位置,因此需要指针用于找到下一个节点。
下面是官方的定义:
线性表的链式存储结构:每个数据元素ai与其直接后继元素ai+1之间的逻辑关系,对数据ai来说,除了存储其本身的信息之外,还需要存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。
1.单链表
n个结点(ai的存储映像)链结成一个链表,即为线性表(a1, a2, …, an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
1.1定义单链表存储结构
#define OK 1
#define ERROR 0
//1.定义链式存储结构
//1.1定义一个节点(包含数据域和指针域)
typedef struct Node
{
int data;
struct Node* next;//指针的数据类型是Node类型
}Node;
//1.2定义一个单链表
typedef struct
{
int length;
struct Node* next;
}*LinkList;//这里LinkList是一个Node类型指针,指向一个节点
//注:其实*Linklist和Node属于相同类型结构体,单独定义是为了体现头节点可以存放链表长度(有没有发现只有变量名字不同)
1.2 创建一个新的带头节点的空单链表
//2.创建一个带头结点的空单链表
int Init_LinkList(LinkList* L)
{
//(1)为空链表和头节点分配内存
LinkList p = (LinkList)malloc(sizeof(LinkList));//空链表
Node* q = (Node*)malloc(sizeof(Node));//头节点
q->next = NULL;//(2)头节点的指针域设置为NULL
p->next = q;//(3)让头指针指向头节点
p->length = 0;//初始单链表长度为0
(*L) = p;
return OK;
}
1.3单链表的插入
//3.单链表的插入
int Insert_LinkList(LinkList* L, int i, int elem)//第二个参数表示插入到第i个元素之前,第三个参数表示传入的值
{
//(1)检查插入位置是否合理
if (i<1 || i>(*L)->length + 1)
{
return ERROR;
}
//(2)搜索第i个节点
Node* p = (*L)->next;//定义一个指针变量等于头指针,用于搜索
for (int j = 1; j < i; j++)//让指针指向第i-1个元素
{
p = p->next;
}
//(3)给新结点分配内存,并插入链表
Node* q = (Node*)malloc(sizeof(Node));
q->data = elem;
q->next = p->next;//①先让q指向第i个元素
p->next = q;//②//再将第i-1个元素指向q
//注:顺序不能改变,若先进行②,则p->next变为q,再进行①相当于让q->next=q,这就不对了
(*L)->length += 1;//链表长度+1
return OK;
}
1.4单链表的删除
//4.单链表的删除
int Delate_LinkList(LinkList* L, int i,int* elem)//删除第i个元素(删除成功则用e存放其值)
{
//(1)检查插入位置是否合理
if (i<1 || i>(*L)->length )
{
return ERROR;
}
//(2)搜索第i个节点
Node* p = (*L)->next;//定义一个指针变量等于头指针,用于搜索
Node* q;//定义一个指针变量用于删除节点
for (int j = 1; j < i; j++)//让指针指向第i-1个元素
{
p = p->next;
}
//(3)先将第i个节点脱离链表,再释放空间(若直接释放则会导致链表元素断开联系)
q = p->next;//①先保存第i个节点
p->next = q->next;//②将第i-1个元素指向第i+1个元素(还没删除第i个)
//注:顺序不能改变,若调换顺序,则会导致链表元素断开联系
free(q);//这里第i个元素被销毁了
(*L)->length -= 1;//链表长度-1
return OK;
}
1.5单链表的查找
//5.单链表的查找
int Find_LinkList(LinkList L, int i, int* elem)//第二个参数表示查找第i个元素,第三个参数表示用*elem保存
{
//(1)检查查找的元素位置是否合理
if (i < 1)
{
return ERROR;
}
Node* p = L->next;//(2)定义一个指针变量指向头节点
int j;
for (j = 0; j < i; j++)//(3)查找
{
p = p->next;
}
if (!p || j > i)//(4)如果表为空或者j>i时,说明没找到
{
return ERROR;
}
*elem = p->data;//保存第i个节点的数据
return OK;
}
1.6单链表的清空与销毁
//6.1单链表的清空(保留头节点)
int Clear_LinkList(LinkList* L)
{
Node* p = (*L)->next->next;//该变量用于搜索,让p指向头结点的下一位
Node* q;//该变量用于释放内存
while (p != NULL)//只要指针没离开单链表
{
q = p;//用q保存即将被清空的节点位置
p = p->next;//让p指向下个节点
free(q);//清空该内存的内容
}
(*L)->next->next = NULL;//将头节点的后继元素设置为NUll
(*L)->length = 0;//链表长度为0
return OK;
}
//6.2单链表的销毁
int Destroy_LinkList(LinkList* L)
{
Node* p = (*L)->next;
Node* q;
while (p != NULL) {
q = p;//用q保存即将被清空的节点位置
p = p->next;//让p指向下个节点
free(q);//清空该内存的内容
}
free((*L));//和清空相比销毁操作还把头节点给清除了
(*L) = NULL;
return OK;
}
1.7打印单链表元素
//7.打印链表元素
void Put_LinkList(LinkList L) {
Node* p = L->next->next; //用于搜索, 让p指向头结点的下一位
for (int i = 0; i < L->length; i++)
{
printf("%d ", p->data);
p = p->next;//打印元素之后让指针指向下一个节点
}
printf("\n");
}
1.8测试代码
//运行测试
int main()
{
LinkList L;
Init_LinkList(&L);
printf("------测试插入10个数------\n");
for (int i = 1; i <= 10; i++) {
Insert_LinkList(&L, i, i);
}
Put_LinkList(L);
int elem,num=3;
/*Find_LinkList(L, num, &elem);
printf("第%d个元素为:%d\n",num-1, elem);*/
printf("------删除第5位的数据------\n");
Delate_LinkList(&L, 5, &elem);
Put_LinkList(L);
printf("------清空单链表------\n");
Clear_LinkList(&L);
Put_LinkList(L);
return 0;
}
1.9运行结果
2.静态链表
使用数组来描述指针。首先我们让数组的元素都是由两个数据域组成,data和cur。数据域data,用来存放数据元素;游标cur相当于单链表的next指针,存放该元素的后继在数组中的下标。
在定义静态链表存储结构之前我们需要了解一个概念:"备用链表"。备用链表通常指未被使用的数组元素。了解了这个概念,才方便我们进行接下来的操作。
2.1定义静态链表存储结构
#define MAXSIZE 100//假定静态链表最大长度为100
#define OK 1//代表操作成功
#define ERROR 0//代表操作失败
//2.1定义静态链表存储结构
typedef struct
{
int data;//存放数据
int cur;//存放备用链表第一个节点下标(默认值为0,表示下一位置为空)
} StaticLinkList[MAXSIZE];
2.2创建一个新的空静态链表
//2.2创建一个新的空静态链表
int Init_StList(StaticLinkList space)
{
//由于静态链表是通过数组下标记录下一个元素位置
//因此该段代码的目的是让space[0]到space[MAXSIZE-1]的cur存储下一位的数组元素下标
//一般习惯于让数组第一个元素存放备用链表第一个节点下标
for (int i = 0; i < MAXSIZE-1; i++)
{
space[i].cur = i + 1;
}
//一般习惯用数组最后一个元素的cur存放第一个插入元素的下标
space[MAXSIZE-1].cur = 0;
return OK;
}
在前面的动态链表中,节点的申请和释放分别借用malloc()和free()两个函数来实现.在静态链表中,我们需要自己实现这两个函数malloc_StList()和free_StList()。
为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新节点。
2.3实现malloc_StList()和free_StList()
//2.3实现malloc函数和free函数
int Malloc_StList(StaticLinkList space)
{
//(1)定义一个临时变量用于返回第一个空闲备用链表下标
int i = space[0].cur;
//(2)将i的下一位作为第一个空闲备用链表下标(方便下次取用)
if (space[0].cur)
{
space[0].cur = space[i].cur;
}
return i;
}
void Free_StList(StaticLinkList space,int i)
{
//(1)先把删除前的第一个空闲备用链表下标给space[i].cur
//也就是说由于删除操作,之前的第一个空闲下标变成了第二个空闲下标
space[i].cur = space[0].cur;
//(2)再把第i个元素作为第一个空闲备用链表下标
space[0].cur = i;
}
2.4统计静态链表长度
//2.4统计静态链表的长度
int Length_StList(StaticLinkList L)
{
//(1)设置计数器
int cnt = 0;
//(2)设置一个变量用于查找cur!=0元素个数
int i = L[MAXSIZE-1].cur;
while (i)
{
//这里两个语句顺序可以调换
i = L[i].cur;
cnt++;
}
return cnt;
}
2.5静态链表的插入
//2.5静态链表的插入
int Insert_StList(StaticLinkList L, int i, int elem)
//第二个参数表示插入到第i个元素之前,第三个参数表示传入的待插入节点的data
{
//(1)首先检查插入位置是否合理
if (i<1 || i>Length_StList(L) + 1)
{
return ERROR;
}
//(2)判断是否有空闲位置,如果有,进行插入,否则插入失败
int rest = Malloc_StList(L);//先获取第一个空闲位置下标
if (rest)
{
L[rest].data = elem;
//(3)如果插入第i个位置,则从L[MAXSIZE-1]开始需要查找i-1次
int k = MAXSIZE - 1;
for (int j = 1; j <= i - 1; j++)
{
k = L[k].cur;//循环结束后k的值正好是第i-1个元素
}
//注:下面两步可能比较抽象,如果不太理解可以自己画图
//(4)把插入前的最后一个元素(第i-1个元素)的cur值交给新插入的元素的cur
L[rest].cur = L[k].cur;
//(5)将新插入的元素下标交给插入前的最后一个元素(第i-1个元素)的cur
L[k].cur = rest;
return OK;
}
return ERROR;
}
2.6静态链表的删除
//2.6静态链表的删除
int Delate_StList(StaticLinkList L, int i)
{
//(1)检查删除的位置是否合理
if (i<1 || i>Length_StList(L))
{
return ERROR;
}
//(2)找到被删除的位置
int j;//作为查找的变量
//(3)如果删除第i个位置,则从从L[MAXSIZE-1]开始需要查找i-1次
int k = MAXSIZE - 1;
for (j = 1; j <= i - 1; j++)
{
k = L[k].cur;
}
//如果理解了2.5插入操作,这两步就很好理解了
j = L[k].cur;//保存即将被删除的位置(j==i)
L[k].cur = L[j].cur;//将L[j].cur交给L[k].cur
Free_StList(L, j);//将节点j回收到备用链表
return OK;
}
2.7打印静态链表元素
//2.7打印静态链表元素
void Put_LinkList(StaticLinkList L)
{
//设置查找变量(由于数组最后一个元素的cur存放了第一个插入元素下标,因此从这里开始查找)
int temp = L[MAXSIZE - 1].cur;
int index = 1;
printf("静态链表有%d个元素:\n", Length_StList(L));
while (temp)
{
printf("第%d个元素:", index);
printf("%d\n", L[temp].data);
temp = L[temp].cur;
index++;
}
}
2.8测试代码
//运行测试
int main()
{
StaticLinkList L;
Init_StList(L);
int num=Length_StList(L);
printf("静态链表有%d个元素:\n", num);
for (int i = 0; i < 10; i++)
{
Insert_StList(L, i, i);
}
Length_StList(L);
Put_LinkList(L);
Delate_StList(L, 3);
Put_LinkList(L);
return 0;
}
2.9运行结果
如果大家觉得这种方式帮助到了自己,希望各位帮作者点个赞!
预告:手撕数据结构之线性表(下):循环链表,双向链表。