学习链表相关(上)

Posted 颜 然

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习链表相关(上)相关的知识,希望对你有一定的参考价值。

目录

一、什么是链表

 二、创建静态单链表

 三、创建动态单链表

1、关于初始化

2、关于内存分配:

3、各结构地址存放情况

尾插法建立单链表

头插法建立单链表

三、链表的增删查改

1、增

2、删

3、查

4、改

四、所有基础实现综合


链表是啥呢?光听名字就知道大概是个有连续性的东西,我第一次听到“链表”这个概念的时候想到的是一节节的火车箱。

上一篇博客讲述了结构体的相关知识,实际上链表是通过结构体和指针来实现的


一、什么是链表

链表是一种动态存储分配的一种结构,相当于结构体们通过指针连接的方式形成一个个节点,并且每一个节点有一个前驱节点和一个后继节点(除了首节点没有前驱节点,尾节点没有后继节点),最后由它们组为一个完整的有序结构。其中指针的追踪保证链表中的每一项都包含着在何处能够找到下一项的信息。

现在我声明一个简单结构体:

struct A

  char firstname[10];  //定义数据域

  struct A *next;  //定义指针域,用于储存后继节点地址
;

struct A *head = NULL;  //头指针初始化为空

 相关单链表结构就是:

*head为头指针,指向头节点;*next为后继节点,用于存储后继节点的地址,方便访问并连接下一节点(这个next应该是和“yan”连在一起的,因为他们都在一个结构当中,图做完没备份改不了了你们凑合看着...)如果想要表明该结构后面没有其它结构,就要把next成员指针设置为NULL,即表示链表结束

这里区分一下头节点首节点

头节点:在单链表的第一个结点前附设一个结点,它没有直接前驱。其数据域可以不存任何信息,指针域指向第一个节点(首节点)的地址。头节点的作用是使所有链表(包括空表)的头指针非空

首节点:存放第一个有效数据的节点。

头节点在首节点之前,它指向首节点的地址。

头节点之所以存在肯定有它的道理。那么为什么要设置头节点呢?

  • 增加头节点后,首节点的地址保存在头节点的指针域中。也就是说第一个数据元素也存在了前驱节点,它就与其他数据元素没啥不一样了,无需进行特殊处理
  • 当链表不设头节点时,假设head为单链表的头指针,它应该指向首节点,则当单链表为长度n为0的空表时,head指针为空。判断空表的条件可记为:head==NULL

 二、创建静态单链表

 静态链表比动态链表简单,所以我们先设置一个静态链表的显示来感受下链表的连接方式:

这种链表为静态链表。所有节点在程序中的内存并不是我们自己申请的,而是系统自动分配的内存空间,用完后系统自动释放

我们可以看得出链表中结构与结构之间主要通过三个指针连接:*head, *p, *next。其中next指针存放在结构体内尾部,方便指向链表的下一结构,具体连接方式可参考代码第15行。

那么问题就来了,为何要设置p指针呢?直接用head指向不就好了吗?这是因为如果直接使用head指针的话,head指针的值就会被改变,程序就会找不到链表的开始处,所以中间需要增加一个p指针。

上面代码的输出结果为:


 三、创建动态单链表

链表的特点之一是其高效性和灵活性,与数组相比而言链表能够更加轻松地对数据进行删除和插入等一系列改动操作,并且能够使得内存空间的合理利用率更高,修改弹性更大

那为什么说使用链表的内存空间利用率更高呢?这体现在动态链表创建时它对内存的分配

所谓动态链表,就是需要我们手动申请内存(使用mallocnew函数)去存放节点,结束后还需要手动释放内存

并且在动态链表中,每个节点没有自己的名字,节点间全靠指针连接。这就意味着一旦某一节点出现了指针断开的情况,后面的节点将会再也无法找回(断啦断啦)

一个完整的结构链表大概可以分为三个板块:创建、显示和释放。接下来谈谈最重要也是最复杂的板块:创建链表

  1. 使用malloc()为各个节点分配内存空间;
  2. 安放、存储好各个节点的地址使其连接;
  3. 收集节点信息。

上代码:

