数据结构——链表(二)

🌈个人主页@ꪔ小林Y

个人专栏《C++小白闯关日记》《C语言小白闯关日记》《数据结构入门------从原理到实战》

🍀代码信条 :每一行代码都是成长的脚印👣,每一次调试成功都是对坚持的回应

数据结构------链表(二)

目录

一.顺序表与链表的对比

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

二.链表的分类

链表结构多样,以下所有情况合起来有8种

  • 带头或不带头

带头链表中的头节结点,不存储任何有效的数据,只用来占位子------"哨兵位"

  • 单向或双向
  • 循环或不循环

三.双向链表(带头双向循环链表)

同理,单链表也叫不带头单向不循环链表

1.双向链表的初始化

  • List.h
c 复制代码
#include<stdio.h>
#include<stdlib.h>
//定义双向链表
typedef int LTDataType ;
typedef struct ListNode LTNode;
struct ListNode {
	LTDataType data;
	struct ListNode* next;//指向下一个节点的指针
	struct ListNode* prev;//指向前一个节点的指针
};
//双向链表的初始化
void LTInit(LTNode** pphead);//形参的改变要影响实参
  • List.c
c 复制代码
#include"List.h"
#include<assert.h>
//初始化
void LTInit(LTNode** pphead)
{
	*pphead = (LTNode*)malloc(sizeof(LTNode));
	if (*pphead == NULL)
	{
		perror("malooc fail!");
		exit(1);
	}
	(*pphead)->data = -1;//哨兵位中不存储有效数据,随便给我一个数字基本用不到
	(*pphead)->next = (*pphead)->prev = *pphead;
}

2.双向链表的尾插和头插

尾插图示:

头插图示:

代码:

  • 头文件"List.h"
c 复制代码
#include<stdio.h>
#include<stdlib.h>
//定义双向链表
typedef int LTDataType ;
typedef struct ListNode LTNode;
struct ListNode {
	LTDataType data;
	struct ListNode* next;//指向下一个节点的指针
	struct ListNode* prev;//指向前一个节点的指针
};
//双向链表的初始化
void LTInit(LTNode** pphead);//形参的改变要影响实参
//尾插
//在双向链表中,增删改查都不会改变哨兵位的节点,所以这里用一级指针
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
  • 实现文件"List.c"
c 复制代码
#include"List.h"
#include<assert.h>
//申请空间创建新节点,初始化
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;
}
//初始化
void LTInit(LTNode** pphead)
{
	*pphead = LTBuyNode(-1);
}
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);//将这个值为x的节点传过去
	newnode->prev = phead->prev;//让新节点的prev指针指向原链表的尾节点
	newnode->next = phead;//新节点的next指针要指向哨兵位
	phead->prev->next = newnode;//原链表尾节点的下一个指针要指向新节点
	phead->prev = newnode;//最后让哨兵位的prev指针指向尾节点
}
//头插
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;
}
  • 测试文件"test.c"
c 复制代码
#include"List.h"
void test01()
{
//初始化
	LTNode* plist = NULL;
	LTInit(&plist);
	//尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	//头插
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);

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

运行结果

  • 尾插测试
  • 头插测试

3.双向链表的尾删

尾删图示:

代码:

  • 注意这里我们首先需要一个判空操作,如果链表为空,则不能继续删除:
c 复制代码
//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
  • 接下来进行尾删:
    List.c
c 复制代码
//尾删
void LTPopBack(LTNode* phead)
{
	assert(!LTEmpty(phead));
	LTNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;
	free(del);
	del = NULL;
}
  • 然后需要写一个打印操作来方便展现:
c 复制代码
//链表的打印
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;//创建一个指针,从头节点的下一个位置开始遍历
	while (pcur != phead)
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}
  • 代码写好了,现在来测试一下
    test.c
c 复制代码
#include"List.h"
void test01()
{
//初始化
	LTNode* plist = NULL;
	LTInit(&plist);
	//尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);
	//尾删
	LTPopBack(plist);
	LTPrint(plist);
}
int main()
{
	test01();
	return 0;
}

运行结果:

4.双向链表的头删

  • 图示:
  • 头删代码:
    List.c
c 复制代码
//链表的头删
void LTPopFront(LTNode* phead)
{
	//注意要先来一个判空操作
	assert(!LTEmpty(phead));
	LTNode* del = phead->next;//先创建一个指针,指向哨兵位的下一个节点
	del -> next -> prev=phead;
	phead->next = del->next;
	free(del);
	del = NULL;
}
  • 测试一下:
    test.c
c 复制代码
#include"List.h"
void test01()
{
	//初始化
	LTNode* plist = NULL;
	LTInit(&plist);
	//尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);
	//头删
	LTPopFront(plist);
	LTPrint(plist);
}
int main()
{
	test01();
	return 0;
}
  • 运行一下,我们可以看到头删成功,退出码为0

