C语言数据结构之线性表-链表篇

发布时间:2024年01月24日

不问花开几许

只向浅笑安然

🎥烟雨长虹,孤鹜齐飞的个人主页

🔥个人专栏

期待小伙伴们的支持与关注!!!


目录

使用链表的原因

单链表的实现索引

?定义链表的结构体

链表的功能?

为节点分配动态内存空间?

打印单链表

?单链表的头插

代码测试#

关于单链表使用二级指针的原因

单链表的尾插

代码测试#

单链表的头删

?编辑

代码测试#

单链表的尾删

代码测试#

单链表的查找?

代码测试#

在指定位置之前插入节点?

代码测试#

在指定位置之后插入节点

代码测试#

?删除指定位置的节点

代码测试#

删除指定位置之后的节点?

代码测试#

?销毁链表

代码测试#

顺序表和链表的区别

关于链表的OJ题?

?移除链表元素

思路#

?链表的中间节点

思路#

反转链表?

思路#

总结#?

使用链表的原因

顺序表的局限性#?

<1>中间或头部的插入删除,时间复杂度为O(N)

<2>增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗

<3>增容一般是呈2倍的增长,势必会有一定的空间浪费

例如:当前容量为100,满了以后增容到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间

因为使用顺序表有种种弊端,所以我们程序猿开发了链表。

链表的简介#

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

<1>从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续

<2>现实中的结点一般都是从堆上申请出来的

<3>从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

链表的种类:

虽然有这么多的链表的结构,但是我们实际中最常?还是两种结构:

单链表?双向带头循环链表

<1>单链表:结构简单,?般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多

<2>带头双向循环链表:结构最复杂,?般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势

我们本篇文章主要讲 单链表 的实现以及几道OJ题,下篇会更新双向循环链表

单链表的实现索引

<1>单链表的头插

<2>单链表的尾插

<3>单链表的头删

<4>单链表的尾删

<5>在指定位置pos之前插入节点

<6>在指定位置pos之后插入节点

<7>在指定位置pos之后删除节点

<8>删除指定位置pos节点

<9>销毁链表

?定义链表的结构体

链表的结构跟火车车厢相似,我们需要根据乘客的数量来增删我们车的数量,但其过程不会影响其他车厢,每节车厢都是独立存在的。

车厢是独立存在的,且每节车厢都有车门。想象?下这样的场景,假设每节车厢的车门都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带?把钥匙的情况下如何从车头走到车尾,答案是:每节车厢里都放?把下?节车厢的钥匙

在前?个节点存放下?个节点的地址,就可以访问下一个节点

typedef int SListType;   //将数据类型重命名,防止以后换类型所带来的麻烦

typedef struct SList
{
	SListType data;      //数据域-车厢号
	struct SList* next;  //指针域-指向下一节车厢的钥匙
}SL;

链表的功能?

为节点分配动态内存空间?

后面我们要在单链表中进行头插和尾插,为了方便和减少代码的重复度,我们分装一个函数用来专门创建新结点

SL* SLMalloc(SListType x)                   //接收SListType类型的值
{
	SL* newNode = (SL*)malloc(sizeof(SL));  //malloc分配动态内存空间
	assert(newNode);                        //判断有没有开辟成功
	newNode->data = x;                      //为该节点的数据域赋值
	newNode->next = NULL;                   //若该节点的下一个节点不存在则置为空
	return newNode;                         //返回空间
}

打印单链表

我们写代码最好是写一点测一点,不要最后才调试,所以我们先把打印接口先规划好

void Print(SL** head)
{
	SL* put = *head;    //定义一个结构体指针put指向头节点
	while (put)         //遍历put打印节点数据
	{
		printf("%d->", put->data);
		put = put->next;
	}
	printf("NULL\n");   //最后一个节点指向的是NULL
}

?单链表的头插

void SLFront(SL** head, SListType x)
{
	assert(head);              //判断链表为不为空
	SL* newNode = SLMalloc(x); //开辟动态空间
	newNode->next = *head;     //将新空间的指针域指向原头节点
	*head = newNode;           //让newNode成为新的头节点
}

