当前位置: 首页 > news >正文

顺序表和链表

目录

  • 线性表
    • 顺序表
      • 概念与结构
      • 分类
        • 静态顺序表
        • 动态顺序表
      • 动态顺序表的实现
        • SeqList.h
        • SeqLIst.c 和 test.c
        • 初始化SLInit
        • 增容SLCheckCapacity
        • 尾插SLPushBack
        • 打印SLPrint
        • 头插SLPushFront
        • 尾删SLPopBack
        • 头删SLPopFront
        • 查找SLFind
        • 任意插SLInsert
        • 任意删SLErase
        • 销毁顺序表SLDestroy
      • 顺序表问题与思考
    • 单链表
      • 概念与结构
        • 节点/结点
        • 链表的性质
      • 单链表的实现
        • SLIst.h
        • SList.c和test.c
        • 打印print
        • 申请新的节点
        • 尾插push_back
        • 头插push_front
        • 尾删pop_back
        • 头删pop_front
        • 查找find
        • 指定节点之前插入数据insert
        • 指定节点之后插入数据insert_after
        • 删除指定节点erase
        • 删除指定节点之后的节点erase_after
        • 销毁单链表destroy
    • 链表的分类
    • 双向链表
      • 概念与结构
      • 实现双向链表
        • List.h
        • List.c和test.c
        • 申请新节点
        • 初始化1
        • 初始化2(推荐)
        • 打印
        • 尾插
        • 头插
        • 尾删
        • 头删
        • 查找
        • 任意位置之前插
        • 删除任意节点
        • 销毁1
        • 销毁2(推荐)

线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。
线性表是一种在实际中广泛使用的数据结构。
常见的线性表:顺序表、链表、栈、队列、字符串…

线性表在逻辑上是线性结构,也就是逻辑上的连续的一条直线。
但在物理结构上并不一定是连续的。线性表在物理上存储时,通常以数组和链式结构的形式存储。

在这里插入图片描述

顺序表

顺序表就是一种在逻辑上连续,物理上也连续的线性表。

在这里插入图片描述

概念与结构

概念:顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,
一般情况下采用数组存储。
在这里插入图片描述

顺序表和数组的区别?
顺序表的底层结构是数组,对数组的封装,实现了常用的增删查改等接口。

在这里插入图片描述

分类

顺序表分为:静态顺序表和动态顺序表。

静态顺序表
//静态循序表的结构
#define N 8
typedef int SLDataType;
typedef struct SeqList
{
	SLDataType data[N]; //定长数组
	int size; //有效数据个数
}SL;

在这里插入图片描述

静态顺序表缺陷:空间给少了不够用,给多了造成空间浪费。

动态顺序表
//动态循序表的结构
typedef int SLDataType;
typedef struct SeqList
{
	SLDataType* data;
	int size; //有效数据个数
	int capacity; //空间容量
}SL;

在这里插入图片描述

动态顺序表按需申请,可增容。 - 一般2倍增容。

动态顺序表的实现

我们分一个.h头文件和2个.c源文件,去实现顺序表。

SeqList就是sequence list 就是顺序表的意思。

在这里插入图片描述

SeqList.h
//动态顺序表的实现

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

typedef int SLDataType;
typedef struct SeqList
{
	SLDataType* data;
	int size; //有效数据个数
	int capacity; //空间容量
}SL;

//初始化
void SLInit(SL* ps);

//尾插
void SLPushBack(SL* ps, SLDataType x);

//打印
void SLPrint(SL* ps);

//头插
void SLPushFront(SL* ps, SLDataType x);

//尾删
void SLPopBack(SL* ps);

//头删
void SLPopFront(SL* ps);

//查找指定数据 - 找到返回下标,找不到返回-1
int SLFind(SL* ps, SLDataType x);

//在指定位置之前插入数据
void SLInsert(SL* ps, int pos, SLDataType x);

//删除指定位置的数据
void SLErase(SL* ps, int pos);

//销毁顺序表
void SLDestroy(SL* ps);
SeqLIst.c 和 test.c

函数功能的实现和测试

初始化SLInit
//初始化
void SLInit(SL* ps)
{
	assert(ps);//防止ps为NULL
	ps->data = NULL;//初始化
	ps->size = ps->capacity = 0;//初始化
}

test.c

	SL sl;//创建一个空的顺序表
	SLInit(&sl);//初始化
	//只有传地址才能改变sl的值

在这里插入图片描述

增容SLCheckCapacity

