基础知识学习---链表基础

1、本栏用来记录社招找工作过程中的内容,包括基础知识学习以及面试问题的记录等,以便于后续个人回顾学习; 暂时只有2023年3月份,第一次社招找工作的过程;

2、个人经历: 研究生期间课题是SLAM在无人机上的应用,有接触SLAM、Linux、ROS、C/C++、DJI OSDK等;
3、参加工作后(2021-2023年)岗位是嵌入式软件开发,主要是服务器开发,Linux、C/C++、网络编程、docker容器、CMake、makefile、Shell脚本、JSON等。

4、求职岗位是嵌入式软件开发、C/C++开发、自动驾驶岗位开发等。

在这里插入图片描述

文章目录

  • 一、单链表
    • 1.1 单链表基础
    • 1.2 创建一个无头节点的单链表
    • 1.3 创建一个有头节点的单链表
    • 1.4 单链表的增删改查操作
  • 二、双向链表
    • 2.1 双链表基础
    • 2.2 双链表的创建和初始化
    • 2.3 双向链表的增删改查

一、单链表

参考:
http://c.biancheng.net/view/3336.html

过程记录

1.1 单链表基础

一个完整的链表需要由以下几部分构成:
1、头指针:一个普通的指针,它的特点是永远指向链表第一个节点的位置。很明显,头指针用于指明链表的位置,便于后期找到链表并使用表中的数据;
2、节点:链表中的节点又细分为头节点首元节点其他节点
头节点:其实就是一个不存任何数据的空节点,通常作为链表的第一个节点。对于链表来说,头节点不是必须的,它的作用只是为了方便解决某些实际问题;
首元节点:由于头节点(也就是空节点)的缘故,链表中称第一个存有数据的节点为首元节点。首元节点只是对链表中第一个存有数据节点的一个称谓,没有实际意义;
其他节点:链表中其他的节点;

因此,一个存储 {1,2,3} 的完整链表结构如下图 所示:
基础知识学习---链表基础_第1张图片
注意:链表中有头节点时,头指针指向头节点;反之,若链表中没有头节点,则头指针指向首元节点。

1.2 创建一个无头节点的单链表

无头节点的链表创建成功后的最终结果如下图所示:
基础知识学习---链表基础_第2张图片
创建一个存储 {1,2,3,4} 且无头节点的链表,C 语言实现代码如下:

#include
#include

typedef struct Link
{
    int data;//数据域,存放数据信息
    struct Link * Next;//指针域,用于指向后继元素
}link;//link为节点名,每一个节点都是一个link结构体

link * initlink();//初始化链表
void print(link * InputLink);//遍历链表并打印里面的data数据

int main()
{
    link * p = initlink();//建立一个链表p,并调用initLink进行初始化
    print(p);//遍历并打印该链表的data

    return 0;
}

/*创建一个存储 {1,2,3,4} 且无头节点的链表,并初始化*/
link * initlink()
{
    link * p = NULL;//先创建一个空的头指针
    
    /*创建首元节点*/
    link *tmp = NULL;
    tmp = (link *)malloc(sizeof(link));
    tmp->data = 1;//首元节点的数据赋值为1
    tmp->Next = NULL;

    p = tmp;//头指针指向首元节点

    /*循环创建3个数据节点*/
    int i;
    for(i = 2; i<5; i++)
    {
        
        link * a = NULL;
        a = (link*)malloc(sizeof(link));
        a->data = i;//节点中的数据设置为i值,即2,3,4
        a->Next = NULL;
   
       tmp->Next = a;//将temp节点与新建立的a节点建立逻辑关系,即前一个的next指向后面的节点

        //第一次循环,将tmp节点由指向首元节点改为指向第一个a节点;剩下的循环,将tmp由指向旧的a改为指向新的a
        //即指针temp每次都指向新链表的最后一个节点,其实就是 a节点,这里写temp=a也对
        
        tmp = tmp->Next;
    }
   

    return p;
}

void print(link * InputLink)
{
    link *tmp = InputLink;//建立一个临时的指针,用来遍历链表,刚开始先指向链表的头指针
    while(NULL!=tmp)//如果tmp不为空,说明有该节点,即可取数据。
    {
        printf("%d ",tmp->data);//打印该节点中的数据
        tmp = tmp->Next;//tmp指向该节点中的Next指针,当遍历到最后一个节点时,该最后节点的Next指针为空,此时tmp也为空,则下一个循环就不会进入了
    }
}