<1>判断链表为不为空

<2>新开辟节点的指针域指向原头节点

<3>让新节点newNode成为新的头节点


代码测试#

这样我们的头插就完成啦

关于单链表使用二级指针的原因

因为单链表没有哨兵位(带头空节点),而我们单链表的头节点是要存储数据的

如果仅仅只是传一级指针,那就是?传值调用?,形参不影响实参

所以为了改变我们的头节点,我们要选择?传址调用 ,就要传二级指针

单链表的尾插

void SLPush_Back(SL** head, SListType x)
{
	assert(head);             //判断链表为不为空
	SL* newNode = SLMalloc(x);//开辟动态内存空间
	if (*head == NULL)        //如果链表为空,newNode就直接是头节点
	{
		*head = newNode;
		return;
	}                         //如果链表不为空
	SL* pur = *head;          //定义一个指针pur遍历到链表的最后一个节点
	while (pur->next)
	{
		pur = pur->next;
	}
	pur->next = newNode;      //将链表最后一个节点的指针域指向新节点
}

<1>因为尾插是要与前一个节点链接的,所以要判断链表是否为空的情况

<2>如果为空,newNode就直接是头节点

<3>如果不为空,我们就定义一个指针pur遍历到链表的最后一个节点

<4>并将最后一个节点的指针域指向新开辟的节点


代码测试#

单链表的头删

void SLPopFront(SL** head)
{
	assert(head);             //判断链表为不为空
	assert(*head);            //判断头指针为不为空
	SL* next = (*head)->next; //我们定义一个next指针接收头节点的下一个节点的指针
	free(*head);              //释放原头节点
	*head = next;             //将next设置为新节点
}

<1>判断链表和头节点是否为空

<2>先让头指针指向下一个节点

<3>在释放掉原头指针


代码测试#

单链表的尾删

void SLPopBack(SL** head)
{
	assert(head);
	assert(*head);
	if ((*head)->next == NULL) //如果头指针后面没有节点就释放头指针
	{
		free(head);
		head = NULL;
		return;
	}
	SL* pulic = *head;         //定义一个指针用来遍历最后一个节点
	SL* puvie = NULL;          
	while (pulic->next)
	{
		puvie = pulic;         //最后一个节点的前驱节点
		pulic = pulic->next;
	}
	puvie->next = NULL;        //断掉最后一个节点和前驱节点的联系
	free(pulic);               //释放最后一个节点
	pulic = NULL;            
}

<1>判断链表和头指针是否为空

<2>判断头指针后面的节点是否为空

<3>如果头指针后面的节点为空就释放头指针

<4>若不为空就定义两个指针puvie先指向NULL、pulic遍历找到尾节点的前驱节点

<5>将尾节点的前驱节点置为NULL,并释放最后一个节点


代码测试#

单链表的查找?

SL* SLFind(SL** head, SListType x)
{
	SL* put = *head;        //定义一个指针put指向头节点
	while (put)             //遍历put找到要查找的目标节点
	{
		if (put->data == x)
		{
			return put;
		}
		put = put->next;
	}
	return NULL;            //若没有找到就置为空
}

代码测试#

在指定位置之前插入节点?

void SLInsert(SL** head, SL* pos, SListType x)
{
	assert(head);     //判断链表为不为空
	assert(*head);    //判断头节点为不为空
	assert(pos);      //判断pos是否合法
	SL* newNode = SLMalloc(x);
	if (pos == *head) //如果pos指向头指针,就头插
	{
		SLFront(head,x);
		return;
	}
	SL* pur = *head;         //如果pos不是头指针,就定义一个指针pur指向头节点
	while (pur->next != pos) //遍历pur到pos的前驱节点
	{
		pur = pur->next;
	}
	pur->next = newNode;     //将pur的指针域指向新节点
	newNode->next = pos;     //新节点的指针域指向pos
}

<1>判断头节点和链表是否为空,pos是否合法

<2>如果pos指向头节点,就头插

<3>如果pos不指向头节点,就定义一个指针pur遍历到pos的前驱节点

<4>将pur的指针域指向新节点,新节点的指针域指向pos,完成牵手任务