在所有的插入动作之前都要判断是否需要增容。

//检查是否需增容
//static是让改函数只能在SeqList.c中使用
static void SLCheckCapacity(SL* ps)
{
	assert(ps);//防止ps为NULL
	if (ps->capacity == ps->size)//只有有效数据等于容量时才需要增容
	{
		int NewCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;//三目操作符定义新容量
		SLDataType* tmp = realloc(ps->data, NewCapacity * sizeof(SLDataType));
		//向堆区申请空间,申请的空间是原来的二倍。
		if (tmp == NULL)//检查是否申请空间失败
		{
			perror("SLCheckCapacity()::realloc()");
			exit(1);
		}
		ps->data = tmp;//把申请的空间给data
		ps->capacity = NewCapacity;//把新容量给顺序表的容量
	}
}
尾插SLPushBack
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
	assert(ps);//防止ps为NULL
	//检查是否需要增容
	SLCheckCapacity(ps);
	//尾插
	ps->data[ps->size++] = x;
	//在尾部插入元素,更新有效数据个数
}
打印SLPrint
//打印
void SLPrint(SL* ps)
{
	assert(ps);//防止ps为NULL
	for (int i = 0; i < ps->size; i++)//遍历顺序表
	{
		printf("%d ", ps->data[i]);//打印顺序表中的数据
	}
	printf("\n");
}

test.c

	//测试尾插
	SLPushBack(&sl, 1);
	SLPrint(&sl);
	SLPushBack(&sl, 2);
	SLPrint(&sl);
	SLPushBack(&sl, 3);
	SLPrint(&sl);
	SLPushBack(&sl, 4);
	SLPrint(&sl);

在这里插入图片描述

头插SLPushFront
//头插
void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);//防止ps为NULL
	//检查是否需要增容
	SLCheckCapacity(ps);
	//头插
	for (int i = ps->size; i > 0; i--)
	//从后向前遍历顺序表,防止数据覆盖
	{
		ps->data[i] = ps->data[i - 1];
	}
	ps->data[0] = x;//头插
	ps->size++;//更新有效数据个数
}

test.c

	//测试头插
	SLPushFront(&sl, 1);
	SLPrint(&sl);
	SLPushFront(&sl, 2);
	SLPrint(&sl);
	SLPushFront(&sl, 3);
	SLPrint(&sl);
	SLPushFront(&sl, 4);
	SLPrint(&sl);

在这里插入图片描述

尾删SLPopBack
//尾删
void SLPopBack(SL* ps)
{
	assert(ps && ps->size);//防止ps为NULL且顺序表中没有数据
	ps->size--;//更新有效数据个数,达到遍历时访问不到尾部数据的目的
}

test.c

	//尾插
	SLPushBack(&sl, 1);
	SLPrint(&sl);
	SLPushBack(&sl, 2);
	SLPrint(&sl);
	SLPushBack(&sl, 3);
	SLPrint(&sl);
	SLPushBack(&sl, 4);
	SLPrint(&sl);

	//测试尾删
	SLPopBack(&sl);
	SLPrint(&sl);
	SLPopBack(&sl);
	SLPrint(&sl);
	SLPopBack(&sl);
	SLPrint(&sl);
	SLPopBack(&sl);
	SLPrint(&sl);
	SLPopBack(&sl);
	SLPrint(&sl);

在这里插入图片描述
第五次尾删时assert代码生效。

头删SLPopFront
//头删
void SLPopFront(SL* ps)
{
	assert(ps && ps->size);//防止ps为NULL且顺序表中没有数据
	for (int i = 0; i < ps->size - 1; i++)
	//从前向后遍历顺序表,防止数据被覆盖
	{
		ps->data[i] = ps->data[i + 1];
	}
	ps->size--;//更新有效数据个数
}

test.c

	//尾插
	SLPushBack(&sl, 1);
	SLPrint(&sl);
	SLPushBack(&sl, 2);
	SLPrint(&sl);
	SLPushBack(&sl, 3);
	SLPrint(&sl);
	SLPushBack(&sl, 4);
	SLPrint(&sl);
	
	//测试头删
	SLPopFront(&sl);
	SLPrint(&sl);
	SLPopFront(&sl);
	SLPrint(&sl);
	SLPopFront(&sl);
	SLPrint(&sl);
	SLPopFront(&sl);
	SLPrint(&sl);
	SLPopFront(&sl);
	SLPrint(&sl);

在这里插入图片描述
第五次头删时assert代码生效。

