【数据结构】线性表之链表

目录

一、顺序表的问题

二、 链表

· 无头单向非循环链表

 1、打印单链表

2、动态申请一个新节点

3、尾插

4、头插

5、尾删

6、头删

7、查找

8、pos前插入

9、pos处删除

10、pos后插入

11、pos后删除

12、销毁链表

 · 一些小tips

 · 代码汇总

· 带头双向循环链表 

1、动态申请一个节点

2、创建一个只有头节点的双向链表

3、打印双向链表

4、验空

 5、pos位置(前)插入

6、pos位置删除 

7、尾插

8、尾删

9、头插

10、头删

11、查找

12、销毁链表

 · 代码汇总 

[补] 顺序表和链表的区别


一、顺序表的问题

上篇博客陈述了顺序表,是线性表的一种,本博客将陈述另一种常见的线性表——链表。

顺序表虽然在存储数据时较为方便,但仍存在一些问题:

1. 顺序表中间/头部的插入和删除效率较低(时间复杂度为O(N))

2. 动态数组增容时需要申请新空间、拷贝数据、释放旧空间,会造成不小的内存消耗

3. 增容一般以2倍增长,这样势必会有一定的空间浪费。(例如当前容量为100,满了以后增容到200,但如果此时只需要再继续插入了5个数据即可,那么就浪费了95个数据空间。)

为了解决以上问题,前人创造了一种新的线性结构来存储数据,它就是链表。 

二、 链表

链表是一种物理结构非连续非顺序的存储结构,其中的数据元素的逻辑顺序由其中的指针链接次序实现,指针链接的每一个结构体都是一个节点

链表的结构多种多样,有单向或双向、带头或不带头、循环或非循环之分。

· 单向或双向

· 带头或不带头

· 循环或非循环

 但实际中,最常用的是以下两种组合:

· 无头单向非循环链表

· 带头双向循环链表

本博客主要梳理以上两种链表,且通过C语言来模拟实现。

· 无头单向非循环链表

struct SListNode
{
	int data;
	struct SListNode* next;
}

无头单向非循环链表又简称单链表。

单链表拥有两个通过结构体封装的结构体成员——当前节点数据元素指向下一个节点的指针

与顺序表一样,它也有一系列通过函数实现的接口,如下:

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;


void SLTPrint(SLTNode* phead);                                     //打印单链表
SLTNode* BuySLTNode(SLTDataType x);                                //动态申请一个新节点
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);   //pos前插入
void SLTErase(SLTNode** pphead, SLTNode* pos);                   //pos处删除
void SLTInsertAfter(SLTNode* pos, SLTDataType x);                //pos后插入
void SLTEraseAfter(SLTNode* pos);                                //pos后删除
void SLTDestroy(SLTNode** pphead);                                //销毁链表

 1、打印单链表

创建一个SLTPrint()函数,因对链表本身进行操作,故参数类型为指向链表头节点的指针(一般访问和控制链表,都是从头节点切入。可以借以下接口慢慢体会这一点);因无须返回值,故返回类型为void。

要打印链表,就得先从头节点的数据开始打印。创建一个临时节点cur,初始化为头节点的地址(代替跑腿)。打印头节点的数据后,将下一个节点的next指针覆盖至当前节点cur,然后重复操作,相当于cur每次打印后,都向后移动到下一个节点。

void SLTPrint(SLTNode* phead)
{
	//phead可为空,空链表仍可打印,故不必用assert断言

	SLTNode* cur = phead;
	while(cur!=NULL)
	{
		printf("%d->", cur->data);    //打印当前节点的数据
		cur = cur->next;    //将下一个节点的地址覆盖至当前结构的地址,相当于cur向后移动一个节点
		
        //cur++;            //注:这是顺序表的写法,不是链表的,无法实现数据调用
	}
	printf("NULL\n");       //尾节点默认为空
}

//[补] 
//cur在链表之间移动,是逻辑结构(为方便理解,形象化画出来的);
//真实情况是cur所存的地址值一直在改变,是物理结构(实实在在的数据在内存中的变化)

