链表
什么是链表
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
链表的连接的方式
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。这个比较抽象,直接上图就比较好理解一些。
首先创建一个结构体,里面有一个元素值,和一个指针
这个表示链表指针的指向
第一个phead表示链表第一个结构体的地址,用1当例子,1表示data的值,0x0012FF80表示指向下一个节点的地址,通过这样每一个指向节点的地址,链表就串联起来了,最后那个0x00000表示为NULL,意思为空他的后面没有指向的值。
链表的创建
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
上面创建了一个结构体,首先使用typedef进行对int类型进行改名,这样我们以后不想存int类型的元素时候直接在typedef上修改即可,然后进行使用typedef对结构体进行简化,方便使用,然后创建一个int类型的元素,和结构体的指针,因为他是指针所以并不会出现无限创建结构体。
链表的打印
void SLTprint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
printf("%d", cur->data);
cur = cur->next;
//cur++这种写法坚决错误,不同于顺序表
}
printf("\n");
}
//尾插的本质是原尾节点存新尾节点的地址
上面是接收指针来进行打印链表,因为有的时候会使用链表头,所以我们创建一个新指针来存放phead,然后通过while进行使用,判定cur是否为空,为空则停止打印,通过cur = cur->next;来指向下一个链表;cur为原节点的地址,cur->next为下一个节点的地址,将下一个赋值给原地址,就进行了迭代;
进行开辟空间
SLTNode* SLTBuyNode(SLTDataType x)
{
//开辟空间
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc");
return;
}
else
{
newnode->data = x;
newnode->next = NULL;
}
return newnode;
}
这个进行开辟空间,开辟空间的地址为newnode
元素值为:newnode->data = x;
newnode->next = NULL;
然后返回是开辟节点的地址;
对链表进行尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead != NULL);
SLTNode* newnode;
newnode=SLTBuyNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//找尾部
SLTNode* cur = *pphead;
while (cur->next != NULL)
{
cur = cur->next;
}
cur->next = newnode;
}
}
首先,这里用的是**pphead,为什么呢,形参是实参的临时拷贝,因为要对指针进行改变,就要传入指针的地址,直接传入指针是不对的,然后要进行断言,因为传入的指针是有可能为NULL(空)的但是指针的地址是“绝对不可能为空的”,所以必须断言然后依然是创建一个新指针来存放pphead进行使用,分两种情况,首先链表为空,则直接赋值插入,链表不为空则通过while来找到尾部“空”,进行插入;
链表的头插
void SLTPushFrot(SLTNode** pphead, SLTDataType x)
{
assert(pphead != NULL);
SLTNode* newnode;
newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
头插就比较简单了,直接让原链表赋值于新开辟空间的next;在把newnde赋值会*pphead;就可以了,链表为空不为空都可以使用,同样的他也是需要对地址进行断言,判断链表地址是否为空;
链表的尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead != NULL);
assert(*pphead != NULL);
if ((*pphead)->next==NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//找尾部
SLTNode* ptr = NULL;
SLTNode* cur = *pphead;
while (cur->next != NULL)
{
ptr = cur;
cur = cur->next;
}
free(cur);
cur = NULL;
ptr->next = NULL;
/*while (cur->next->next != NULL)这样写也可以,指向下下一个的指针是否为空
{
ptr = cur;
cur = cur->next;
}
free(cur);
cur = NULL;*/
}
}
这个是链表的尾删;这个使用双断言
assert(pphead != NULL);链表地址不能为空
assert(*pphead != NULL);链表也不能为空,链表都为空了还删他干嘛;
我们分为两种情况首先是链表只有一个元素,我们直接删除free(*pphead)即可
链表为多个元素,ok,我们先找到尾部,cur->next为空才是尾部,然后使用ptr存放尾部前一个元素的地址,然后对前一个地址上的元素cur->next置为空free,然后free(*pphead)即可;找尾部两种方法均可
cur->next->next表示下一个节点的下一个节点他属于是多走一步
链表的头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead != NULL);
SLTNode* cur = *pphead; // cur 指向头节点
*pphead = cur->next; // 更新头指针指向下一个节点
free(cur); // 释放原来的头节点
cur = NULL; // 将 cur 置为 NULL,虽然在这里 cur 已经没有用了
}
经过上面的一系列的,这个头删就非常简单了,首先双断言,然后储存原头节点,将头节点下一个赋值给头节点,然后释放原头节点;
链表的查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cut = phead;
if (cut == NULL)
{
// 链表为空,直接返回NULL
return NULL;
}
while(cut != NULL)
{
if (cut->data == x)
{
return cut;
}
cut = cut->next;
}
return NULL;
}
这个是链表的查找,由于并没有改变链表,所以不需要使用**pphead,* phead就可以查找,
这里分两种情况,找到和没找到,找到返回位置指针,没找到返回null;
这里说明一下
if (cut == NULL)
{
// 链表为空,直接返回NULL
return NULL;
}
这个是判断是否为空链表的,为空链表则直接返回null,空链表找不到也返回null
选择插入在pos之前插入
//选择插入在pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead != NULL);
assert(pos != NULL); // 检查 pos 不为 NULL
if (*pphead==pos)
{
SLTPushFrot(pphead, x);
}
SLTNode* prev = *pphead;
while (prev->next!=pos)
{
assert(prev != NULL); // 确保 prev 不是 NULL
prev = prev->next;
}
SLTNode* newnode = SLTBuyNode(x);
prev->next= newnode;
newnode->next = pos;
}
首先进行双断言assert(pphead != NULL);
assert(pos != NULL); // 检查 pos 不为 NULL,pos是要选择指向的元素的指针,SLTDataType x表示插入的值
首先分情况,如果是头节点直接调用头插函数即可,若不是,则通过SLTFind找到要插入的元素的指针,然后通过while找到插入的元素的指针的前一个指针,然后进行重新赋值,连接链表
选择插入在pos之前删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead != NULL);
assert(*pphead != NULL);
assert(pos != NULL); // 检查 pos 不为 NULL
if (*pphead == pos)
{
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
依然是先断言,判断是否为空,然后进行分情况判断,若为头节点直接,调用头删函数即可;若不是,用while找到pos前一个节点,让前一个节点的next指向pos的next,在free(pos)即可
选择插入在pos之后添加和删除
void SLTInsertAfter( SLTNode* pos, SLTDataType x)
{
assert(pos != NULL); // 检查 pos 不为 NULL
SLTNode* newnode = SLTBuyNode(x);
SLTNode* tum = pos->next;
pos->next = newnode;
newnode->next = tum;
}
//pos位置后面删除
void SLTEraseAfter(SLTNode* pos)
{
assert(pos->next != NULL); // 检查 pos后面 不为 NULL
SLTNode* tum = pos->next;
pos->next = tum->next;
free(tum);
tum = NULL;
}
因为比较简单所以就一起写了,因为在pos之后,所以不需要找前一个节点,调用SLTFind函数得到pos的指针,但可以不用使用头指针;
添加是直接在先保存pos->next,然后在用pos->next = newnode;用新节点指向pos的next,newnode->next = tum;原来的指向newnode的next即可
删除是最方便的了,首先检测pos的next不能为空为空则不用删除,然后创建一个新指针,是pos的next赋值tum,然后把tum的next指向指向pos的next,这样就跳过了pos后面那个节点;在free(tum);就可以了
释放链表
void SLTDestroy(SLTNode** pphead)
{
if (*pphead==NULL)
{
return;
}
SLTNode* cur = *pphead;
SLTNode* cur2 = NULL;
while (cur)
{
cur2 = cur->next;
free(cur);
cur = cur2;
}
*pphead = NULL;
}
在这个函数中创建了两个指针,一个用于释放链表一个用于储存链表的下一个next,这样就可以成功释放链表了
结尾
总之,链表作为一种灵活而强大的数据结构,在编程中应用广泛。掌握链表的基本原理和操作,不仅有助于提升编程能力,也为解决实际问题提供了有效的工具和思路。希望本文能够帮助读者更好地理解和应用链表,欢迎大家继续深入学习和探索数据结构与算法的世界!
如果有任何其他建议或想法或者发现错误,我非常乐意听取。并努力改进。