【初阶数据结构】单链表

请添加图片描述

前言

  上个内容我们讲到顺序表的增删查改的一些操作,但是我们也发现了使用顺序表时会存在一些问题,比如说:在中间/头部插入数据时效率较为低下、增容时可能需要拷贝数据释放空间,会降低运行效率、增容时也会造成空间浪费。而此时我们就可以考虑线性表中的另外一种结构:链表。

链表的概念、结构以及分类

1.链表的概念、结构

  链表是一种物理结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
  链表的结构与火车类似,火车每节车厢都是独立存在的,使用的时候再连接或者断开。链表的结构也是由这一个个“车厢”组成的,我们称其为)节点(结点),两个jie所表达的意思是一样的。
  节点主要由两个部分组成:

  1. 当前节点要保存的数据
  2. 下一个节点的地址(指针变量)

  链表的结构如下图所示:
在这里插入图片描述

  由于我们每个节点是独立申请的,只有在需要时才去申请一块节点的空间,所以我们需要通过指针变量来保存下一个节点位置才能从当前节点找到下一个节点。
  由此我们可以得到每个节点所对应的结构体代码:

typedef int SLTDateType;
typedef struct SListNode
{
	SLTDateType data;  //节点数据
	struct SListNode* next;  //指针变量用来保存下一个节点的地址
}SLTNode;

注意:

  • 链式结构在逻辑上式是连续的,在物理结构上不一定连续
  • 节点一般从上申请
  • 从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能连续,可能不连续

2.链表的分类

  链表的结构多种多样,可以分为8种不同的链表结构。

  • 带头和不带头(即带哨兵位和不带哨兵位,哨兵位中不会存储有效数据)
  • 单向和双向(即单向链表和双向链表)
  • 循环和不循环

  我们下面所说的是:单向不带头不循环链表。

3.创建链表节点

  当我们想使用链表时,首先得先有节点,由节点组成链表,此时我们需要malloc一个个去申请节点的空间,代码如下:

SLTNode* SLBuyNode(SLTDataType x)
{
   SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
   if(newnode == NULL)
   {
      perror("malloc fail");
      exit (-1);
   }
   newnode->data = x;
   newnode->next = NULL;
}

在这里插入图片描述
newnode->next需要指向为空,否则会越界,变为野指针,非常危险。

4.链表的打印

  与顺序表一样,我们在进行链表的增删查改操作后,想要知道我们写的代码是否正确,我们可以将其打印出来,方便查看其是否正确。代码如下所示:

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

  pcur首先指向了头结点的地址,pcur->next中保存的是下一个节点的地址,因此将pcur->next赋给pcur就是将pcur走向下一个节点,从而实现打印。

5.链表的尾插

  在尾插时,我们首先要先找到尾节点,再将尾结点的next指针指向要插入的节点,即完成了尾插。但是在插入时我们还需判断一下这个链表是否为空,如果为空,那么我们直接将新节点放入头结点的指针中。先上代码:
在这里插入图片描述

  测试打印结果如下:

在这里插入图片描述

  我们发现明明我们传了指针去调用这个节点进行尾插,但是最后的结果依然没有变化。那是因为我们传进来的是头结点的指针,形参会生成一个新的空间,修改的只是这个新空间,实参的空间并没有被修改,也就是实参并没有真正被修改,这样的传参我们称之为传值调用形参只是实参的一份拷贝,我们想要真正通过形参的改变影响实参,就需要通过传址调用,将实参的地址传递给形参,对应要用二级指针来接收。
  对应的关系如下图所示:
在这里插入图片描述
  正确的代码如下:

void SLTPushBack(SLTNode** pphead, SLTDataType x)//尾插
{
	assert(pphead);//指向第一个节点的指针不能为空
	//*pphead是指向第一个节点的指针
	SLTNode* newnode = SLBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		//petail指向了尾结点。
		ptail->next = newnode;
	}
}

  测试结果如下:
在这里插入图片描述
  当我们知道链表的尾删是用二级指针来接收后,顾名思义,后面的所涉及到需要更改链表节点时都需要传二级指针。理解了这个之后,后面的东西就是易如反掌。

6.链表的头插

在这里插入图片描述

  我们只要建立一个新节点,并让新节点的next指针指向第一个节点,然后让头结点指向newnode即可,代码如下所示:

void SLTPushFront(SLTNode** pphead,SLTDataType x)
{
   assert(pphead);
   SLTNode* newnode = SLTBuyNode(x);
   newnode->next = *pphead;
   *pphead = newnode;
}

  头插后结果如下:
