双向链表 —— 初始化、尾插、头插、bool类型判断、尾删、头删、查找数据、在pos位置之后插入结点、删除指定位置结点、销毁链表、打印函数

目录

一、双向链表

1、链表的分类

1.单向或者双向

2.带头或者不带头 

3.循环或者不循环 

2、概念与结构

3、实现双向链表

1.申请新的结点空间

2.初始化

3.尾插(哨兵位之前或最后一个有效结点之后)

4.头插(只能在哨兵位之后和第一个有效结点之前)

5.bool类型判断

6.尾删 (哨兵位之前的一个结点,即最后一个有效结点)

7.头删(只能在哨兵位之后的第一个有效结点)

8.查找数据

9.在pos位置之后插入结点

10.删除指定位置结点

11.销毁链表

12.打印函数

二、完整实现双链表的三个文件

List.h

List.c

test.c

三、顺序表与链表的分析 


一、双向链表

1、链表的分类

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

链表说明: 

1.单向或者双向

2.带头或者不带头 

注意:

        博主在单链表中(不带头)表述的“头结点”只是为了表示这是链表的第一个结点,并不是哨兵位结点。这种表述是不规范的,只是为了让大家好理解而已。

3.循环或者不循环 

重点:循环类型的尾结点的next指针不为空(NULL) 

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

第一,单链表:不带头单向不循环链表

        结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。

第二,双向链表:带头双向循环链表

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

2、概念与结构

注意:
第一, 这⾥的“带头”跟前⾯我们说的“头结点”是两个概念,实际前⾯的在单链表阶段称呼不严
谨,但是为了同学们更好的理解就直接称为单链表的头结点;
第二, 双向链表结构相较于单链表来说结构要复杂一些,但是接口的实现(功能实现函数)上要比单链表简单很多;
第三, 带头链表里的头结点,实际为“哨兵位”,哨兵位结点不存储任何有效元素,只是站在这⾥“放哨的”;
第四,双向链表的结点结构:数据+指向后一个结点的指针+指向前一个结点的指针。
        

3、实现双向链表

1.申请新的结点空间

//申请新的空间结点
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	newnode->data = x;
	//prev  next
	newnode->next = newnode->prev = newnode;

	return newnode;
}

思路分析:

        通过malloc函数申请一块大小为LTNode的结构体大小的结点空间给指针变量newnode,再判断newnode是否为空(NULL),若为空则申请失败和退出程序,若不为空则将数据存在结点的数据位置,再让newnode的next和newnode的prev都分别指向它自己本身,形成循环。

 

2.初始化

//test.c
LTNode* plist = NULL;
LTInit(&plist);

//List.h
//初始化
void LTInit(LTNode** pphead)
{
	//创建一个头结点(哨兵位)
	*pphead = LTBuyNode(-1);
}

思路分析:

        首先将plist指针变量初始化置为空(NULL),再使用通过LTBuynode函数申请新的结点空间作为头结点,即哨兵位,它不存储数据,所以传实参-1来表示无数据。

调试监视窗口显示初始化如下: 

 扩展:只传一级指针初始化的测试运行代码:

//只传一级指针的初始化
void LTInit(LTNode* phead)
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

 

3.尾插(哨兵位之前或最后一个有效结点之后)

//尾插(哨兵位之前或最后一个有效结点之后)
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* newnode = LTBuyNode(x);
	//phead  phead->prev  newnode
	newnode->next = phead;
	newnode->prev = phead->prev;

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

思路分析:

(1)首先通过assert断言判断phead头结点(哨兵位)的地址是否为空;

(2)再通过LTBuyNode函数来申请新的空间结点赋值给newnode指针变量;

(3)再改变指针的指向,即将newnode的next指针指向phead,newnode的prev指针指向phead的prev指针(即旧的尾结点);

(4)再让旧的尾结点的next指针指向新结点,最后让phead的prev指向新结点newnode。

注意:

双向链表为空的情况:只有一个哨兵位。

我们需要明白两个概念:第一,第一个结点:第一个有效的结点;第二,哨兵位:头结点 。

基本的尾插的方法跟上面的差不多,流程图如下: 

重点:新申请的结点插到头结点的前面相当于尾插,两者关系是等价的。

 测试用例运行结果:

测试时尾插空指针时,会出现:

4.头插(只能在哨兵位之后和第一个有效结点之前)

//头插(只能在哨兵位之后和第一个有效结点之前)
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* newnode = LTBuyNode(x);

	//phead  newnode  phead->next(d1)
	newnode->next = phead->next;
	newnode->prev = phead;

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

思路分析:

(1)首先assert断言判断phead头结点的地址是否为空(NULL);

(2)再通过LTBuyNode函数来申请新的空间结点赋值给newnode指针变量;

(3)改变指针方向,即将newnode的next指针指向phead的next(第一个有效结点处);

(4)再让newnode的prev指针指向phead头结点;

(5)然后将第一个有效结点的prev指针指向newnode新结点;

(6)最后将头结点的next指针指向newnode。

 测试用例运行结果:

5.bool类型判断

//bool类型判断
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

