老当益壮,宁移白首之心;穷且益坚,不坠青云之志。 ——王勃
目录
前言:
一.单链表的定义
二.链表的几种模型
三.前期的准备
四.单链表的实现
1.单链表的尾插
2.打印函数
2.单链表的头插
3.单链表的头删
4.单链表的尾删
5.查找数字操作
6.把pos位置的数字删除
7.在pos位置前面的位置插入数字
五.全部代码
1.SList.h:
2.SList.c:
3.test.c:
距离我们学习顺序表已经过去了二十天了,最近一直太忙了,一直学习单链表,这两天把链表学了一下,准备和大家分享一下。过两天再把双链表更了。望老铁们支持一下。
虽然上次我们实现的是动态的顺序表,但是顺序表有以下的缺点:
1.如果空间不够了,需要增容。增容会付出一定性能消耗,其次可能存在一定的空间浪费。
2.头部或者中部左右的插入删除效率低。时间复杂度是O(N)。
根据以上的缺点,大佬设计出了链表。
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
这里需要存放一个数据元素和一个地址所以我们还是和顺序表一样创建结构体来存放。
typedef int SLTDataType;
typedef struct SLTNode
{
SLTDataType data;
struct SLTNode* next;
}SLTNode;
注意这里一定不能写成:
链表的优点:
1.链表的内存空间不连续。
2.如果知道要处理节点的前一个位置,则进行插入和删除的复杂度为O(1);
3.如果不知道要处理节点的前一个位置,则进行插入和删除的复杂度为O(N)。
4.头插、头删的效率高,时间复杂度是O(1)。
5.没有空间限制,不会溢出,可以存储很多元素。
链表的结构是有无头结点,有无循环,是单链还是双链。它们一共组合起来有八种结构。
第一种:无头无循环单链
第二种:无头无循环双链
第三种:无头循环单链
第四种:无头循环双链
第五种:有头无循环单链
第六种:有头无循环双链
第七种:有头循环单链
第八种:有头循环双链
而我们主要讲两种,一种是今天要说的无头无循环单链表,另一种是有头有循环双链表。
这两个图都是我们想象出来的,为逻辑结构,实际上链表真正的结构是物理结构。
因为地址链表的地址是不连续的,所以这里的地址都是假设的。
在实现链表的时候我们还是一样创建三个文件:
1.SList.h:存放各种的头文件和函数的声明。
2.SList.c:各种函数的实现。
3.test.c:执行代码的整体逻辑。
这里我们就不像顺序表那样写菜单了,如果有兴趣可以参考顺序表的菜单实现。
因为后面多处会用到申请新结点,所以我们把申请新结点封装成一个函数,每次使用的时候,只需要调用一下这个函数即可,用起来非常的方便。
申请新结点:
SLTNode* BuySListNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;//最后一个next赋值为NULL
return newnode;
}
尾插:
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode=BuySListNode(x);
if (*pphead == NULL)
{//这里一点定要判断一下,第一次尾插的时候就是NULL
//如果*pphead为空的时候,必须把它设置为新结点
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)//当链表结束的时候,最后一个next都是赋值为NULL
{
tail = tail->next;
}
tail->next = newnode;
}
}
尾插了之后我们可以打印出来看一下,我们在写代码的时候一定要写一部分,测试一部分。不要把代码写完了才开始测试,到时候一堆bug,非常的难受。所以我现在写一个打印函数来测测尾插有没有问题。
void SListprint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)//也就是看上一个结点的next地址为不为空
{
printf("%d ", cur->data);
cur = cur->next;//这里的地址不是连续的,一定不能使用cur++
}
printf("\n");
}
头插是非常简单的,也就是在第一个结点的前面插入一个数字。
把新结点的next链接到一个结点上,再把开辟的新结点赋值为第一个结点。
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;//开辟的新节点赋值为第一个结点
}
头删就是把第一个结点的地址给free掉,但是我们不能直接free掉,如果我们直接free掉,就找不到下一个结点的位置了,所以再我们free掉之前,我们要先保存一下第二个结点的地址,最后再把
第一个结点给free掉,再把第二个结点改为第一个结点。
void SListPopFront(SLTNode** pphead)
{
SLTNode* next = (*pphead)->next;//记录第二个结点的位置
free(*pphead);
*pphead = NULL;
*pphead = next;//把第二个结点改为第一个结点
}
对于链表的尾删我们要考虑仔细点,一共有三种情况:
第一种:如果链表为空,也没有要删的,直接return就好了。
第二种:如果链表只有一个结点,我们直接free掉即可。
第三种:如果链表有两个或者两个以上的结点时,我们设置两个指针,一个找最后的位置,一个找倒数第二个位置,我们找到next为空的地方,然后释放掉这个结点。然后把上一个结点的next赋值为NULL。
void SListPopBack(SLTNode** pphead)
{
SLTNode* tail = *pphead;
SLTNode* prev = NULL;
//1.链表为空链表的时候
if (*pphead == NULL)
{
return;
}
//2.链表只有一个节点的时候
else if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//3.链表有两个及两个以上的节点时
else
{
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
这个非常简单,就是从第一个结点开始遍历整个链表,如果找到了,直接返回这个地址即可,如果遍历完了都没找到,直接返回NULL。
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
删除数字,肯定链表中要有这个数字才行,所以在删除之前我们要调用查找函数,看看要删除的数字有没有。
如果第一个结点就是我们要删除的数字,就相当于是头删,我们直接调用头删的函数即可。
不是第一个结点的时候,我们就要判断。
void SListErase(SLTNode** pphead, SLTNode* pos)
{
if (*pphead == pos)
{
SListPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
肯定链表中要有pos这个位置数字才行,所以在插入之前我们要调用查找函数,看看要pos位置的数字有没有。再往pos位置前面的位置插入数字
这个也是分两种情况,如果链表结点只有一个,就相当于是头插,直接调用头插的函数。
第二种也是和pos位置删除的函数实现差不多,都是设置两个指针实现。
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
if (pos == *pphead)
{
SListPushFront(pphead, x);
}
else
{
SLTNode* newnode = BuySListNode(x);
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
#pragma once
#include
#include
typedef int SLTDataType;
typedef struct SLTNode
{
SLTDataType data;
struct SLTNode *next;
}SLTNode;
//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SListPushFront(SLTNode** pphead, SLTDataType x);
//头删
void SListPopFront(SLTNode** pphead);
//尾删
void SListPopBack(SLTNode** pphead);
//打印
void SListprint(SLTNode* phead);
//查找数字
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
//在pos前面插入x
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在pos位置的删除
void SListErase(SLTNode** pphead, SLTNode* pos);
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
//申请新的节点
SLTNode* BuySListNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode=BuySListNode(x);
if (*pphead == NULL)
{//这里一点定要判断一下,第一次尾插的时候就是NULL
//如果*pphead为空的时候,必须把它设置为新结点
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)//当链表结束的时候,最后一个next都是赋值为NULL
{
tail = tail->next;
}
tail->next = newnode;
}
}
//打印
void SListprint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL");
printf("\n");
}
//头插
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//头删
void SListPopFront(SLTNode** pphead)
{
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = NULL;
*pphead = next;
}
//尾删
void SListPopBack(SLTNode** pphead)
{
SLTNode* tail = *pphead;
SLTNode* prev = NULL;
//1.链表为空链表的时候
if (*pphead == NULL)
{
return;
}
//2.链表只有一个节点的时候
else if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//3.链表有两个及两个以上的节点时
else
{
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
//查找数字
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//在pos前面插入x
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
if (pos == *pphead)
{
SListPushFront(pphead, x);
}
else
{
SLTNode* newnode = BuySListNode(x);
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
//在pos位置的值删除
void SListErase(SLTNode** pphead, SLTNode* pos)
{
if (*pphead == pos)
{
SListPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
SLTNode* plist = NULL;
void SListtest1()
{
//尾插
printf("尾插的值为:\n");
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListprint(plist);
printf("头插之后为:\n");
SListPushFront(&plist, 0);
SListPushFront(&plist, 10);
SListprint(plist);
//头删
printf("头删之后为:\n");
SListPopFront(&plist);
SListPopFront(&plist);
SListprint(plist);
//尾删
printf("尾删之后为:\n");
SListPopBack(&plist);
SListPopBack(&plist);
SListprint(plist);
}
void SListtest2()
{
printf("尾插的值为:\n");
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListprint(plist);
printf("头插之后为:\n");
SListPushFront(&plist, 0);
SListPushFront(&plist, 10);
SListPushFront(&plist, 20);
SListprint(plist);
printf("尾删之后为:\n");
SListPopBack(&plist);
SListPopBack(&plist);
SListprint(plist);
printf("\n");
printf("删除pos位置的值之后为:\n");
SLTNode* pos = SListFind(plist, 2);
if (pos)
{
SListErase(&plist, pos);
}
SListprint(plist);
printf("在pos位置前面插入数字后为:\n");
pos = SListFind(plist, 1);
if (pos)
{
SListInsert(&plist, pos, 10);
}
SListprint(plist);
}
int main()
{
//SListtest1();
SListtest2();
return 0;
}
总结:这就是单链表的全部内容,总之实现起来是不复杂的,主要还是靠自己理解,自己动手敲代码才行。