2、动态申请一个新节点

创建一个BuySLTNode()函数,因建立节点需要存储的数据,故参数为节点的数据;因建立完毕须返回创建的节点的地址,故参数类型为链表的结构体类型SLTNode*。

用malloc申请一个动态空间,然后将所传参数赋给当前数据data,因当下没有下一个节点存在,故将next指针指向空。

SLTNode* BuySLTNode(SLTDataType x)
{
	//开辟空间
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}

	//赋值
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

该BuySLTNode()函数会频繁地在其他接口函数中复用。

3、尾插

创建一个SLTPushBack()函数,因对链表的头节点本身和插入数据进行操作,故参数为头节点的二级指针和待插入的数据;因无须返回值,故返回类型为空。

注:该函数涉及链表头节点的部分需传二级指针,即头节点指针的指针,这是因为直接通过传参头节点指针访问和控制链表,不会对链表有实际的改变,也可能有野指针的隐患。例如,在下列代码中,头节点为空时,传参为头节点指针,直接连接新的尾节点,就可能出现野指针。

要进行尾插,就需要先找尾,再尾插。用待插入的数据创建一个新的节点后,向后找到尾节点,再将新节点链接在原先的尾节点上,此时新创建的节点即为新的尾节点。

void SLTPushBack(SLTNode** pphead/*SLTNode* phead*/, SLTDataType x)
{
	assert(pphead);        //空链表可插入,故phead无须断言,但pphead需断言
    SLTNode* newnode = BuySLTNode(x);       //用待插入的数据,创建一个新节点
	if (*pphead == NULL/*phead == NULL*/)   //若头节点为空,则无须找尾,直接将创建的新节点连接上即可
	{		
		*pphead = newnode;
	}
	else                            //头节点不为空就找尾
	{
		SLTNode* tail = *pphead;      //定义一个链表结构体tail(尾巴)去代替跑腿
		while (tail->next != NULL)    //找尾
		{
			tail = tail->next;
		}
		tail->next = newnode;        //将旧尾部与新创建的链表连接起来
	}
}	


//尾插的本质:原本的尾节点中,要存储新尾节点的地址

关于传参二级指针的更多说明:

//[补]传参
void Func(int* ptr)
{
	ptr = (int*)malloc(sizeof(int));
}
int main()
{
	int* px = NULL;
	Func(px);
	return 0;
}
//ptr的改变不会影响px

void Func2(int** pptr)
{
	**pptr = (int*)malloc(sizeof(int));
}
int main()
{
	int* px = NULL;
	Func2(&px);
    free(px);
	return 0;
}
//形参为二级指针pptr,接收一级指针px的地址
//此时改变pptr可影响px

//这其实是c语言的一种缺陷,在c++中并没有如此麻烦
//本博客旨在更好地展示和还原底层,故选用c语言模拟实现

4、头插

创建一个SLTPushFront()函数,因对链表的头节点本身和插入数据进行操作,故参数为头节点的二级指针和待插入的数据;因无须返回值,故返回类型为空。

 创建一个新节点后,将新节点与原先的头节点连接,使新节点成为新的头节点。

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;    //将新节点的next指针指向原先的头节点,使两者连接起来
	*pphead = newnode;          //头节点的二级指针置为新节点指针
}

5、尾删

创建一个SLTPopBack()函数,因对链表的头节点本身进行操作,故参数为头节点的二级指针;因无须返回值,故返回类型为空。

要进行尾删,就必须先找到尾,再尾删。

void SLTPopBack(SLTNode** pphead)
{
    //检查链表是否为空
	assert(pphead);
	assert(*pphead);

	//若不为空,
	//情况1:只有一个节点
	//情况2:有多个节点
	//故:
	if ((*pphead)->next == NULL)    //头节点的next指针为空,则说明链表当前只有一个节点
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
        //找尾
		SLTNode* tail = *pphead;
		while (tail->next->next != NULL)    //当前节点的下下个节点为空时,就找到尾了
		{
			tail = tail->next;
		}
		free(tail->next);                   //释放tail的下一个节点(真正的尾节点)
		tail->next = NULL;
	}
}
//注:
//tail->next->next为空则停止找尾、释放tail->next,
//这样写是为了控制链表的边界和相关节点的指针

