目录

顺序表和链表

目录

线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。

线性表是一种在实际中广泛使用的数据结构。

常见的线性表:顺序表、链表、栈、队列、字符串...

线性表在逻辑上是线性结构,也就是逻辑上的连续的一条直线。

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

顺序表

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

概念与结构

概念:顺序表是用一段物理地址连续 的存储单元依次存储数据元素的线性结构,

一般情况下采用数组 存储。

顺序表和数组的区别?

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

分类

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

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

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

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

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

动态顺序表的实现

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

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

SeqList.h
c 复制代码
//动态顺序表的实现

#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
c 复制代码
//初始化
void SLInit(SL* ps)
{
	assert(ps);//防止ps为NULL
	ps->data = NULL;//初始化
	ps->size = ps->capacity = 0;//初始化
}

test.c

c 复制代码
	SL sl;//创建一个空的顺序表
	SLInit(&sl);//初始化
	//只有传地址才能改变sl的值
增容SLCheckCapacity

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

c 复制代码
//检查是否需增容
//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
c 复制代码
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
	assert(ps);//防止ps为NULL
	//检查是否需要增容
	SLCheckCapacity(ps);
	//尾插
	ps->data[ps->size++] = x;
	//在尾部插入元素,更新有效数据个数
}
打印SLPrint
c 复制代码
//打印
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

c 复制代码
	//测试尾插
	SLPushBack(&sl, 1);
	SLPrint(&sl);
	SLPushBack(&sl, 2);
	SLPrint(&sl);
	SLPushBack(&sl, 3);
	SLPrint(&sl);
	SLPushBack(&sl, 4);
	SLPrint(&sl);
头插SLPushFront
c 复制代码
//头插
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

c 复制代码
	//测试头插
	SLPushFront(&sl, 1);
	SLPrint(&sl);
	SLPushFront(&sl, 2);
	SLPrint(&sl);
	SLPushFront(&sl, 3);
	SLPrint(&sl);
	SLPushFront(&sl, 4);
	SLPrint(&sl);
尾删SLPopBack
c 复制代码
//尾删
void SLPopBack(SL* ps)
{
	assert(ps && ps->size);//防止ps为NULL且顺序表中没有数据
	ps->size--;//更新有效数据个数,达到遍历时访问不到尾部数据的目的
}

test.c

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
c 复制代码
//头删
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

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
c 复制代码
//查找指定数据 - 找到返回下标,找不到返回-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

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
c 复制代码
//在指定位置之前插入数据
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

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
c 复制代码
//删除指定位置的数据
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

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
c 复制代码
//销毁顺序表
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

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. 从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能联系,可能不连续

结合前面学到的结构体知识,我们可以给出每个节点对应的结构体代码:

假设当前保存的节点的数据类型为整型:

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

当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要

保存整型数据,也要保存下一个节点的地址(当下一个节点为空时保存的地址为空)。

每一个节点都能找到下一个节点,所有的节点连起来,就成了一条线,

在这条线上我们能找到每一个节点。

单链表的实现

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

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

SLIst.h
c 复制代码
//单链表的动态实现

#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
c 复制代码
//打印
void print(Node** pph)
{
	assert(pph);//防止pph为空
	Node* cur = *pph;//记录第一个节点的地址
	while (cur)//遍历单链表
	{
		printf("%d->", cur->data);//打印每个节点的数据
		cur = cur->next;//找下一个节点
	}
	printf("NULL\n");//最后一个节点的next为NULL
}
申请新的节点
c 复制代码
//申请新的节点
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
c 复制代码
//尾插
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

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
c 复制代码
//头插
void push_front(Node** pph, SListDataType x)
{
	assert(pph);//防止pph为NULL
	Node* newnode = buyNode(x);//申请新节点
	newnode->next = *pph;//新节点的下一个节点指向第一个节点
	*pph = newnode;//*pph指向新节点,完成头插
}

test.c

c 复制代码
	//测试头插
	push_front(&phead, 5);
	print(&phead);
	push_front(&phead, 6);
	print(&phead);
	push_front(&phead, 7);
	print(&phead);//7 6 5
尾删pop_back
c 复制代码
//尾删
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

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

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);

	//测试头删
	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
c 复制代码
//查找指定节点的数据 - 找到返回节点,找不到返回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

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
c 复制代码
//在指定节点之前插入数据
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
c 复制代码
//在指定节点之后插入数据
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

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
c 复制代码
//删除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

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
c 复制代码
//删除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

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
c 复制代码
//销毁链表
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

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
c 复制代码
#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
申请新节点
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
c 复制代码
//初始化
void LTInit(LTNode** pph)
{
	assert(pph);//防止pph为NULL
	*pph = LTBuyNode(-1);//创建头节点,元素无效,赋值为-1
}

