数据结构深度剖析链表全集:结构实现、分类与底层原理全解析

文章目录

1. 链表的概念及结构

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

链表的结构跟火车车厢相似,淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某节车厢去掉/加上,不会影响其他车厢,每节车厢都是独立存在的。

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

最简单的做法:每节车厢里都放一把下一节车厢的钥匙。

与顺序表不同的是,链表里的每节"车厢"都是独立申请下来的空间,我们称之为"结点/节点" 节点的组成主要有两个部分:当前节点要保存的数据和保存下一个节点的地址(指针变量)。

图中指针变量plist保存的是第一个节点的地址,我们称plist此时"指向"第一个节点,如果我们希望plist"指向"第二个节点时,只需要修改plist保存的内容为0x0012FFA0。

链表中每个节点都是独立申请的(即需要插入数据时才去申请一块节点的空间),我们需要通过指针变量来保存下一个节点位置才能从当前节点找到下一个节点。

结合前面学到的结构体知识,我们可以给出每个节点对应的结构体代码:
假设当前保存的节点为整型:

c 复制代码
struct SListNode
{
	int data;//节点数据
	struct SListNode* next; //指针变量⽤保存下⼀个节点的地址
}; 

所以当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数据,也需要保存下一个节点的地址(当下一个节点为空时保存的地址为空)。
当我们想要从第一个节点走到最后一个节点时,只需要在前一个节点拿上下一个节点的地址(下一个节点的钥匙)就可以了。

补充说明:

1. 链式结构在逻辑上是连续的,在物理结构上不一定连续

2. 节点一般是从堆上申请的

3. 从堆上申请来的空间,是按照一定策略分配出的每次申请的空间可能连续,可能不连续

2. 单链表的实现

2.1 SList.h文件

c 复制代码
#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);

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

//删除pos节点
void SLErase(SLTNode** pphead, SLTNode* pos);

//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);

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

2.2 SList.c文件

c 复制代码
#define _CRT_SECURE_NO_WARNINGS

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

SLTNode* SLTBuyNode(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);
	//空链表和非空链表的处理
	SLTNode* newnode = SLTBuyNode(x);
	if (*pphead == NULL)//空链表的处理
	{
		*pphead = newnode;
	}
	else//非空链表的处理
	{

		//要先找到链表的尾节点,再将尾节点和新节点连接起来

		//找尾
		SLTNode* ptail = *pphead;//从最开始往后去找尾
		while (ptail->next != NULL)
		{
			ptail = ptail->next;
		}
		//ptail指向尾节点
		ptail->next = newnode;

	}


}

//链表的头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);

	SLTNode* newnode = SLTBuyNode(x);
	//使newnode->next和第一个节点连接在一起
	newnode->next = *pphead;
	*pphead = newnode;
}


//链表的尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);//链表不能为空
	//链表只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//链表有多个节点
	else
	{
		// prev:记录尾节点的前一个节点删(完后它会变成新尾节点)
		// ptail:记录最后一个节点(要删除的目标)
		SLTNode* prev = *pphead;
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		free(ptail);
		ptail = NULL;
		prev->next = NULL;
		//倒数第二个节点变成新尾节点,尾节点next必须是NULL
	}
}

//链表的头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);//链表不能为空
	
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next; 
}



//链表的查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	while (pcur != NULL)//不希望修改phead
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;

	}
	//找不到
	return NULL;
}



//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(*pphead);//链表不能为空
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	//若pos == *pphead说明是头插
	if (pos == *pphead)
	{
		//调用头插代码
		SLTPushFront(pphead, x);
	}
	else
	{
		//1.先找pos的前一个节点
		SLTNode* prev = *pphead;//使prev等于第一个节点让其往后找pos的前一个节点
		//如果prev的下一个节点是pos的话就找到了pos的前一个节点
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//跳出循环找到了此时prev就是pos的前一个节点


		//2.将prev newnode pos 连接在一起
		//让newnode的next指针指向pos,让prev的next指针指向newnode
		newnode->next = pos;
		prev->next = newnode;

		//当pos等于*pphead时,代码走不通
	}

}



//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);

	SLTNode* newnode = SLTBuyNode(x);

	newnode->next = pos->next;
	pos->next = newnode;

}



//删除pos节点
void SLErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);

	//pos是头节点
	if (pos == *pphead)
	{
		SLTNode* next = (*pphead)->next;
		free(*pphead);
		*pphead = next;//这里和头删代码一致可以调用
	}
	else//pos不是头节点
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}



