各位同学,大家好,我叫小敖。今天给大家分享数据结构之一链式存储结构,下面是对链表简单介绍,希望大家能理解。
链表介绍
链表是一种物理存储单元上非连续、非顺序的存储结构**,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1).
顺序表和链表差异
顺序表是一种连续的存储结构,数据元素在内存中占据一块连续的存储空间。它可以通过下标来直接访问元素,因此支持随机访问。顺序表可以使用数组来实现,每个元素占据固定大小的内存空间。
顺序表优点
随机访问性能好:由于顺序表的元素在内存中连续存储,可以通过下标直接访问元素,因此随机访问的时间复杂度为O(1)。
存储效率高:顺序表不需要额外的存储空间来存储指针,只需要存储元素本身,因此相对于链表来说,存储效率较高。
缺点:
插入和删除操作复杂:由于顺序表是连续存储的,当需要插入或删除元素时,需要移动其他元素来腾出空间或填补空缺,因此时间复杂度为(O n)
其中n是元素的个数。
存储空间固定:顺序表在初始化时需要指定固定大小的存储空间,如果元素数量超过了预设的大小,需要进行扩容操作,这可能导致时间和空间的浪费。看下图 模拟头删的操作 还是很不方便的。
链表(Linked List): 链表是一种离散的存储结构
,数据元素存储在称为节点的单元中,每个节点 包含数据和指向下一个节点的指针。通过链接各个节点,形成数据的逻辑顺序。
优点:
插入和删除操作简单:由于链表的节点通过指针链接,插入和删除操作只需要修改指针的指向,不需要移动其他元素,因此时间复杂度为O(1)。
灵活使用内存空间:链表可以根据实际情况动态分配内存空间,只需要在插入新节点时分配新的内存,因此避免了固定大小的存储空间的限制。
缺点:
随机访问性能差:由于链表中的元素并不连续存储,访问元素需要通过指针依次遍历,因此随机访问的时间复杂度为O(n),其中n是元素的个数。
需要额外的存储空间:链表的每个节点需要额外的指针来指向下一个节点,这样会占用额外的存储空间,相对于顺序表来说,存储效率较低。
链表分为数值域和指针域,每个指针域里存储下个元素的地址,最后一个指针域 存储的是空(NULL)。
链表内容和输出链表
首先我们创造三个文件 分别为SList.h的头文件 负责各个函数的声明,SList.c链表这个文件来定义函数功能 ,test来负责函数传参 给链表赋值。
链表的定义
SList.h
typedef int SLTDataType; //类型重新定义
typedef struct SListNode//类型重新定义
{
SLTDataType data;//数组域
struct SListNode* next;//指针域
}SLTNode;
链表的打印
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;//cur存储头节点
while (cur != NULL)//如果不为空 就接着往下找 我们前面介绍过 最后一个节点next为空
{
printf("%d->", cur->data);//访问节点里data元素 并且打印
cur = cur->next;//把下一个节点next赋给cur 然后进行循环
}
printf("NULL\n");//空地址 循环结束输出
}
我们知道每个节点里的指针域都存储下一个地址,所以靠next找下一个地址,然后结束以后打印个NULL,这里cur存储头节点是个好习惯,如果这个遍历链表函数打印完 ,还想在头部做动作那边 ,直接找phead就好。
链表的头插
我们在上面已经在SList.h文件声明了链表的 数据域和指针域,但是他只是个模型,实际上我们并没有创造链表,这时候就需要malloc一个结点。
SList.c
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
这里通过malloc创建了个 节点,但是如果我们想进行头部插入话,还得两个指针域部分,然后把指针域的next 指向头。我们来详细拆分 先看完整代码。
void SLPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
newnode->next = *pphead;
*pphead = newnode;
}
这里创建一个链表SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));,我们知道头插 插的是链表newnode->data = x;//这里给链表的data存储赋值x
newnode->next = NULL;//然后链表的next赋值为NULL
newnode->next = pphead;//新创建链表的next被原先头部地址赋值
pphead = newnode;**//pphead原先头的位置 赋值给新的头 就是你动态开辟的newnode
链表头插总结
链表头部插入得malloc一个节点,然后给创建出来的节点data部位赋值,next部位指向原先头节点的位置(哪里NULL是个好习惯置空),然后pphead指向新的节点 另外提一嘴为什么要用二级指针,因为*pphead赋值的next得存储下一个节点位置。
> test.c
SLPushFront(&plist, 4);
链表尾插
首先我们知道链表尾部插入或者是头部插入,再或者是任意插入,都离不开要创造链表,那我们可不可以把这个创建链表的功能单独写成一个函数呢。
SList.c
SLTNode* BuyLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//这里创造个节点
if (newnode == NULL)//然后判断
{
perror("malloc fail");
return NULL;
}
newnode->data = x;//data赋值x
newnode->next = NULL;//next设置为空
return newnode;//返回节点
}
接下来我们正式开始玩尾插的玩法了,我们先画图理解思路。这里定义了一个tail指针 ,我们找到尾端空就停下,把新开辟这个节点 赋值给tail最后那个位置 next处 然后尾部插入就成功了
`SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;`
但是我们得考虑一种假设链表为空呢?那我们这里就判断一下链表是不是为空,是的话直接插入就可以了。
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
链表头删
我们先理解思路,我们用del来存储头指针位置pphead,然后我们pphead往下走一步 然后释放掉del 这样链表就头删成功了。好这里不过多分享
void SLPopFront(SLTNode** pphead)
{
assert(pphead); // 链表为空,pphead也不为空,因为他是头指针plist的地址
assert(*pphead); // 链表为空,不能头删。(当然你还可以用温柔的检查)
SLTNode* del = *pphead;
*pphead = (*pphead)->next;
free(del);
链表尾删
首先如果链表为空就不能删除了,如果我们直接暴力检查的一下。
assert(pphead);
assert(*pphead);
如果只有一个节点话 ,我们是不是就自己删除了,直接free掉就好。
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
```c
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail = *pphead;//头节点
// 找尾
while (tail->next->next)//next next为空停止循环
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
链表查找
如何链表查找 很简单 我们遍历链表 判断数值域部分
是不是等于我们要查找数字就好,如果是就返回,如果不是得接着指向下一个位置,如果查不到就返回空指针
代码如下 这里不做过多解释
SLTNode* STFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
在pos之前插入
我们要在pos之前新插入一个链表,那是不是我们得找到pos这个位置呀
但是我们如果,定义一个头部指针让他每次都走向下一个,那么假设pos就是第一个呢?很简单我们头插就好。
if (*pphead == pos)
{
SLPushFront(pphead, x);
}
while (prev->next != pos)
{
prev = prev->next;
}
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
//assert(*pphead);
if (*pphead == pos)
{
SLPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuyLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
如果prev第一个节点等于pos就进行头插,否则就是pphead赋值给prev,prev->next等于pos就停下来prev是pos前一个节点,然后创建一个节点,prev 指针域等于next=newnode这个节点 然后newnode这个指针域等于pos位置地址.
在pos之后插入
// 在pos之后插入
void SLInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuyLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
这里思路是 创建一个节点,然后在创造节点next位置 指针域里存储pos->next下一个节点,pos->next节点等于newnode地址。
删除pos任意位置
这里的思路了和pos之前删除差不多不做过多解释,直接附上代码。
void SLErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SLPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
删除中间节点
void SLEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
}
读一次 del赋值pos next节点就是第二个节点 然后让pos->next=del->next 等于第三个节点,然后释放掉中间节点。
剩下给大家展示完整的代码
SList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
void SLTPrint(SLTNode* phead);
void SLPushFront(SLTNode** pphead, SLTDataType x);
void SLPushBack(SLTNode** pphead, SLTDataType x);
void SLPopFront(SLTNode** pphead);
void SLPopBack(SLTNode** pphead);
// 单链表查找
SLTNode* STFind(SLTNode* phead, SLTDataType x);
// 在pos之前插入
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
void SLInsertAfter(SLTNode* pos, SLTDataType x);
// 删除pos位置的值
void SLErase(SLTNode** pphead, SLTNode* pos);
// 删除pos位置后面的值
void SLEraseAfter(SLTNode* pos);
SList.c
#include"Slist.h"
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
SLTNode* BuyLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SLPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead); // 链表为空,pphead也不为空,因为他是头指针plist的地址
//assert(*pphead); // 不能断言,链表为空,也需要能插入
SLTNode* newnode = BuyLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//void SLPushBack(SLTNode* phead, SLTDataType x)
//{
// SLTNode* tail = phead;
// while (tail != NULL)
// {
// tail = tail->next;
// }
//
// SLTNode* newnode = BuyLTNode(x);
// tail = newnode;
//}
void SLPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead); // 链表为空,pphead也不为空,因为他是头指针plist的地址
//assert(*pphead); // 链表为空,可以尾插
SLTNode* newnode = BuyLTNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SLPopFront(SLTNode** pphead)
{
assert(pphead); // 链表为空,pphead也不为空,因为他是头指针plist的地址
assert(*pphead); // 链表为空,不能头删。(当然你还可以用温柔的检查)
SLTNode* del = *pphead;
*pphead = (*pphead)->next;
free(del);
// 一个节点
// 多个节点
//if ((*pphead)->next == NULL)
//{
// free(*pphead);
// *pphead = NULL;
//}
//else
//{
// SLTNode* del = *pphead;
// //*pphead = del->next;
// *pphead = (*pphead)->next;
// free(del);
//}
}
void SLPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
// 没有节点(空链表)
// 暴力检查
//assert(*pphead);
// 温柔的检查
/*if (*pphead == NULL)
{
return;
}*/
// 一个节点
// 多个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail = *pphead;
// 找尾
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
SLTNode* STFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
// 在pos之前插入
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
//assert(*pphead);
if (*pphead == pos)
{
SLPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuyLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
// 在pos之后插入
void SLInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuyLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
// 删除pos位置的值
void SLErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SLPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
void SLEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
}
test.c
#include"SList.h"
void TestSList1()
{
SLTNode* plist = NULL;
SLPushFront(&plist, 1);
SLPushFront(&plist, 2);
SLPushFront(&plist, 3);
SLPushFront(&plist, 4);
/*plist = SLPushFront(plist, 1);
plist = SLPushFront(plist, 2);
plist = SLPushFront(plist, 3);
plist = SLPushFront(plist, 4);*/
SLTPrint(plist);
SLPushBack(&plist, 5);
SLTPrint(plist);
}
void TestSList2()
{
SLTNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLTPrint(plist);
SLPopBack(&plist);
SLTPrint(plist);
SLPopBack(&plist);
SLTPrint(plist);
SLPopBack(&plist);
SLTPrint(plist);
SLPopBack(&plist);
SLTPrint(plist);
//SLPopBack(&plist);
}
void TestSList3()
{
SLTNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLTPrint(plist);
SLTNode* pos = STFind(plist, 3);
if (pos)
pos->data = 30;
SLTPrint(plist);
}
void TestSList4()
{
SLTNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLTPrint(plist);
SLTNode* pos = STFind(plist, 3);
if (pos)
{
SLInsert(&plist, pos, 30);
}
SLTPrint(plist);
pos = STFind(plist, 2);
if (pos)
{
SLInsertAfter(pos, 20);
}
SLTPrint(plist);
pos = STFind(plist, 2);
if (pos)
{
SLErase(&plist, pos);
}
SLTPrint(plist);
}
int main()
{
TestSList4();
return 0;
}