今天我们讲我们数据结构的另一个重要的线性结-----链表,
什么是链表
链表是一种在 物理存储上不连续,但是在逻辑结构上通过指针链接下一个节点的形成一个连续的结构。
他和我们的火车相似,我们的元素是可以类比成车厢,需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。
这就挺现了一个好处:就是我们在插入删除的时候不用将其他的元素进行移动,我们只需要将指针的指向进行改变就行。所以在我们考虑使用顺序表还链表的时候,就可以考虑我们是否需要频繁的插入和删除我们的元素。
链表的在机内的存储
我们的链表在机内存储是不连续的,是分散的,我们要想找到我们的下一个节点就需要一个指针指向我们的下一个节点,这样来考虑的话我们的每个节点就需要一个数据域和一个指针域,用一个结构体。
为什么还需要指针变量来保存下⼀个节点的位置? 链表中每个节点都是独⽴申请的(即需要插⼊数据时才去申请⼀块节点的空间),我们需要通过指针 变量来保存下⼀个节点位置才能从当前节点找到下⼀个节点。
那我们的节点的结构体就可以写出来了:
struct SListNode
{
int data; //节点数据
struct SListNode* next; //指针变量⽤保存下⼀个节点的地址
};
链表的特点
由于链表在存储空间是上不是连续的,我们在来插入删除的时候就不需要去移动,使我们的时间复杂度降低。
而且我们的链表是用一个节点申请一个节点,不会出现节点空间不够和空间浪费的现象。
但是这不方便我们经常访问元素,因为我们要从头节点一个一个的去遍历,每次的访问都需要我们从头开始这就导致我们的访问不方便,是顺序存取。
用链表来实现接口
我们的来拿表有很多种分类,有这几种特性结合在一起:带不带头(就是我们的哨兵位),循不循环,是双向的还是单向的;
我们这里使用的是不带头的单链表。
我们用链表来实现一些接口:增删查改等。
我们先来完成我们的链表的插入:头插和尾插;
我们有一个功能就是我们需要去频繁地申请空间,这样我们可以去包装成一个函数:
创建新的节点
创建新的节点其实就是使用我们动态申请空间函数的应用了,malloc和calloc。
在我们使用完这个函数之后,我们还需要去判断一下是否申请成功。如果空间申请不成功我们可以报个错。
代码:
//申请新的节点
SLTNode* SLBuyNode(SLTDataType x)
{
SLTNode* newNode = (SLTNode*)malloc (sizeof(SLTNode));
if (newNode==NULL)
{
perror("malloc:");
exit(1);
}
newNode->data = x;
newNode->next = NULL;
return newNode;
}
头插:
我们的头插是在我们我们的第一个节点的前面插入,如果我们有哨兵位的话就是在 哨兵位的后面进行插入,我们这里实现的是不带哨兵位的。
那我们这里就有2个问题:就是当我们的链表为空时,我们应该怎么办,和我们的形参参应该是一级指针还是二级指针?
其实当我们的链表为空时,和我们不为空时是一样的处理方法,先创建一个新的节点,使这个新节点的下一个节点指向我们的刚开始的头节点,如果为空,新节点的next指针就指向空
而对于我们的传参,我们应该知道的是,我们在主函数中创建的是一个ListNode*的结构体指针变量phead,我们如果传一级指针的话,那我们的形参就是我们实参的一个拷贝,我们形参的改变并不会改变影响到实参,但是我们在进行头插的时候我们是需要改变我们的实参phead的指向的,所以我们就需要传我们的地址,而我们的一级指针的地址,需要用二级指针来接收。
所以我们这两个问题解决了就可以两我们的代码写出来了。
原来的链表:
插入之后
代码:
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newNode = NULL;
newNode = SLBuyNode(x);
newNode->next = *pphead;
*pphead= newNode;
}
尾插:
我们的尾插就是在我们的链表中的尾部插入,而我们的尾插比头插麻烦的是,我们寻找我们的链表的尾部,因为我们的链表在机内的存储是分开的,我们并不知道我们的尾节点在哪,只能从头开始遍历,找到我们的尾节点,而我们的尾节点是next指针为空的节点,我们通过一个while循环即可,
但是这里又有一个不同的是,当我们的链表为空时,和我们不为空时的是不同的,为空时我们需要将我们的phead指针指向我们的新节点,而不为空时就需要我们去遍历找我们的尾节点,这也告诉我们这里需要传地址,用二级指针接收,
原来的链表:
插入之后
代码:
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newNode = SLBuyNode(x);
SLTNode* p = *pphead;
//空链表
if (*pphead==NULL)
{
*pphead = newNode;
}
else {
//找尾节点
while (p->next!=NULL)
{
p = p->next;
}
p->next = newNode;
}
}
再来实现我们的删除操作:头删和尾删
头删:
我们的头删和我们的头插刚刚相反,是在我们的头部删除。
我们的删除不是简简单单的的改变phead指针的位置,我们需要量将我们节点空间释放,否则会导致内存泄露,因为你向我们的我们的栈区申请的,如果我们的申请了而不将我们的空间还给我们的系统,我们的空间是有限的,当我们很多申请的空间都没有归还,那我们的可能会出现一系列严重的问题。
所以我们在改变我们头指针的位置是还需要将我们删除的那个节点空间释放。
当我们的链表为空时,我们不能进行删除操作,需要断言一下,
代码:
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else {
SLTNode* p = (*pphead)->next;;
free(*pphead);
*pphead = p;
}
}
尾删:
尾删和我们的尾插相反,是删除我们的尾部节点,他和我们的尾插一样,需要去寻找我们的尾部节点。
当我们链表为空时我们不可以去删除,我们在这里需要断言一下。
代码
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);//空链表
SLTNode* p = *pphead;
if ((*pphead)->next == NULL)//只有一个元素
{
free(*pphead);
*pphead = NULL;
}
else {
//找到尾部
while (p->next->next != NULL)
{
p = p->next;
}
free(p->next);
p->next = NULL;
}
}
查找元素
我们的查找元素很简单,我们只需要去遍历我们的链表,去和我们要找的的元素进行比较,如果相等我们就返回这个节点的地址,如果遍历完整个链表还没有找到这个与之相等的节点,就说明链表中没有该节点,此时我们返回空(NULL),在这之前我们的进行判断我们的链表为不为空。
代码:
//查找(按照元素查找)
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* p = phead;
if (phead==NULL)
{
printf("该链表中没有元素\n");
exit(0);
}
else {
//找元素
while (p != NULL && p->data != x)
{
p = p->next;
}
if (p == NULL)
{
return NULL;
}
else {
return p;
}
}
}
打印链表中的元素
我们打印链表中的元素也是将我们的链表遍历,叫我们的节点中的数据域的值给打印出来,当然我们在遍历之前也要判断一下链表尾部为空。
代码
//打印元素
void SLTPrint(SLTNode* phead)
{
SLTNode* p = phead;
while (p)
{
printf("%d->", p->data);
p = p->next;
}
printf("NULL\n");
}
在特点的节点插入和删除:
我们在指定的节点删除和插入,我们需要先在我们的链表中寻找一下有没有该节点,找到该节点后,如果我们是插入,我们只需要创建一个新的节点,我们先要将我们的新节点的next指针改成我们找到的节点的next,再去改变我们该节点的next指针,改成我们插入节点的指针,
对于删除,我们需要找到我们目标节点的前一个节点,因为我们的链表是单向的,当我们找到我们目标节点,但是我们却找不到他的前一个节点,所以我们是要找到目标节点的前一个节点,我们需要一个新的指针来帮我们记住我们需要删除节点,再来将我们目标节点的前一个节点的next改变成我们的目标节点的下一个节点,在将我们的目标节点的空间释放。
代码:
//查找(按照元素查找)
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* p = phead;
if (phead==NULL)
{
printf("该链表中没有元素\n");
exit(0);
}
else {
//找元素
while (p != NULL && p->data != x)
{
p = p->next;
}
if (p == NULL)
{
return NULL;
}
else {
return p;
}
}
}
总代码:
我们需要将我们的文件管理好:我们的头文件和函数的声明就用一个头文件ListNode.h存放,我们实现的接口就用一个ListNode.c文件来存放。
ListNode.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include"Contact.h"
typedef struct person SLTDataType;
typedef struct SListNode
{
//
SLTDataType data;//放数据
struct SListNode* next; //指向下一个节点
}SLTNode;
//打印
void SLTPrint(SLTNode* phead);
SLTNode* SLBuyNode(SLTDataType x);
//头部插入删除/尾部插入删除
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);
ListNode.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SLQ.h"
//打印元素
void SLTPrint(SLTNode* phead)
{
SLTNode* p = phead;
while (p)
{
printf("%d->", p->data);
p = p->next;
}
printf("NULL\n");
}
//申请新的节点
SLTNode* SLBuyNode(SLTDataType x)
{
SLTNode* newNode = (SLTNode*)malloc (sizeof(SLTNode));
if (newNode==NULL)
{
perror("malloc:");
exit(1);
}
newNode->data = x;
newNode->next = NULL;
return newNode;
}
//尾部插入删除
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newNode = SLBuyNode(x);
SLTNode* p = *pphead;
//空链表
if (*pphead==NULL)
{
*pphead = newNode;
}
else {
//找尾节点
while (p->next!=NULL)
{
p = p->next;
}
p->next = newNode;
}
}
//销毁
void SListDesTroy(SLTNode** pphead)
{
SLTNode* L = *pphead;
while (*pphead)
{
L = (*pphead)->next;
free(*pphead);
*pphead = L;
}
}
//头部插入删除
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newNode = NULL;
newNode = SLBuyNode(x);
newNode->next = *pphead;
*pphead= newNode;
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);//空链表
SLTNode* p = *pphead;
if ((*pphead)->next == NULL)//只有一个元素
{
free(*pphead);
*pphead = NULL;
}
else {
//找到尾部
while (p->next->next != NULL)
{
p = p->next;
}
free(p->next);
p->next = NULL;
}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else {
SLTNode* p = (*pphead)->next;;
free(*pphead);
*pphead = p;
}
}
//查找(按照元素查找)
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* p = phead;
if (phead==NULL)
{
printf("该链表中没有元素\n");
exit(0);
}
else {
//找元素
while (p != NULL && p->data != x)
{
p = p->next;
}
if (p == NULL)
{
return NULL;
}
else {
return p;
}
}
}
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(*pphead);//空链表的时候
assert(pos);
SLTNode* newNode = SLBuyNode(x);
SLTNode* p = *pphead;
if (pos == p)//说明在是头插也可以使用我们的头插函数(我这里未使用,自己写)
{
newNode->next = *pphead;
*pphead = newNode;
}
else {
while (p->next != pos)
{
p = p->next;
}
newNode->next = p->next;
p->next = newNode;
}
}
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos && *pphead);
SLTNode* p = *pphead;
if (*pphead == pos)//头删
{
*pphead = (*pphead)->next;
free(pos);
}
else {
while (p->next != pos)
{
p = p->next;
}
p->next =pos->next;
free(pos);
pos = NULL;
}
}
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode*newNode=SLBuyNode(x);//增加新节点
newNode->next = pos->next;
pos->next = newNode;
}
//删除pos之后的节点
void SLTEraseAfter(SLTNode * pos)
{
assert(pos);
if (pos->next == NULL)
{
;
}
else {
SLTNode* p = pos->next;
pos->next = p->next;
free(p);
p = NULL;
}
}