数据结构-双链表

发布于:2025-08-04 ⋅ 阅读:(9) ⋅ 点赞:(0)

学习完单链表,现在继续学习双链表

一、双链表结构

带头双向循环链表(简称:双链表)

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

带头链表里的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这⾥“放哨 的” “哨兵位”存在的意义: 遍历循环链表避免死循环

二、双链表的实现

定义双链表节点结构

与单链表相比,双链表由于是双向的,因此它多了一个前驱指针

typedef int LTDataType;

typedef struct ListNode
{
    LTDataType data;
    struct ListNode* prev;//保存指向前一个节点的指针
    struct ListNode* next;//指向下一个节点的指针
}LTNode;

说明

双链表是带头链表,即它的头节点(哨兵节点)是不可变的,它作用是站岗放哨,为后续的有效节点标明位置,因此,与单链表不同的是,双链表在函数传参的过程中传的是一级指针,不再是二级指针(保证哨兵节点不可改变)

在接下来的双链表实现中,为保持接口一致性,全部传一级指针

双链表初始化 InitList

单链表为空的意思就是空链表,链表内无任何节点,而双链表为空时,此时链表中只剩下一个头节点(哨兵节点),无有效节点,代表双链表为空的意思

因此,在插入数据之前,双链表必须初始化到只有一个头结点的情况

创建一个函数LTBuyNode,表示申请节点,在初始化时给链表申请一个哨兵节点

思考一下,是否可以将哨兵节点的prev指针和next指针初始化为NULL?显然是不可以的

双链表是循环链表,它的首尾相连的,因此,链表的prev指针和next指针应该在初始化时指向哨兵节点本身

//申请节点
LTNode* LTBuyNode(LTDataType x)
{
    LTNode* node = (LTNode*)malloc(sizeof(LTNode));
    if(node == NULL)
    {
        perror("LTBuyNode-malloc");
        exit(1);
    }
    node->data = x;
    node->next = node->prev = node;
    return node;
}
//初始化 - 传一级指针的写法
LTNode* InitList()
{
    LTNode* phead = LTBuyNode(-1);//也可以传其他数字,表示初始化哨兵节点为某一个值
    return phead;
}

//初始化 - 传二级指针的写法
void InitList(LTNode** pphead)
{
    //给双链表创建一个哨兵节点
    *pphead = LTBuyNode(-1);
}

测试:

//初始化
LTNode* plist = InitList();

//LTNode* plist = NULL;
//InitList(&plist);

打印链表 LTPrint

打印链表的步骤基本上和单链表相同,但是要注意,打印双链表的节点时,哨兵节点并不需要打印出来,是从链表的第一个有效节点开始打印(即哨兵节点的后一个节点);

在while循环条件部分不同:双链表头尾相连,如果按照单链表的循环条件pcur进行打印,会陷入无限循环,因此,我们要在尾节点的next指针为头节点时,停止打印

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

尾部插入/头部插入

尾插 LTPushBack

我们知道,链表的尾节点的next指针就是头节点phead(尾节点->next=phead),头节点phead的prev指针就是尾节点(即phead->prev=尾节点)【首尾相连】,我们要做的就是将创建的新节点newnode尾插到原链表的尾部作为新的尾节点并与头节点首尾相连。

首先要做的是将newnode尾插到原链表中,将newnode的prev指针指向phead->prev(原尾节点),然后将newnode的next指针指向头节点phead,这样就将newnode节点插入到了原链表中

接着更新新的尾节点与头节点头尾相连,将原链表的尾节点phead->prev的next指针指向newnode,然后newnode成为新的尾节点 phead->prev

void LTPushBack(LTNode* phead,LTDataType x)
{
    assert(phead);
    LTNode* newnode = LTBuyNode(x);

    //1.先在尾部插入新节点
    newnode->prev = phead->prev;
    newnode->next = phead;
    //2.更新新的尾节点与头节点进行首尾相连
    phead->prev->next = newnode;
    phead->prev = newnode;
}

头插 LTPushFront

要注意,我们所说的头插是头插到第一个有效节点的前面,即头节点(哨兵节点)的后面

(将新节点插入到哨兵节点前面,就是尾插)

首先还是将新节点newnode插入到原链表中,将newnode插入到头节点phead和第一个有效节点phead->next之间,然后更新newnode为新的第一个有效节点:

将phead->next(原第一个有效节点)的prev指针指向newnode,再将newnode的prev指针指向phead,或者先将newnode的prev指针指向phead,再通过newnode找到原第一个有效节点(newnode->next),将newnode->next的prev指针指向newnode