运行结果:
在这里插入图片描述

1.3 创建一个有头节点的单链表

有头节点的链表创建成功后的最终结果如下图所示:
基础知识学习---链表基础_第3张图片

创建一个存储 {1,2,3,4} 且有头节点的链表,C 语言实现代码如下:


#include
#include

typedef struct Link
{
    int data;//数据域,存放数据信息
    struct Link * Next;//指针域,用于指向后继元素
}link;//link为节点名,每一个节点都是一个link结构体

link * initlink();//初始化链表
void print(link * InputLink);//遍历链表并打印里面的data数据

int main()
{
    link * p = initlink();//建立一个链表p,并调用initLink进行初始化
    print(p);

    return 0;
}

/*创建一个存储 {1,2,3,4} 且有头节点的链表,并初始化*/
link * initlink()
{
    link * p = (link *)malloc(sizeof(link));//创建头结点
    link *tmp = p;
    int i ;
    for(i = 1;i<5;i++)
    {
        link * a = (link *)malloc(sizeof(link));
        a->data = i;
        a->Next = NULL;

        tmp ->Next = a;
        tmp = tmp->Next;
    }

    return p;
}

/*使用带有头节点创建链表的方式,则print函数总打印链表中的数据时,要跳过头结点,因为头结点中是没有数据的,所以判断条件为“当下一个节点中的Next不为NULL时,即说明它不是最后一个节点,就打印此” */
void print(link * InputLink)
{
    link *tmp = InputLink;//建立一个临时的指针,用来遍历链表,刚开始先指向链表的头指针
    while(NULL!=tmp->Next)//判断tmp->Next是否为空,不为空说明后面有节点,即不是最后一个节点;为什么不判断tmp?因为tmp第一次代表头结点,头结点的data没数据。
    {
        printf("%d ",tmp->Next->data);//打印tmp节点下一个中的数据,即跳过了第一个节点也就是头结点
        tmp = tmp->Next;//指向下一个节点
    }
}


运行结果:
在这里插入图片描述

1.4 单链表的增删改查操作

在写增删改查函数时,一定要时刻参照单链表的图,如下:

基础知识学习---链表基础_第4张图片

增删改查总代码:

#include
#include

typedef struct Link
{
    int data;//数据域,存放数据信息
    struct Link * Next;//指针域,用于指向后继元素
}link;//link为节点名,每一个节点都是一个link结构体

link * initlink();//初始化链表
void print(link * InputLink);//遍历链表并打印里面的data数据
link * Insert(link * TargetLink,int data_new,int Add);//向链表中插入元素
link * Delate(link *TargetLink,int pos_del);//删除链表中的指定节点
link* deleteNode(link* head, int val);//删除指定元素
link * update(link *TargetLink ,int date_new,int pos_update);//修改链表中的元素
int select(link *TargetLink,int date_select);//在链表中查找元素

int main()
{
    link * p = initlink();//建立一个链表p,并调用initLink进行初始化
    printf("原链表:");
    print(p);
    printf("\n");

    link * Add = Insert(p,5,2);//调用插入函数,向原链表中第二个节点插入一个数字5
    printf("插入元素后的链表:");
    print(p);
    printf("\n"); 

    link * Del = Delate(p,2);//调用删除函数,删除第二个节点   
    printf("删除第2个节点后的链表:");
    print(p);
    printf("\n"); 


    link * Del_ = deleteNode(p,2);//调用删除函数,删除数据为2的节点   
    printf("删除数字为2的节点后的链表:");
    print(p);
    printf("\n"); 


    link * up = update(p,5,2);//调用修改函数,将原链表第二个位置的数字修改为5
    printf("修改元素后的链表:");
    print(p);
    printf("\n");

    int pos_sec =  select(p,5);//调用查找函数,在链表中查找数字5对应的节点位置
    printf("数字5是第%d个节点\n",pos_sec);

    return 0;
}

