【数据结构】双向链表“0”基础知识讲解 + 实战演练

一、概念与结构

如图所示:双向链表是由一个一个的结点组成,这里的结点有三个组成部分:

cpp 复制代码
struct ListNode
{
	int data;
	struct ListNode* next;//指向后一个结点的指针
	struct ListNode* prev;//指向前一个结点的指针
};

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

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

双向链表为空:他还是要满足带头、双向、循环这几个特点,实际上他只有一个哨兵位指针,他的next指针指向自己,prev指针也指向自己。

二、实现双向链表

2.1、List.h
cpp 复制代码
#pragma once

#define _CRT_SECURE_NO_WARNINGS

#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;

//双向链表的初始化
//要改变头结点plist就要传入二级指针否则形参改变不了实参
void LTInit(LTNode** pphead);

//尾插
//在双向链表中,增删改查都不会改变哨兵位结点
void LTPushBack(LTNode* phead, LTDataType x);

//头插
//注意这里的头插是将newnode放到head之后,node1之前
//如果只是单纯的放在head前面那就和尾插无异了
void LTPushFront(LTNode* phead, LTDataType x);

//判断链表是否为空
bool LTEmpty(LTNode* phead);

//打印双向链表
//只需要打印node1、node2、node3...不需要打印phead这个哨兵位
void LTPrint(LTNode* phead);

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

//头删
//删除的是head后面的结点,并不是删除哨兵位
void LTPopFront(LTNode* phead);

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

//任意位置的插入
//双向链表我们知道其中一个的位置就可以找到任意位置的数据
void LTInsert(LTNode* pos, LTDataType x);

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

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

//销毁双向链表
//销毁的实际也是改变,要使用二级指针
void LTDesTroy(LTNode** pphead);
2.2、List.c
cpp 复制代码
#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("malloc fail!");
		exit(1);
	}
	(*pphead)->data = -1;//我们可以随意的先赋个值,不使用
	(*pphead)->next = (*pphead)->prev = *pphead;
	//现在就是一个空的双向链表

	//等价代码: *pphead = LTBuyNode(-1);
}

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	//phead phead->prev newnode
	newnode->prev = phead->prev;
	newnode->next = phead;

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

//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* newnode = LTBuyNode(x);
	//phead newnode phead->next
	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);
	//pos newnode pos->next
	newnode->prev = pos;
	newnode->next = pos->next;

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

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

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

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

//删除pos位置的结点
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;
}

//销毁双向链表
void LTDesTroy(LTNode** pphead)
{
	LTNode* pcur = (*pphead)->next;
	while (pcur != *pphead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//销毁头结点
	free(*pphead);
	*pphead = NULL;
}

这里为了保持接口的一致性,我们可以改一下初始化和销毁的代码方便我们后期更好的使用和记忆

cpp 复制代码
//初始化2
LTNode* LTInit2()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

//销毁2
void LTDesTroy2(LTNode* phead)
{
	LTNode* pcur = (phead)->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//销毁头结点
	free(phead);
	phead = NULL;
}
2.3、test.c
cpp 复制代码
#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);
	LTPushFront(plist, 4);*/

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

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

	//查找
	LTNode* pos = LTFind(plist, 2);
	/*if (pos)
	{
		printf("找到了!\n");
	}
	else
	{
		printf("未找到!\n");
	}*/

	//在pos之后插入数据
	//LTInsert(pos, 416);
	//LTPrint(plist);

	//在pos之前插入数据
	LTInsertFront(pos, 416);
	LTPrint(plist);


	//删除pos位置的数据
	LTErase(pos);
	LTPrint(plist);

	//销毁
	LTDesTroy(&plist);

}

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

三、顺序表与链表的分析

其实大家现在都处于一个代码的初级阶段,会经常碰到不会写的题目,可能一个题目思考半个多小时也没有什么思路,我们这时候要做的就是"死磕"代码,多去画图理解结构与理清思路,没有一蹴而就的成就,只有千锤百炼的沉淀,共勉之!

相关推荐
将车2444 小时前
C++实现二叉树搜索树
开发语言·数据结构·c++·笔记·学习
Larry_Yanan4 小时前
QML学习笔记(四十)QML的FileDialog和FolderDialog
笔记·qt·学习
NiKo_W5 小时前
Linux 线程控制
linux·数据结构·内核·线程·进程·线程控制
梵得儿SHI5 小时前
Java 反射机制核心类详解:Class、Constructor、Method、Field
java·开发语言·反射·class·constructor·java反射·java反射机制
hbqjzx5 小时前
记录一个自动学习的脚本开发过程
开发语言·javascript·学习
PyHaVolask5 小时前
数据结构与算法分析
数据结构·算法·图论
小王C语言5 小时前
封装红黑树实现mymap和myset
linux·服务器·算法
Sirens.5 小时前
Java核心概念:抽象类、接口、Object类深度剖析
java·开发语言·github
幸运小圣5 小时前
Set数据结构【ES6】
javascript·数据结构·es6