查找SLFind
//查找指定数据 - 找到返回下标,找不到返回-1
int SLFind(SL* ps, SLDataType x)
{
	assert(ps);//防止ps为NULL
	for (int i = 0; i < ps->size; i++)
	//遍历顺序表与x相等的数据
	{
		if (x == ps->data[i])
			return i;//找到了,返回下标
	}
	return -1;//找不到返回-1
}

test.c

	//尾插
	SLPushBack(&sl, 1);
	SLPrint(&sl);
	SLPushBack(&sl, 2);
	SLPrint(&sl);
	SLPushBack(&sl, 3);
	SLPrint(&sl);
	SLPushBack(&sl, 4);
	SLPrint(&sl);
	
	//测试查找指定数据 - 找到返回下标,找不到返回-1
	int find = SLFind(&sl, 3);
	if (find != -1)
		printf("找到了,下标是:%d\n", find);
	else
		printf("找不到\n");

在这里插入图片描述

任意插SLInsert
//在指定位置之前插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);//防止ps为NULL
	assert(pos >= 0 && pos <= ps->size);//pos的合法范围
	//检查是否需要增容
	SLCheckCapacity(ps);

	for (int i = ps->size; i > pos; i--)
	//从后向前遍历[pos, size]的数据,防止数据被覆盖
	{
		ps->data[i] = ps->data[i - 1];
	}
	ps->data[pos] = x;//指定位置插
	ps->size++;//更新有效数据个数
}

test.c

	//尾插
	SLPushBack(&sl, 1);
	SLPrint(&sl);
	SLPushBack(&sl, 2);
	SLPrint(&sl);
	SLPushBack(&sl, 3);
	SLPrint(&sl);
	SLPushBack(&sl, 4);
	SLPrint(&sl);
	
	//测试在指定位置之前插入数据
	SLInsert(&sl, 1, 5);
	SLPrint(&sl);//1 5 2 3 4
}

在这里插入图片描述

任意删SLErase
//删除指定位置的数据
void SLErase(SL* ps, int pos)
{
	assert(ps && ps->size);//防止ps为NULL且顺序表中没有数据
	assert(pos >= 0 && pos < ps->size);//pos的合法范围
	for (int i = pos; i < ps->size - 1; i++)
	//从前向后遍历顺序表,防止数据被覆盖
	{
		ps->data[i] = ps->data[i + 1];
	}
	ps->size--;//更新有效数据个数
}

test.c

	//尾插
	SLPushBack(&sl, 1);
	SLPrint(&sl);
	SLPushBack(&sl, 2);
	SLPrint(&sl);
	SLPushBack(&sl, 3);
	SLPrint(&sl);
	SLPushBack(&sl, 4);
	SLPrint(&sl);
	
	//测试删除指定位置的数据
	SLErase(&sl, 2);
	SLPrint(&sl);//1 2 4

在这里插入图片描述

销毁顺序表SLDestroy
//销毁顺序表
void SLDestroy(SL* ps)
{
	assert(ps);//防止ps为NULL
	if (ps->data)//data不为NULL才释放空间
		free(ps->data);
	//将顺序表还原
	ps->data = NULL;
	ps->capacity = ps->size = 0;
}

test.c

	//尾插
	SLPushBack(&sl, 1);
	SLPrint(&sl);
	SLPushBack(&sl, 2);
	SLPrint(&sl);
	SLPushBack(&sl, 3);
	SLPrint(&sl);
	SLPushBack(&sl, 4);
	SLPrint(&sl);

	 //测试销毁顺序表
	SLDestroy(&sl);

在这里插入图片描述

顺序表问题与思考

  1. 中间/头部的插⼊删除,时间复杂度为O(N)
  2. 增容需要申请新空间,拷⻉数据,释放旧空间。会有不⼩的消耗。
  3. 增容⼀般是呈2倍的增⻓,势必会有⼀定的空间浪费。

那么如何解决以上问题呢?

单链表

单链表是一种逻辑上连续,物理上不连续的线性表。

在这里插入图片描述

概念与结构

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

在这里插入图片描述
淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。
将火车里的某节车厢去掉/加上,不会影响其他车厢,每节车厢都是独立存在的。

在链表里,每节“车厢”是什么样的呢?

在这里插入图片描述

节点/结点

与顺序表不同的是,链表里的每节“车厢”都是独立申请下来的空间,我们称之为“节点/结点”。

节点主要由两个部分组成:当前节点要保存的数据和保存下一个节点的地址(指针变量)。

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

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