#if 1
/*创建一个存储 {1,2,3,4} 且有头节点的链表,并初始化*/
link * initlink()
{
    link * p = (link *)malloc(sizeof(link));//创建头结点
    link *tmp = p;
    int i ;
    for(i = 1;i<5;i++)
    {
        link * a = (link *)malloc(sizeof(link));
        a->data = i;
        a->Next = NULL;

        tmp ->Next = a;
        tmp = tmp->Next;
    }

    return p;
}

/*使用带有头节点创建链表的方式,则print函数总打印链表中的数据时,要跳过头结点,因为头结点中是没有数据的,所以判断条件为“当下一个节点中的Next不为NULL时,即说明它不是最后一个节点,就打印此” */
void print(link * InputLink)
{
    link *tmp = InputLink;//建立一个临时的指针,用来遍历链表,刚开始先指向链表的头指针
    while(NULL!=tmp->Next)//判断tmp->Next是否为空,不为空说明后面有节点,即不是最后一个节点;为什么不判断tmp?因为tmp第一次代表头结点,头结点的data没数据。
    {
        printf("%d ",tmp->Next->data);//打印tmp节点下一个中的数据,即跳过了第一个节点也就是头结点
        tmp = tmp->Next;//指向下一个节点
    }
}


/*
功能:向链表中插入元素
入参:
link *TargetLink 要插入数据的目标链表(带有头结点)
int data_new  要插入的元素数据
int Add   要插入的位置,比如Add为2,则将新元素插在节点2的位置上

*/
link * Insert(link * TargetLink,int data_new,int Add)
{
    link *tmp = TargetLink;
    int i ;
    for(i = 1;i<Add;i++)//因为有头结点的存在,所以不必单独判断是否是插入在首元节点之前还是之后,整个步骤都是一样的,因为都是插入在头结点之后的。不用单独判断Add是否为1
    {
        if(NULL == tmp->Next)//说明没找到符合Add位置的节点,即超出了原链表的范围
        {
            printf("error:Add Not in Link\n");
            return TargetLink;
        }
        tmp = tmp->Next;
    }
    //创建新节点
    link * New = (link *)malloc(sizeof(link));
    New ->data = data_new;
    //向链表中插入节点
    New->Next = tmp->Next;
    tmp->Next = New;
    return TargetLink;
}


/*
功能:向链表中删除元素
入参:
link *TargetLink 要删除数据的目标链表(带有头结点)
int pos_del  要删除的节点位置
*/
link * Delate(link *TargetLink,int pos_del)
{
    link *tmp = TargetLink;
    int i ;
    for(i = 1;i<pos_del;i++)
    {
        if(NULL == tmp->Next)//说明没找到符合删除位置的节点,即超出了原链表的范围
        {
            printf("error:没有该节点\n");
            return TargetLink;
        }
        tmp = tmp->Next;//此时的tmp已经指向了要删除的节点的前面的节点
    }  
    //单独设置一个指针,指向要删除的节点,用于释放该节点
    link * del = tmp->Next;//因为此时tmp指向的是要删除节点的前面的节点,因此被删除节点时tmp->Next
    tmp->Next = tmp->Next->Next;//tmp里的Next指针指向下一个节点里的Next指针
    free(del);//手动释放该节点,防止内存泄漏
    return TargetLink;
}


/*
功能:向链表中删除元素
入参:
link *head 要删除数据的目标链表(不带头结点)
int val  要删除的数据
*/
link* deleteNode(link* head, int val) {
	int pos = 1;//用于要删除的是第几个节点
	link *tmp1 = head;//用于指向被删除的节点
	link *tmp2 = head;//用于指向被删除节点的前一个节点
	while(NULL != tmp1 )
	{

		if(val == tmp1 -> data)//如果是要删除的数据
		{
			if(1 == pos)//如果是第一个节点
			{
				head = tmp1 -> Next;//需要将头指针指向后一个节点,因为第一个要被删除
			}
			else//如果被删除的不是头结点
			{
				tmp2 -> Next = tmp1 -> Next;//被删除节点的前一个节点的Next指向被删除节点的后一个节点
				tmp1 -> Next = NULL;//被删除的节点的Next指向空
				free(tmp1);//释放掉要被删除的节点
			}					
			return head;
			
		}
		else//如果不是要删除的数据,则记录继续往下遍历
		{
			tmp2 = tmp1;//此处:tmp2记录下tmp1的前一个节点
			tmp1 = tmp1 -> Next;//tmp1继续往下
			pos ++;//记录被删除的是第几个节点
		}
	}
	return head;
}