6、头删

创建一个SLTPopFront()函数,因对链表的头节点本身进行操作,故参数为头节点的二级指针;因无须返回值,故返回类型为空。

要进行头删,就必须先将第二个节点设为新的头节点,再删除原先的头节点。

void SLTPopFront(SLTNode** pphead)
{
	assert(*pphead != NULL);
	SLTNode* first = *pphead;    //先将头节点储存在first变量
	*pphead = first->next;       //再将first变量的下一个节点(即头节点的下一个节点)设为新的头节点
	free(first);                 //释放原先的头节点
	first = NULL;
}

7、查找

创建一个SLTFind()函数,因对链表的头节点本身进行操作,故参数为头节点的指针;因需返回要查找的数据所在节点的地址,故返回类型为SLTNode*。

查找数据实则是查找数据所在的节点。遍历链表逐个节点匹配即可。

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)    //因无须修改节点,故此时不用传二级指针了
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)    //从头节点开始,逐个节点匹配要查找的数据
		{
			return cur;        //找到后,返回要查找数据所在的当前节点的地址
		}
		cur = cur->next;       
	}
	return NULL;                //遍历链表后还没找到,就返回空
}

8、pos前插入

创建一个SLTInsert()函数,因对链表的头节点本身、要插入的节点位置和插入数据进行操作,故参数为头节点的二级指针、待插入节点位置的指针和待插入的数据;因无须返回值,故返回类型为空。

若pos位置为头节点,则直接复用头插即可;若不为头节点,则先遍历链表找到pos位置,再创建一个新节点插入pos位置前。

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);        //此处链表不可为空,最好加断言
	
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);    //若插入的位置为头节点,直接复用头插即可
	}
	else                            //不为头节点则遍历链表,寻找待插入节点
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySLTNode(x);    //找到后,插入创建的新节点
		prev->next = newnode;
		newnode->next = pos;
	}
}

9、pos处删除

创建一个SLTErase()函数,因对链表的头节点本身、要删除的节点位置进行操作,故参数为头节点的二级指针和待删除节点位置的指针;因无须返回值,故返回类型为空。

若pos位置为头节点,直接复用头删即可;若不为头节点,则先遍历链表找到pos位置,找到后,将pos的前一个节点和pos的后一个节点连接,再删除pos节点。

(以下代码为“找到pos的前一个节点而非pos节点”,是因为这样方便控制变量和调整链表)

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	assert(*pphead);    //若pos不为空,则*pphead基本不为空,故其实此时断不断皆可

	if (*pphead == pos)
	{
		SLTPopFront(pphead);    //若删除的位置为头节点,直接复用头删即可
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;    //不为头节点则遍历链表,寻找pos节点的前一个节点
		}
		prev->next = pos->next;    //找到后,将pos的前一个节点和pos的后一个节点连接
		free(pos);                 //再释放pos
        //pos = NULL;//此后pos不再使用,置空与否皆可
	}
}

10、pos后插入

创建一个SLTInsertAfter()函数,因对要插入的节点位置和插入数据进行操作,故参数为待插入节点位置的指针和待插入的数据;因无须返回值,故返回类型为空。

创建一个新节点,将其连接在pos节点与pos的下一个节点之间即可。

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);

	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;     //将pos的下一个节点,赋给创建的新节点的next指针
	pos->next = newnode;           //然后将新节点赋给pos节点的next指针
                                   //从而将新节点插入在pos节点与pos的下一个节点之间
}

11、pos后删除

创建一个SLTEraseAfter()函数,因对要删除的节点进行操作,故参数为待删除节点的指针;因无须返回值,故返回类型为空。