在这里插入图片描述

7.链表的尾删

在这里插入图片描述

  当我们尾删的时候,我们首先要进行判断链表不能为空, 然后要判断链表是否只有一个节点,若只有一个,则尾删后要将其空间释放;最后在尾删时将最后一个节点释放,尾节点指针往前走一个,使前一个节点成为尾节点。参考代码如下:

void SLTPopBack(SLTNode** pphead)
{
   assert(pphead && *pphead);  //链表不能为空
   if((*pphead)->next == NULL) //->优先级高于*
   {
      free(*pphead);
      *pphead = NULL;
   }
   else
   {
   SLTNode* prev = *pphead;
   SLTNode* ptail = *pphead;
   while(ptail->next)  //找尾节点以及尾节点的前一个节点
   {
      prev = ptail;
      ptail = ptail->next;
   }
   free(ptail);
   ptail = NULL;
   prev->next = NULL;
   }
}

  尾删后结果如下:

在这里插入图片描述

8.链表的头删

在这里插入图片描述
  我们要删除头节点之前首先要先找到头节点的下一个节点后才能删除释放头结点,并将原头节点指针向后移,参考代码如下:

void SLTPopFront(SLTNode** pphead)
{
   assert(pphead && *pphead);
   SLTNode* next = (*pphead)->next;  //将头节点的下一个节点保存起来
   free(*pphead);
   *pphead = next;
}

  测试头删结果如下:
在这里插入图片描述

9.链表的查找

  我们需要在这个链表中查找一个节点是否存在,我们通过遍历这个链表即可。我们需要注意的是,在查找链表的过程中,我们并没有修改链表当中的值,也就是我们在调用查找这个函数的时候,只需要传一级指针即可。 代码如下所示:

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
   SLTNode* pcur = phead;
   while(pcur)
   {
      if(pcur->data == x)
         return pcur;
      pcur = pcur->next
   }
   return NULL;
}

  测试查找结果如下:
在这里插入图片描述

10.在指定位置前插入数据

在这里插入图片描述
  在指定位置pos前插入数据,首先我们要找到pos节点的前一个节点prev,我们要先将newnode节点的next指针指向pos节点,然后改变prev节点的next指针,使其指向newnode。(按图上顺序就是先红后绿)切记!!!二者不可改变次序,否则会找不到pos节点。写完后我们发现,当pos节点是头节点时,代码会运行报错,是因为我们找不到prev节点,此时则要使用头插进行插入。代码如下所示:

void SLTInsert(SLTNode** pphead,SLTNode* pos, SLTDataType x)
{
   assert(pphead && *pphead);
   assert(pos);
   SLTNode* newnode = SLTBuyNode(x);
   if(pos == *pphead)  //说明是头插
      SLTPopFront(pphead, x);
   else
   {
      SLTNode* prev = *pphead;
      while(prev->next != pos)
         prev = prev->next;
      newnode->next = pos;
      prev->next = newnode;
   }
}

  指定位置插入数据测试运行结果如下:

在这里插入图片描述

11.在指定位置之后插入数据

在这里插入图片描述
  由于我们只用在指定位置pos之后插入数据,pos的位置我们是知道的,因此不需要再传头结点指针了。如图所示,我们同样要以先红后绿的顺序进行写代码,参考代码如下:

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
   assert(pos);
   SLTNode* newnode = SLTBuyNode(x);
   newnode->next = pos->next;
   pos->next = newnode;
}

  测试运行结果如下:
在这里插入图片描述

12.删除pos节点

在这里插入图片描述
  删除pos节点,我们同样要知道pos节点的前一个节点prev,和后一个节点pos->next,删除pos节点后将这两个节点连接起来。但是同样的,当pos节点为头节点时,我们找不到prev这个节点,因此这要单独讨论,将它进行头删即可。参考代码如下:

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
   assert(pphead && *pphead && pos);
   if(pos == *pphead)
   {
      SLTPopFront(pphead);
   }
   else
   {
      SLTNode* prev = *pphead;
      while(prev->next != pos)
         prev = prev->next;
      prev->next = pos->next;
      free(pos);
      pos = NULL; 
   }
}

  测试运行结果如下:
在这里插入图片描述

13.销毁链表

  当我们进行完链表的增删查改操作后,我们要记得每个节点都是由动态申请内存得到的,所以必须要销毁链表,就要把节点一个一个销毁掉,参考代码如下:

void SLisDesTroy(SLTNode** pphead)
{
   assert(pphead && *pphead);
   SLTNode* pcur = *pphead;
   while(pcur)
   {
      SLTNode* next = pcur->next;
      free(pcur);
      pcur = next;
   }
   *pphead = NULL;
}

  至此链表的所有操作也都结束了,下面是完整代码,同样分为了三个文件。

全部代码

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 SLTPushBack(SLTNode** pphead, SLTDataType x);//尾插
void SLTPushFront(SLTNode** pphead, SLTDataType x);//头插

void SLTPopBack(SLTNode** pphead);//尾删
void SLTPopFront(SLTNode** pphead);//头删

SLTNode* SLTFind(SLTNode* phead, SLTDataType x);//查找

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//在POS位置之前插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);//在指定位置之后插入数据

void SLTErase(SLTNode** pphead, SLTNode* pos);//删除pos节点
void SLTEraseAfter(SLTNode* pos);//删除pos节点之后的数据

void SlistDesTory(SLTNode** pphead);//销毁链表

SList.c

#include"SList.h"

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

SLTNode* SLBuyNode(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 SLTPushBack(SLTNode** pphead, SLTDataType x)//尾插
{
	assert(pphead);//指向第一个节点的指针不能为空
	//*pphead是指向第一个节点的指针
	SLTNode* newnode = SLBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		//petail指向了尾结点。
		ptail->next = newnode;
	}
}

void SLTPushFront(SLTNode** pphead, SLTDataType x)//头插
{
	SLTNode* newnode = SLBuyNode(x);
	assert(pphead);
	newnode->next = *pphead;
	*pphead = newnode;
}

void SLTPopBack(SLTNode** pphead)//尾删
{
	assert(pphead && *pphead);//链表也不能为空
	if ((*pphead)->next == NULL)//链表中只有一个节点
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* prev = *pphead;
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		free(ptail);
		ptail = NULL;
		prev->next = NULL;
	}
}

void SLTPopFront(SLTNode** pphead)//头删
{
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)//查找
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead && pos);
	SLTNode* newnode = SLBuyNode(x);
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		newnode->next = pos;
		prev->next = newnode;
	}
	
}

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

void SLTErase(SLTNode** pphead, SLTNode* pos)//删除pos节点
{
	assert(pphead && *pphead && pos);
	if (pos == *pphead)
	{
		SLTPopFront(pphead);
	}
	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
	pos = NULL;
}

void SLTEraseAfter(SLTNode* pos)//删除pos节点之后的数据
{
	assert(pos);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

void SlistDesTory(SLTNode** pphead)//销毁链表
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

test.c

void SListTest02()
{
	SLTNode* plist = NULL;//指向链表的头结点的指针,设定为空
	SLTPushBack(&plist, 5);
	SLTPushBack(&plist, 6);
	SLTPushBack(&plist, 7);
	SLTPushBack(&plist, 8);
	printf("尾插后:");
	SLTPrint(plist);

	SLTPushFront(&plist, 4);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 1);
	printf("头插后:");
	SLTPrint(plist);

	SLTPopBack(&plist);
	SLTPopBack(&plist);
	printf("尾删后:");
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPopFront(&plist);
	printf("头删后:");
	SLTPrint(plist);

	/*SLTNode* find = SLTFind(plist, 4);
	printf("要查找的数字是4\n");
	if (find == NULL)
		printf("没找到!\n");
	else
		printf("找到了!\n");*/
	 
	SLTNode* find = SLTFind(plist, 4);
	SLTInsert(&plist, find, 66);
	printf("指定位置前插入:");
	SLTPrint(plist);
	SLTInsertAfter(find, 88);
	printf("指定位置后插入:");
	SLTPrint(plist);

	SLTNode* find1 = SLTFind(plist, 66);
	SLTNode* find2 = SLTFind(plist, 88);
	SLTErase(&plist, find1);
	SLTErase(&plist, find2);
	printf("删除指定位置的数据:");
	SLTPrint(plist);
	
	SLTEraseAfter(find);
	printf("删除指定位置之后的数据:");
	SLTPrint(plist);
}

int main()
{
	//SListTest01();
	SListTest02();
	return 0;
}

  今天的内容到此结束啦,感谢大家观看,如果大家喜欢,希望大家一键三连支持一下,如有表述不正确,也欢迎大家批评指正。
在这里插入图片描述

  • 27
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值