/*
功能:链表中修改元素
入参:
link *TargetLink 要修改数据的目标链表(带有头结点)
int date_new    要修改成的数据
int pos_update  要修改的节点位置
*/
link * update(link *TargetLink ,int date_new,int pos_update)
{
    link *tmp = TargetLink;
    int i ;
    for(i = 0;i<pos_update;i++)
    {
        if(NULL == tmp->Next)//说明没找到符合修改位置的节点,即超出了原链表的范围
        {
            printf("error:没有该节点\n");
            return TargetLink;
        }
        tmp = tmp->Next;//此时的tmp已经指向了要修改的节点
    }  
    tmp->data = date_new;  
    return TargetLink; 
}


/*
功能:在链表中查找目标元素,返回找到的位置,未找到返回-1
入参:
link *TargetLink 要查找数据的目标链表(带有头结点)
int date_select  要查找到数据
*/
int select(link *TargetLink,int date_select)
{
    link *tmp = TargetLink;
    int i = 1;//从首元节点开始,第一次循环即首元节点的位置
    while(NULL != tmp->Next)//因为有头结点,头结点没数据,所以需要跳过头结点,直接找首元节点的数据;所以头指针的Next为首元节点,即跳过头结点
    {
        if(date_select == tmp->Next->data)
        {
            return i;//找到了,返回i
        }
        tmp = tmp->Next;
        i++;
    }
    return -1;
    
}

#endif

执行结果:
基础知识学习---链表基础_第5张图片

二、双向链表

2.1 双链表基础

从名字上理解双向链表,即链表是 “双向” 的,如图 1 所示:
基础知识学习---链表基础_第6张图片

双向,指的是各节点之间的逻辑关系是双向的,但通常头指针只设置一个,除非实际情况需要

从图 1 中可以看到,双向链表中各节点包含以下 3 部分信息(如图 2 所示):
指针域:用于指向当前节点的直接前驱节点;
数据域:用于存储数据元素。
指针域:用于指向当前节点的直接后继节点;
基础知识学习---链表基础_第7张图片
因此,双链表的节点结构用 C 语言实现为:

typedef struct line{
    struct line * prior; //指向直接前趋
    int data;
    struct line * next; //指向直接后继
}line;

2.2 双链表的创建和初始化

同单链表相比,双链表仅是各节点多了一个用于指向直接前驱的指针域。因此,我们可以在单链表的基础轻松实现对双链表的创建。

需要注意的是,与单链表不同,双链表创建过程中,每创建一个新节点,都要与其前驱节点建立两次联系,分别是:

  • 将新节点的 prior 指针指向直接前驱节点;
  • 将直接前驱节点的 next 指针指向新节点;

创建好后的链表如下图所示:
基础知识学习---链表基础_第8张图片

具体代码实现:

#include
#include

typedef struct Line
{ 
    struct Line * prior;//指向直接前驱
    int  data;
    struct Line * next;//指向直接后继
}line;

line * lineInit();//双链表初始化函数
int print(line * TargetLine);//遍历打印双链表里的数据

line * lineInit()
{
    line *head = NULL;//定义一个双链表的头指针

    line * tmp = (line *)malloc(sizeof(line));//创建第一个节点(首元节点)并分配内存
    tmp->prior = NULL;
    tmp->data = 1;
    tmp->next = NULL;

    head = tmp;//头指针指向第一个节点

    int i;
    for(i = 2;i<4;i++)
    {
        line * body = (line *)malloc(sizeof(line));//创建后续的节点
        body ->prior=tmp;//前驱指针指向前面的节点
        body ->data = i;
        body ->next = NULL;//后继指针指向空
        tmp ->next = body;//前面节点的后继指针指向当前节点
        
        tmp = tmp->next;//tmp永远指向当前最后一个节点
    }
    return head;
    
}