思路分析:

        bool类型判断的头文件是#include<stdbool.h>,断言判断phead是否为空,然后通过返回判断phead的next指针是否等于phead的值,即判断是否为空的链表(NULL)。

 

6.尾删 (哨兵位之前的一个结点,即最后一个有效结点)

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

	//phead  prev(del->prev)  del(phead->prev)
	LTNode* del = phead->prev;
	LTNode* prev = del->prev;

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

	free(del);
	del = NULL;
}

思路分析:

(1)首先assert断言判断phead头结点地址是否为空(NULL)和!LETmpty(phead)是否为真;

(2)定义尾结点(phead的prev指针指向的结点)为del,定义del的prev指针指向的结点为prev;

(3)改变结点指针的指向,即使prev的next指针指向phead头结点;使phead的prev指针指向prev,使prev成为新的尾结点;

(4)最后再释放del结点,置为空(NULL)。

 

测试用例运行结果:

7.头删(只能在哨兵位之后的第一个有效结点)

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

	//phead  del(phead->next)  del->next  
	LTNode* del = phead->next;
	del->next->prev = phead;
	phead->next = del->next;

	free(del);
	del = NULL;
}

思路分析:

(1)首先assert断言判断phead头结点地址是否为空(NULL)和!LETmpty(phead)是否为真;

(2)定义第一个有效数据结点(phead的next指针指向的结点)为del;

(3)改变指针的指向,使del的下一个结点(del的next指针指向的结点)的prev指针指向phead;再使phead的next指针指向del的下一个结点;最后再释放del结点并置为空(NULL)。

 测试用例运行结果:

测试时头删到空指针时,会出现:

8.查找数据

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

思路分析:

(1)首先assert断言判断phead结点地址是否为空(NULL);

(2)定义第一个有效数据的结点为pcur,进入while循环判断条件,判断pcur是否不等于phead,若不等于phead,则进入循环判断pcur的data是否为所想查找的数据,若是则返回pcur,若不是则让pcur往后移一个结点;

(3)最后若不等于phead,则跳出循环返回NULL,即查找不到所想查找的数据。

测试代码运行结果:

9.在pos位置之后插入结点

//在pos位置之后插⼊结点
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* newnode = LTBuyNode(x);

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

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

思路分析:

(1) 首先assert断言报错指定位置pos结点的地址是否为空(NULL);

(2)通过LTBuyNode函数申请新的空间结点给定义的结构体指针变量newnode;

(3)改变指针的指向,即将newnode结点的next指针指向pos的下一个位置(即pos的next指针指向结点位置);将newnode的prev指针指向pos结点;

(4)最后将pos的下一个结点(即pos的next指针指向结点位置)的prev指针指向newnode结点处;将pos的next指针指向newndoe。

测试代码运行结果:

10.删除指定位置结点

//删除指定位置结点
void LTErase(LTNode* pos)
{
	assert(pos);
	//pos->prev  pos  pos->next

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

	free(pos);
	pos = NULL;
}

思路分析:

(1) 首先assert断言判断pos结点处的地址是否为空;

(2)改变指针的指向,使pos的前驱结点(pos的prev指针指向的位置)的next指针指向pos的下一个结点位置处(pos的next指针指向的结点位置);

(3)使pos的下一个结点位置(pos的next指针指向的结点位置)的prev指针指向pos的前一个结点(pos的prev指针指向的位置);

(4) 最后再释放pos结点,并且置为空(NULL)。

 

 测试代码运行结果:

11.销毁链表

//销毁
void LTDesTroy(LTNode** pphead)
{
	assert(pphead && *pphead);
	LTNode* pcur = (*pphead)->next;
	while (pcur != *pphead)
	{
		LTNode* Next = pcur->next;
		free(pcur);
		pcur = Next;
	}
	//销毁哨兵位结点
	free(*pphead);
	*pphead = NULL;
	pcur = NULL;
}

思路分析:

(1) 首先assert断言判断pphead(头结点地址的地址)和*pphead(头结点的地址)是否为空(NULL);

(2)使头结点的下一个结点(即(*pphead)->next)定义为pcur结构体指针变量;

(3)进入循环判断条件判断pcur是否不等于*pphead(头结点的地址),若是不等于*pphead则进入循环,将pcur的next指针指向的结点定义为Next结构体指针变量,再释放pcur,让pcur指针变量往后走一个结点位置;

(4)若条件不符合,则跳出循环,直接销毁哨兵位结点,释放头结点并置为空(NULL),并也将pcur指针变量置为空(NULL)。

 

测试代码运行结果:

扩展:只传一级指针的销毁测试运行代码:

//只传一级指针的销毁
//传一级指针,需要手动将plist置为NULL
void LTDesTroy2(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* Next = pcur->next;
		free(pcur);
		pcur = Next;
	}
	//销毁哨兵位结点
	free(phead);
	phead = pcur = NULL;
}

测试代码运行结果如下,可以成功销毁链表,但是需要手动将plist置为NULL: 

12.打印函数

//打印函数
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

思路分析:

(1) 定义pcur结构体指针变量为头结点的下一个结点(即phead的next指针指向的结点位置);