#include <stdio.h> 
#include <stdlib.h>
struct Stu *create(int n);     //创建链表
void print(struct Stu *head);  //显示链表
struct Stu
	int id;
	char name[50];
	struct Stu *next;
;

//声明完成
int main()

	int n;
	struct Stu *head = NULL;   //创建头指针,初始化head指针为NULL
	printf("请输入你想要创建的节点个数:\\n");
	scanf("%d",&n);
	head = create(n);     //引用
	print(head);          //引用


//上为主函数,引用了两个自定义函数,接下来这就是这两个自定义函数的定义
struct Stu *create(int n)

	struct Stu *head,*p,*end;   						//定义头节点,普通节点,尾节点 
	head = (struct Stu *)malloc(sizeof(struct Stu)); 	//给头节点申请内存 
	end = head;        									//设置空表, 即头尾地址一致 
	for(int i=0;i<n;i++)
								                    	//利用for循环向链表中添加数据 
		p = (struct Stu *)malloc(sizeof(struct Stu));   //给普通节点申请内存空间 
		scanf("%d %s",&p->id,p->name);              	//给数据域赋值 
		end->next = p;				                	//让上一个节点的数据域指向当前节点 
		end = p;     				             		//end指向当前节点,最终end指向尾节点 
	
	end->next = NULL;                                   //给end的指针域置空表示链表结束
	return head;                                        //返回头节点的地址 


void print(struct Stu *head)

	struct Stu *t = head;
	int j =1;
	t = t -> next;       //不打印头节点 (如果不明白不打印它的原因,可返回上面“头节点和首节点的区别”中查看)
	while(t != NULL)
	
		printf("%d\\n%d\\n%s\\n", j, t->id, t->name);
		t = t->next;
		j++;
	

输出结果如下:

 其中第一个自定义函数为创建链表,第二个自定义函数为显示链表。具体步骤作用可通过代码注释查看,下面是一些需要注意的点:

1、关于初始化

刚开始设置头指针(head)时,记得将其初始化为NULL

再者就是刚开始创建链表时,需设空表,即将头尾指针地址设为一致(end = head)。

2、关于内存分配:

上面的链表中主要进行了两部分内存分配,一个是对头节点,一个是对普通节点。不难发现分配内存的形式是:

指向节点的指针名(即其地址) = (struct 结构名 *)malloc(sizeof(struct 结构名));

记得一定要先分配地址,再进行指针连接节点的操作。

如果你不放心,想检查malloc()是否成功请求到内存时,就可检查malloc的返回值是否为NULL如果返回NULL,则表明未获取到相对应的内存。

3、各结构地址存放情况:

第一个结构的地址存放在head指针中
之后每个结构的地址存放在上一结构的next指针成员中。

来看看上面代码的核心部分,也就是它的创建链表部分:

尾插法建立单链表:

尾插法建立链表算是一个较为常规的操作。其实从设断点的第30和31行代码可以看得出来,end指针和p指针是同时移动的,这看起来好像让你感觉这俩指针多此一举,觉得取其一不就好了。nonono,还是那句话,p指针的作用是用于连接end指针主要是追踪最新节点,当没有新的节点加入时好把最后一个节点内的next成员设为NULL代表链表结束因此设断点的这两步无论是删除其一还是调换顺序,都会导致链表断开,没有输出

在程序的for循环中,每每输入一次p就会更新一个新结构,也就可以理解为:在for循环中scanf语句后面的两个赋值语句中,对于等号而言,左边是上一个节点的信息,而右边是新节点的信息

头插法建立单链表:

和尾插法的代码做个对比其实就能看得出来,头插法和尾插法其实区别不大。因为头插法的尾节点已经固定,所以我们就不需要额外再设置end指针去跟踪尾节点了。 同时头插法也删除了尾插法第25行的空表设置操作,主要区别依旧在第30行和31行。


三、链表的增删查改

1、增

插入元素的位置有三种:头、中、尾,但插入形式都是一样的:

1)将新节点的 next 指针指向插入位置后的节点;

2)将插入位置前节点的 next 指针指向插入节点;