void LTPushFront(LTNode* phead,LTDataType x)
{
    assert(phead);
    LTNode* newnode = LTBuyNode(x);
    
    //1.先在头节点和第一个有效节点之间插入一个新节点,表示头插
    newnode->next = phead->next;
    newnode->prev = phead;
    //2.更新新的节点为新的第一个有效节点
    phead->next->prev = newnode;
    phead->next = newnode;
    //phead->next = newnode;
    //newnode->next->prev = newnode;
}

尾部删除/头部删除

尾删 LTPopBack

能够进行尾删的条件:链表有效且链表不能为空(只有一个哨兵节点)

要将尾节点(phead->prev)删除,将它的前一个节点phead->prev->prev变成新的尾节点:将phead->prev->prev的next指针指向phead,将phead的prev指针指向phead->prev->prev,然后将原尾节点释放掉

但是,要注意,此时已经找不到原尾节点的位置了,无法进行释放操作,因此,在进行以上操作前,先将原尾节点phead->prev存放在一个指针变量del中

void LTPopBack(LTNode* phead)
{
    //链表必须有效且链表不能为空(只有一个哨兵位)
    assert(phead && phead->next != phead);
    
    LTNode* del = phead->prev;
    del->prev->next = phead;
    phead->prev = del->prev;

    //删除del节点
    free(del);
    del = NULL;
}

头删 LTPopFront

能够进行头删的条件:链表有效且链表不能为空

头删是删除链表的第一个有效节点,使第二个有效节点成为新的第一个有效节点

首先先将要删除的phead->next节点存放在指针del中,然后将phead的next指针改为del->next(第二个有效节点),再将del->next的prev指针指向phead,然后将del释放掉

void LTPopFront(LTNode* phead)
{
    assert(phead && phead->next != phead);
    LTNode* del = phead->next;
    phead->next = del->next;
    del->next->prev = phead;
    
    free(del);
    del = NULL;
}

查找链表 LTFind

通过遍历链表找到想要找的节点数据,如果找到了,返回这个节点的指针,如果没有找到,返回NULL

LTNode* LTFind(LTNode* phead,LTDataType x)
{
    LTNode* pcur = phead->next;
    while(pcur != phead)
    {
        if(pcur->data == x)
        {
            return pcur;
        }
        pcur = pcur->next;
    }
    //没有找到
    return NULL;
}

在pos位置之后插入数据 LTInsert

需要将新节点newnode插入在pos和pos->next之间,即先将newnode的prev指针指向pos,newnode的next指针指向pos->next,然后将newnode节点变成新的pos->next节点

注意有一种情况:当pos的位置是尾节点时,就是要在phead->prev和phead之间插入数据,即尾插

但是我们在对该函数传参时,并不需要用到phead(在pos位置之后插入节点只需知道pos和pos->next,以及要插入的数据即可),因此,不能直接调用尾插函数。不过我们并不需要调用尾插函数也可以做到,因为将newnode插入pos和pos->next之间的做法也适用于尾插的情况

这里不再画图讲解

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位置之前插入数据 LTInsertBefort

需要将newnode节点插入在pos->prev与pos之间,先将newnode的prev指针指向pos->prev,再将newnode的next指针指向pos,表示插入新节点成功,当然,这也有一种情况:当pos的围位置是第一个有效节点时,就是头插的方法,不过我们也不需要调用函数使用,因为以上的操作已经覆盖了头插做法

void LTInsertBefort(LTNode* pos,LTDataType x)
{
    assert(pos);
    LTNode* newnode = LTBuyNode(x);

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

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

删除pos节点

删除pos节点,直接修改pos->next的prev指针为pos->prev,pos->prev的next指针为pos->next,然后将pos节点释放掉即可

void LTErase(LTNode* pos)
{
    assert(pos);
    pos->next->prev = pos->prev;
    pos->prev->next = pos->next;

    free(pos);
    pos = NULL;
}

销毁链表 LTDestroy

在遍历销毁链表之前,应该保存一下下一个节点的指针,避免后续找不到位置

void LTDestroy(LTNode* phead)
{
    assert(phead);
    LTNode* pcur = phead->next;
    while(pcur != phead)
    {
        LTNode* next = pcur->next;
        free(pcur);
        pcur = next;
    }
    //此时pcur指向phead,而phead还没有被销毁
    free(phead);
    phead = NULL;
}
注意

LTErase 和 LTDestroy 函数参数理论上要传二级指针,因为我们需要让形参的改变影响到实参,但为了保持接口一致性传一级指针,传一级指针存在的问题:当形参phead置为NULL后,实参plist不会被修改为NULL,因此解决办法:调用完方法后手动将实参置为NULL

//测试删除pos节点
LTErase(find);
find = NULL;
LTPrint(plist);

//销毁链表
LTDestroy(plist);
plist = NULL;

三、完整代码

List.h

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

typedef int LTDataType;
//定义双向链表节点的结构
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

//声明双向链表中提供的方法
//初始化
//void InitList(LTNode** pphead); 
LTNode* InitList();

//打印链表
void LTPrint(LTNode* phead);

//在插入数据之前,链表必须初始化到只有一个头结点的情况
//不改变哨兵位的地址,因此传一级指针即可
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);

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

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

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

