数据结构 单链表的简单理解和基本操作

Posted 林慢慢i

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构 单链表的简单理解和基本操作相关的知识,希望对你有一定的参考价值。

前言:本章主要内容是数据结构中的单链表。



1.为什么需要链表?

1.1顺序表的缺陷

1.动态增容有性能消耗

2.需要头部插入数据,需要挪动数据

鉴于以上两点缺陷,我们引入链表。

链表的概念及结构概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。


1.2链表逻辑结构如下:

注释:链表结点进行了分层,上层用于存储数据 ; 下层用指针指向下一个结点,以达到连接目的


1.3物理结构如下:

(下图仅为了举个例子,在堆中物理结构未必线性连续)


1.4顺序表和单链表物理结构的对比:

1.顺序表是利用realloc一次性动态调整出一大块空间,所以这一大块空间中的每个单元,地址都是连续的。

2.单链表每一次都是用的malloc开辟的一个空间,那么每个空间的地址一定是不一样且不连续的。


2.单链表的代码实现

程序名功能
SList.h创建单链表,完成一些函数的声明、结构的定义、头文件引用等
SList.c实现单链表各个函数的定义
test.c测试单链表所需函数是否正确

2.1 定义单链表

typedef int SLTDataType; //方便以后修改数据类型

struct SListNode
{
    SLTDatType data;
    struct SListNode* next;     
}SLTNode; //把结构体名改短一点
思考下为何使用typedef?

如果一开始我们就确定了结构体中的变量类型,后续在项目过程中如果需要对这个变量类型进行调整,那么所需的操作是很繁琐的。故使用typedef,后续若是需要修改,改动typedef就足够了。


2.2 单链表的空间开辟

SLTNode* BuySLTNode(SLTDatType elem)
{
	SLTNode* newnode = (SLTNode* )malloc(sizeof(SLTNode));//记得引头文件
    if(newnode == NULL)
    {
    	perror("错误原因:");
        exit(-1);
    }
    newnode->data = elem;
    newnode->next = NULL;    
    return newnode;
}

2.3 单链表的尾插

完成尾插前先回忆下单链表的结构:phead(头指针)指向头结点(第一个结点),之后的每个结点的下层指针next指向下一个结点,其中尾结点的next为空.

实现思路:

1.遍历找到最后一个结点(即其next为空)

2.malloc动态开辟一个空间存储数据,然后把新开辟的空间的next置为空.

3.使用尾结点的next连接新开辟的空间

错误程序:
void SListPushBack(SLTNode* phead,SLTDataType elem)
{

    //第一步:找尾结点,  即cur->next 等于 NULL
    SLTNode* cur = phead;
    while(cur->next != NULL) //cur用于迭代
    {
        cur = cur->next;
    }
    //第二步:开辟新空间
    SLTNode* newnode = BuySLTNode(elem);
    //第三步:连接
    cur->next = newnode;
}
上述尾插代码有错误,思考下哪里出现bug?

1.phead未进行判空,当链表为空时,phead就为空,会引发异常。

2.函数传参错误plist的类型为SLTNode *,而我们形参类型也是SLTNode *,这属于值传递,值传递相当于 形参是实参的一份临时拷贝,形参的改变并不会影响实参的值。想要修改实参的值就需要进行传址操作,在这里传plist的地址.形参用二级指针。

修改后程序
void SListPushBack(SLTNode** pphead, SLTDataType elem)
{
	assert(pphead); //pphead不可以为空指针.
	if (*pphead == NULL)
	{
		*pphead = BuySLTNode(elem);
	}
	else
	{
		//第一步:找尾结点,  即cur->next 等于 NULL
		SLTNode* cur = *pphead;
		while (cur->next != NULL) //cur用于迭代
		{
			cur = cur->next;
		}
		//第二步:开辟新空间
		SLTNode* newnode = BuySLTNode(elem);
		//第三步:连接
		cur->next = newnode;
	}
}

2.4单链表的头插

函数需要改变phead的值,所以我们的形参需要二级指针。

1.创建新节点

2.新节点链接原来的头结点

3.让phead指针指向新节点

void SListPushFront(SLTNode** pphead,SLTDataType elem)
{
    assert(pphead);
    //第一步,创建新节点
    SLTNode* newnode = BuySLTNode(elem);
    //第二步,新结点链接原来的头结点
    newnode->next = *pphead;
    //第三步,phead指针指向新节点
    *pphead = newnode;
}

2.5单链表的数据打印

直接用for遍历