//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);//pos的下一个节点不能为空不然删什么呢

	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
	   

}



//销毁链表
void SListDesTroy(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);

	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//pcur为空链表中所有节点都删除完成
	*pphead = NULL;


}

2.3 test.c文件

c 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include "SList.h"
void SListTest01()
{
	//链表是由一个一个节点组成
	//先手动创建几个节点

	/*链表是由一个一个节点组成需要保存一个数据就去申请一块空间
	不涉及增容的操作所以使用malloc*/
	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));//创建的第一个节点
	node1->data = 1;

	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));//创建的第二个节点
	node2->data = 2;

	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));//创建的第三个节点
	node3->data = 3;

	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));//创建的第四个节点
	node4->data = 4;


	//将四个节点连接起来
	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;

	//调用链表的打印
	SLTNode* plist = node1;//让plist指向第一个节点
	SLTPrint(plist);
}
void SListTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);
	
		/*
	*plist 是第一个节点
	 plist 是指向第一个节点的指针
	 &plist 是指向第一个节点的指针的地址

	 形参的改变要影响实参,必须传地址
	*/




	//测试销毁链表
	SListDesTroy(&plist);
	SLTPrint(plist);




	//测试删除pos之后的节点
	//SLTNode* find = SLTFind(plist, 4);
	//SLTEraseAfter(find);
	//SLTPrint(plist);



	//测试删除pos节点
	//SLTNode* find = SLTFind(plist, 3);
	//SLErase(&plist, find);
	//SLTPrint(plist);




	//测试在指定位置之前插入数据
	//SLTNode* find = SLTFind(plist, 4);
	//SLTInsert(&plist, find ,11);
	//SLTPrint(plist);


	//测试在指定位置之后插入数据
	//SLTNode* find = SLTFind(plist, 1);
	//SLTInsertAfter(find, 11);
	//SLTPrint(plist);


	//测试链表的查找
	//SLTNode* find = SLTFind(plist, 30);
	//if (find == NULL)
	//{
	//	printf("没有找到\n");
	//}
	//else
	//{
	//	printf("找到了\n");
	//}







	//测试链表的头插
	//SLTPushFront(&plist, 6);
	//SLTPrint(plist);
	//SLTPushFront(&plist, 7);
	//SLTPrint(plist);
	//SLTPushFront(&plist, 8);
	//SLTPrint(plist);


	//测试链表尾删
	//SLTPopBack(&plist);
	//SLTPrint(plist);
	//SLTPopBack(&plist);
	//SLTPrint(plist);
	//SLTPopBack(&plist);
	//SLTPrint(plist);
	//SLTPopBack(&plist);
	//SLTPrint(plist);


	//测试链表的头删
	//SLTPopFront(&plist);
	//SLTPrint(plist);
	//SLTPopFront(&plist);
	//SLTPrint(plist);
	//SLTPopFront(&plist);
	//SLTPrint(plist);
	//SLTPopFront(&plist);
	//SLTPrint(plist);


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

3. 基于链表的一些经典OJ题目

3.1 移除链表元素

题目描述:

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

示例:


题解:

创建一个新链表,然后在原链表中找值不为val的节点,将其尾插到新链表中。

c 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val)
{
    //创建一个新的空链表
    ListNode* newHead,*newTail;
    newHead = newTail = NULL;

    //定义一个指针遍历原链表
    
    ListNode* pcur = head;
    while(pcur)
    {
        //循环遍历原链表找不为val的节点尾插到新的空链表 
        if(pcur->val != val)
        {
            //因为是空链表所以第一个插入的节点既是头也是尾

            //链表为空
            if(newHead == NULL)
            {
                newHead = newTail = pcur;
            }
            //链表不为空
            else
            {
                newTail->next = pcur;
                newTail = newTail->next;

            }
        }
        pcur = pcur->next;
    }
    if(newTail)
        newTail->next = NULL;
    //将最后一个5节点插入新链表后必须将其next置为空,因为其是指向原链表的6节点
    //pcur走到空
    return newHead;
}

3.2 反转链表

题目描述:

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

示例:


题解:

思路1:

创建新链表,将原链表中的节点拿过来头插就实现了反转链表。

思路2:

创建3个指针n1指向空n2指向链表头节点,n3指向n2的下一个节点让n2的next不再指向n3而是指向n1,再让n1走到n2,n2走到n3,n3走到它的下一个节点重复以上步骤直到n2.n3走到空,此时n1就是新链表的头节点。