5.双向链表的查找操作

  • 这里我们可以直接进行查找
    List.c
c 复制代码
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;
}
  • 测试:
    test.c
c 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
void test01()
{
	//初始化
	LTNode* plist = NULL;
	LTInit(&plist);
	//尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);
	//测试查找
	LTNode* pos = LTFind(plist, 2);
		if (pos)
		{
			printf("找到了!\n");
	}
		else {
			printf("未找到\n");
		}

}
int main()
{
	test01();
	return 0;
}
  • 运行结果:

6.在双向链表pos位置后插入

  • 图示:

这里会有两种情况,但插入方式都是一样的:

情况一:pos在除了最后一个位置的其他位置

情况二:pos在最后一个位置:

  • 现在我们来写代码:
    List.c
c 复制代码
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	//首先来申请一个新节点
	LTNode* newnode = LTBuyNode(x);
	//现在来修改指针指向
	newnode->prev = pos;
	newnode->next = pos->next;
	pos->next->prev = newnode;
	pos->next = newnode;
}
  • 测试一下:
c 复制代码
#include"List.h"
void test01()
{
	//初始化
	LTNode* plist = NULL;
	LTInit(&plist);
	//尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);
	//测试查找
	LTNode* pos = LTFind(plist, 2);
		//在pos后插入
	LTInsert(pos, 100);
	LTPrint(plist);

}
int main()
{
	test01();
	return 0;
}
  • 运行结果:

7.删除双向链表中pos位置的节点

  • 图示:
  • 代码:
    List.c
c 复制代码
//删除pos位置的节点
void LTErase(LTNode* pos)
{
	assert(pos);
	pos->prev->next = pos->next;
	pos->next -> prev = pos->prev;
	free(pos);
	pos = NULL;
}
  • 测试:
    test.c
c 复制代码
#include"List.h"
void test01()
{
	//初始化
	LTNode* plist = NULL;
	LTInit(&plist);
	//尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);
	//测试删除pos位置的节点
	LTErase(pos);
	LTPrint(plist);
}
int main()
{
	test01();
	return 0;
}
  • 运行结果:

8.双向链表的销毁

注意在这里销毁的时候,哨兵位位置的节点也要销毁掉

我们先来创建一个指针pcur从哨兵位的后面一个节点开始遍历删除,直到走到哨兵位,最后销毁哨兵位的节点

  • 现在实现代码:
    List.c
c 复制代码
//双向链表的销毁
void LTDesTroy(LTNode** pphead)
{
	LTNode* pcur = (*pphead)->next;
	while (pcur != *pphead)
	{
		LTNode* next = pcur->next;//销毁当前节点前首先得把下一个节点存起来 
		free(pcur);
		pcur = next;
	}
	//销毁头节点
	free(*pphead);
	*pphead = NULL;
}
  • 测试一下:
c 复制代码
#include"List.h"
void test01()
{
	//初始化
	LTNode* plist = NULL;
	LTInit(&plist);
	//尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);
	//销毁
	LTDesTroy(&plist);
}
int main()
{
	test01();
	return 0;
}

9.双向链表的优化

在这些链表操作中,有的使用一级指针,有的使用二级指针,会带来一些不必要的麻烦,现在我们来优化一些初始化和销毁代码,使其统一使用一级指针,以保证接口的一致性 ,下面我们来看优化代码:
List.c

c 复制代码
// 初始化代码的优化
LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);//使用一个已有的方法创建头节点
	return phead;//返回一个指向头结点的指针
}
// 双向链表的销毁优化操作:
void LTDesTroy(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//销毁头节点
	free(phead);
	phead = NULL;
}

test.c

c 复制代码
//为了保持接口一致性
//初始化优化测试
LTNode* plist = LTInit();
//优化销毁测试
LTDesTroy(plist);
plist = NULL;

9.完整代码

  • 头文件"List.h"
c 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//定义双向链表
typedef int LTDataType ;
typedef struct ListNode LTNode;
struct ListNode {
	LTDataType data;
	struct ListNode* next;//指向下一个节点的指针
	struct ListNode* prev;//指向前一个节点的指针
};
//双向链表的初始化
/*void LTInit(LTNode** pphead);*///形参的改变要影响实参
//初始化优化:
LTNode* LTInit();
//双向链表的销毁
//void LTDesTroy(LTNode** pphead);//哨兵位位置的节点也要销毁,所以传二级指针
// 双向链表的销毁优化操作,为了保持接口的一致性:
void LTDesTroy(LTNode* phead);
//尾插
//在双向链表中,增删改查都不会改变哨兵位的节点,所以这里用一级指针
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//判断链表是否为空
bool LTEmpty(LTNode* phead);
//尾删
void LTPopBack(LTNode* phead);
//链表的打印
void LTPrint(LTNode* phead);
//链表的头删
void LTPopfront(LTNode* phead);
//链表的查找操作
LTNode* LTFind(LTNode* phead, LTDataType x);
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//删除pos位置的节点
void LTErase(LTNode* pos);
  • 实现文件"List.c"