将pos节点和pos的下下节点连接起来,再删除pos的下一个节点即可。

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);

	SLTNode* del = pos->next;    //将pos的下一个节点地址用del变量存起来
	pos->next = del->next;       //将del的下一个节点(即pos的下下个节点)赋给pos的next指针
                                 //相当于连接pos节点和pos的下下个节点
	free(del);                   //释放del指向的节点(即pos的下个节点)
	del = NULL;                  //del原本代表的值仍可能使用,故此处需置空
}

12、销毁链表

创建一个SLTDestroy()函数,因对链表的头节点本身进行操作,故参数为头节点的二级指针;因无须返回值,故返回类型为空。

遍历链表,将逐个节点释放即可。

void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
		SLTNode* cur = *pphead;    //cur变量代替跑腿
		while (cur)                //将逐个节点释放
		{
			SLTNode* tmp = cur->next;    //将cur的下一个节点用tmp暂时存起来,
			free(cur);
			cur = tmp;                   //释放cur所指的当前节点后,又赋给cur
                                         //方便后续cur继续向后移动
		}
	
	*pphead = NULL;                      //全部释放结束后,将头节点置空
}

 · 一些小tips

1、关于是否二级指针传参:只访问链表的时候传一级指针即可,无须传二级指针;但调整改动链表的时候,须传二级指针。

2、关于访问或遍历链表:一般通过一个代替跑腿(初始值为头节点的指针)的结构指针变量。

 · 代码汇总

SList.h

#define _CRT_SECURE_NO_WARNINGS

#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);
SLTNode* BuySLTNode(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);
void SLTEraseAfter(SLTNode* pos);
void SLTDestroy(SLTNode** pphead);

SList.c

#include"SList.h"


void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while(cur!=NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

SLTNode* BuySLTNode(SLTDataType x)
{	
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);

	SLTNode* newnode = BuySLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}	

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}
void SLTPopFront(SLTNode** pphead)
{
	assert(*pphead != NULL);
	SLTNode* first = *pphead;
	*pphead = first->next;
	free(first);
	first = NULL;
}

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySLTNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	assert(*pphead);

	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);

	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}
void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
		SLTNode* cur = *pphead;
		while (cur)
		{
			SLTNode* tmp = cur->next;
			free(cur);
			cur = tmp;
		}
	
	*pphead = NULL;
}

· 带头双向循环链表 

带头双向循环链表又简称双向链表,与单链表最大的区别是,节点与节点之间首尾相连

除了当前节点数据元素之外,它还有两个结构体成员,指向上一个节点的指针指向下一个节点的指针,首尾相连的特性也是通过这两个结构体成员实现的。

同时,它还拥有一系列通过函数实现的接口,如下:

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;
	LTDataType data;
}LTNode;


LTNode* BuyListNode(LTDataType x);      //动态申请一个新节点
LTNode* LTInit();                       //创建一个只有头节点的双向链表     
void LTPrint(LTNode* phead);            //打印双向链表
bool LTEmpty(LTNode* phead);            //验空
void LTPushBack(LTNode* phead, LTDataType x);     //尾插
void LTPopBack(LTNode* phead);                    //尾删
void LTPushFront(LTNode* phead, LTDataType x);    //头插
void LTPopFront(LTNode* phead);                   //头删 
void LTInsert(LTNode* pos, LTDataType x);         //pos处插入
void LTErase(LTNode* pos);                        //pos处删除
LTNode* LTFind(LTNode* phead, LTDataType x);      //查找
void LTDestroy(LTNode* phead);                    //销毁链表

1、动态申请一个节点

LTNode* BuyListNode(LTDataType x)        //传入数据元素x来创建新节点
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

    //动态申请空间后,将数据变量data、指向上一个节点的指针prev
    //和指向下一个节点的指针next,分别初始化为空和传入的数据元素x
	node->next = NULL;
	node->prev = NULL;
	node->data = x;

	return node;
}

2、创建一个只有头节点的双向链表

LTNode* LTInit()
{
	LTNode* phead = BuyListNode(-1);    //暂时以数据元素-1创建一个头节点
	
    //因为是双向链表,故需首尾相连
    //将头节点与自己首尾相连
    phead->next = phead;        
	phead->prev = phead;

	return phead;
}

