写在前面
首先,学习历程重重困难,文章一定会难以避免的冗长,所以在前面先附上我制作的思维导图,这个是线性表总的思维导图hh(包括链表),大家可以先点进去看,思维导图是用的知犀思维导图,不过我觉得好像不用下载那个点开链接输入密码后也可以看,我觉得思维导图的表现力应该比文字更好hh,所以大家可以先看这个思维导图,有个大致的思路hh
思维导图在这里!密码:1836
正文
线性表的链式存储结构
由上一章我们讲的线性表的顺序存储结构。它是有缺点的,当然最大的缺点就是在插入和删除这一块,我们需要移动大量的元素,对此我们就想,我们应该如何让这个操作变简单呢?想想看,如果我们就是要存储一定的元素并且中间要留有足够的空间以便后面有可能要插入的所有元素一一插入,那我们还不如就像是地下党接头一样,没有必要我非得在这儿,你非得在那儿,我们只需要记住彼此的暗号,在哪里见面,和谁见面都由知道这个暗号的人决定就可以了,也就是说,我们的每一个元素,只需要知道对方的地址就可以,而且是下一个元素的地址,这样,我们由第一个元素,就可以在遍历的过程中知道所有元素的地址了,也就是下面这张图:
线性表的链式存储结构定义
说了那么多,也都道出了单链表的比较通俗的看法,那么我们的线性表链式存储结构到底是什么呢?
n个结点(a(i)的存储映像)链结成一个链表。即为线性表(a1,a2…an)的链式存储结构
线性表存储结构的一些概念
- 数据域:存储元素的地方
- 指针域:存储的直接后继位置
- 指针/链:指针域中存储信息的部分
- 结点:a(i)的存储映像,由数据域和指针域组成
- 头指针:链表中第一个结点的存储位置
- 头结点:单链表的第一个结点前附设一个结点
及如下图所示:
头指针与头结点的异同
头指针
- 是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
- 头指针具有标志作用,所以常用头指针冠以链表的名字
- 无论链表是否为空,头指针均不为空。头指针是链表的必备元素
头结点
- 是为了操作方便统一而设立的,放在第一元素的结点前,其数据域一般无意义(也可以存放链表长度)
- 有了头结点,对在第一元素的结点前插入结点和删除第一结点,其操作就与其他结点的操作统一了
- 头结点不一定是链表的必要元素
线性表存储结构代码的描述
我们不关心内存中的实际位置,我们只关心各个元素之间的逻辑关系,就比如说之前那个地下党的例子,我们不关心你在哪个地方我在哪个地方,只要我们心里清楚自己对接的人是谁,他的暗号是什么,无论在何地我们都可以对接,故而我们只关心谁指向谁,比如P是指向元素a(i)的指针,那么P->next
就是a(i)的指针域,它指向的是a(i+1)元素,P->date
就是数据元素,及如下图所示:
故而我们可以写出线性表的单链表存储结构:
typedef struct Node
{
ElemType data;//对应数据
struct Node *next;//下一结点位置
}Node;
typedef struct Node *LinkList; /* 定义LinkList */
单链表
每个结点中只包含一个指针域叫做单链表
在单链表的学习之前,我们为了后面的代码简洁易懂,现在前面有所提示:
#include "stdio.h"
#include "string.h"
#include "ctype.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */
Status visit(ElemType c)
{
printf("%d ",c);
return OK;
}
好了,有了以上基础,我们这就开始我们的单链表之旅吧!
初始化单链表
我们搞定了单链表存储结构,接下来我们就该考虑怎么初始化他,我们要用到malloc
函数,就是随机开辟一片空间给到我们的结点,这个在以后我们也会经常见到
/* 初始化链式线性表 */
Status InitList(LinkList *L)
{
*L=(LinkList)malloc(sizeof(Node)); /* 产生头结点,并使L指向此头结点 */
if(!(*L)) /* 存储分配失败 */
return ERROR;
(*L)->next=NULL; /* 指针域为空 */
return OK;
}
查看是否为空表
有些时候比如我们要删掉某个链表,我们还想看一看链表是不是已经被删掉了,我们就要用到这个操作
/* 初始条件:链式线性表L已存在。操作结果:若L为空表,则返回TRUE,否则返回FALSE */
Status ListEmpty(LinkList L)
{
if(L->next)//如果指针有指向
return FALSE;//不是空表
else
return TRUE;
}
重置为空表
有些时候我们需要将他重置为空表,这样我们就可以减少空间的使用,只用一个链表,利用每个不同问题的解决先后顺序来操作,可以省空间
我们的思路如下:
- 声明一指针
p
和q
- 将第一个结点赋值给
p
- 循环
- 将下一个结点
- 释放
p
- 将
q
赋值给p
代码如下:
/* 初始条件:链式线性表L已存在。操作结果:将L重置为空表 */
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;
}
返回元素个数
由于单链表寻找数时还是需要遍历每一个结点的,因为我们不知道每个结点是什么,这也是单链表的缺点和顺序储存结构的优点,故而如果我们用到单链表,我们就需要计算一下它的长度
/* 初始条件:链式线性表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(LinkList L)
{
int i=0;
LinkList p=L->next; /* p指向第一个结点 */
while(p)
{
i++;
p=p->next;
}
return i;
}
读取某个元素
就如同我们在计算单链表的长度是说的那样,单链表中我们要找的元素我们没办法一开始就知道他在哪儿,故而我们必须要从头找,我们的思路是:
- 声明一个指针p指向链表第一个结点,初始化j从零开始
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1
- 若到链表末端p为空,则说明第i个结点不存在
- 否则直接查找成功,返回结点p的数据
实现如下:
/* 初始条件:链式线性表L已存在,1≤i≤ListLength(L) */
/* 操作结果:用e返回L中第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; /* 让p指向下一个结点 */
++j;
}
if ( !p || j>i )
return ERROR; /* 第i个元素不存在 */
*e = p->data; /* 取第i个元素的数据 */
return OK;
}
返回L中第1个与e满足关系的数据元素的位序
即寻找这个单链表中的一个元素,并返回它的位序
int LocateElem(LinkList L,ElemType e)
{
int i=0;
LinkList p=L->next;
while(p)
{
i++;
if(p->data==e) /* 找到这样的数据元素 */
return i;
p=p->next;
}
return 0;
}
插入一个元素
对于插入,我们不需要惊动其他结点,我们只需要在要插入的相邻的两个结点之间做一些变动即可
我们需要将p的后继结点赋值给s的后继,再将s赋值给p的后继,也就是:
s->next = p->next; /* 将p的后继结点赋值给s的后继 */
p->next = s; /* 将s赋值给p的后继 */
对于单链表表头和表尾的情况,是一样的
表头:
表尾:
我们可以就此总结出插入一个元素的思路:
- 声明一个指针p指向链表头结点,初始化j从1开始;
- 构造一个新节点(我们要用
malloc
函数申请) - 再找到第i-1个结点(遍历)
- 若找到链表末尾p为空,则说明第i个结点不存在
- 否则查找成功,在系统中生成一个空结点s
- 将数据元素e赋值给
s->next=p->next; p->next=s;
- 返回成功
我们来看一下代码实现:
/* 初始条件:链式线性表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;
}
删除某个元素
我们还是先遍历,直到我们要删除的那个元素为止,然后我们只需要让它的前继结点的指针绕过,指向它的后继结点即可
我们所需要做的,其实就一步:
q = p->next;
p->next = q->next; /* 将q的后继赋值给p的后继 */
故而我们的算法思路是这样的:
- 声明一个指针
p
指向链表头结点,初始化j
从1开始; - 当
j<i
时,就遍历链表,让p
的指针后移,不断地指向下一个结点,j
累加1 - 若到链表末尾
p
为空,则说明第i
个结点不存在 - 否则查找成功,将欲删除的结点
p->next
赋值给q
- 单链表的删除语句:
p->next=q->next;
- 将
q
结点中的数据赋值给e
,作为返回; - 释放
q
结点 - 返回成功
实现如下:
/* 初始条件:链式线性表L已存在,1≤i≤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;
}
输出所有的元素
对于这个,我们只需要参照上面的遍历求长度的方法即可,我们遍历的同时输出·我们的值就可以了
/* 初始条件:链式线性表L已存在 */
/* 操作结果:依次对L的每个数据元素输出 */
Status ListTraverse(LinkList L)
{
LinkList p=L->next;
while(p)
{
visit(p->data);//看上方有提示(在一开始单链表的时候)
p=p->next;
}
printf("\n");
return OK;
}
单链表的整表创建
头插法
我们创造单链表的过程就是一个动态生成链表的过程,他所占用的空间和位置是不需要提前就预定划分的
我们从空表建起,依次建立每个元素的结点,并逐个插入链表
算法思路:
- 声名一指针
p
和计数器变量i
- 初始化一空链表
L
- 让L的头结点的指针指向
NULL
,即建立一个带头结点的单链表 - 循环:
- 生成一个新结点赋值给
p
- 随机生成一数字赋值给p的数据域
p->date
- 将
p
插入到头结点与前一新结点之间
实现如下:
- 生成一个新结点赋值给
/* 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */
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; /* 随机生成100以内的数字 */
p->next = (*L)->next;
(*L)->next = p; /* 插入到表头 */
}
}
尾插法
其实我们根本没有必要这么干,根据先来后到原理,我们也可以将所有的新结点放在后面
代码如下
/* 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */
void CreateListTail(LinkList *L, int n)
{
LinkList p,r;
int i;
srand(time(0)); /* 初始化随机数种子 */
*L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */
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; /* 表示当前链表结束 */
}
这两步解释如下:
最后我们的主函数可以这样写:
int main()
{
LinkList L;
ElemType e;
Status i;
int j,k;
i=InitList(&L);
printf("初始化L后:ListLength(L)=%d\n",ListLength(L));
for(j=1;j<=5;j++)
i=ListInsert(&L,1,j);
printf("在L的表头依次插入1~5后:L.data=");
ListTraverse(L);
printf("ListLength(L)=%d \n",ListLength(L));
i=ListEmpty(L);
printf("L是否空:i=%d(1:是 0:否)\n",i);
i=ClearList(&L);
printf("清空L后:ListLength(L)=%d\n",ListLength(L));
i=ListEmpty(L);
printf("L是否空:i=%d(1:是 0:否)\n",i);
for(j=1;j<=10;j++)
ListInsert(&L,j,j);
printf("在L的表尾依次插入1~10后:L.data=");
ListTraverse(L);
printf("ListLength(L)=%d \n",ListLength(L));
ListInsert(&L,1,0);
printf("在L的表头插入0后:L.data=");
ListTraverse(L);
printf("ListLength(L)=%d \n",ListLength(L));
GetElem(L,5,&e);
printf("第5个元素的值为:%d\n",e);
for(j=3;j<=4;j++)
{
k=LocateElem(L,j);
if(k)
printf("第%d个元素的值为%d\n",k,j);
else
printf("没有值为%d的元素\n",j);
}
k=ListLength(L); /* k为表长 */
for(j=k+1;j>=k;j--)
{
i=ListDelete(&L,j,&e); /* 删除第j个数据 */
if(i==ERROR)
printf("删除第%d个数据失败\n",j);
else
printf("删除第%d个的元素值为:%d\n",j,e);
}
printf("依次输出L的元素:");
ListTraverse(L);
j=5;
ListDelete(&L,j,&e); /* 删除第5个数据 */
printf("删除第%d个的元素值为:%d\n",j,e);
printf("依次输出L的元素:");
ListTraverse(L);
i=ClearList(&L);
printf("\n清空L后:ListLength(L)=%d\n",ListLength(L));
CreateListHead(&L,20);
printf("整体创建L的元素(头插法):");
ListTraverse(L);
i=ClearList(&L);
printf("\n删除L后:ListLength(L)=%d\n",ListLength(L));
CreateListTail(&L,20);
printf("整体创建L的元素(尾插法):");
ListTraverse(L);
return 0;
}
我们可以用它来测试我们的上面所说的结构体是否正确,是怎么用的,大家一定要多敲哇哇哇!
总结
本文章我们一起学习了线性表的链式存储结构以及链式存储结构中最简单的结构——单链表,单链表的优点在于插入和删除时比较方便,并且可以随时添加元素不受限制,但是在查找和遍历时不如顺序存储结构方便,因此我们要多重考虑,谨慎使用。