双向链表详解

双向链表

链表的分类

这个概念是由哪个问题衍生出来的?

链表的结构⾮常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:
在这里插入图片描述

1.单向或者双向

在这里插入图片描述

2.带头或者不带头

在这里插入图片描述

这个概念和我们之前学的概念有什么关系?

在单链表中我们会去说头节点,并不是意思是说,单链表是带头链表,而是指的是单链表的第一个节点,方便理解而已

3.循环或者不循环

在这里插入图片描述

这个概念和我们之前学到的概念有什么关系?

环型链表

虽然有这么多的链表的结构,但是我们实际中最常⽤还是两种结构:单链表双向带头循环链表

注意点:单链表的全称:单向不带头不循环链表

那它之后又可以应用到哪些方面呢?

  1. ⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦ 结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。
  2. 带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都是带头 双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带来很多优势,实 现反⽽简单了,后⾯我们代码实现了就知道了。

带头双向循环链表的实现

概念与结构

在这里插入图片描述

注意:这⾥的“带头”跟前⾯我们说的“头结点”是两个概念,实际前⾯的在单链表阶段称呼不严 谨,但是为了同学们更好的理解就直接称为单链表的头结点。

带头链表⾥的头结点,实际为“哨兵位”,哨兵位结点不存储任何有效元素,只是站在这⾥“放哨 的”

双向链表中节点的结构

双向链表的节点结构:数据+指向下一节点的指针+指向前一个节点的指针

代码如下:

struct ListNode
{
    int data;
    struct ListNode*next;
    struct ListNode*
}

这个概念和之前学的单链表的节点有什么区别?

单链表是单向的所以只有指向下一节点的指针,而双向链表是双向的所以还要包括指向前一节点的指针

扩容

双向链表和单链表的区别在于:双向链表有哨兵位,也就是带头,我们在扩容的时候需要传一个无效的数据去占位

在这里插入图片描述

代码如下:

LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	newnode->data = x;
	//prev next
	newnode->prev = newnode->next = newnode;
}

//创建一个头节点(哨兵位)
	*pphead = LTBuyNode(-1);

尾插

宏观上是在干什么?

在这里插入图片描述

看上图我们可以知道要让头节点和最后新加上的节点建立联系,和单链表相比不需要遍历整个链表

代码如下:

//尾插
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;
}

头插

这个概念是由哪个问题衍生出来的?

这里头插和单链表的不同之处在于头插在phead前面的元素不算头插,算作是尾插。因为双向链表是双向的,所以是尾插,但是插在phead的后面性质就不一样了,就意味着是头插

在这里插入图片描述

代码如下:

//头插
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->prev = newnode;
}

尾删

还需要去判断双向链表是否只有哨兵位

代码如下:

bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	//prev 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;
}

在pos节点位置之后插入数据

代码如下:

//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);
	//pos pos->next newnode
	newnode->next = pos->next;
	newnode->prev = pos;
	pos->next->prev = newnode;
	pos->next = newnode;
}

删除指定位置的节点

代码如下:

//删除指定位置数据
void LTErase(LTNode* pos)
{
	assert(pos);
	pos->prev ->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

初始化和销毁

为什么把这两个放一起谈呢,因为初始化和销毁双链表要用到二级指针,传址调用才能改变实参,要把哨兵位初始化或者销毁,所以要用到二级指针,但是为了实现接口都是一级指针我们需要做出一些调整

代码如下:

//初始化
LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}


//销毁
void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* Next = pcur->next;
		free(pcur);
		pcur = Next;
	}
	free(phead);
	phead=pcur = NULL;
}

这两个方法和之前二级指针的方法相比就像是原来的是拿着一个空瓶子去装饮料,另一个是已经封装好的饮料

整体代码如下:

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);
LTNode* LTInit2();
//插入
//第一个参数传一级指针还是二级指针,要看pphead指向的节点会不会改变
//如果发生改变,那么pphead的改变要影响实参,传二级指针
//如果不发生改变,pphead不会影响实参,传一级指针
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//打印
void LTPrint(LTNode* phead);
//删除
void LTPopBack(LTNode* phead);
void LTPopFront(LTNode* phead);
bool LTEmpty(LTNode* phead);
LTNode* LTFind(LTNode* phead, LTDataType x);
//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x);
//删除指定位置数据
void LTErase(LTNode* pos);
//销毁
void LTDestroy(LTNode** pphead);
void LTDestroy2(LTNode*phead);//传一级,需要手动把plist置空

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->prev = newnode->next = newnode;
}
//初始化
void LTInit(LTNode** pphead)
{
	//创建一个头节点(哨兵位)
	*pphead = LTBuyNode(-1);
}
//尾插
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->prev = newnode;
}
//打印
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	//prev 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 pos->next newnode
	newnode->next = pos->next;
	newnode->prev = pos;
	pos->next->prev = newnode;
	pos->next = newnode;
}
//删除指定位置数据
void LTErase(LTNode* pos)
{
	assert(pos);
	pos->prev ->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}
//销毁
//销毁
void LTDestroy(LTNode** pphead)
{
	assert(pphead && *pphead);
	LTNode* pcur = (*pphead)->next;
	while (pcur != pphead)
	{
		LTNode* Next = pcur->next;
		free(pcur);
		pcur = Next;
	}
	pcur = NULL;
	//销毁哨兵位节点
	free(*pphead);
	*pphead = 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;
}
LTNode* LTInit2()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

Test.c

#include"List.h"

void ListTest01()
{
	//创建双向链表变量
	//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);
	//LTPrint(plist);
	//LTPushFront(plist, 2);
	//LTPrint(plist);
	//LTPushFront(plist, 3);
	//LTPrint(plist);
	//LTPushFront(plist, 4);
	//LTPrint(plist);
	//
	//LTPopBack(plist);
	//LTPrint(plist);
	//LTPopBack(plist);
	//LTPrint(plist);
	//LTPopBack(plist);
	//LTPrint(plist);
	//LTPopBack(plist);
	//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, 3);
	//if (pos == NULL)
	//{
	//	printf("没有找到!\n");
	//}
	//else
	//{
	//	printf("找到了!\n");
	//}
	//LTInsert(pos, 11);
	//LTErase(pos);
	//LTPrint(plist);
	
	//LTDesTroy(&plist);
	LTDesTroy2(plist);
	plist = NULL;
}
int main()
{
	ListTest01();
	return 0;
}

顺序表和链表的分析

在这里插入图片描述

  • 42
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值