3、打印双向链表

void LTPrint(LTNode* phead)
{
	assert(phead);
	printf("<=head=>");               //先打印“头”
	LTNode* cur = phead->next;
	while (cur != phead)              //遍历链表,逐个节点地打印数据
	{
		printf("%d <=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

4、验空

bool LTEmpty(LTNode* phead)    //返回值为真/假(为空/非空),故返回类型为bool
{
	assert(phead);    

	return phead->next == phead;    //当头节点的next指针仅指向头节点,说明链表为空
}

 5、pos位置(前)插入

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* newnode = BuyListNode(x);

    //将新节点连接在pos前的节点与当前pos位置的节点之间
    //新节点将取代原pos位置的节点占在pos位置,原pos位置的节点成为当前pos位置的下一个节点
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

6、pos位置删除 

void LTErase(LTNode* pos)
{
	assert(pos);

	LTNode* p = pos->prev;
	LTNode* n = pos->next;
    
    //将pos前后连接,然后释放pos
	p->next = n;
	n->prev = p;
	free(pos);
	//pos=NULL;
}

7、尾插

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);    //phead不可能为空,为空必然是传错了

	LTNode* newnode = BuyListNode(x);    //与单链表类似,创建一个新节点然后尾插
	LTNode* tail = phead->prev;
    
    //将新节点与双向链表的尾部连接起来
	tail->next = newnode;    //尾的next接新节点
	newnode->prev = tail;    //新节点的prev接尾
	newnode->next = phead;    //新节点的next接头
	phead->prev = newnode;    //头的prev接新节点

}

优化,复用LTInsert():

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTInsert(phead, x);
}

8、尾删

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));    //链表不为空,尾删才有意义

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;    //这样写是为了方便控制
    
    //将尾的上一个节点与头节点相接
	tailPrev->next = phead;
	phead->prev = tailPrev;

    //然后释放尾节点
	free(tail);
	tail = NULL;
}

优化,复用LTErase ():

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTErase(phead->prev);
}

9、头插

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* newnode = BuyListNode;
	LTNode* first = phead->next;

    //将新节点连接在原头节点与其下一个节点之间
	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = first;
	first->prev = newnode;
}
//这里头插是插入在头后面的位置,否则与尾插效果重复了

 优化,复用LTInsert():

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTInsert(phead->next, x);
}

10、头删

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTErase(phead->next);    //复用LTErase()
}

11、查找

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

12、销毁链表

void LTDestroy(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;

    //从头节点的下一个节点开始遍历链表,逐个释放
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}

    //最终,释放头节点
	free(phead);
	//phead=NULL;
}

 · 代码汇总 

List.h

#define _CRT_SECURE_NO_WARNINGS

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

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;
	LTDataType data;
}LTNode;
LTNode* BuyListNode(LTDataType x);
LTNode* LTInit();
void LTPrint(LTNode* phead);
bool LTEmpty(LTNode* phead);
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);
void LTInsert(LTNode* pos, LTDataType x);
void LTErase(LTNode* pos);
LTNode* LTFind(LTNode* phead, LTDataType x);
void LTDestroy(LTNode* phead);

List.c


#include"list.h"

LTNode* BuyListNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	node->next = NULL;
	node->prev = NULL;
	node->data = x;

	return node;
}
LTNode* LTInit()
{
	LTNode* phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

void LTPrint(LTNode* phead)
{
	assert(phead);
	printf("<=head=>");
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d <=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTInsert(phead, x);
}

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTErase(phead->prev);
}

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTInsert(phead->next, x);
}

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTErase(phead->next);
}

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* newnode = BuyListNode(x);

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

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

void LTErase(LTNode* pos)
{
	assert(pos);

	LTNode* p = pos->prev;
	LTNode* n = pos->next;

	p->next = n;
	n->prev = p;
	free(pos);
}
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

void LTDestroy(LTNode* phead)
{
	assert(phead);

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

[补] 顺序表和链表的区别

ps:缓存利用率可参考存储体系结构以及局部原理性。