c 复制代码
#include"List.h"
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;
}
//初始化
//void LTInit(LTNode** pphead)
//{
//	//*pphead = (LTNode*)malloc(sizeof(LTNode));
//	//if (*pphead == NULL)
//	//{
//	//	perror("malooc fail!");
//	//	exit(1);
//	//}
//	//(*pphead)->data = -1;//哨兵位中不存储有效数据,随便给我一个数字基本用不到
//	//(*pphead)->next = (*pphead)->prev = *pphead;
//	*pphead = LTBuyNode(-1);
//}
// 初始化代码的优化
LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);//使用一个已有的方法创建头节点
	return phead;//返回一个指向头结点的指针
}
//双向链表的销毁
//void LTDesTroy(LTNode** pphead)
//{
//	LTNode* pcur = (*pphead)->next;
//	while (pcur != *pphead)
//	{
//		LTNode* next = pcur->next;//销毁当前节点前首先得把下一个节点存起来 
//		free(pcur);
//		pcur = next;
//	}
//	//销毁头节点
//	free(*pphead);
//	*pphead = NULL;
//}
// 双向链表的销毁优化操作:
void LTDesTroy(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//销毁头节点
	free(phead);
	phead = NULL;
}
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);//将这个值为x的节点传过去
	newnode->prev = phead->prev;//让新节点的prev指针指向原链表的尾节点
	newnode->next = phead;//新节点的next指针要指向哨兵位
	phead->prev->next = newnode;//原链表尾节点的下一个指针要指向新节点
	phead->prev = newnode;//最后让哨兵位的prev指针指向尾节点
}
//头插
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;
}
//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
//链表的打印
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;//创建一个指针,从头节点的下一个位置开始遍历
	while (pcur != phead)
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

//尾删
void LTPopBack(LTNode* phead)
{
	assert(!LTEmpty(phead));
	LTNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;
	free(del);
	del = NULL;
}
//链表的头删
void LTPopFront(LTNode* phead)
{
	//判空
	assert(!LTEmpty(phead));
	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);
	//现在来修改指针指向
	newnode->prev = pos;
	newnode->next = pos->next;
	pos->next->prev = newnode;
	pos->next = newnode;
}
//删除pos位置的节点
void LTErase(LTNode* pos)
{
	assert(pos);
	pos->prev->next = pos->next;
	pos->next -> prev = pos->prev;
	free(pos);
	pos = NULL;
}
  • 测试文件"test.c"
c 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
void test01()
{
	//初始化
	/*LTNode* plist = NULL;
	LTInit(&plist);*/
	//初始化优化
	LTNode* plist = LTInit();
	//尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);
	//头插
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);*/
	//尾删
	LTPopBack(plist);
	LTPrint(plist);
	//头删
	LTPopFront(plist);
	LTPrint(plist);
	//测试查找
	LTNode* pos = LTFind(plist, 2);
		if (pos)
		{
			printf("找到了!\n");
	}
		else {
			printf("未找到\n");
		}
	//测试删除pos位置后的节点
	LTInsert(pos, 100);
	//测试删除pos位置的节点
	LTErase(pos);
	LTPrint(plist);
	//销毁
	/*LTDesTroy(&plist);*/
	//为了保持接口一致性,销毁优化测试
	LTDesTroy(plist);
	plist = NULL;
}
int main()
{
	test01();
	return 0;
}
相关推荐
勇闯逆流河4 小时前
【C++】用红黑树封装map与set
java·开发语言·数据结构·c++
一只鱼^_4 小时前
第 167 场双周赛 / 第 471 场周赛
数据结构·b树·算法·leetcode·深度优先·近邻算法·迭代加深
Seeing54 小时前
数据结构----树
数据结构
Word码6 小时前
[排序算法]希尔排序
c语言·数据结构·算法·排序算法
QT 小鲜肉6 小时前
【数据结构与算法基础】05. 栈详解(C++ 实战)
开发语言·数据结构·c++·笔记·学习·算法·学习方法
靠近彗星6 小时前
3.4特殊矩阵的压缩存储
数据结构·人工智能·算法
网安INF7 小时前
Python核心数据结构与函数编程
数据结构·windows·python·网络安全
.格子衫.8 小时前
018数据结构之队列——算法备赛
数据结构·算法
浩泽学编程8 小时前
【源码深度 第1篇】LinkedList:双向链表的设计与实现
java·数据结构·后端·链表·jdk