链表的性质
  1. 链式结构在逻辑上是连续的,在物理上不一定连续
  2. 节点一般是从堆上申请的
  3. 从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能联系,可能不连续

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

struct SListNode
{
	int data; //节点数据
	struct SListNode* next; //保存下一个节点的地址
};

当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要
保存整型数据,也要保存下一个节点的地址(当下一个节点为空时保存的地址为空)。

每一个节点都能找到下一个节点,所有的节点连起来,就成了一条线,
在这条线上我们能找到每一个节点。

单链表的实现

我们分一个.h头文件和2个.c源文件,去实现单链表。

SList就是singly-linked list,就是单链表的意思。

在这里插入图片描述

SLIst.h
//单链表的动态实现

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

//节点的创建
typedef int SListDataType;
typedef struct SListNode
{
	SListDataType data; //节点的数据
	struct SListNode* next; //指向下一个节点的指针
}Node;

//打印
void print(Node** pph);

//尾插
void push_back(Node** pph, SListDataType x);

//头插
void push_front(Node** pph, SListDataType x);

//尾删
void pop_back(Node** pph);

//头删
void pop_front(Node** pph);

//查找指定节点的数据 - 找到返回节点,找不到返回NULL
Node* find(Node** pph, SListDataType x);

//在指定节点之前插入数据
void insert(Node** pph, Node* pos, SListDataType x);

//在指定节点之后插入数据
void insert_after(Node* pos, SListDataType x);

//删除pos节点
void erase(Node** pph, Node* pos);

//删除pos之后的节点
void erase_after(Node* pos);

//销毁单链表
void destroy(Node** pph);
SList.c和test.c

函数功能的实现和测试

打印print
//打印
void print(Node** pph)
{
	assert(pph);//防止pph为空
	Node* cur = *pph;//记录第一个节点的地址
	while (cur)//遍历单链表
	{
		printf("%d->", cur->data);//打印每个节点的数据
		cur = cur->next;//找下一个节点
	}
	printf("NULL\n");//最后一个节点的next为NULL
}
申请新的节点
//申请新的节点
Node* buyNode(SListDataType x)
{
	Node* newnode = (Node*)malloc(sizeof(Node));//向堆区申请一个节点的空间
	if (newnode == NULL)//判断是否申请成功
	{
		perror("buyNode");
		exit(1);
	}
	//给新的节点赋值
	newnode->data = x;
	newnode->next = NULL;
	return newnode;//将新的节点返回
}
尾插push_back
//尾插
void push_back(Node** pph, SListDataType x)
{
	assert(pph);//防止pph为NULL
	Node* newnode = buyNode(x);//申请新的节点
	
	if (*pph == NULL)//空链表
	{
		*pph = newnode;//空链表直接插入新节点
	}
	else//非空链表
	{
		Node* tail = *pph;//tail是用来找单链表的尾节点
		while (tail->next)//用tail->next是否为NULL,判断是否找到了尾节点
		{
			tail = tail->next;//找下一个节点
		}
		tail->next = newnode;//跳出循环,此时tail指向尾节点,尾插
	}
}

test.c

	Node* phead = NULL;
	//测试尾插
	push_back(&phead, 1);
	print(&phead);
	push_back(&phead, 2);
	print(&phead);
	push_back(&phead, 3);
	print(&phead);
	push_back(&phead, 4);
	print(&phead);

在这里插入图片描述

头插push_front
//头插
void push_front(Node** pph, SListDataType x)
{
	assert(pph);//防止pph为NULL
	Node* newnode = buyNode(x);//申请新节点
	newnode->next = *pph;//新节点的下一个节点指向第一个节点
	*pph = newnode;//*pph指向新节点,完成头插
}

test.c

	//测试头插
	push_front(&phead, 5);
	print(&phead);
	push_front(&phead, 6);
	print(&phead);
	push_front(&phead, 7);
	print(&phead);//7 6 5

在这里插入图片描述

尾删pop_back
//尾删
void pop_back(Node** pph)
{
	assert(pph && *pph);//防止pph为NULL且单链表是一个空链表
	//链表中只有一个节点
	if ((*pph)->next == NULL)
	{
		//释放第一个节点
		free(*pph);
		*pph = NULL;
	}
	//链表中有多个节点
	else
	{
		Node* ptail = *pph;//ptail负责找尾节点
		Node* pprev = *pph;//pprev负责找尾节点的前一个结点
		while (ptail->next)//判断ptail是否指向尾节点
		{
			pprev = ptail;//保存当前的ptail
			ptail = ptail->next;//ptail继续找尾
		}
		//释放尾节点
		free(ptail);
		ptail = NULL;
		//尾节点的前一个节点的next指针需置空
		pprev->next = NULL;
	}
}