反转前:

反转后:

此处代码使用思路2:

c 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) 
{
    //判空
    if(head == NULL)
    {
        return head;
    }
    ListNode* n1,*n2,*n3;
    n1 = NULL;
    n2 = head;
    n3 = n2->next;
    while(n2)//n2为空说明反转链表完成跳出循环
    {
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        if(n3)//n3到最后会走到空不能对空指针解引用
         n3 = n3->next;
    }
        return n1;
}

3.3 合并两个有序链表

题目描述:

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例:


题解:

创建新的空链表,遍历原链表,将节点值小的节点拿到新链表中进行尾插操作,比较时有一个指针会先走到空比如l1先走到空此时就把l2中剩余的节点尾插到新链表中

c 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode; 
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    //判空(当两个输入链表中,有一个或两个都是空链表时,代码会空指针崩溃。)
    if(list1 == NULL)
    {
        return list2;
    }
    if(list2 == NULL)
    {
        return list1;
    }
    ListNode* l1 = list1;
    ListNode* l2 = list2; 
    //创建指针l1/l2分别指向两个原链表的头


    //创建的新链表
    ListNode* newHead , *newTail;
    //newHead = newTail = NULL;
    


	/*创建两个指针newHead和newTail分别指向新链表的头和尾*/
    newHead = newTail = (ListNode*)malloc(sizeof(ListNode));
    /*此时链表不为空,头尾指针指向了一个有效的地址(节点)此时
    就不需要判断newHead为不为空的代码*/


    while(l1 && l2)
    {
        if(l1->val < l2->val)
        {
            // l1拿下来尾插到新链表
            //新链表不为空直接尾插
            newTail->next = l1;
            //更新newTail
            newTail = newTail->next;
            l1 = l1->next;
        }
        else
        {
            //l2拿下来尾插到新链表
            newTail->next = l2;
            newTail = newTail->next;
            l2 = l2->next;
        }
    }
    /*跳出循环有两种情况:
    要么l1先走到空了,要么l2先走到空了*/
    if(l2)
    {
        newTail->next = l2;
    }
    if(l1)
    {
        newTail->next = l1;
    }
    ListNode* ret = newHead->next;//因为 newHead是malloc出来的哨兵位真正的链表从它后面开始
    //动态申请的空间手动释放
    free(newHead);
    newHead = NULL;
    return ret;
}

3.4 链表的中间节点

题目描述:

给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。

示例:

题解:

思路1:

遍历,count计节点数,直接返回(count/2)节点的next节点

思路2:快慢指针
原理在于2slow等于fast

对于奇数链表:
定义一个快指针fast一次走两步 ,定义一个慢指针slow一次走一步, 初始情况下他们都指向头节点 当fast->next == NULL时不再继续执行 此时slow刚好指向中间节点

对于偶数链表:
定义一个快指针fast一次走两步 定义一个慢指针slow一次走一步 初始情况下他们都指向头节点当fast == NULL 时不再执行此时slow刚好指向第二个中间结点。

此处代码使用思路2:

c 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head)
{
    ListNode* slow = head;
    ListNode* fast = head;
    while(fast != NULL && fast->next != NULL)
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    //此时slow刚好指向中间节点
    return slow;
}

3.5 循环链表的经典应用: 环形链表的约瑟夫问题

什么是循环链表?

让单链表的尾节点 next 指针不再指向 NULL 而是指向当前链表的第一个节点,使链表成环这就是循环链表。


著名的约瑟夫问题:

据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

题目描述:

编号为 1 到 n 的 n 个人围成一圈。从编号为 1 的人开始报数,报到 m 的人离开。
下一个人继续从 1 开始报数。
n-1 轮结束以后,只剩下一个人,问最后留下的这个人编号是多少?

数据范围: 1 ≤ n , m ≤ 10000

示例:


题解:

思路:使用循环链表

按照游戏规则数到 m 的人离开对应循环链表就是释放该链表,但是在释放链表之前应该把他前一个节点的 next 指针指向他后一个节点然后再释放节点,循环报 m 直到剩下一个节点


抽象到代码上:

定义两个指针 pcur 和 prev,pcur 为当前报数节点,prev 始终处在 pcur 的前一个节点。
从 pcur 指向的节点开始报数。
从报数 1 到报数 m-1,每报一个数,做一次:prev 走到 pcur 所在节点,pcur 走到下一个节点。
当报到数字 m 时,执行:
prev->next = pcur->next;释放 pcur 指向的节点;再令 pcur = prev->next,继续下一轮循环报数。

