链表创建的陷阱与细节

本文详细介绍了链表的逻辑与物理结构,重点讨论了链表节点的创建、头插和尾插的陷阱,包括二级指针的使用、内存管理、成员类型重命名,以及如何正确处理指针作为函数参数的场景。
摘要由CSDN通过智能技术生成

链表是线性表的一种,它在逻辑结构上是连续的,在物理结构上是非连续的。

也就是说链表在物理空间上是独立的,可能是东一块西一块的。如下顺序表和链表在内存空间上的对比:

而链表的每一块空间是如何产生联系实现在逻辑结构上是连续的呢?

链表的每一块内存称为一个结点(或节点),结点我们用结构体类型的变量来申请空间,其中的一个(或多个)成员用来存放有效数据,另一个成员来存放下一个结点的地址,那样的话我们就可以通过地址访问到下一个结点。

如下:

本章只是对链表创建过程的细节和陷阱进行分析解决,并不会对链表的各种增删查改进行一一讲解,但这些操作会在文章末尾给出原码。

关于一个简单的链表,以下是头文件的声明:

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLDatatype;
typedef struct SLNode
{
	SLDatatype val;
	struct SLNode* next;
}SLNode;
void SLN_HeadAdd(SLNode** pphead, SLDatatype x);//插入结点(头插)
void SLN_EndAdd(SLNode** pphead, SLDatatype x);//插入结点(尾插)
SLNode* SLN_Find(SLNode** pphead, SLDatatype x);//查找元素所在的结点
void SLN_fLocAdd(SLNode** pphead, SLNode* Loc, SLDatatype x);//指定位置后插入
void SLN_pLocAdd(SLNode* Loc, SLDatatype x);//指定位置后插入

void SLN_Print(SLNode* phead);//打印链表

void SLN_HeadDele(SLNode** pphead);//删除结点(头删)
void SLN_EndDele(SLNode** pphead);//删除结点(尾删)
void SLN_LocDele(SLNode** pphead, SLNode* Loc);//指定位置删除

void SLN_Free(SLNode** pphead);//销毁链表释放内存

 细节1:成员类型的重命名

在这个声明结点的过程把int类型重命名为SLDatatype,后面要改储存的数据类型的话,不必再去写的函数中一个一个的修改,只需要在重命名这里一次性修改就可以。

然后这里还把 struct SLNode 重命名为SLNode可以方便后面简写。

现在我们来看插入

因为每次插入必然需要申请结点空间很繁琐,所以我们来封装一个函数专门用来申请结点空间

如下:

SLNode* SLNAdd(SLDatatype x)//x为需要储存的元素
{
	SLNode* pk = (SLNode*)malloc(sizeof(SLNode));
	assert(pk);
	pk->val = x;
	pk->next = NULL;
	return pk;
}

头插

陷阱1:是否使用二级指针

你真正理解什么情况需要传二级指针参数,什么情况不用,为什么用吗?

对于头插,初学者可能会写出下面这样的代码:

void SLN_HeadAdd(SLNode* phead, SLDatatype x)
{
	SLNode* padd = SLNAdd(x);
	if (ps==NULL)
		phead = padd;
	else
	{
		padd->next = phead;
		phead = padd;
	}
}

可以看出这里对参数phead做了更改,如这个表达式

                phead = padd;

但显然这里phead是临时变量,当函数结束后就销毁了,而实参并没有任何变化。

                                                        注意:malloc申请的空间并没有销毁

如何解决?

有两个方法:

就是把更改后的头结点返回去,再用原头结点去接收,如下:

SLNode* SLN_HeadAdd(SLNode* phead, SLDatatype x)
{
	SLNode* padd = SLNAdd(x);
	if (phead==NULL)
		phead = padd;
	else
	{
		padd->next = phead;
		phead = padd;
	}
    return phead;
}

此外还可以通过直接改变原头结点解决,即通过对指针的解引用来实现在函数内改变,而头结点是一个指向结构体的地址(即一个一级指针),改变它就需要一个二级指针参数来接收头结点的地址,然后对这个参数解引用从而达到改变头结点(也就是结构体的地址)的目的。

如下:

void SLN_HeadAdd(SLNode** pphead, SLDatatype x)
{
	SLNode* padd = SLNAdd(x);
	if (*pphead==NULL)
		*pphead = padd;
	else
	{
		padd->next = *pphead;
		*pphead = padd;
	}
}

细节2:指针作为形参目的

下面函数实现的是链表的尾删:


void SLN_EndDele(SLNode* phead)
{
    SLNode* ph = phead;
    if (!ph)
        return;
    SLNode* pk = ph;
    while (ph->next)
    {
        pk = ph;
        ph = ph->next;
    }
    pk->next = NULL;
    free(ph);
} 

我们来思考这个函数为什么不用二级指针也不用返回头结点就能完成,又为什么不用指针完成不了。注意该过程只对结点的成员进行了改变结点的销毁,而要在一个函数内改变结构体成员,是需要得到该成员的地址的,而一个一级指针就刚好满足了这个要求的->就相当于得到该成员的地址并且对他它解引用。而这里这个函数用一级指针作为参数的作用真正用于的是以下两部

                 pk->next = NULL;
                 free(ph);

一个是需要改变成员所以需要一级指针,另一个是需要释放内存所以需要一级指针,没有这两部这个函数的参数完全可以不是指针变量。

例如一个链表的打印函数可以这么写:

void SLN_Print(SLNode head)
{
	while (head.next)
	{
		printf("%d->", head.val);
		 head = *(head.next);
	}
	printf("NULL\n");
}

要注意的是这个表达式head = *(head.next);因为head.next是struct SLNode* 类型head是struct SLNode类型所以这个需要对head.next解引用。

总结:(1)当一个函数需要改变结点的地址时需要传二级指针(或传一级指针但要返回头结点)。(2)当一个函数只需要改变结点成员或销毁结点的时候只需要传一级指针。(3)当一个函数对结点以及成员不做任何改变的时候只需要传一个结构体变量

注意:这里结点指的是结构体的地址。

 陷阱2:运算符优先级问题

下面是一个关于头插的函数:

void SLN_HeadDele(SLNode** pphead)
{
	assert(pphead);
	if (*pphead == NULL)
		return;
	SLNode* ph = (*pphead)->next;
	free(*pphead);
	*pphead = ph;
}

我们要注意的是这个语句:

        SLNode* ph = (*pphead)->next;

在初学者很容易写为 SLNode* ph = *pphead->next;但要注意->成员访问运算符的优先级是比解引用操作符的优先级要高的,这里要不要忘记用()明确运算的优先。

陷阱3:结点前驱丢失问题

在初学者经常犯的一个错误就在对链表进行增删查该等操作过程中常常会把操作的结点后面的结点给弄丢,一旦丢失再也无法找到。

例如一个链表的指定结点之后插入结点的函数这样写:

void SLN_pLocAdd(SLNode* Loc, SLDatatype x)
{
	SLNode* padd = SLNAdd(x);
	Loc->next = padd;
}

或这么写:

void SLN_pLocAdd(SLNode* Loc, SLDatatype x)
{
	SLNode* padd = SLNAdd(x);
	Loc->next = padd;
    padd->next=Loc->next;
}

两种写法都是错误的,第一种写法直接将Loc原本所指向的结点丢失

第二种写法相当于padd->next=padd;不仅丢失了Loc用来指向的结的,还造成打印和尾插等操作的死循环。

正确的写法是

void SLN_pLocAdd(SLNode* Loc, SLDatatype x)
{
    SLNode* padd = SLNAdd(x);
    padd->next = Loc->next;
    Loc->next = padd;
}

 陷阱4:不可逆向遍历缺陷

要知道链表不可逆向遍历的,得到一个结点是无法访问到上一个结点的,只能往下访问。所以如果害怕某个结点在遍历过程中丢失的话,就需要新的变量来把它储存。

例如一个链表的尾删函数:

void SLN_EndDele(SLNode* phead)
{
	SLNode* ph = phead;
	if (!ph)
		return;
	SLNode* pk = ph;
	while (ph->next)
	{
		pk = ph;
		ph = ph->next;
	}
	pk->next = NULL;
	free(ph);
	ph = NULL;
}

其中表达式

                pk = ph;

用变量pk对ph的值进行储存之后再改变ph。

