数据结构与算法(二)——线性表(中)

目录

一、前情提要

二、单链表——最简单的链式存储结构

   (一)单链表的结构

   (二)结点与头指针

   (三)头结点与首元结点

三、单链表的相关操作


一、前情提要

        上篇文章我们讨论了什么是抽象数据类型,线性表的线性存储链式存储。并给出了线性储存的代码。本篇将围绕链式储存的概念,给出介绍和代码分析。可能有人会想,为啥线性表还有(下)这一篇。。主要是后面还有什么静态链表、循环链表、双向链表等等会在下一篇介绍也会给出代码。这一篇主要讲链式储存中单链表的概念和代码。

二、单链表——最简单的链式存储结构

        在学C语言的时候大家应该接触过链表这个东西,简单的来说就是用指针将两个不同的结点链接起来。类似于一根链子连接两个不同的物体。

         上面是链表的概念,何为单链表呢?单链表顾名思义就是单向的,遍历链表只能一个方向遍历,因为指针是有方向的(A->B)。后面的双链表就是既可以从左往右遍历也可以从右向左遍历(A->B&&B->A)。

   (一)单链表的结构

              单链表是由结点组成的,每个结点里会存在两个部分:指针域和数据域。

              为了表示每个数据元素Ai与其后继数据元素A(i+1)之间的逻辑关系,对于数据元素Ai来说,除了存储本身的信息之外,还要存储一个“指示其直接后继的信息”(即直接后继的存储位置)。

              我们把存储数据元素信息的域称为数据域。把存储直接后继位置的域称为指针域。

              由这两部分组成数据元素Ai,称之为结点(Node)

                                                                                                               ————《大话数据结构》

              上面一段话我摘自一本书中,我觉得解释的很清楚直接贴上去了。

              n个结点连结成一个链表,即为线性表(A1,A2,A3...,An)的链式存储结构。由于链表中只有1个指针域,所以只能单向的遍历,所以叫单链表。

     

     (二)结点与头指针

        对于线性表来说,总会有头有尾。单链表就是最简单的线性表,所以肯定有个头。

        我们把链表中第一个结点的存储位置叫做头指针 。所以说知道头指针就掌握了整个单链表。d111ecd86af84f419a8a1a0bc98e5a6c.png

       注意:第一个结点里的指针域和头指针不是一个玩意,没有任何的联系。指针域是指向下一个节点的指针,头指针是指向第一个结点的指针。

       说完了头再说一下尾。最后一个结点一样有两个区域,一个数据域储存数据,另一个是指针域指向为“空”(NULL),因为它没有后继了嘛。

      (三)头结点与首元结点

        头结点首元结点是两个不同的概念。

        头结点:头指针指向的结点,它的数据域中不存在数据。如果有数据他就是首元结点,如果没有数据他就是头结点。

        首元结点:第一个数据域中有数据的结点。如果存在头结点,那么首元结点就是头结点后面的第一个结点。

        从上面定义就可以看出来,头结点并不是必需的,它可以没有头结点,头指针直接指向首元结点。这么做确实没问题。逻辑上行得通。但计算机里面存在一定有它的道理。为什么存在理由如下。

      

我们先看有头结点的单链表

03738f7dff764589ae4b7494c25aea95.png

       这是有头结点的单链表,此时我要插入两个结点。这俩结点肯定是插到头结点后面,因为头结点后面的结点才有意义。

        具体步骤是:

    1.先创建新的结点。

f2de6cc04db94409b90925224f99c16d.png

    2.然后将新的结点中指针域指向要插入的那个位置上的结点。 

caf0a413a67a4419aca0c68b8281fc23.png

    3.再将上一个位置结点的指针域修改成指向新的结点

f8b28a25c02f470382e648c9c8522bb9.png

由上面的步骤我们可以看出来,添加的这两个结点,步骤完全相同,可以用同一份代码完成。

反观没有头结点的单链表

9d19fe21a8674cecb133f1d81eb139cc.png

     此时我要添加两个新的结点进去。我选择一个放在首元结点前面,这样新的结点就变成了首元结点,另外一个放在首元结点后面。

     我们再按上面步骤来一遍。

