🌈个人主页 :@ꪔ小林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;
}