c 复制代码
#include <stdlib.h>
typedef struct ListNode ListNode;
//创建节点
ListNode* buyNode(int x)
{
    ListNode* node = (ListNode*)malloc(sizeof(ListNode));
    if(node == NULL)
    {
        exit(1);
    }
    node->val = x;
    node->next = NULL;
    return node;
}
//创建带环链表
ListNode* createCicle(int n)
{
    //先创建第一个节点
    ListNode* phead = buyNode(1);
    ListNode* ptail = phead;
    for (int i = 2; i <= n; i++) 
    {
        ptail->next = buyNode(i);//尾插新节点
        ptail = ptail->next;
    }
    ptail->next = phead;//让链表首尾相连成环

    return ptail;
    /*
    return ptail; 而不是 return phead;,
    是为了方便初始化"前驱指针",从而简化约瑟夫问题的删除操作
    在环形单链表中,想要删除一个节点,必须找到它的前一个节点,
    执行 prev->next = pcur->next 才能把 pcur 从环中断开。
    如果 createCicle 返回头节点 phead,
    那么我们还需要再遍历一圈才能找到尾节点(因为 phead 的前驱就是尾节点)
    */
}

int ysf(int n, int m ) 
{
    //根据n创建带环链表
    ListNode* prev = createCicle(n);
    ListNode* pcur = prev->next;
    int count = 1;

    while(pcur->next != pcur)//当链表中只有一个节点时跳出循环
    {
        if(count == m)
        {
            //报到m需要销毁pcur节点
            prev->next = pcur->next;
            free(pcur);
            pcur = prev->next;

            count = 1;//重置计数
        }
        else
        {
            //未报到m不需要销毁节点
            prev = pcur;
            pcur = pcur->next;
            count++;
        }
    }
    //此时剩下的一个节点就是要返回的节点里的值
    return pcur->val;
}

3.6 分割链表

题目描述:

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有小于 x 的节点都出现在大于或等于 x 的节点之前。
你不需要保留每个分区中各节点的初始相对位置。

示例:


题解:

思路1:

在原链表上进行修改若pcur节点的值小于x,往后走若pcur节点的值大于或等于x , 尾插在原链表后,删除旧节点。
缺点是需要频繁的去重新连接链表且要定义多个指针以及时间复杂度高等问题。

思路2:

创建新链表设置一个哨兵位,遍历原链表若pcur节点的值小于x头插在新链表中,若pcur节点的值大于或等于x尾插在新链表中。
这种方法已经十分完美了硬要说缺点的话那就是破坏了小于 x 的节点之间的相对顺序,但本题可以破坏顺序。

思路3:

创建2个新链表:大链表和小链表
小链表:定义两个指针lessHead / lessTail表示小链表 的头和尾,若pcur节点的值小于x, 尾插在小链表中

大链表:greaterHead / greaterTail表示大链表的头尾若pcur节点的值大于或等于x , 尾插在大链表中

两个链表都使用尾插的方法且为了避免判空使用哨兵位 最后将小链表的尾节点和大链表的第一个有效节点首尾相连(注意不要和哨兵位相连)

此处代码使用思路3:

c 复制代码
typedef struct ListNode ListNode;
struct ListNode* partition(struct ListNode* head, int x) 
{
    //处理节点为空的情况
    if(head == NULL)
    {
        return head;
    }

    //创建两个带头链表(大链表和小链表)
    ListNode* lessHead, *lessTail;
    ListNode* greaterHead, *greaterTail;
    //创建哨兵位
    lessHead = lessTail = (ListNode*)malloc(sizeof(ListNode));
    greaterHead = greaterTail = (ListNode*)malloc(sizeof(ListNode));

    //遍历原链表,将原链表中的节点尾插到大小链表中
    ListNode* pcur = head;
    while(pcur)
    {
        if(pcur->val < x)
        {
            //尾插到小链表中
            lessTail->next = pcur;
            lessTail = lessTail->next;
        }
        else
        {
            //尾插到大链表中
            greaterTail->next = pcur;
            greaterTail = greaterTail->next;
        }
        pcur = pcur->next;
    }