1.先创建新的结点。

505545b843684e34ae8441c21dee2eb4.png

 2.然后将新的结点中指针域指向要插入的那个位置上的结点。 

        e4f7526994334a50b60e8f77cda4ee05.png

  3.再将上一个位置结点的指针域修改成指向新的结点

到了这一步,咱就发现问题了。新插入的结点2按照步骤完全没问题。

但是新插入的结点1是在首元结点前面,首元结点上一个位置不存在结点,要想插入只能改变头指针。

综上所述,我们可以看出新插入的结点1和新插入的结点2,所需要的步骤不一样,要两份不同的代码。这就很蛋疼。所以一般情况下,为了一致性我们都会去采用带有头结点的单链表。

三、单链表的相关操作

         不论哪种存储方式相关操作都差不多,比如:输出表中数据(PrintList),清空表中数据(MakeEmpty),查找数据(Find),插入元素(Insert),删除元素(Delete),返回某个位置上的元素(FindKth)。

         直接上代码吧,光说有点抽象,注释我尽量写的详细。

         其中有几个操作得细细品味一下,自己动手操作。

#include <stdio.h>
#include <stdlib.h>

//此处Node和LinkList的区别,Node是结点,LinkList就是头指针。 
typedef struct NodeList
{
	int data;
	struct NodeList* Next;
}Node,*LinkList;

LinkList InitList()
{
	LinkList Ptr=(Node*)malloc(sizeof(Node));//声明头指针,并开辟一块内存。这块地方就是头结点。
	if(Ptr==NULL)
	{
		printf("申请内存失败\n"); 
	} 
	else
	{
		Ptr->Next=NULL;//先开始只有头结点,头结点后面指向空。 
	}
	return Ptr; 
} 

//输出该链表上所有元素
void PrintList(LinkList Ptr)//传入头指针。 
{
	if(Ptr->Next==NULL)
	{
		printf("该链表没有结点\n");
		return; 
	}
	
	for(Node* i=Ptr->Next;i!=NULL;i=i->Next)  //循环遍历就好 
	{
		printf("%d ",i->data);
		
	}
	printf("\n");
} 

//查找对应的元素 
Node* Find(LinkList Ptr,int x)
{
	for(Node* i=Ptr;i!=NULL;i=i->Next) //从头结点开始遍历,一直到最后指针域指向NULL 
	{
		if(i->data==x)   //判断一下 
		{
			printf("已找到对应元素所对应结点\n");
			return  i;   //i是地址,是所对应结点的地址。 
		}
	}
	printf("未找到对应元素所对应结点\n");//都遍历完了还没有return回去,说明没找到。 
	return NULL; 
} 

//头插 ,在头结点之后插一个结点 
void HeadInsert(LinkList Ptr,int x)
{
	Node* New_Node=(Node*)malloc(sizeof(Node)); //为新的结点开辟一块内存
	if(New_Node==NULL)
	{
		printf("开辟内存失败\n");
		return; 
	} 
	New_Node->data=x;     //给新的结点赋值
	New_Node->Next=Ptr->Next; //新的结点Next指针指向头结点指向的地方。 
	Ptr->Next=New_Node;   //头指针的Next指针指向新结点的地方。 
} 

//尾插
void TailInsert(LinkList Ptr,int x)
{
	Node* New_Node=(Node*)malloc(sizeof(Node));//为新的结点开辟内存 
	if(New_Node==NULL)
	{
		printf("开辟内存失败\n");
		return; 
	}
	for(Node* i=Ptr;i!=NULL;i=i->Next)//由于是单链表找到末尾必须从头遍历 
	{
		if(i->Next==NULL)  //找到之后 
		{
			New_Node->Next=i->Next; //新的结点指针指向该结点后面的地方。 
			New_Node->data=x;       //给新结点赋值 
			i->Next=New_Node;       //该结点指针指向新结点 
			return;
		}
	}
} 