千万注意1和2的先后顺序不能对调!!必须是先接后、再接前,如果顺序对调则会导致插入位置后的这部分链表丢失,就无法进行下一步连接操作了。

下面我们设计一个函数,在单链表中的第n个位置上插入新节点。

void ADD(struct Stu *head, int n)     
    
	struct Stu *p = head,*pr;
	pr = (struct Stu*)malloc(sizeof(struct Stu));  //pr是指向新插节点的指针

	printf("请输入要插入的数据\\n");
	scanf("%d %s",&pr->id,pr->name);
	int i = 0;

    while( i<n-1 && p!=NULL )   //使p指向将要插入节点n-1的位置 
              
    	p = p->next;
		i++;
	   
		pr->next = p->next;   //将新建节点的地址指向将要插入节点的后一个节点的地址 
		p->next = pr;        //使插入节点指向新建节点 

首先从头节点开始,找到链表中的第n-1个节点的地址p,然后在其后面插入第n个节点。插入时,先将新节点的指针pr指向原来的第n个节点,然后再将n-1个节点指向新的节点,即完成插入操作。

2、删

删除链表中的节点实质上就是对节点的摘除,同时需要注意释放不用的内存空间

1)将结点从链表中摘下来;

2)手动释放节点,回收被已删除节点占用的存储空间;

 其实摘取节点的操作很简单,就是:

temp->next=temp->next->next;

下面我们设置一个函数,删除单链表的第n个节点:

void DELETE(struct Stu *head,int n)  //删除n处的节点
          
	struct  Stu *p = head,*pr;
	int i =0;
	while(i<n-1 && p!=NULL)     //找到第n-1个节点
         
		p = p->next;  
		i++;
	
	if(p!=NULL)   //p不能指向尾节点之后的节点(n节点不存在)
               
	    pr= p->next;   //pr指向第n个节点 
		p->next = pr->next;    //连接删除节点左右两边的节点(把这个节点忽视掉了)
		free(pr);        //释放删除节点内存
	 else
		printf("节点不存在\\n"); 
	
 

 你会发现除了最后一个if语句的内容,前面的操作和“增”的操作代码类似。所以其实链表中的各类操作都是大同小异,主要难点只是在于其连接方式罢了(会有点容易乱)。

3、查

在链表中查找指定节点通常使用遍历的方法:从表头依次遍历表中节点,用被查找元素与各节点数据元素进行比对,直到比对成功或遍历至链表最末端的 NULL(查找失败)。

接下来我们构造函数查找节点元素,其中p为原链表,elem表示被查找元素:

int selectElem(struct Stu * p,int id)

    struct Stu * t=p;  //新建一个指针t,初始化为头指针 p
    int i=1;
   
    while (t->next)    //由于头节点的存在,因此while中的判断为t->next
    
        t=t->next;
        if (t->id==id) 
        
			printf("yes");  //表示查找到含有此元素的节点
            return i;
        
        i++;
    
	printf("no");  //表示没有查找到
    return -1;  //程序执行至此处,表示查找失败

在查找到想要查找的节点时,也可以选择打印输出该节点数据域的内容

同时也要注意,在遍历有头节点的链表时,需避免头节点对测试数据的影响。因此在遍历时,应该建立使用上面代码中的遍历方法,直接越过头节点对链表进行有效遍历

4、改

链表节点的修改,其实就只需通过遍历找到存储此元素的节点,然后对节点中的数据域做更改操作即可。

依旧是定义函数操作,上代码:

void change(struct Stu *head,int n)

	struct Stu *p = head;
	int i = 0;
	while(i<n && p!=NULL)   //使p指向需修改节点 
        
		p = p->next;
		i++;
	
	if(p != NULL)
                 
	    printf("请输入修改之后的值:\\n");
	   scanf("%d %s",&p->id,p->name);	
	else
		printf("节点不存在!\\n");
	 

四、所有基础实现综合:

(有部分注释还没注,所以显得有些粗糙大家将就看)

#include <stdio.h> 
#include <stdlib.h>
struct Stu *create(int n);     //创建链表
void print(struct Stu *head);  //显示链表
int selectElem(struct Stu * p,int id);  //查找节点
typedef struct Stu
	int id;
	char name[50];
	struct Stu *next;
