目录
引子
顺序表的问题
问题:1. 中间 / 头部的插入删除,时间复杂度为 O(N)2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。3. 增容一般是呈 2 倍的增长,势必会有一定的空间浪费。例如当前容量为 100 ,满了以后增容到200,我们再继续插入了 5 个数据,后面没有数据插入了,那么就浪费了 95 个数据空间。
因此为了弥补这一缺点引入了单链表的形式 -- 可以一定程度上解决顺序表的问题
下面给出了链表的结构
实际上是地址指向 -- 这里的箭头是为了方便理解,实际并不存在
添加一个数据就开一点空间
单链表
SList.h
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next; //利用 next 来指向下一个节点
}SLTNode;
//创立一个节点(开辟新空间) -- 返回开辟节点的地址
SLTNode* BuySLTNode(SLTDataType x);
//打印
void SListPrint(SLTNode *phead);
//头插
void SListPushFront(SLTNode **phead,SLTDataType x);
//尾插
void SListPushBack(SLTNode** phead, SLTDataType x);
//头删
void SListPopFront(SLTNode** pphead);
//尾删
void SListPopBack(SLTNode** pphead);
//链表的查找
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
//某个位置插入 -- 这里就不再用下标了 -- 默认在pos之前插入 -- 因为库函数一般都是如此,当然后插要简单的多
void SListInsert(SLTNode** pphead, SLTNode *pos, SLTDataType x);
//某个位置插入(在节点后面插入) -- 这个容易实现
void SListInsertAfter(SLTNode* pos, SLTDataType x);
//某个位置删除pos -- pos当前位置删除
void SListErase(SLTNode** pphead, SLTNode* pos);
//某个位置删除pos -- pos后面位置删除
void SListEraseAfter(SLTNode** pphead, SLTNode* pos);
//销毁
void SListDestory(SLTNode** pphead);
SList.c
#include "SList.h"
//打印
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next; //把下一个节点的地址给cur -- 往下一个节点走
}
printf("NULL\n");
}
//创立一个节点(开辟新空间)
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//头插
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead); //pphead 是plist的地址是不可能为空的,所以如果pphead是空说明有问题出现了
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead; //这里得使用二级指针,因为改的是phead的指向,而不是改它所指向的值
*pphead = newnode; //更新头
}
//尾插 -- 先要找到尾部
void SListPushBack(SLTNode** phead, SLTDataType x)
{
assert(phead);
SLTNode* newnode = BuySLTNode(x);
//有两种情况:第一种为空、第二种不为空
//第一种情况要改变plist,所以要使用二级指针
//第二种情况只要找到next就行了,一级指针可以实现
//链表为空:插入第一个节点,要改变SListNode*
//链表不为空:尾插,要改变结构体SListNode(里面的next)
//1、空
if (*phead == NULL)
{
*phead = newnode;
}
else//2、不为空
{
SLTNode* tail = *phead;
while (tail->next != NULL) //找到尾部
{
tail = tail->next;
}
tail->next = newnode;
}
}
//头删
void SListPopFront(SLTNode** pphead)
{
assert(pphead); //二级指针一定不为空,因为是一级指针的地址,如果为空就报错了
//温柔的检查
//if (*pphead == NULL)
//{
// return;
//}
//正常删除不会有问题,但是当删多了就会崩溃掉,所以要检查*pphead(就是plist)是不是空,是空就不可以删除了
//暴力的检查 -- 库里面常用
assert(*pphead != NULL);
SLTNode* del = *pphead;
*pphead = (*pphead)->next; //这里要括号,因为优先级是一样的
free(del);
del = NULL;
}
//尾删
//如果直接找尾,free掉空间,就相当与野指针了,上一个节点还是指向这个空间,但是里面的值都变成随机值了 -- 不合适
//正确方式应该找到前一个节点
//两种写法
//第一种:创立两个指针,一前一后走
void SListPopBack(SLTNode** pphead)
{
assert(pphead);
//暴力的检查 -- 同样的也需要检查 -- 不可以删多
assert(*pphead != NULL);
//只有一共节点的时候
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else//多个节点的时候
{
//第一种:创立两个指针,一前一后走
//assert(pphead);
//SLTNode* prev = *pphead;
//SLTNode* tail = *pphead;
//while (tail->next != NULL)
//{
// prev = tail;
// tail = tail->next;
//}
//prev->next = NULL;
//free(tail); //要改变结构体就要用结构体的指针
//tail = NULL;
//第二种:直接找
SLTNode* tail = *pphead;
while (tail->next->next != NULL)//但是当只有一个节点的时候就有问题了,上面的方法一样,所以要单独拿出来
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
//找到某一个节点
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
//不等于就找下一个节点
cur = cur->next;
}
//没找到就返回NULL
return NULL;
}
//某个位置插入(前插) -- 这里就不再用下标了 -- 默认在pos之前插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
//当只有一个节点的时候就会出问题 -- 因为会找不到
//这种情况相当于头插了
if (pos == *pphead)
{
SListPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
//暴力的检查 -- 同样的也需要检查
assert(prev);
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
//某个位置插入(后插) -- 这个容易实现
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next; //注意顺序,不然就找不到下一个了
pos->next = newnode;
}
//某个位置删除pos -- pos当前位置删除
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos); //理论上pos不应该为空
if (*pphead == pos) //只有一个节点,相当于头删
{
SListPopFront(pphead); //直接调用头删
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos )
{
prev = prev->next;
//检查pos是不是链表中的节点
assert(prev);
}
prev->next = pos->next;
free(pos);
//pos = NULL; -- 这里置空并没有用,因为出去的时候就会销毁掉,要使用二级指针,不过不要这样做,要改变很多,而且没必要
//这种就和free一样 -- free并不会置空指针,需要使用者置空
}
}
//某个位置删除pos -- pos后面位置删除
void SListEraseAfter(SLTNode** pphead, SLTNode* pos)
{
assert(pos);
if (pos->next == NULL) //如果没有就直接返回就可以了
{
return;
}
else
{
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
}
//销毁
//销毁的时候先要保存下一个节点的地址,不然free掉节点就找不到下一个节点了
void SListDestory(SLTNode** pphead) //里面致空不影响外面的plist,应该使用二级指针
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL; //置空
}
Test.c
#include "SList.h"
//头插、头删测试
void TestSList1()
{
SLTNode* plist = NULL; //改什么值就要传什么的地址(局部是不行的)
SListPushFront(&plist, 1); //得传地址,因为要改结构体指针,所以要传结构体指针的地址
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPrint(plist);
SListPopFront(&plist); //删四次
SListPrint(plist);
SListPopFront(&plist);
SListPrint(plist);
SListPopFront(&plist);
SListPrint(plist);
SListPopFront(&plist);
SListPrint(plist);
//销毁
SListDestory(&plist);
}
//尾插、尾删测试
void TestSList2()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPrint(plist);
SListPopBack(&plist); //删四次
SListPrint(plist);
SListPopBack(&plist);
SListPrint(plist);
SListPopBack(&plist);
SListPrint(plist);
SListPopBack(&plist);
SListPrint(plist);
//SListPopBack(&plist); //多删一次
//SListPrint(plist);
//销毁
SListDestory(&plist);
}
//销毁、查找测试
TestSList3()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPrint(plist);
SLTNode* pos = SListFind(plist, 3);
if (pos)
{
printf("找到了\n");
//查找到就可以修改了,因为返回的是指针,可以直接修改
pos->data *= 10;
}
else
{
printf("没找到\n");
}
SListPrint(plist);
pos = SListFind(plist, 1); //在1前面插就相当与头插了
if (pos)
{
SListInsert(&plist, pos, 30);
}
SListPrint(plist);
SListDestory(&plist);
}
//某个位置的删除测试
void TestSList4()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPrint(plist);
//删除某一个
SLTNode* pos = SListFind(plist, 4);
if (pos)
{
SListErase(&plist, pos);
//这里要注意pos的野指针问题 -- pos节点的地址是不变的,但是里面的值变成随机值了,所以要置空
pos = NULL;
}
SListPrint(plist);
//删头
pos = SListFind(plist, 1);
if (pos)
{
SListErase(&plist, pos);
}
SListPrint(plist);
}
//单链表并不会很好的解决顺序表的所以问题,单链表适合头部的操作,删除、插入之类的 -- 时间复杂度达到了O(1)
//如果要任意高效插入删除需要 -- 用到双向链表
//单链表开始是不需要初始化的 -- 开始置空就行了
int main()
{
//TestSList1();
//TestSList2();
//TestSList3();
TestSList4();
return 0;
}
数据结构是不需要写什么菜单之类的,不过这个单链表还是有些问题无法得到很好的解决,于是就又有了进一步的链表,双向链表
结束语
枝上柳绵吹又少,天涯何处无芳草。
宋·苏轼 《蝶恋花·春景》