int print(line * TargetLine)
{
    line * tmp = TargetLine;//新建tmp指针指向双链表头指针,用来遍历双链表
    while(NULL != tmp )
    {
        if(NULL == tmp -> next)
            printf("%d",tmp->data);
        else
            printf("%d <-> ",tmp->data);
        tmp = tmp ->next;//tmp指向后面的节点
    }
    return 0;
}

int main()
{
    line * source = lineInit();
    printf("初始化的链表为:");
    print(source);
    printf("\n");
    return 0;
}

运行结果:
在这里插入图片描述

2.3 双向链表的增删改查

#include
#include

typedef struct Line
{ 
    struct Line * prior;//指向直接前驱
    int  data;
    struct Line * next;//指向直接后继
}line;

line * lineInit();//双链表初始化函数
int print(line * TargetLine);//遍历打印双链表里的数据
line * Insert(line *Targetline, int date_new, int Add);//向链表中插入元素
line * Delate(line *TargetLine,int pos_del);//向链表中删除元素
line * update(line *TargetLine ,int date_new,int pos_update);//修改链表中的元素
int select_double(line *TargetLine,int date_select);//在链表中查找元素

line * lineInit()
{
    line *head = NULL;//定义一个双链表的头指针

    line * tmp = (line *)malloc(sizeof(line));//创建第一个节点(首元节点)并分配内存
    tmp->prior = NULL;
    tmp->data = 1;
    tmp->next = NULL;

    head = tmp;//头指针指向第一个节点

    int i;
    for(i = 2;i<4;i++)
    {
        line * body = (line *)malloc(sizeof(line));//创建后续的节点
        body ->prior=tmp;//前驱指针指向前面的节点
        body ->data = i;
        body ->next = NULL;//后继指针指向空
        tmp ->next = body;//前面节点的后继指针指向当前节点
        
        tmp = tmp->next;//tmp永远指向当前最后一个节点
    }
    return head;
    
}

int print(line * TargetLine)
{
    line * tmp = TargetLine;//新建tmp指针指向双链表头指针,用来遍历双链表
    while(NULL != tmp )
    {
        if(NULL == tmp -> next)
            printf("%d",tmp->data);
        else
            printf("%d <-> ",tmp->data);
        tmp = tmp ->next;//tmp指向后面的节点
    }
    return 0;
}

/*
功能:向链表中插入元素
入参:
line *Targetline 要插入数据的目标链表(带有头结点)
int data_new  要插入的元素数据
int Add   要插入的位置,比如Add为2,则将新元素插在节点2的位置上
*/

line * Insert(line *Targetline, int date_new, int Add)
{
    //printf("Insert\n");
    line * tmp = Targetline;//定义临时指针,刚开始指向双链表头指针,后面用来遍历整个链表

    /*先判断Add是否在双链表的位置中*/
    int i;
    for(i = 1;i<Add-1;i++)//为什么是Add-1呢?因为目的是找到要插入位置的前一个结点
    {
        if(NULL == tmp)
        {
            printf("ERROR:Add Not exist");
            return Targetline;
        }
        tmp = tmp->next;
        //printf("i is %d\n",i);
    }
    //经过for循环出来后,此时tmp指向的节点就是咱们要插入的位置的前一个节点
    if(1 == Add)//如果是插在第一个节点的位置,需要单独处理,因为要改变头指针,头指针改为指向新建的节点,此时没进上面的for循环,tmp还是指向头节点的
    {
        line * New_head = (line *)malloc(sizeof(line));
        New_head ->prior = NULL;//新的头结点前驱指针指向NULL
        New_head->data = date_new;
        New_head->next =tmp;//新的头结点的后继指针指向旧的头结点
        tmp->prior= New_head;//旧的头结点的前驱指针指向新的头结点

        Targetline = New_head;//头指针指向新的头结点
    }
    else//如果不是插在第一个节点的位置
    {
        if(NULL == tmp->next)//说明tmp是最后一个节点了,即需要查到最后一个节点后面
        {
            line * body_final = (line *)malloc(sizeof(line));
            body_final ->prior = tmp;//新的结点前驱指针指向插入位置的前一个节点
            body_final->data = date_new;
            body_final->next = NULL;//新的结点的后继指针指向空 

            tmp->next = body_final;//原来的最后一个节点的后继指针指向新建立的节点,此时原来的最后一个节点变成了倒数第二个节点,新建立的节点变成了最后一个节点
        }
        else//此时说明是插入到中间位置
        {
            line * body_new = (line *)malloc(sizeof(line));
            body_new ->prior = tmp;//新的结点前驱指针指向原节点的前驱节点
            body_new->data = date_new;
            body_new->next =tmp->next;//新的结点的后继指针指向原结点

            tmp ->next ->prior = body_new;//原节点的前驱指针指向新节点
            tmp ->next = body_new;//旧节点的前一个节点的后继指针指向新节点
            
        }

    }

    return Targetline;
}