代码测试#

在指定位置之后插入节点

void SLInsertAfter(SL* pos, SListType x)
{
	assert(pos);                //判断pos是否合法
	SL* newNode = SLMalloc(x);  
	newNode->next = pos->next;  //新节点的指针域存储pos的下一个节点
	pos->next = newNode;        //pos的指针域存储新节点的指针
}

<1>判断pos位置是否合法

<2>先将新节点的指针域存储pos的下一个节点

<3>pos的指针域存储新节点的指针

注意:<2>、<3>的顺序不能交换,不然找不到3节点以及之后的节点


代码测试#

?删除指定位置的节点

void FreePos(SL**head,SL* pos)
{
	assert(head);      //判断链表为不为空
	assert(*head);     //判断头节点为不为空
	assert(pos);       //判断pos是否合法
	if (*head == pos)  //如果pos指向头节点就进行头删
	{
		SLPopFront(head);
		return;
	}
	SL* pur = *head;         //如果pos不指向头节点,就定义一个指针pur
	while (pur->next != pos) //遍历pur指向pos前驱节点的位置
	{
		pur = pur->next;
	}
	pur->next = pos->next;   //pur的指针域指向pos的下一个节点
	free(pos);               //牵手成功就删掉pos的联系
	pos = NULL;
}

<1>判断头节点、链表是否为空,pos是否合法

<2>如果pos指向头节点就进行头删

<3>如果pos不指向头节点就定义一个指针pur

<4>遍历pur指向pos前驱节点的位置,让pur的指针域指向pos的下一个节点

<5>释放pos节点


代码测试#

删除指定位置之后的节点?

void FreePosRight(SL* pos)
{
	assert(pos);                //判断pos是否合法
	assert(pos->next);          //判断pos之后的节点是否为空
	SL* pur = pos->next;        //定义一个指针pur指向要释放的节点
	pos->next = pos->next->next;//pos在于要删除的节点的下一个节点完成牵手
	free(pur);                  //释放pur的所在节点的空间
	pur = NULL;
}

<1>判断pos是否合法

<2>判断pos之后的节点是否为NULL

<3>定义一个指针pur指向要被(分手)释放的节点

<4>pos在与该节点的下一个节点完成牵手

<5>在pos与将要(分手)释放的节点的下一个节点完成牵手后删除该节点的联系方式

<6>之后我们在释放该节点的空间就好了


代码测试#

?销毁链表

void SListDesTroy(SL** head)
{
	assert(head);    //判断链表为不为空
	assert(*head);   //判断头节点为不为空
	SL* put = *head; //定义一个指针指向头节点
	while (put)      //循环销毁链表
	{
		SL* next = put->next;
		free(put);
		put = next;
	}
	*head = NULL;
}

<1>判断头节点和链表是否为空

<2>定义一个指针put指向头节点

<3>遍历put循环释放链表的空间达到销毁的目的


代码测试#

这样我们的单链表的主要功能就已经实现啦!!!

接下来我们在题目中找找灵感

顺序表和链表的区别

不同点顺序表链表
存储空间上物理上一定连续逻辑上连续,但物理上不一定连续
随机访问支持:O(1)不支持:O(N)
任意位置插入或者删除元素可能需要搬移元素,效率低 O(N)只需修改指针指向
插入动态顺序表,空间不够时需要扩容没有容量的概念
应用场景元素高效存储+频繁访问任意位置插入和删除频繁
缓存利用率

关于链表的OJ题?

?移除链表元素

题目链接:移除链表元素


给你一个链表的头节点?head?和一个整数?val?,请你删除链表中所有满足?Node.val == val?的节点,并返回?新的头节点?。

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

示例 2:

输入:head = [], val = 1
输出:[]

示例 3:

输入:head = [7,7,7,7], val = 7
输出:[]

提示:

  • 列表中的节点数目在范围?[0, 104]?内
  • 1 <= Node.val <= 50
  • 0 <= val <= 50

思路#

三指针的思想:快慢双指针+一个遍历链表的指针

创建新链表,遍历原链表中不是val的节点,并尾插到新链表中