void SListPrint(SLTNode* phead)
{
    SLTNode* cur = phead;
    while(cur->next != NULL)
    {
        printf("%d->",cur->data);
    }
    printf("NULL\\n");
}

2.6 单链表的尾删

1.考虑正常情况,如下图。

2.考虑结点数量只有0或1时。

1.从头开始遍历,找到倒数第二个结点。

2.free掉最后一个结点

3.将原本的倒数第二个结点的next释放掉

void SListPopBack(SLTNode** pphead)
{
    assert(pphead);  //结点数0
    assert(*pphead);  //结点数1
    
    if((*pphead)->next == NULL)//只有一个结点时
    {
        free(*pphead);
        *pphead = NULL;
        return;
    }

	//多节点,第一步,找倒数第二个结点
	SLTNode* cur = *pphead;
	while (cur->next->next != NULL) 
	{
		cur = cur->next;
	}
	//第二步,free掉最后一个结点
	free(cur->next);
	//第三步,将现结点释放掉,置NULL
	cur->next = NULL;
}

2.7 单链表的头删

1.先把第二个结点的地址记下来。

2.释放第一个结点。

3.phead链接到原来的第二个结点

void SListPopFront(SLTNode** pphead)
{
	assert(pphead);
	//0结点
	assert(*pphead);
	//1结点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
    
	//多结点,第一步,保留第二个结点地址
	SLTNode* next = (*pphead)->next;
	//第二步,释放第一个结点
	free(*pphead);
	//第三步,连接第二个
	*pphead = next;
}

2.8 单链表的长度查找

这个函数没有修改phead,所以选择值传递。

int SListSize(SLTNode* phead)
{
    SLTNode* cur = phead;
    int size = 0;
    while(cur->next != NULL)
    {
        size++;
        cur = cur->next;
    };
    return size;
}

2.9 单链表的判空操作

bool SListEmpty(SLTNode* phead)
{
	return phead == NULL;
}

2.10 单链表的值查找操作

如果可以找到,就返回那个结点,如果找不到,返回空指针。

SLTNode* SListFind(SLTNode* phead, SLTDataType elem)
{
    SLTNode* cur = phead;
    while(cur->data != elem)
    {
        cur = cur->next;
    }
    if(cur->data==elem)
    {
        return cur;
    }
    return NULL;
}

2.11 单链表的删除操作

1.找到目标结点之前位置

2.提前保存目标结点后位置

3.销毁目标结点

4.链接原目标结点之前的位置与原目标结点之后的位置

void SListErase(SLTNode** pphead,SLTNode* pos)
{
    assert(pphead);
    //0结点情况
    assert(*pphead);
    //1结点时.相当于头删,直接调用头删.
    if((*pphead)->next == NULL)
    {
        SListPopFront(pphead);
    }
    else
    {
        SLTNode* cur = *pphead;
        while(cur->next != pos)
        {
            cur = cur->next;
        }
        SLTNode* two_next = pos->next;
        free(pos);
        cur->next = two_next;
    }
}

2.12 单链表的插入操作

1.找到目标结点之前结点

2.创建新结点

3.新结点链接目标结点

4.原目标结点之前的结点链接新结点

void SListInsert(SLTNode** pphead,SLTNode* pos,SLTDataType elem)
{
	assert(pphead);
	assert(pos);
    //当第一个结点便是目标结点,其实就是头查
	if (*pphead== pos)
	{
		SListPushFront(pphead,elem);
	}
    //当多个结点时
	else
	{
		SLTNode* pre = *pphead;
		while (pre->next  != pos)
		{
			pre = pre->next;
		}
		SLTNode* next = BuySLTNode(elem);//创建准备插入的结点
		next->next = pos;
		pre->next = next;
	}
}

2.13 单链表的销毁操作

遍历free销毁

void SListDestory(SLTNode** pphead)
{
	assert(pphead);

	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);

		cur = next;
	}

	*pphead = NULL;
}

3.源码链接

https://gitee.com/linkylo/c_code_2021/tree/master/c_code_2021_8_12


数据结构的单链表内容到此设计结束了,感谢您的阅读!!!如果内容对你有帮助的话,记得给我三连(点赞、收藏、关注)——做个手有余香的人。

以上是关于数据结构 单链表的简单理解和基本操作的主要内容,如果未能解决你的问题,请参考以下文章

数据结构--单链表简单代码实现(总结)

单链表~增删查改(附代码)~简单实现

数据结构代码(用C语言) 单链表的插入和删除

理解单链表的反转(java实现)

栈的简单实现-单链表实现

数据结构单链表的创建以及简单操作