数据结构链表——单链表
- 概念及结构
- 单链表的实现
- 结构体类型的定义和头文件
- 接口函数
- 打印链表
- 创建新节点
- 尾插
- 头插
- 尾删
- 头删
- 查找
- 任意插入
- 指定位置之前插入
- 指定位置之后插入
- 指定位置删除
- 指定位置后删除
- 单链表空间的销毁
概念及结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
类似于小火车
注意:
1.从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
2.现实中的结点一般都是从堆上申请出来的
3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
单链表的实现
结构体类型的定义和头文件
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;//数据类型
typedef struct SListNode
{
SLTDataType data;//数据
struct SListNode* next;//指针
}SLTNode;//结构体类型
这里结构体数据类型的命名“SLT”为“Sequence List”的缩写 “Data”代表"数据",“Type”代表类型
接口函数
void SLTPrint(SLTNode* phead);//打印链表
SLTNode* BuySLTtNode(SLTDataType x);//创建新节点
void SLTPushBack(SLTNode** pphead, SLTDataType x);//尾插
void SLTPushFront(SLTNode** pphead, SLTDataType x);//头插
void SLTPopBack(SLTNode** pphead);//尾删
void SLTPopFront(SLTNode** pphead);//头删
SLTNode* SLTtFInd(SLTNode* phead, SLTDataType x);//查找
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//任意位置之前插入
void SLTErase(SLTNode** pphead, SLTNode* pos);//任意位置删除
void SLTInsertAfter(SLTNode* pos, SLTDataType x);//在指定位置后插入
void SLTEraseAfter(SLTNode* pos);//在指定位置后删除
打印链表
void SLTPrint(SLTNode* phead)//打印链表
{
SLTNode* cur = phead;//定义一个结构体指针变量来遍历链表
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");//打印尾指针NULL
}
创建新节点
因为后面的头插尾插需要创建新节点,所以干脆写个函数,避免代码的冗余
SLTNode* BuySLTNode(SLTDataType x)//创建新节点
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//开辟空间
if (NULL == newnode)//检查malloc:取消对NULL的引用
{
perror("BuySLTNode malloc");
return -1;
}
newnode->data = x;//赋值x
newnode->next = NULL;//尾指针为NULL
return newnode;//返回头指针newnode
}
尾插
于是就有代码:
void SLTPushBack(SLTNode* phead, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);//创建要插入的节点
//找尾
SLTNode* cur = phead;//遍历链表
while (cur->next != NULL)
{
cur = cur->next;
}
//找到后跳出循环此时cur->next=NULL
cur->next = newnode;//链接新节点
}
测试后发现没有考虑到空链表的情况,这个代码并不能实现空链表的尾插
分情况讨论:
- 空链表: 直接将新节点指针赋值到原来的头指,来修改传入的头指针。这时候就需要考虑到形参只是时实参的临时拷贝,要调用函数修改一级指针,那么传参传参就需要传二级指针。
总结: 修改一级头指针就需要传参二级指针 - 非空链表: 首先找尾指针,然后将新节点链接到尾指针
那么上正确代码:
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);//创建要插入的节点
if (*pphead == NULL)//如果为空链表
{
*pphead = newnode;//修改头指针
}
else//非空链表
{
//找尾
SLTNode* tail = *pphead;//遍历链表
while (tail->next != NULL)
{
tail = tail->next;
}
//找到后跳出循环此时tail->next=NULL
tail->next = newnode;//链接新节点
}
}
头插
首先先分析一下:
- 空链表: 同尾插一样,直接将新节点赋值到传入的头节点即可
- 非空链表
细节直接看代码:
void SLTPushFront(SLTNode** pphead, SLTDataType x)//头插
{
assert(pphead);//防止传入空指针,对NULL的解引用
SLTNode* newnode = BuySLTNode(x);
if (*pphead == NULL)
{
*pphead = newnode;//将新节点赋值到头节点
}
else
{
SLTNode* first = *pphead;//初始化第一个节点的指针
newnode->next = first;//将新节点指向第一个节点
*pphead = newnode;//将新节点赋值到头节点
}
}
其实也可以这样写:
阅读性比较差
void SLTPushFront(SLTNode** pphead, SLTDataType x)//头插
{
assert(pphead);//防止传入空指针,对NULL的解引用
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;//将原链表的头节点(第一个节点)赋值给新节点的尾指针
*pphead = newnode; //再将原来链表的头节点改变,将新节点的头指针赋值给原链表的头节点
}
尾删
首先分析一下:
- 如果链表为空 那肯定不能删,直接断言就好
- 如果链表有一个节点 ,这时候找不到尾节点的前一个节点,所以单拿出来讨论,这时候直接释放头节点空间,然后将头节点置空,因为修改头节点所以要形参用二级指针
- 链表节点个数大于1 这时候只需要找prev节点,然后释放prev->next,然后置空
细节直接看代码:
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);//防止传入空指针,对NULL的解引用
assert(*pphead);//空链表不可以删除
//只有一个节点的链表
if ((*pphead)->next == NULL)
{
free(*pphead);//释放空间
*pphead = NULL;
}
else//链表大于一个节点
{
//找尾节点和尾节点的前一个节点
SLTNode* prev = *pphead;
while (prev->next->next != NULL)
{
prev = prev->next;
}
//此时pren->next->next=NULL,prev->next为尾节点
free(prev->next);//释放尾节点的空间
prev->next = NULL;//尾指针置为空
}
}
头删
分析:
如果链表只有一个节点也需要修改头指针,所以传二级指针,细节直接看代码
void SLTPopFront(SLTNode** pphead)//头删
{
assert(pphead);//防止传入空指针,对NULL的解引用
assert(*pphead);//空链表不可以删除
SLTNode* first = *pphead;//初始化第一个节点的数据
*pphead= first->next;//头节点指向第一个节点的下一个节点
free(first);//释放空间
first = NULL;//野指针置为空
}
查找
查找就是对链表的遍历,没有对头指针的修改,所以只需要传一级指针就可以。
找到后返回对应的指针
找不到返回NULL
比较简单直接上代码:
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;//初始化一个结构体指针遍历链表,意为当前指针,当然用头指针也可以哦
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
任意插入
实现单链表的随意位置插入可以在指定位置的前一个插入或者在指定位置后一个插入。
任意插入函数声明:
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
void SLTInsert(SLTNode** pphead, SLTDataType pos, SLTDataType x);
这两种其实大体思路一样,只是使用方式不同。
第一种就在主函数调用查找函数;第二种就是在插入函数中调用查找函数。
指定位置之前插入
分析:
- 如果指定位置为第一个节点,那么即为头插,头插需要修改头节点,所以传二级指针
- 链表大于一个节点
细节直接看代码:
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//指定位置之前插入
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SLTPushFront(pphead, x);//头插
}
else
{
SLTNode* newnode = BuySLTNode(x);//创建新节点
SLTNode* prev = *pphead;//初始化指定位置的前一个节点的指针
//找到pos的前一个节点
while (prev->next != pos)
{
prev = prev->next;
}
//跳出循环时prev->next=pos
prev->next = newnode;//pos前一个节点链接新节点
newnode->next = pos;//新节点链接pos节点
}
}
指定位置之后插入
分析:
- 在指定位置之后插入,就不需要传参头指针
- 空链表也不能插入
比较简单直接上代码:
void SLTInsertAfter(SLTNode* pos, SLTDataType x)//在指定位置后插入
{
assert(pos);//空链表不可以进行指定位置后插入
SLTNode* newnode = BuySLTNode(x);//创建新节点
newnode->next = pos->next;
pos->next = newnode;//这里这两个的顺序必须为这样的
}
指定位置删除
分析:
- 空链表不能删除,要删除的位置不为NULL(断言处理即可)
- 删除位置为头指针,就是头删,需要改变头指针,所以穿二级指针
- 正常位置分析如下:
细节直接看代码:
void SLTErase(SLTNode** pphead, SLTNode* pos)//指定位置位置删除
{
assert(pphead);
assert(pos);
//sassert(*pphead);//pos不为空那么就间接证明不是空链表
if (*pphead == pos)//要删除的位置为头指针
{
SLTPopFront(pphead);//头删
}
else//删除位置在第一个节点之后
{
//找pos的前一个节点prev
SLTNode* prev = *pphead;//初始化为头指针,然后遍历找pos的前一个节点
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;//pos前一个节点链接pos后一个节点
free(pos);//释放要删除位置的空间
//pos = NULL;//野指针置空 这一步有没有无所谓,因为传入的pos为一级指针
//函数内部改变并不会影响实参的值,所以调用完该函数后要将传入的pos置空,防止野指针的生成
}
}
指定位置后删除
分析:
- 在指定位置后删除,不需要传头指针
- 空链表不可删除,pos不为空
- 正常节点
比较简单直接上代码:
void SLTEraseAfter(SLTNode* pos)//在指定位置后删除
{
assert(pos);
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
单链表空间的销毁
因为单链表的空间时动态分配的,内存放在堆区,没有在栈区,堆区的内存不会因为函数的调用结束而销毁,需要我们自行销毁
上代码:
void SLTDestroy(SLTNode** pphead)
{
SLTNode* cur = *pphead;
while (cur)//粗人指向尾节点NULL结束
{
SLTNode* temp = cur->next;//临时指针变量将cur->next的值存储起来
free(cur);//释放空间
cur = temp;//将cur指向下一个节点
}
*pphead = NULL;//将野指针置空
}