<1>定义三个指针pur、left、right

<2>pur指向原链表的头节点,left、right先置为NULL

<3>创建一个新链表,将不是val的节点尾插到新链表

<4>尾插完后right往后走,准备接收尾插节点

<5>遇到val时,pur直接跳过,并不进行尾插操作

<6>直到pur->next = NULL时跳出循环

<7>注意:遍历完原链表后right->next一定要置为NULL,因为节点5还存着节点val的地址

<8>最后返回头节点 left 便可完成此题


typedef struct ListNode ListNode;//结构体的重命名,为了以下书写简单
struct ListNode* removeElements(struct ListNode* head, int val) {
    ListNode *left,*right;       //定义新链表的快慢指针
    left = right = NULL;   
    ListNode* pur = head;        //在定义一个指针指向头节点
    while(pur)   
    {
        if(pur->val != val)      //遍历pur判断是不是val,不是val就尾插
        {
            if(left ==NULL)          //判断头节点为不为空
            {
                left = right = pur;  //若为空新插入的节点就是头节点
            }
            else                 
            {                        //尾插
                right->next = pur;
                right = right->next;
            }
        }
        pur = pur->next;          //是val就继续遍历
    }
    if(right)
    {
        right->next = NULL;       //断掉尾节点和其他节点的联系
    }
    return left;
}

?链表的中间节点

题目链接:链表的中间节点


给你单链表的头结点?head?,请你找出并返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

输入:head = [1,2,3,4,5]
输出:[3,4,5]
解释:链表只有一个中间结点,值为 3 。

输入:head = [1,2,3,4,5,6]
输出:[4,5,6]
解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点。

提示:

  • 链表的结点数范围是?[1, 100]
  • 1 <= Node.val <= 100

思路#

快慢双指针:定义两个指针left、right分别指向头节点,left走一步,right走两步

链表节点是偶数的情况:

链表节点是奇数的情况:

如图所示

<1>left走一步right走两步,到?right?或者?right->next?为NULL的情况下left刚好是链表的中间节点

<2>这个时候我们只要返回?left 所在节点就可以啦??


typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
    ListNode *left,*right;   //定义快慢指针
    left = right = head;     //快慢指针都指向头节点
    while(right&&right->next)//这里分奇偶讨论,奇数是right,偶数是right->next,不为空就进行循环
    {
        left = left->next;        //慢指针走一步
        right = right->next->next;//快指针走两步
    }
    return left;
}

反转链表?

题目链接:反转链表


给你单链表的头节点?head?,请你反转链表,并返回反转后的链表。

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

输入:head = [1,2]
输出:[2,1]

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中节点的数目范围是?[0, 5000]
  • -5000 <= Node.val <= 5000

思路#

三指针:创建三个指针,分别记录前驱节点,当前节点,后继节点,改变原链表的指针方向

<1>创建三个指针,分别记录前驱节点n1,当前节点n2,后继节点n3

<2>让n2指向原链表的头节点,n1指向NULL、n3指向n2的下一个节点

<3>开始让头节点与下一个节点断开并指向NULL成为我们新创建链表的尾节点

<4>完成上述操作后,n1、n2、n3分别往后走直到n2指向NULL为止

<5>这样原来的尾节点成为头节点,原来的头节点成为尾节点,链表被反转了


typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) 
{
    if(head == NULL)//判断链表是否为空
    {
        return head;
    }    
    ListNode *n1,*n2,*n3;
    n1 = NULL,n2 = head,n3 = head->next;
    while(n2)             //如果n2不为空就进入循环
    {
        n2->next = n1;    //修改n2的指向
        n1 = n2;          //交换位置达成移动的效果
        n2 = n3;
        if(n3)            //判断n3是否为空
        n3 = n3->next;    //n3往后走
    }
    return n1;
}

总结#?

<1>在遇到不会写的链表接口时最好画图模拟

<2>链表在分手后记得断掉联系方式

<3>链表题很多涉及到指针,所以最先要往双指针的方向去想

C++算法中两夫妻的故事-双指针

文章来源:https://blog.csdn.net/2301_79201049/article/details/135759478
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。