     greaterTail->next = NULL;
    /*因为在原链表中5节点处的next是指向2节点的
    经过大小链表的拆分后2节点成为了小链表的lessTail
    这就导致大小链表建立起了死循环所以需要把5节点处的
    next指针置空(非常坑爹的点也是链表的神奇之处)
    还有当链表中只有一个节点时执行到这里要
    将小链表的尾节点和大链表的第一个有效节点首尾相连
    但是大链表的有效节点为空所以哨兵位的next为随机值
    所以还要将next指针初始化*/


    //将小链表的尾节点和大链表的第一个有效节点首尾相连
    lessTail->next = greaterHead->next;

    
    //跳过哨兵位返回小链表里的第一个有效节点
    ListNode* ret = lessHead->next;

    //释放哨兵位
    free(lessHead);
    free(greaterHead);
    lessHead = greaterHead = NULL;
    return ret;
}

4.链表的分类

链表的结构非常多样,以下情况组合起来就有8种(2x2x2)链表结构:



虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构: 单链表 和 双向带头循环链表

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

2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

5. 双向链表

5.1 双向链表的结构


注意:这里的"带头"跟前面我们说的"头节点"是两个概念,实际在前面的单链表阶段称呼不严谨,但是为了更好的理解就直接称为单链表的头节点。
带头链表里的头节点,实际为"哨兵位",哨兵位节点不存储任何有效元素,只是站在这里"放哨的"
"哨兵位"存在的意义:遍历循环链表避免死循环。

5.2 双向链表的实现

1. List.h 文件

定义双向链表节点的结构以及声明双向链表中提供的方法

c 复制代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//定义双向链表节点的结构



typedef int LTdataType;
typedef struct ListNode
{
	LTdataType data;
	struct ListNode* next;//指向后一个节点
	struct ListNode* prev;//指向前一个节点

}LTNode;


//声明双向链表中提供的方法


//打印双向链表
void LTPrint(LTNode* phead);


//双向链表的初始化
void LTInit(LTNode** pphead);


//双向链表的销毁
void LTDesTroy(LTNode* phead);


//双向链表的尾插
void LTPushBack(LTNode* pphead, LTdataType x);//插入数据之前链表必须初始化到只有一个头节点的情况


//双向链表的头插
void LTPushFront(LTNode* pphead, LTdataType x);


//双向链表的尾删
void LTPopBack(LTNode* phead);


//双向链表的头删
void LTPopFront(LTNode* phead);


//在pos位置插入数据
void LTInsert(LTNode* pos, LTdataType x);


//删除pos节点
void LTErase(LTNode* pos);


//查找数据
LTNode* LTFind(LTNode* phead, LTdataType x);

2. List.c 文件

实现双向链表的方法

c 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include "List.h"

//动态申请节点
LTNode* LTBuyNode(LTdataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)//申请失败
	{
		perror("malloc fail!");
		exit(1);
	}
	//申请成功
	node->data = x;
	node->next = node->prev = node; // 新节点初始时,前驱和后继都指向自己

	return node;
}
//双向链表的初始化
void LTInit(LTNode** pphead)
{
	//给双向链表创建一个哨兵位
	*pphead = LTBuyNode(-1);

}
//打印双向链表
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->",pcur->data);
		pcur = pcur->next;

	}
	printf("\n");
}


//双向链表的尾插
void LTPushBack(LTNode* phead, LTdataType x)//插入数据之前链表必须初始化到只有一个头节点的情况
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

	newnode->prev = phead->prev;
	newnode->next = phead;

	phead->prev->next = newnode;
	phead->prev = newnode;

}

//双向链表的头插
void LTPushFront(LTNode* phead, LTdataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

	newnode->next = phead->next;
	newnode->prev = phead;

	phead->next->prev = newnode;
	phead->next = newnode;

}

//双向链表的尾删
void LTPopBack(LTNode* phead)
{
	//链表必须有效且不能为空
	assert(phead && phead->next != phead);
	
	LTNode* del = phead->prev;

	del->prev->next = phead;
	phead->prev = del->prev;

	//删除del
	free(del);
	del = NULL;

}

//双向链表的头删
void LTPopFront(LTNode* phead)
{
	//链表必须有效且不能为空
	assert(phead && phead->next != phead);
	LTNode* del = phead->next;

	phead->next = del->next;
	del->next->prev = phead;

	//删除del节点
	free(del);
	del = NULL;


}


//查找数据
LTNode* LTFind(LTNode* phead, LTdataType x)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	//没有找到
	return NULL;
}