/*
功能:向双链表中删除元素
入参:
line *TargetLine 要删除数据的目标链表
int pos_del  要删除的节点位置
*/
line * Delate(line *TargetLine,int pos_del)
{
    line *tmp = TargetLine;
    int i;
    for(i = 1;i<pos_del;i++)
    {
        if(NULL == tmp ->next)
        {
            printf("ERROR:pos_del Not exit");
            return TargetLine;
        }
        tmp = tmp ->next;//此时tmp指向了要删除的节点
    }
    if(1 == pos_del)//如果要删除的是头结点
    {
        tmp ->next->prior = NULL;//原头结点的下一个节点的前驱指针指向空
        TargetLine = tmp->next;//头指针指向原头结点的下一个节点

        tmp ->next = NULL;//原头结点的后驱指针指向空

        line * Del = tmp;//新建一个指针指向要删除的节点,用于释放内存
        free(Del);

    }
    else if(NULL == tmp ->next)//tmp->next为空说明要删除的是尾节点
    {        
        tmp ->prior->next = NULL;//要删除节点的前一个节点的后驱指针指向空
        tmp ->prior = NULL;
        free(tmp);
    }
    else//要删除的既不是头结点,也不是尾节点
    {
        tmp ->prior->next = tmp ->next;//要删除节点的前一个节点的后驱指针指向要删除节点的下一个节点
        tmp ->next->prior = tmp ->prior;//要删除节点的后一个节点的前驱指针指向要删除节点的前一个节点

        tmp ->prior =NULL;
        tmp ->next = NULL;
        free(tmp);
    }

    return TargetLine;
}

/*
功能:链表中修改元素
入参:
line *TargetLine 要修改数据的目标链表(带有头结点)
int date_new    要修改成的数据
int pos_update  要修改的节点位置
*/
line * update(line *TargetLine ,int date_new,int pos_update)
{
    line *tmp = TargetLine;
    int i ;
    for(i = 1;i<pos_update;i++)
    {
        if(NULL == tmp)//说明没找到符合修改位置的节点,即超出了原链表的范围
        {
            printf("error:没有该节点\n");
            return TargetLine;
        }
        tmp = tmp->next;//此时的tmp已经指向了要修改的节点
    }  
    tmp->data = date_new;  
    return TargetLine; 
}


/*
功能:在链表中查找目标元素,返回找到的位置,未找到返回-1
入参:
line *TargetLine 要查找数据的目标链表(带有头结点)
int date_select  要查找到数据
*/
int select_double(line *TargetLine,int date_select)
{
    line *tmp = TargetLine;
    int i = 1;//从首元节点开始,第一次循环即首元节点的位置
    while(NULL != tmp)
    {
        if(date_select == tmp->data)
        {
            return i;//找到了,返回i
        }
        tmp = tmp->next;
        i++;
    }
    return -1;
    
}


int main()
{
    line * source = lineInit();
    printf("初始化的链表为:");
    print(source);
    printf("\n");

    source = Insert(source,4,4);//将数字4插入到第三个节点
    printf("插入节点后的链表为:");
    print(source);
    printf("\n");

    source = Delate(source,4);//删除第4个节点
    printf("删除节点后的链表为:");
    print(source);    
    printf("\n");

    source = update(source,6,3);//修改第3个节点为6
    printf("修改节点后的链表为:");
    print(source);    
    printf("\n");    

    int pos_sec =  select_double(source,6);//调用查找函数,在链表中查找数字5对应的节点位置
    printf("数字6是第%d个节点\n",pos_sec);

    return 0;
}



执行结果:
在这里插入图片描述

你可能感兴趣的:(求职过程记录,学习,链表,数据结构)