test.c

	//尾插
	push_back(&phead, 1);
	print(&phead);
	push_back(&phead, 2);
	print(&phead);
	push_back(&phead, 3);
	print(&phead);
	push_back(&phead, 4);

	//测试尾删
	pop_back(&phead);
	print(&phead);
	pop_back(&phead);
	print(&phead);
	pop_back(&phead);
	print(&phead);
	pop_back(&phead);
	print(&phead);
	pop_back(&phead);
	print(&phead);

在这里插入图片描述

第五次删除时链表已经为空,断言报错。

头删pop_front
//头删
void pop_front(Node** pph)
{
	assert(pph && *pph);//防止pph为NULL且单链表是一个空链表
	Node* next = (*pph)->next;//创建临时变量next保存第二个节点的地址
	free(*pph);//释放第一个节点
	*pph = next;//让*pph指向第二个节点,完成头删
}

test.c

	//尾插
	push_back(&phead, 1);
	print(&phead);
	push_back(&phead, 2);
	print(&phead);
	push_back(&phead, 3);
	print(&phead);
	push_back(&phead, 4);

	//测试头删
	pop_front(&phead);
	print(&phead);
	pop_front(&phead);
	print(&phead);
	pop_front(&phead);
	print(&phead);
	pop_front(&phead);
	print(&phead);
	pop_front(&phead);
	print(&phead);

在这里插入图片描述

第五次删除时链表已经为空,断言报错。

查找find
//查找指定节点的数据 - 找到返回节点,找不到返回NULL
Node* find(Node** pph, SListDataType x)
{
	assert(pph);//防止pph为NULL
	Node* pcur = *pph;//记录第一个节点的地址
	while (pcur)//遍历单链表
	{
		if (x == pcur->data)//找数据
			return pcur;//找到了,返回该数据对应的节点
		pcur = pcur->next;//找下一个节点
	}
	return NULL;//找不到,返回NULL
}

test.c

	//尾插
	push_back(&phead, 1);
	print(&phead);
	push_back(&phead, 2);
	print(&phead);
	push_back(&phead, 3);
	print(&phead);
	push_back(&phead, 4);

	//测试查找指定元素
	Node* f = find(&phead, 4);
	if (f == NULL)
		printf("找不到\n");
	else
		printf("找到了\n");

在这里插入图片描述

指定节点之前插入数据insert
//在指定节点之前插入数据
void insert(Node** pph, Node* pos, SListDataType x)
{
	//防止pph为NULL且单链表为空且pos为NULL
	assert(pph && *pph && pos);
	if (*pph == pos)//如果pos指向第一个节点,直接头插
		push_front(pph, x);
	else//pos不是第一个节点
	{
		Node* newnode = buyNode(x);//申请新节点
		Node* pprev = *pph;//记录第一个节点
		while (pprev->next != pos)
		//pprev指向的节点的下一个节点不是pos指向的节点,就跳出循环
		{
			pprev = pprev->next;/找下一个节点
		}
		newnode->next = pos;//让新的节点的next指针指向pos指向的节点
		pprev->next = newnode;//pprev指向的节点的next指针存放新节点的地址
	}

}
test.c

