我们已经掌握过了一种存储的数据结构了——顺序表。为什么我们还要引进链表呢?其实,顺序表存在了很多的问题,为了解决这些问题,我们就引进了链表的概念。
顺序表存在的问题:
我们上一节学习了顺序表,顺序表是用一段物理地址连续的存储单元来存储数据的线性结构。而链表是采用一段物理地址不连续的存储结构,链表的基本操作是通过指针指向的方式实现的,通过指针的指向,可以使链表更加灵活。
链表是由众多节点组成的,链表中的每个元素称为节点。
节点的组成:
所有节点的地址不是连续的,链表在物理结构上不一定是线性的,但在逻辑结构上是线性的。
链表的形式非常多样化
这节我们讲无头单向不循环链表,下一节我们讲带头双向循环链表
//定义单链表的节点
typedef struct SListNode
{
SLDataType data;//数据域
struct SListNode* next;//指针域
}SLTNode;
//打印单链表
//不会改变链表的头指针,传一级指针
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);//循环遍历单链表
cur = cur->next;//拿到当前节点的指针域,就可以找到下一个节点
}
printf("NULL\n");
}
//动态申请一个节点,并返回节点的地址
SLTNode* BuySLTNode(SLDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
//检查是否开辟成功
if (newnode == NULL)
{
printf("malloc fail\n");
return;
}
//赋值操作
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//释放所有节点
void SListDestory(SLTNode** pphead)
{
assert(pphead != NULL);//断言
SLTNode* cur = *pphead;//找到第一个节点
while (cur != NULL)//遍历链表
{
SLTNode* next = cur->next;//先存储前一个节点的next值,防止把它释放了,找不到下一个节点了
free(cur);//释放节点
cur = next;
}
*pphead = NULL;//置空
}
//尾插
void SListPushBack(SLTNode** pphead, SLDataType x)
{
SLTNode* newnode = BuySLTNode(x);//先动态申请一个节点
if (*pphead == NULL)
{
*pphead = newnode;//当单链表中没有元素时, 这个节点给pphead当成单链表的第一个元素
}
else//单链表中已经有元素了
{
//需要找到尾结点的指针
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail->next;
}
tail->next = newnode;//找到最后一个节点,并把newnode赋给最后一个节点的next值
}
}
我这这里切记要记住,我们传plist的时候,一定要传pilist的指针。我们要传址而不是传值,传值只是创建一个临时变量phead,只会改变phead的值,并不会改变我们想要的plist的值。当我们传plist的值时,因为当链表为空时,我们要创建第一个节点,我们要改变plist的指向,让他指向第一个节点。但是初始的plist和phead都是指向NULL,调用函数后,phead指向了新节点,而plist还是指向了NULL。
注意2:
我们在进行尾插时,一定要分两步,当链表为空时,当链表不为空时。当链表为空时,plist直接指向新节点;当链表不为空时,先要找到链表的最后一个节点,然后将尾结点的next指向新节点。
//头插
void SListPushFront(SLTNode** pphead, SLDataType x)
{
assert(pphead != NULL);//断言
SLTNode* newnode = BuySLTNode(x);//动态申请一个节点
//注意顺序不能搞反了
newnode->next = *pphead;//新节点指向单链表的第一个节点,即新节点的next指针指向plist
*pphead = newnode;//plist指向头插的新节点
}
测试一下头插:
注意:单链表的头插我们不需要考虑单链表是否为空的情况,不管单链表是否为空,这样写都是正确的
单链表的尾删一共有两种思路
思路一:找到单链表尾结点的上一个节点,通过上一个节点的next指针,找到最后一个节点
思路二:利用双指针的思路,分别找到单链表的尾结点和他的上一个节点
//尾删
void SListPopBack(SLTNode** pphead)
{
assert(pphead != NULL);//断言,检查是否传参错误
assert(*pphead);//断言,单链表不能为空
//分三种情况
//1.空,已经用断言排除过了
//2.一个节点
//3.一个节点以上
if ((*pphead)->next == NULL)//单链表只有一个节点
{
free(*pphead);//释放节点
*pphead = NULL;
}
//思路一:找到尾结点的上一个节点
//else//单链表有多个节点
//{
// SLTNode* tail = *pphead;
// while (tail->next->next != NULL)//找到尾结点的上一个节点
// {
// tail = tail->next;
// }
// free(tail->next);//释放尾结点
// tail->next = NULL;//置空
//}
//思路二:双指针
else//单链表有多个节点
{
SLTNode* prev = NULL;//尾结点的上一个节点
SLTNode* tail = *pphead;//尾结点
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);//删除尾结点
prev->next = NULL;//置空
}
}
//头删
void SListPopFront(SLTNode** pphead)
{
assert(pphead != NULL);//断言,检查是否传参错误
assert(*pphead);//断言,单链表不能为空
SLTNode* next = (*pphead)->next;//plist指向头结点的下一个节点,直接跳过第一个节点
free(*pphead);//释放头结点
*pphead = next;//plist指向原来的第二个节点
}
//查找指定数据
SLTNode* SListFind(SLTNode* phead, SLDataType x)
{
SLTNode* cur = phead;
while (cur != NULL)//遍历单链表
{
if (cur->data == x)
{
return cur;
}
}
return NULL;//未找到,返回NULL
}
为什么不在pos位置前插入指定值?
//在pos的后面插入x
void SListInsert(SLTNode* pos, SLDataType x)
{
assert(pos);//断言
SLTNode* newnode = BuySLTNode(x);//动态申请一个节点
newnode->next = pos->next;//新节点的next指向pos位置的后一个节点
pos->next = newnode;//pos的next指向新节点的位置
}
//删除pos位置的节点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
assert(*pphead);
//当pos正好为第一个节点时,相当于头删
if (*pphead == pos)
{
SListPopFront(pphead);
}
else//pos在链表的中间位置
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;//pos前一个节点指向pos的后一个节点
free(pos);//释放pos
pos = NULL;//置空
}
}
#pragma once
#include
#include
#include
typedef int SLDataType;
//定义单链表的节点
typedef struct SListNode
{
SLDataType data;//数据域
struct SListNode* next;//指针域
}SLTNode;
//不会改变链表的头指针,传一级指针
void SListPrint(SLTNode* phead);//打印单链表
//可能会改变链表的头指针,传二级指针
void SListPushBack(SLTNode** pphead, SLDataType x);//尾插
void SListPushFront(SLTNode** pphead, SLDataType x);//头插
void SListPopBack(SLTNode** pphead);//尾删
void SListPopFront(SLTNode** pphead);//头删
SLTNode* SListFind(SLTNode* phead, SLDataType x);//查找指定数据
void SListInsert(SLTNode* pos, SLDataType x);//在pos的后面插入x
void SListErase(SLTNode** pphead, SLTNode* pos);//删除pos位置的节点
#include "SList.h"
//动态申请一个节点,并返回节点的地址
SLTNode* BuySLTNode(SLDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
//检查是否开辟成功
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//赋值操作
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//释放所有节点
//打印单链表
//不会改变链表的头指针,传一级指针
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);//循环遍历单链表
cur = cur->next;//拿到当前节点的指针域,就可以找到下一个节点
}
printf("NULL\n");
}
//释放所有节点
//void SListDestory(SLTNode** pphead)
//{
// assert(pphead != NULL);//断言
// SLTNode* cur = *pphead;//找到第一个节点
// while (cur != NULL)//遍历链表
// {
// SLTNode* next = cur->next;//先存储前一个节点的next值,防止把它释放了,找不到下一个节点了
// free(cur);//释放节点
// cur = next;
// }
// *pphead = NULL;//置空
//}
//尾插
void SListPushBack(SLTNode** pphead, SLDataType x)
{
assert(pphead != NULL);
SLTNode* newnode = BuySLTNode(x);//先动态申请一个节点
if (*pphead == NULL)
{
*pphead = newnode;//当单链表中没有元素时, 这个节点给pphead当成单链表的第一个元素
}
else//单链表中已经有元素了
{
//需要找到尾结点的指针
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;//找到最后一个节点,并把newnode赋给最后一个节点的next值
}
}
//头插
void SListPushFront(SLTNode** pphead, SLDataType x)
{
assert(pphead != NULL);//断言
SLTNode* newnode = BuySLTNode(x);//动态申请一个节点
newnode->next = *pphead;//新节点指向单链表的第一个节点,即新节点的next指针指向plist
*pphead = newnode;//plist指向头插的新节点
}
//尾删
void SListPopBack(SLTNode** pphead)
{
assert(pphead != NULL);//断言,检查是否传参错误
assert(*pphead);//断言,单链表不能为空
//分三种情况
//1.空,已经用断言排除过了
//2.一个节点
//3.一个节点以上
if ((*pphead)->next == NULL)//单链表只有一个节点
{
free(*pphead);//释放节点
*pphead = NULL;
}
//思路一:找到尾结点的上一个节点
//else//单链表有多个节点
//{
// SLTNode* tail = *pphead;
// while (tail->next->next != NULL)//找到尾结点的上一个节点
// {
// tail = tail->next;
// }
// free(tail->next);//释放尾结点
// tail->next = NULL;//置空
//}
//思路二:双指针
else//单链表有多个节点
{
SLTNode* prev = NULL;//尾结点的上一个节点
SLTNode* tail = *pphead;//尾结点
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);//删除尾结点
prev->next = NULL;//置空
}
}
//头删
void SListPopFront(SLTNode** pphead)
{
assert(pphead != NULL);//断言,检查是否传参错误
assert(*pphead);//断言,单链表不能为空
SLTNode* next = (*pphead)->next;//plist指向头结点的下一个节点,直接跳过第一个节点
free(*pphead);//释放头结点
*pphead = next;//置空
}
//查找指定数据
SLTNode* SListFind(SLTNode* phead, SLDataType x)
{
SLTNode* cur = phead;
while (cur != NULL)//遍历单链表
{
if (cur->data == x)
{
return cur;
}
}
return NULL;//未找到,返回NULL
}
//在pos的后面插入x
void SListInsert(SLTNode* pos, SLDataType x)
{
assert(pos);//断言
SLTNode* newnode = BuySLTNode(x);//动态申请一个节点
newnode->next = pos->next;//新节点的next指向pos位置的后一个节点
pos->next = newnode;//pos的next指向新节点的位置
}
//删除pos位置的节点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
assert(*pphead);
//当pos正好为第一个节点时,相当于头删
if (*pphead == pos)
{
SListPopFront(pphead);
}
else//pos在链表的中间位置
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;//pos前一个节点指向pos的后一个节点
free(pos);//释放pos
pos = NULL;//置空
}
}