线性单向链接表
前言
最近两周开始学习指针、结构体,抽空整理了一下链表部分。
线性表链式存储结构包含单链表、双链表、循环链表及有序表这种线性表,单链表的基本运算方式的实现又有很多种类,在代码实现上我们又有不同的语句和逻辑。在这里我们只探讨线性单向链接表的常用知识点和运算实现方法(参考C语言程序设计基础—>数据结构教程)
在写链表的程序时,我们通常写一个函数,然后再调用函数。
一、链表的概述
线性表的链式存储结构称为链表,每个存储结点包含数据域和指针域,在采用链式存储时,在每个结点中除包含数据域以外只设置一个指针域用于指向其后继结点,这样构成的链表称线性单向链接表。
头指针:每个链表有一个头结点,并同故宫头结点的指针唯一标识该链表,称之为头指针。
首指针:指向首结点或开始结点的指针称为首指针。
尾指针:指向尾结点的指针称为尾指针。
存储密度=
二、单链表的基本运用
(一)声明结点类型
用结构体的方式来声明结点类型,在结构体内有存储数据和指向下一个结点的指针。
struct node
{
int value;//存储结点的数据
struct node *next;//指向下一个结点的指针
};
复制代码
按照结构体的声明方式,我们也惯用另一种方式声明结点
typedef struct LNode//用typedef声明结点时一定不能省略LNode,没有LNode的标记就无法声明next类型
{
ElemType value;
struct LNode *next;
}Linknode;
复制代码
以第一种方式为例,声明了该结点类型后,我们得到的是这样的结构
我们类比线性双向链表,在一个结构体内就会有两个指针分别指出和指入
struct node
{
int value;
struct node *last;
struct node *next;
};
复制代码
我们得到的是这样一个结构
我们需要用一个指针记录链表的开始,也就是始终指向第一个结点的变量
struct node *first = NULL;
复制代码
(二)创建结点
三个步骤
1、为结点分配内存空间
2、把数据存储到结点中
3、把结点插入到链表中
这个时候我们需要一个过渡的变量来临时指向该结点,直到结点插入到链表中
struct node *new_node;//新建一个指针来过渡插入结点
new_node = malloc(sizeof(struct node));//为新结点动态分配内存空间,主义sizeof()是待分配的类型
复制代码
那么现在new_node指针就指向了这样一个内存块,正好放得下一个node结构
然后我们把数据存储到新的结点中去,有如下两种方式
(*new_node).value = 10;//这种方式是结构体中惯用的访问其中的某个成员名称
new_node->value = 10;//这是*和.的组合运算符,能达到同样的目的(更方便的方法)
复制代码
(三)在链表的开始处插入结点
实现插入新结点这一个过程主要是两个语句
new_node->next = first//新的结点中next指针指向first已有结点
first = new_node;//让first指向新的结点,这样就在左侧插入的新的结点
复制代码
图示如下:
以下依次插入10和20的结点就能够很清晰地理解到整个插入新结点的过程,我们直接看书上的例子,把图示理解到了思路就非常清晰。我们假设插入结点时链表为空。
每次插入一个新的结点后,这个用于临时指向新结点的指针new_node就完成了使命,并且在插入下一个结点之前需要重新分配内存空间,再次重复以上操作即可完成从链表开始处插入结点。
(四)编写一个插入结点的函数
struct node *add_to_list(struct node *list,int n)//list是旧链表中首结点的指针,n为插入数据
{
struct node *new_node;
new_node = malloc(sizeof(struct node));
if(new_node == NULL)//判断动态分配的new_node是否为空指针,空则退出
{
printf("Error.");
exit(EXIT_FAILURE);
}
new_node->value = n;//将n赋值给new_node结点中的value
new_node->next = list;//新结点的next指针指向原来链表
return new_node;
//当时我在想是不是少了一个语句 list=new_node,这样才能成功将将插入的新结点串成一个新的链表
//但是在最后有一个 return new_node语句可以将 new_node返回到 list,从而实现对 list的更新
复制代码
示意图图如下:
其实我觉得这个插入新结点的方式很像tRNA的转运方式,依次加入后再继续运输,tRNA上所携带的三个氨基酸就是一个结点中的三个成员,tRNA运输氨基酸在mRNA上的连接方式是肽链的长链接到短链上,而链表中结点时新的接到旧链的左端(倒序)
这个时候插入数据就可以直接调用函数即可
add_to_list(first,10);//这里的实际参数first就是上面的list,插入的数据是10
add_to_list(first,20);//同上
复制代码
PS. 我们需要注意的这样插入的结点时倒序的,最后插入的结点在最左边
(五)编写一个用户录入数的函数
利用上面插入新结点的已有函数,我们可以让用户进行输入操作
struct node *read_numbers(void)
{
struct node *first = NULL;
int n;
printf("Enter a series of numbers:");
for(;;)
{
scanf("%d",&n);
if(n == 0)//定义的输入n==0就结束循环直接返回
{
return first;//到结束命令就返回到链表结束循环
}
first = add_to_list(first,n);//每次循环都更新链表
}
复制代码
(六)搜索链表
for循环的灵活性让我们首选for循环来遍历链表的元素,并用一个指针p来跟踪定位当前的结点位置
struct node *search_list(struct node *list,int n)
{
struct node *p;//用指针p来跟踪结点
for(p = list;p != NULL;p = p->next)
{
if(p->value == n)
{
return p;
}
}
return NULL;//如果没有找到n,那么返回为空指针
}
复制代码
(七)从链表中删除结点
三个步骤:
1、定位需要删除的结点
2、改变前一个结点,让它绕过要删除的结点并指向下一个结点
3、调用free函数收回删除结点占用的内存空间
最直接的思路当然是用搜索链表的方法来定位要删除数据所在的结点,但是!用add_to_list函数时搜索到n则停止搜索并return,不能执行接下来的绕过删除点的操作。只需要再加入一个追踪指针,那么两个跟踪指针一个指向当前的结点,而另一个指针指向该结点前面的那一个,也就是指向的前驱结点
sturct node *delecte_from_list(struct node *list,int n)
{
struct node *cur,*prev;
for(cur = list,prev = NULL;cur != NULL && cur->value != n;prev = cur,cur = cur->next);
//先让开始的cur指向list,prev为空,在cur没有移动到链表末端或值为n时运行
//注意这个地方的for循环!是一个独立的for();搜索完毕后进行判断
//每次循环结束cur赋给prev,cur往后移动一个结点,这时cur仍然是prev的后继结点
if(cur == NULL)//如果cur移动到了末端都没有找到,说明就找不到了,直接返回原来的list
{
return list;
}
if(prev == NULL)//如果prev为空,说明prev还没有加入战斗,list指向下一个结点即可
{
list = list->next;
}else{
prev->next = cur->next;
//其他情况说明定位的结点在中间,那么让删除点前面的结点指向cur的下一个结点即可
}
free(cur);//调用free函数收回其占用的内存空间
return list;
}
复制代码
还有另一种方式可以绕过删除结点,用指针p来跟踪结点位置,在删除结点b的前驱结点处a处经过以下过程
绕过过程如图:
还可以用另外一种代码来实现删除结点
p->next = p->next->next;//用一个指针p来定位删除结点 绕过结点连接成新的链表
free(p->next);//删除b所在结点
复制代码
示意图如下:
从链表中删除结点后:
PS.在单链表中删除一个结点需要找到它的前驱结点!
单链表的建立:头插法 尾插法(接 数据结构 暂略)
以上方法要求大一上期掌握,以下内容接大一下部分 数据结构基础
三、线性表常用运算在单链表中的实现(接 数据结构)
我们用如下的方式声明一个结构体
typedef struct LNode//这样才能在结构体内声明指针next
{
ElemType data;
struct LNode *next;
}LinkNode;
复制代码
(一)初始化线性表
//创建一个空的链接表 时间复杂度O(1)
void InitList(LinkNode *&L)
{
L=(LinkNode *)malloc(sizeof(LinkNode));
L->next = NULL;//创建头结点,next为NULL
}
复制代码
(二)销毁线性表
释放整个单链表L占用的内存空间,逐一释放全部结点的空间。用两个相邻的指针cur和prev,指针prev指向的结点是指针cur指向的结点的前驱结点(初始时,prev指向头结点,cur指向首结点),类似于用两个指针来定位删除结点并且绕过的过程,当cur指针不为空的时候进行循环,释放结点prev,然后prev和cur同时向后移动一个结点。循环结束后prev指向尾结点,再释放prev即可实现。
//时间复杂度O(n),其中n为链表中结点的个数
void DestroyList(LinkNode *&L)
{
LinkNode *prev = L,*cur = L->next;
while(cur!=NULL)//扫描链表(地毯式搜索)
{
free(prev);
prev = cur;
cur = prev->next;//这两个语句可以将cur和prev同时向后移动一个结点
}
free(prev);//循环结束后cur为NULL,prev指向尾结点,将其释放
}
复制代码
图示类似于用两个指针对链表中的结点进行删除的过程
(三)求线性表的长度
//时间复杂度O(n)
int ListLength(LinkNode *L)
{
int n=0;
LinkNode *p = L;//p指向头结点
while(p->next != NULL)
{
n++;//依次计数
p = p->next;
}
return n;
}
复制代码
(四)输出线性表
//时间复杂度O(n)
void DispList(LinkNode *L)
{
LinkNode *p = L->next;//注意从首指针不是头指针开始
while(p != NULL)
{
printf("%d",p->data);//输出p指向的结点中存储的data数据
}
复制代码
以下链表运算方法暂略:
(五)判断线性表是否为空
(六)求线性表中的某个数据元素值
(七)按元素值查找
(八)插入数据元素
(九)删除数据元素