//在值为k的结点后面插
void FindInsert(LinkList Ptr,int k,int x)
{
	Node* Tar_Node=Find(Ptr,k);//找到目标结点 
	if(Tar_Node==NULL)
	{
		printf("不存在该结点\n");
		return;
	}
	Node* New_Node=(Node*)malloc(sizeof(Node)); //步骤和上面一样 
	New_Node->data=x;
	New_Node->Next=Tar_Node->Next;
	Tar_Node->Next=New_Node; 
} 

//删除值为k的结点
void Delete(LinkList Ptr,int k)
{
	if(Ptr->Next==NULL)
	{
		printf("该表为空\n");
		return; 
	}
	Node* Tar_Node=Find(Ptr,k);  //找到与之对应的结点,并记录为目标结点Tar_Ptr 
	if(Tar_Node==NULL)           //如果没找到就返回 
	{
		printf("没有匹配的节点\n");
		return; 
	}
	Node* Pre_Node=Ptr;           //删除的时候需要找到要删除结点的上一个结点。因为我们的想法是A->B->C,使之变成A->C。 
	                            //可以看出要删除B必须找到A的结点,改变结点的Next指针 
	while(Pre_Node!=NULL&&Pre_Node->Next!=Tar_Node)//利用循环找到目标结点的上一个结点 
	{
		Pre_Node=Pre_Node->Next;
	}
	Pre_Node->Next=Tar_Node->Next; //上一个结点的Next指针指向改变成目标结点的下一个。就是绕过目标节点 
	free(Tar_Node);               //最后释放掉要删除节点的内存 
	Tar_Node=NULL;
} 

//清空表中数据
void MakeEmpty(LinkList Ptr)   
{                              
                                
	if(Ptr->Next==NULL)     //先判断是否为空 
	{
		printf("此表为空\n");
		return;
	}
    Node* Last=Ptr->Next;   
	Node* Front;                //先定义两个Node*的指针。一前一后 
    while(Last!=NULL)           //先开始Last指向首元结点,然后进入循环。
	{                           //然后Front指向Last的Next结点。
    	Front=Last->Next;       //这一步的原因是,我们要保证删除节点之后,还能找下一个节点。
    	free(Last);             //然后删除Last所指结点,解决完这一个,就开始下一个。
    	Last=Front;             //最后Last指针指向Front所在结点,这一步对应的上面Front=Last->Next。 
	}                           //只有Front在之前记录Last下一个结点的位置,这样free(Last)之后Last才能定位到下一个位置上。 
	Ptr->Next=NULL;          //一直循环直到表中除了头结点都为空。最后将头指针的Next指向NULL。 
}                               //千万别忘了最后一步,如果忘了,头指针的Next指向的是一块被释放过的区域。 
 
int main()
{
	LinkList Pointer=InitList();
	HeadInsert(Pointer,1);
	HeadInsert(Pointer,2);
	HeadInsert(Pointer,3);
	HeadInsert(Pointer,4);
	PrintList(Pointer);
	
	TailInsert(Pointer,5);
	PrintList(Pointer);
	
	FindInsert(Pointer,2,99);
	PrintList(Pointer);
	
	Delete(Pointer,2);
	PrintList(Pointer);
	
	MakeEmpty(Pointer);
	PrintList(Pointer);
	
	HeadInsert(Pointer,5);
	PrintList(Pointer);
	return 0;
} 

3f44b5ae9c874652bcf92935a530451b.png

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
武汉理工大学的数据结构算法实验包括线性的应用、栈的应用、树的应用、无向图的应用、有向图的应用、查找与排序、哈夫曼编码等内容。这些实验旨在帮助学生掌握数据结构的设计和应用,包括图的存储结构和创建方法、图的遍历方法、迪杰斯特拉算法、最小生成树的概念、普里姆算法等。此外,还有一个名为"简易计算器程序"的实验项目,项目内容是编写程序来模拟简单运算器的工作,实现对达式的计算。希望通过这些实验,学生能够加深对数据结构算法的理解,并提升编程能力。123 #### 引用[.reference_title] - *1* *3* [武汉理工大学数据结构课内实验](https://blog.csdn.net/mo_zhe/article/details/112733427)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] - *2* [武汉理工大学数据结构综合实验——图与景区信息管理系统实践](https://blog.csdn.net/mo_zhe/article/details/112771599)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值