//在pos位置之前插入数据
void LTInsertFront(LTNode* pos, LTDataType x);

//删除pos节点
void LTErase(LTNode* pos);

//销毁链表
void LTDestroy(LTNode* phead);
List.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"

//申请节点
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc");
		exit(1);
	}
	node->data = x;
	node->next = node->prev = node;
	return node;
}
//初始化
//void InitList(LTNode** pphead)
//{
//	//给双链表申请一个哨兵位
//	*pphead = LTBuyNode(-1);
//}
LTNode* InitList()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}
//打印链表
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}
//尾插
void LTPushBack(LTNode* phead, LTDataType x) //将新节点插入到头节点的前面,即尾插
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

	//phead phead->prev newnode
	//1.先在尾部插入新节点
	newnode->prev = phead->prev;
	newnode->next = phead;
	//2.更新新的尾节点与头节点进行首尾相连
	phead->prev->next = newnode;
	phead->prev = newnode;
}
//头插
void LTPushFront(LTNode* phead, LTDataType x) //头插,要往头节点后面插入
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

	//phead newnode phead->next
	//1.先在头节点和第一个有效节点之间插入一个新节点,表示头插
	newnode->next = phead->next;
	newnode->prev = phead;

	//2.更新新的节点为新的第一个有效节点
	phead->next->prev = newnode;
	phead->next = newnode;
	//也可以这样写
	/*phead->next = newnode;
	newnode->next->prev = newnode;*/
}
//尾删
void LTPopBack(LTNode* phead)
{
	//链表必须有效且链表不能为空(只有一个哨兵位)
	assert(phead && phead->next != phead);

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

	//删除del节点
	free(del);
	del = NULL;
}
//头删
void LTPopFront(LTNode* phead)
{
	assert(phead && phead->next != phead);

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

	//删除del节点
	free(del);
	del = NULL;
}
//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	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->prev = pos;
	newnode->next = pos->next;

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

	pos->prev->next = newnode;
	pos->prev = newnode;
}
//删除pos节点
void LTErase(LTNode* pos)
{
	//pos理论上来说不能为phead,但是没有参数phead,无法增加校验
	assert(pos);
	//pos->prev pos pos->next
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;

	free(pos);
	pos = NULL;
}
//销毁链表
void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//此时pcur指向phead,而phead还没有被销毁
	free(phead);
	phead = NULL;
}
test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"

void ListTest01()
{
	//初始化
	/*LTNode* plist = NULL;
	InitList(&plist);*/
	LTNode* plist = InitList();

	//测试尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPrint(plist);

	//测试头插
	/*LTPushFront(plist, 1);
	LTPrint(plist);
	LTPushFront(plist, 2);
	LTPrint(plist);
	LTPushFront(plist, 3);
	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);*/

	//测试查找链表
	LTNode* find = LTFind(plist, 2);
	/*if (find == NULL)
	{
		printf("找不到\n");
	}
	else
	{
		printf("找到了\n");
	}*/
	
	//测试在pos位置之后插入数据
	/*LTInsert(find, 66);
	LTPrint(plist);*/

	//测试在pos位置之前插入数据
	/*LTInsertFront(find, 88);
	LTPrint(plist);*/

	//测试删除pos节点
	LTErase(find);
	find = NULL;         //LTErase 和 LTDestroy 函数参数理论上要传二级指针,因为我们需要让形参的改变影响到实参,但为了保持接口一致性传
	LTPrint(plist);      //一级指针,传一级指针存在的问题:当形参phead置为NULL后,实参plist不会被修改为NULL,因此解决办法:调用完方法后
	                     //手动将实参置为NULL
	//销毁链表
	LTDestroy(plist);
	plist = NULL;
}
int main()
{
	ListTest01();
	
	return 0;
}


网站公告

今日签到

点亮在社区的每一天
去签到