test.c

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

test.c

c 复制代码
	LTNode* phead = LTInit();//接收返回的头节点
打印
c 复制代码
//打印
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");
}
尾插
c 复制代码
//尾插
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

c 复制代码
	LTNode* phead = LTInit();

	//测试尾插
	LTPushBack(phead, 1);
	LTPrint(phead);
	LTPushBack(phead, 2);
	LTPrint(phead);
头插
c 复制代码
//头插
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

c 复制代码
	//测试头插
	LTPushFront(phead, 3);
	LTPrint(phead);
	LTPushFront(phead, 4);
	LTPrint(phead);
尾删
c 复制代码
//尾删
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

c 复制代码
	//尾插
	LTPushBack(phead, 1);
	LTPrint(phead);
	LTPushBack(phead, 2);
	LTPrint(phead);
	
	//测试尾删
	LTPopBack(phead);
	LTPrint(phead);
	LTPopBack(phead);
	LTPrint(phead);
	LTPopBack(phead);
	LTPrint(phead);

第三次删除时链表中没有有效节点,所以断言报错。

头删
c 复制代码
//头删
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

c 复制代码
	//尾插
	LTPushBack(phead, 1);
	LTPrint(phead);
	LTPushBack(phead, 2);
	LTPrint(phead);
	
	//测试头删
	LTPopFront(phead);
	LTPrint(phead);
	LTPopFront(phead);
	LTPrint(phead);
	LTPopFront(phead);
	LTPrint(phead);

第三次删除时链表中没有有效节点,所以断言报错。

查找
c 复制代码
//查找
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

c 复制代码
	//尾插
	LTPushBack(phead, 1);
	LTPrint(phead);
	LTPushBack(phead, 2);
	LTPrint(phead);
	
		//测试查找
	LTNode* find = LTFind(phead, 2);
	if (find)
		printf("找到了\n");
	else
		printf("没找到\n");
任意位置之前插
c 复制代码
//在任意位置之前插入节点
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

c 复制代码
	//尾插
	LTPushBack(phead, 1);
	LTPrint(phead);
	LTPushBack(phead, 2);
	LTPrint(phead);

	//测试任意之前插
	LTNode* find = LTFind(phead, 2);
	LTInsert(find, 5);
	LTPrint(phead);
删除任意节点
c 复制代码
//删除任意节点
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

c 复制代码
	//尾插
	LTPushBack(phead, 1);
	LTPrint(phead);
	LTPushBack(phead, 2);
	LTPrint(phead);

	//测试删任意节点
	LTNode* find = LTFind(phead, 2);
	LTErase(find);
	find = NULL;//删完之后find为野指针,需置空
	LTPrint(phead);
销毁1
c 复制代码
//销毁
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

c 复制代码
	//尾插
	LTPushBack(phead, 1);
	LTPrint(phead);
	LTPushBack(phead, 2);
	LTPrint(phead);
	
	//测试销毁
	LTDestroy(&phead);

出销毁,phead也为空,不会出现野指针情况。

销毁2(推荐)
c 复制代码
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

c 复制代码
	LTDestroy(phead);
	phead = NULL;

phead需手动置空

推荐销毁2的原因是:销毁1传的是二级指针,而其他的双向链表函数都是传的一级指针,

违反了接口一致性,对于销毁2,虽然需要手动置空,但既然我们已经销毁链表了,大概

率是不会再用phead了,所以推荐销毁2。

本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
三体世界1 小时前
C++ List的模拟实现
java·c语言·开发语言·数据结构·c++·windows·list
小百小摆2 小时前
Acwing6118 蛋糕游戏
数据结构·c++·算法
Tryagein4 小时前
【数据结构】并查集
数据结构·算法
Joe_Wang54 小时前
[数据结构]并查集(系统整理版)
数据结构·c++·算法·leetcode·并查集
Tryagein6 小时前
【数据结构】树与森林
数据结构
小陈的进阶之路8 小时前
数据结构(并查集,图)
数据结构·c++·算法
dokii18 小时前
leetcode199 二叉树的右视图
数据结构·算法·leetcode
UP_Continue9 小时前
排序--归并排序--非递归
数据结构·算法·排序算法
ahahahahaha233310 小时前
相似度计算 ccf-csp 2024-2-2
数据结构·c++·算法
2402_8813193010 小时前
3.28学习总结
数据结构·学习·算法