#include"SLNode.h"
SLNode* SLNAdd(SLDatatype x)//
{
	SLNode* pk = (SLNode*)malloc(sizeof(SLNode));
	assert(pk);
	pk->val = x;
	pk->next = NULL;
	return pk;
}
void SLN_HeadAdd(SLNode** pphead, SLDatatype x)
{
	assert(pphead);
	SLNode* ps = *pphead;
	SLNode* padd = SLNAdd(x);
	if (!ps)//相当于if(pk==NULL)
		*pphead = padd;
	else
	{
		padd->next = ps;
		*pphead = padd;
	}
}
//SLNode* SLN_HeadAdd(SLNode* phead, SLDatatype x)
//{
//	SLNode* ph = SLNAdd(x);
//	if (phead == NULL)
//	{
//		return ph;
//	}
//	else
//	{
//		ph->next = phead;
//		return ph;
//	}
//
//}
void SLN_EndAdd(SLNode** pphead, SLDatatype x)
{
	assert(pphead);
	SLNode* ps = *pphead;
	SLNode* padd = SLNAdd(x);
	if (!ps)
		*pphead = padd;
	else
	{
		while (ps->next)
		{
			ps=ps->next;
		}
		ps->next = padd;
	}
}
void SLN_Print(SLNode* phead)
{
	while (phead)
	{
		printf("%d->", phead->val);
		phead = phead->next;
	}
	printf("NULL\n");
}
//void SLN_Print(SLNode head)
//{
//	while (head.next)
//	{
//		printf("%d->", head.val);
//		 head = *(head.next);
//	}
//	printf("NULL\n");
//}
SLNode* SLN_Find(SLNode** pphead, SLDatatype x)
{
	assert(pphead);
	SLNode* ph = *pphead;
	while (ph)
	{
		if (ph->val == x)
			return ph;
		ph = ph->next;
	}
	return NULL;
}
void SLN_fLocAdd(SLNode** pphead, SLNode* Loc, SLDatatype x)
{
	assert(pphead);
	SLNode* ph = *pphead;
	SLNode* pd = ph, * padd = SLNAdd(x);
	if (Loc==ph)
	{
		SLN_HeadAdd(pphead, x);
		return;
	}
	while (ph->next)
	{
		if (ph == Loc)
		{
			padd->next = Loc;
			pd->next = padd;
			return;
		}
		pd = ph;
		ph = ph->next;
	}
}
void SLN_pLocAdd(SLNode* Loc, SLDatatype x)
{
	SLNode* padd = SLNAdd(x);
	padd->next = Loc->next;
	Loc->next = padd;
}
void SLN_HeadDele(SLNode** pphead)
{
	assert(pphead);
	if (*pphead == NULL)
		return;
	SLNode* ph = (*pphead)->next;//陷阱
	free(*pphead);
	*pphead = ph;
}
void SLN_EndDele(SLNode** pphead)
{
	assert(pphead);
	SLNode* ph = *pphead;
	if (!ph)
		return;
	SLNode* pk = ph;
	while (ph->next)
	{
		pk = ph;
		ph = ph->next;
	}
	pk->next = NULL;
	free(ph);
	ph = NULL;
}
//void SLN_EndDele(SLNode* phead)
//{
//	SLNode* ph = phead;
//	if (!ph)
//		return;
//	SLNode* pk = ph;
//	while (ph->next)
//	{
//		pk = ph;
//		ph = ph->next;
//	}
//	pk->next = NULL;
//	free(ph);
//	ph = NULL;
//}
void SLN_LocDele(SLNode** pphead, SLNode* Loc)
{
	assert(pphead);
	if (*pphead == NULL)
		return;
	SLNode* ph=*pphead,*pk=ph;
	if (Loc == ph)
	{
		SLN_HeadDele(pphead);
		return;
	}
	while (ph)
	{
		if (ph == Loc)
		{
			pk->next = ph->next;
			free(ph);
			ph = NULL;
			return;
		}
		pk = ph;
		ph = ph->next;
	}
}
void SLN_Free(SLNode** pphead)
{
	assert(pphead);
	while (*pphead)
	{
		SLNode* ph = (*pphead)->next;
		free(*pphead);
		*pphead = ph;
	}
	
}
  • 99
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 130
    评论
评论 130
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值