```c
	//尾插
	push_back(&phead, 1);
	print(&phead);
	push_back(&phead, 2);
	print(&phead);
	push_back(&phead, 3);
	print(&phead);
	push_back(&phead, 4);

	//测试在指定节点之前插入数据
	Node* f = find(&phead, 4);
	insert(&phead, f, 8);
	print(&phead);//1 2 3 8 4

在这里插入图片描述

指定节点之后插入数据insert_after
//在指定节点之后插入数据
void insert_after(Node* pos, SListDataType x)
{
	assert(pos);//防止pos为NULL
	Node* newnode = buyNode(x);//申请新节点
	//将newnode节点插在pos和pos->next节点之间
	newnode->next = pos->next;
	pos->next = newnode;
}

test.c

	//尾插
	push_back(&phead, 1);
	print(&phead);
	push_back(&phead, 2);
	print(&phead);
	push_back(&phead, 3);
	print(&phead);
	push_back(&phead, 4);
	
	//测试在指定节点之后插入数据
	Node* f = find(&phead, 4);
	insert_after(f, 9);
	print(&phead);//1 2 3 4 9

在这里插入图片描述

删除指定节点erase
//删除pos节点
void erase(Node** pph, Node* pos)
{
	//防止pph为NULL且单链表为空且pos为NULL
	assert(pph && *pph && pos);
	if (pos == *pph)//pos是第一个节点就头删
		pop_front(pph);
	else//pos不是第一个节点
	{
		Node* pprev = *pph;//记录第一个节点
		while (pprev->next != pos)
		//pprev指向的节点的下一个节点是pos指向的节点就跳出循环
		{
			pprev = pprev->next;//找下一个节点
		}
		pprev->next = pos->next;//pprev指向的节点的next指针存放pos的下一个节点的地址
		//释放要被删除的节点
		free(pos);
		pos = NULL;
	}
}

test.c

	//尾插
	push_back(&phead, 1);
	print(&phead);
	push_back(&phead, 2);
	print(&phead);
	push_back(&phead, 3);
	print(&phead);
	push_back(&phead, 4);

	//测试删除pos节点
	Node* f = find(&phead, 3);
	erase(&phead, f);
	print(&phead);//1 2 4

在这里插入图片描述

删除指定节点之后的节点erase_after
//删除pos之后的节点
void erase_after(Node* pos)
{
	assert(pos && pos->next);//防止pos为NULL且pos->next为NULL
	Node* del = pos->next;//保存要删除的节点
	pos->next = del->next;//让pos的next指针存放要删的节点的下一个节点
	free(del);//释放要删除的节点
	del = NULL;
}

test.c

	//尾插
	push_back(&phead, 1);
	print(&phead);
	push_back(&phead, 2);
	print(&phead);
	push_back(&phead, 3);
	print(&phead);
	push_back(&phead, 4);
	
	//测试删除pos之后的节点
	Node* f = find(&phead, 2);
	erase_after(f);
	print(&phead);//1 2 4

在这里插入图片描述

销毁单链表destroy
//销毁链表
void destroy(Node** pph)
{
	assert(pph && *pph);//防止pph为NULL且链表为空
	Node* pcur = *pph;//记录第一个节点
	
	while (pcur)//遍历单链表
	{
		Node* next = pcur->next;//保存当前节点的下一个节点
		free(pcur);//释放当前节点
		pcur = next;//让当前节点指向保存的节点
	}
	*pph = NULL;//*pph指向的第一个节点已经释放,最后给*pph置空
}

test.c

	//尾插
	push_back(&phead, 1);
	print(&phead);
	push_back(&phead, 2);
	print(&phead);
	push_back(&phead, 3);
	print(&phead);
	push_back(&phead, 4);

	//测试销毁链表
	destroy(&phead);
	print(&phead);

在这里插入图片描述

链表的分类

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

在这里插入图片描述

链表说明:

在这里插入图片描述
虽然有这么多的链表的结构,但是我们实际中最常用的还是两种结构:单链表和双向链表

双向链表

概念与结构

在这里插入图片描述
带头链表中的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,值起“放哨”作用。

实现双向链表

在这里插入图片描述

List.h
#pragma once

//双向链表的实现

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

//双向链表节点的结构
typedef int LTDataType;
typedef struct ListNode
{
	int data;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;

初始化
//void LTInit(LTNode** pphead);

LTNode* LTInit();

//尾插
void LTPushBack(LTNode* ph, LTDataType x);

//打印
void LTPrint(LTNode* ph);

//头插
void LTPushFront(LTNode* ph, LTDataType x);

//尾删
void LTPopBack(LTNode* ph);

//头删
void LTPopFront(LTNode* ph);

//查找
LTNode* LTFind(LTNode* ph, LTDataType x);

//在任意位置之前插入节点
void LTInsert(LTNode* pos, LTDataType x);

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

销毁
//void LTDestroy(LTNode** pph);

void LTDestroy(LTNode* ph);
List.c和test.c
申请新节点
//申请新节点
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));//申请节点大小的空间
	if (newnode == NULL)//判断是否申请失败
	{
		perror("malloc fail!");
		exit(1);
	}
	newnode->data = x;//节点元素赋值
	newnode->next = newnode->prev = newnode;//循环链表,自己指向自己,成环
	return newnode;
}
初始化1
//初始化
void LTInit(LTNode** pph)
{
	assert(pph);//防止pph为NULL
	*pph = LTBuyNode(-1);//创建头节点,元素无效,赋值为-1
}

test.c

	LTNode* phead = NULL;
	LTInit(&phead);
初始化2(推荐)
LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);//申请头节点
	return phead;//返回头节点
}

test.c

	LTNode* phead = LTInit();//接收返回的头节点
打印
//打印
void LTPrint(LTNode* ph)
{
	assert(ph);//防止ph为NULL
	LTNode* pcur = ph->next;//pcur指向头节点的下一个节点
	while (pcur != ph)//遍历一趟
	{
		printf("%d->", pcur->data);//打印
		pcur = pcur->next;//找下一个节点
	}
	printf("\n");
}
尾插
//尾插
void LTPushBack(LTNode* ph, LTDataType x)
{
	assert(ph);//防止ph为NULL
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//新节点的prev指针指向尾节点
	newnode->prev = ph->prev;
	//新节点的next指针指向头节点
	newnode->next = ph;
	//尾节点的next指针指向新节点
	ph->prev->next = newnode;
	//头节点的prev指针指向新节点
	ph->prev = newnode;
}

test.c

	LTNode* phead = LTInit();

	//测试尾插
	LTPushBack(phead, 1);
	LTPrint(phead);
	LTPushBack(phead, 2);
	LTPrint(phead);

在这里插入图片描述

头插
//头插
void LTPushFront(LTNode* ph, LTDataType x)
{
	assert(ph);//防止ph为NULL
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//新节点的prev指针指向头节点
	newnode->prev = ph;
	//新节点的next指针指向第一个节点
	newnode->next = ph->next;
	//第一个节点的prev指针指向新节点
	ph->next->prev = newnode;
	//头节点的next指针指向新节点
	ph->next = newnode;
}

test.c

	//测试头插
	LTPushFront(phead, 3);
	LTPrint(phead);
	LTPushFront(phead, 4);
	LTPrint(phead);

在这里插入图片描述

尾删
//尾删
void LTPopBack(LTNode* ph)
{
	//防止ph为NULL且保证链表中有有效节点
	assert(ph && ph->next != ph);
	//保存要删除的尾节点
	LTNode* del = ph->prev;
	//头节点的prev指针指向倒数第二个节点
	ph->prev = del->prev;
	//倒数第二个节点的next指针指向头节点
	del->prev->next = ph;
	//释放尾节点
	free(del);
	del = NULL;
}

test.c

	//尾插
	LTPushBack(phead, 1);
	LTPrint(phead);
	LTPushBack(phead, 2);
	LTPrint(phead);
	
	//测试尾删
	LTPopBack(phead);
	LTPrint(phead);
	LTPopBack(phead);
	LTPrint(phead);
	LTPopBack(phead);
	LTPrint(phead);

在这里插入图片描述
第三次删除时链表中没有有效节点,所以断言报错。

头删
//头删
void LTPopFront(LTNode* ph)
{
	//防止ph为NULL且保证链表中有有效节点
	assert(ph && ph->next != ph);
	//保存要删除的第一个节点
	LTNode* del = ph->next;
	//头节点的next指针指向第二个节点
	ph->next = del->next;
	//第二个节点的prev指针指向头节点
	del->next->prev = ph;
	//释放要删除的第一个节点
	free(del);
	del = NULL;
}

test.c

	//尾插
	LTPushBack(phead, 1);
	LTPrint(phead);
	LTPushBack(phead, 2);
	LTPrint(phead);
	
	//测试头删
	LTPopFront(phead);
	LTPrint(phead);
	LTPopFront(phead);
	LTPrint(phead);
	LTPopFront(phead);
	LTPrint(phead);

在这里插入图片描述第三次删除时链表中没有有效节点,所以断言报错。

查找
//查找
LTNode* LTFind(LTNode* ph, LTDataType x)
{
	assert(ph);//防止ph为NULL
	//让pcur指向第一个节点
	LTNode* pcur = ph->next;
	while (pcur != ph)//遍历一遍链表
	{
		if (pcur->data == x)//找到返回该节点
			return pcur;
		pcur = pcur->next;//找下一个节点
	}
	return NULL;//找不到,返回NULL
}

test.c

	//尾插
	LTPushBack(phead, 1);
	LTPrint(phead);
	LTPushBack(phead, 2);
	LTPrint(phead);
	
		//测试查找
	LTNode* find = LTFind(phead, 2);
	if (find)
		printf("找到了\n");
	else
		printf("没找到\n");

在这里插入图片描述

任意位置之前插
//在任意位置之前插入节点
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//防止pos为NULL
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//新节点的prev指针指向pos的前一个节点
	newnode->prev = pos->prev;
	//新节点的next指针指向pos节点
	newnode->next = pos;
	//pos前一个节点的next指针指向新节点
	pos->prev->next = newnode;
	//pos的prev指针指向新节点
	pos->prev = newnode;
}

test.c

	//尾插
	LTPushBack(phead, 1);
	LTPrint(phead);
	LTPushBack(phead, 2);
	LTPrint(phead);

	//测试任意之前插
	LTNode* find = LTFind(phead, 2);
	LTInsert(find, 5);
	LTPrint(phead);

在这里插入图片描述

删除任意节点
//删除任意节点
void LTErase(LTNode* pos)
{
	assert(pos);//防止pos为NULL
	//pos的前一个结点的next指针指向pos的下一个节点
	pos->prev->next = pos->next;
	//pos的下一个节点的prev指针指向pos的前一个结点
	pos->next->prev = pos->prev;
	//释放pos节点
	free(pos);
	pos = NULL;
}

test.c

	//尾插
	LTPushBack(phead, 1);
	LTPrint(phead);
	LTPushBack(phead, 2);
	LTPrint(phead);

	//测试删任意节点
	LTNode* find = LTFind(phead, 2);
	LTErase(find);
	find = NULL;//删完之后find为野指针,需置空
	LTPrint(phead);

在这里插入图片描述

销毁1
//销毁
void LTDestroy(LTNode** pph)
{
	assert(pph);//防止pph为NULL
	//pcur指向第一个节点
	LTNode* pcur = (*pph)->next;
	while (pcur != *pph)//遍历一遍链表
	{
		LTNode* next = pcur->next;//next指向pcur的下一个节点
		free(pcur);//释放pcur节点
		pcur = next;//把pcur的下一个节点给pcur
	}
	free(*pph);//释放头节点
	*pph = NULL;
}

test.c

	//尾插
	LTPushBack(phead, 1);
	LTPrint(phead);
	LTPushBack(phead, 2);
	LTPrint(phead);
	
	//测试销毁
	LTDestroy(&phead);

在这里插入图片描述
出销毁,phead也为空,不会出现野指针情况。

销毁2(推荐)
void LTDestroy(LTNode* ph)
{
	assert(ph);
	LTNode* pcur = ph->next;
	while (pcur != ph)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(ph);
	ph = NULL;
}

test.c

	LTDestroy(phead);
	phead = NULL;

在这里插入图片描述
phead需手动置空

推荐销毁2的原因是:销毁1传的是二级指针,而其他的双向链表函数都是传的一级指针,
违反了接口一致性,对于销毁2,虽然需要手动置空,但既然我们已经销毁链表了,大概
率是不会再用phead了,所以推荐销毁2。
请添加图片描述

相关文章:

  • 一周学会Flask3 Python Web开发-SQLAlchemy数据迁移migrate
  • 数据结构与算法:数组相关力扣题:27.移除元素、977.有序数组的平方、209.长度最小的子数组、59. 螺旋矩阵 II
  • msyql--基本操作之运维篇
  • Tasklet_等待队列_工作队列
  • 【LeetCode 题解】算法:15.三数之和
  • IP 地址查询网站
  • 基于 CLIP 的文本与视频编码及相关知识解析
  • ngx_http_core_location
  • Cookie、Session 与 Token:核心区别与应用场景解析
  • centos 7 部署FTP 服务用shell 搭建脚本,使用时稍微修改自己所需需求
  • 深克隆和浅克隆(建造者模式,内含简版)
  • 解码未来:DeepSeek开源FlashMLA,推理加速核心技术,引领AI变革
  • 低功耗可编程RTU在热网监控中的应用
  • 抽象工厂设计模式及应用案例
  • 如何在阿里云linux主机上部署Node.Js
  • ADB介绍
  • 《C语言数据类型取值范围:一场数字的“极限挑战”之旅》
  • CSS 中opacity属性和rgba颜色表示法中透明度的区别及应用场景
  • Kubernetes高级应用之-重启策略
  • 【数据库发展史】
  • 大港做网站/国外域名注册
  • 邢台做网站推广价格/南宁seo外包靠谱吗
  • 槐荫区城乡建设委员会网站/中国疫情最新消息
  • 公司网站建设 wordpress/百度推广代理怎么加盟
  • 网站开发投标文件/百度软文推广怎么做
  • 哪个餐饮店微网站做的有特色/网络营销实训个人总结