(2)然后进入循环判断条件判断pcur是否不为头结点,若是则进入循环直接打印pcur的data数据,再将pcur往后走一个结点单位;

(3)直到pcur为头结点,跳出循环。

二、完整实现双链表的三个文件

List.h

#pragma once

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

//定义双向链表结点的结构
typedef int LTDataType;
typedef struct ListNode
{
    LTDataType data;
    struct ListNode* next; //指针保存下⼀个结点的地址
    struct ListNode* prev; //指针保存前⼀个结点的地址
}LTNode;

//为了保持接口的一致性,可以把接口优化都为一级指针,如下:
//初始化
void LTInit(LTNode** pphead);//二级
//只传一级指针的初始化
/*void LTInit(LTNode* phead);*///一级
//销毁
void LTDesTroy(LTNode** pphead);//二级
//只传一级指针的销毁
/*void LTDesTroy2(LTNode* phead);*///一级
//传一级指针,需要手动将plist置为NULL

//打印函数
void LTPrint(LTNode* phead);

//插入:
//第一个参传一级还是二级,要看pphead指向的结点会不会发生改变
//如果发生改变,那么pphead的改变要影响实参,传二级
//如果不发生改变,pphead不会影响实参,传一级
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);

//删除:
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);

//bool类型判断
bool LTEmpty(LTNode* phead);

//在pos位置之后插入结点
void LTInsert(LTNode* pos, LTDataType x);

//删除指定位置结点
void LTErase(LTNode* pos);

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

List.c

#define  _CRT_SECURE_NO_WARNINGS 1
#include<List.h>

//申请新的空间结点
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	newnode->data = x;
	//prev  next
	newnode->next = newnode->prev = newnode;

	return newnode;
}

//初始化
void LTInit(LTNode** pphead)
{
	//创建一个头结点(哨兵位)
	*pphead = LTBuyNode(-1);
}
//只传一级指针的初始化
//void LTInit(LTNode* phead)
//{
//	LTNode* phead = LTBuyNode(-1);
//	return phead;
//}

//销毁
void LTDesTroy(LTNode** pphead)
{
	assert(pphead && *pphead);
	LTNode* pcur = (*pphead)->next;
	while (pcur != *pphead)
	{
		LTNode* Next = pcur->next;
		free(pcur);
		pcur = Next;
	}
	//销毁哨兵位结点
	free(*pphead);
	*pphead = NULL;
	pcur = NULL;
}
//只传一级指针的销毁
//传一级指针,需要手动将plist置为NULL
//void LTDesTroy2(LTNode* phead)
//{
//	assert(phead);
//	LTNode* pcur = phead->next;
//	while (pcur != phead)
//	{
//		LTNode* Next = pcur->next;
//		free(pcur);
//		pcur = Next;
//	}
//	//销毁哨兵位结点
//	free(phead);
//	phead = pcur = NULL;
//}

//打印函数
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

//bool类型判断
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

//尾插(哨兵位之前或最后一个有效结点之后)
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* newnode = LTBuyNode(x);
	//phead  phead->prev  newnode
	newnode->next = phead;
	newnode->prev = phead->prev;

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

//头插(只能在哨兵位之后和第一个有效结点之前)
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* newnode = LTBuyNode(x);

	//phead  newnode  phead->next(d1)
	newnode->next = phead->next;
	newnode->prev = phead;

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

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

	//phead  prev(del->prev)  del(phead->prev)
	LTNode* del = phead->prev;
	LTNode* prev = del->prev;

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

	free(del);
	del = NULL;
}

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

	//phead  del(phead->next)  del->next  
	LTNode* del = phead->next;
	del->next->prev = phead;
	phead->next = del->next;

	free(del);
	del = NULL;
}

//查找数据
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	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);

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

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

//删除指定位置结点
void LTErase(LTNode* pos)
{
	assert(pos);
	//pos->prev  pos  pos->next

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

	free(pos);
	pos = NULL;
}

test.c

#define  _CRT_SECURE_NO_WARNINGS 1
#include<List.h>

void ListTest01()
{
	//创建双向链表变量
	LTNode* plist = NULL;
	LTInit(&plist);
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	/*LTPushBack(NULL,1);*/
	/*LTPrint(plist);*/

	/*LTPushFront(plist, 1);
	LTPrint(plist);
	LTPushFront(plist, 2);
	LTPrint(plist);
	LTPushFront(plist, 3);
	LTPrint(plist);
	LTPushFront(plist, 4);
	LTPrint(plist);*/

	尾删
	//LTPopBack(plist);
	//LTPrint(plist);

	头删
	//LTPopFront(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	//LTPrint(plist);

	//LTNode* pos = LTFind(plist, 1);
	//if (pos == NULL)
	//{
	//	printf("没有找到!\n");
	//}
	//else
	//{
	//	printf("找到了!\n");
	//}
	/*LTInsert(pos, 11);*/
	//LTErase(pos);
	//LTPrint(plist);
	LTDesTroy(&plist);
}

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

三、顺序表与链表的分析 

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

总的来说,顺序表和链表没有哪一个绝对的更好,存在即合理。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值