//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTdataType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);

	newnode->next = pos->next;
	newnode->prev = pos;

	pos->next->prev = newnode;
	pos->next = newnode;
}


//删除pos节点
void LTErase(LTNode* pos)
{
	assert(pos);

	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;

	free(pos);
	pos = NULL;

}



//双向链表的销毁
void LTDesTroy(LTNode* phead)
{
	assert(phead);

	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;

	}
	//此时pcur指向phead,而phead还没有被销毁
	free(phead);
	phead = NULL;
}

3. test.c 文件

主函数测试代码逻辑

c 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include "List.h"

void ListTest01()
{
	LTNode* plist = NULL;
	LTInit(&plist);

	//测试双向链表的尾插
	LTPushBack(plist, 1);
	//LTPrint(plist);
	LTPushBack(plist, 2);
	//LTPrint(plist);
	LTPushBack(plist, 3);
	LTPrint(plist);

	//测试双向链表的尾删
	//LTPopBack(plist);
	//LTPrint(plist);
	//LTPopBack(plist);
	//LTPrint(plist);
	//LTPopBack(plist);
	//LTPrint(plist);


	//双向链表的头插
	//LTPushFront(plist, 1);
	//LTPrint(plist);
	//LTPushFront(plist, 2);
	//LTPrint(plist);
	//LTPushFront(plist, 3);
	//LTPrint(plist);

	//测试双向链表的头删
	//LTPopFront(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	//LTPrint(plist);


	//测试双向链表的查找数据
	LTNode* find = LTFind(plist, 3);
	//if (find == NULL)
	//{
	//	printf("找不到\n");
	//}
	//else
	//{
	//	printf("找到了\n");
	//}

	//测试删除pos节点
	LTErase(find);
	find = NULL;
	LTPrint(plist);



	//测试在pos位置之后插入数据
	//LTInsert(find, 66);
	//LTPrint(plist);


	//测试双向链表的销毁
	LTDesTroy(plist);
	plist = NULL;
	/*
	LTErase和LTDestroy参数理论上要传二级,因为我们需要让形参的改变影
	响到实参,但是为了保持接口一致性才传的一级~
	传一级存在的问题是,当形参phead置为NULL后,实参plist不会被修改
	为NULL,因此解决办法是:调用完方法后手动将实参置为NULL~
	*/

}
int main()
{
	ListTest01();
	return 0;
}

6. 顺序表和双向链表的优缺点分析

核心结论:

优先选顺序表:当你的场景以查询、随机访问、批量遍历为主,元素数量变化不大,且需要使用二分查找等依赖随机访问的算法时,顺序表是更优选择

优先选双向链表:当你的场景以频繁增删、数据量动态波动大为主,需要双向遍历,或内存碎片化严重无法分配连续大块内存时,双向链表更适配。

到这里呢数据结构的开胃菜部分顺序表链表已经学习完毕了,在下一期我们会继续学习队列栈等数据结构。

相关推荐
Black蜡笔小新1 小时前
自动化AI算法训练服务器DLTM企业级AI模型工作站构筑企业AI自主可控新模式
人工智能·算法·自动化
Languorous.1 小时前
C++数据结构高阶|跳表(Skip List)深度解析:从原理到手写实现,面试高频考点全覆盖
数据结构·c++·list
Justice Young1 小时前
数据结构:邻接矩阵和邻接表的区别
数据结构
坚果派·白晓明1 小时前
【鸿蒙PC三方库移植适配框架解读系列】第六篇:关键注意事项与最佳实践
c语言·开发语言·c++·华为·harmonyos·开源鸿蒙
童先生1 小时前
华为云、阿里云、AWS签名机制详解! AK/SK + HMAC-SHA256 签名鉴权!
算法·阿里云·华为云·云计算
承渊政道1 小时前
【贪心算法】(经典实战应用解析(二):最⻓递增⼦序列、递增的三元⼦序列、最⻓连续递增序列、买卖股票的最佳时机、买卖股票的最佳时机II)
数据结构·c++·学习·算法·leetcode·贪心算法·哈希算法
li星野1 小时前
动态规划十题通关:从爬楼梯到编辑距离(Python + C++)
c++·python·学习·算法·动态规划
披着假发的程序唐1 小时前
STM32 H743 MPU的配置使用方法
linux·c语言·c++·驱动开发·stm32·单片机·mcu
栈溢出了1 小时前
GAT(Graph Attention Network)学习笔记
人工智能·深度学习·算法·机器学习