BE;

int main()

	int n;
	struct Stu *head = NULL;              //创建头指针 
	printf("请输入你想要创建的节点个数:\\n");
	scanf("%d",&n);
	head = create(n);
	ADD(head,3);
	DELETE(head,2);
	change(head,3);
	print(head);
	selectElem(head,2);
	


void change(struct Stu *head,int n)     

	struct Stu *p = head;
	int i = 0;
	while(i<n && p!=NULL)   //使p指向需修改节点 
        
		p = p->next;
		i++;
	
	if(p != NULL)
                 
	    printf("请输入修改之后的值:\\n");
	   scanf("%d %s",&p->id,p->name);	
	else
		printf("节点不存在!\\n");
	 


int selectElem(struct Stu * p,int id)

    struct Stu * t=p;  //新建一个指针t,初始化为头指针 p
    int i=1;
   
    while (t->next)    //由于头节点的存在,因此while中的判断为t->next
    
        t=t->next;
        if (t->id==id) 
        
			printf("yes");
            return i;
        
        i++;
    
	printf("no");
    return -1;  //程序执行至此处,表示查找失败


struct Stu *create(int n)

	struct Stu *head,*p;   				        		//定义头节点,普通节点
	head = (struct Stu *)malloc(sizeof(struct Stu)); 	//给头节点申请内存 

	for(int i=0;i<n;i++)
								                    	//利用for循环向链表中添加数据 
		p = (struct Stu *)malloc(sizeof(struct Stu));   //给普通节点申请内存空间 
		scanf("%d %s",&p->id,p->name);              	//给数据域赋值 
		p->next = head->next;                           //新节点指向原来的首节点
		head->next = p;                                 //链表的头节点指向新节点
	
	return head;                                        //返回头节点的地址 


void ADD(struct Stu *head, int n)     
    
	struct Stu *p = head,*pr;
	pr = (struct Stu*)malloc(sizeof(struct Stu));  //pr是指向新插节点的指针

	printf("请输入要插入的数据\\n");
	scanf("%d %s",&pr->id,pr->name);
	int i = 0;

    while( i<n-1 && p!=NULL )   //使p指向将要插入节点n-1的位置 
              
    	p = p->next;
		i++;
	   
		pr->next = p->next;   //将新建节点的地址指向将要插入节点的后一个节点的地址 
		p->next = pr;        //使插入节点指向新建节点 


void DELETE(struct Stu *head,int n)  //删除n处的节点
          
	struct  Stu *p = head,*pr;
	int i =0;
	while(i<n-1 && p!=NULL)     //找到第n-1个节点
      
		p = p->next;  
		i++;
	
	if(p!=NULL)   //p不能指向尾节点之后的节点(n节点不存在)
           
	    pr= p->next;   //pr指向第n个节点 
		p->next = pr->next;    //连接删除节点左右两边的节点(把这个节点忽视掉了)
		free(pr);        //释放删除节点内存
	 else

		printf("节点不存在\\n"); 
	
 

void print(struct Stu *head)

	struct Stu *t = head;
	int j =1;
	t = t->next;       //不打印头节点 
	while(t != NULL)
	
		printf("%d\\t%d\\t%s\\n",j,t->id,t->name);
		t = t->next;
		j++;
	

链表魅力无穷大(虽然有时候会让你很暴躁。

篇幅多少有点大了,还会开篇链表(下)写一下无头节点单链表的增删查改、链表逆置、循环链表、双链表以及二叉树和链表之间等等。

链表学透彻了打出来上百行代码去实现不同的功能,有点心还能整得花里胡哨些,做出来真的很有成就感。

以上是关于学习链表相关(上)的主要内容,如果未能解决你的问题,请参考以下文章

Wagtail - 在页面上呈现带有相关片段和标签的数据时遇到问题

剑指offer题目系列三(链表相关题目)

Jekyll 偏移代码片段高亮的初始行

Redis学习之列表类型详解

AngularJS的学习笔记

数据结构22:数组和广义表