双向链表的功能实现

这篇具有很好参考价值的文章主要介绍了双向链表的功能实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

      前言:我们已经学习并知道了单链表的实现,链表的实现方式不只是单链表一种,今天我们就来介绍一种新的结构,就是双链表结构,本质上是将节点间进行双向链接,从而使一些操作更加容易实现。

双向链表的功能实现

目录

1.双向链表的简介

2.双向链表的实现

带头节点的哨兵位的创建和初始化

为什么要使用哨兵位?

节点的头、尾插和头、尾删

插入和删除函数

完整程序代码及测试代码

List.h

List.cpp

Test.cpp

3.金句频道


1.双向链表的简介

        双向链表(Doubly Linked List)是一种常见的线性数据结构,它相比于单向链表多了一个指针,可以支持双向遍历。

       每个节点在双向链表中都有两个指针,一个指向前一个节点,一个指向后一个节点,因此可以从任意一个节点开始,依次遍历前一个节点和后一个节点。双向链表可以对数据进行插入和删除操作,这些操作相比较单向链表来说更加方便和高效。

双向链表的功能实现

 双向链表的特点包括:

1. 每个节点都有两个指针,一个指向前一个节点,一个指向后一个节点。
2. 可以从任意一个节点开始,依次遍历前一个节点和后一个节点。
3. 可以对数据进行插入和删除操作,保证了操作的高效性和方便性。
4. 相对于单向链表来说,占用更多的存储空间,因为需要多一个指针来指向前一个节点。

     

      需要注意的是,当插入或删除一个节点时,需要同时修改其前后两个节点的指针,才能保证链表结构的完整性。

2.双向链表的实现

带头节点的哨兵位的创建和初始化

为什么要使用哨兵位?

       链表中哨兵位(Sentinel)是一种特殊的节点,位于链表头部或尾部,不存储任何实际的数据。引入哨兵位的主要目的是为了简化链表的操作,提高代码的可读性和可维护性。

使用哨兵位的优点主要包括:

1. 简化代码逻辑:引入哨兵位后,链表的头尾节点都指向哨兵位而不是NULL,这样在任何情况下,我们都不需要对头尾节点进行特判,也不需要用二级指针进行传址了(链表中引入二级指针的目的,其实就是为了在头结点为空的情况下,节点还能正常创建并使用,说白了就是为了“对NULL指针进行解引用操作”)。这样能够简化代码逻辑,减少代码复杂度,提高代码可读性和可维护性。

2. 避免空指针异常:使用哨兵位可以避免在链表为空或链表操作时出现空指针异常。如果没有哨兵位,我们需要在头尾节点为空时进行特判,否则可能会导致程序崩溃。

LTNode* LTInit()
{
	Listnode head = (Listnode)malloc(sizeof(LTNode));
	assert(head);//断言,防止开辟不成功
	head->data = -1;
	//初始让头结点的前驱和后继都指向自己,这样可以便与判断链表是否为空并且与尾结点指向哨兵位的操作上统一
	head->next = head->prev = head;
	return head;
}

节点的头、尾插和头、尾删

        这些操作基本与单链表的操作大同小异,只是多了一个前驱结点的操作,这里需要注意以下几点:

1.注意对可能为NULL的指针进行断言避免出现野指针和堆错误问题;

2.对不再使用的空间进行及时的释放,避免内存泄漏。

//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	
	assert(phead);
	LTNode* newnode = new LTNode(x);
	LTNode* first = phead->next;

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

	newnode->next = first;
	first->prev = newnode;
	

	//LTInsert(phead->next, x);
}

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	Listnode newnode = new LTNode(x);

	//这里最好将两个及以上解引用的操作分开
	LTNode *first = phead->prev;
	first->next = newnode;
	newnode->next = phead;
	newnode->prev = first;
	phead->prev = newnode;

	//或者可以直接调用插入函数
	//LTInsert(phead, x);
}

//头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	
	LTNode* p = phead->next;
	LTNode* ppnext = p->next;

	free(p);
	phead->next=ppnext;
	ppnext->prev=phead;
	
	//LTErase(phead->next);
}

//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	free(tail);
	tailPrev->next = phead;
	phead->prev = tailPrev;
	

	//LTErase(phead->prev);
}

       细心地朋友可能已经注意到了,这些不同的函数内部都有相同的LTInsertLTErase函数,这不是偶然,我们将不同方式插入或删除的函数进行对比,就会发现,它们的代码具有一定的逻辑重复性,存在逻辑上互通的部分,我们可以将这个部分归纳为一个函数,像头插和尾插,我们可以写一个在任意节点前或后插入的函数,再将不同的节点需要的参数分别传入,就能实现不同的功能,尾删和头删也是同理,这样就大大简化了代码,减少了使用指针出错的可能性。

插入和删除函数

// 在pos之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//断言,判断pos是否合法

	Listnode newnode = new LTNode(x);
	Listnode pre = pos->prev;
	pre->next = newnode;
	newnode->next = pos;
	newnode->prev = pre;
	pos->prev = newnode;

}

// 删除pos位置的值
void LTErase(LTNode* pos)
{
	assert(pos);
	Listnode front = pos->prev;
	Listnode behind = pos->next;
	front->next = behind;
	behind->prev = front;
	free(pos);
	pos = NULL;
}

完整程序代码及测试代码

List.h

#pragma once

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

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;
	LTDataType data;
	//C++构造函数//可不写
	ListNode(LTDataType d = 0, ListNode* pr = NULL, ListNode* ne = NULL)
	{
		data = d, prev = pr, next = ne;
	}
}LTNode,*Listnode;

//创建并初始化头结点
LTNode* LTInit();

//打印带头结点的单链表
void LTPrint(LTNode* phead);

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

//尾插
void LTPushBack(LTNode* phead, LTDataType x);

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

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

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

//查找值为x的节点
LTNode* LTFind(LTNode* phead, LTDataType x);

// 在pos之前插入
void LTInsert(LTNode* pos, LTDataType x);

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

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

List.cpp

#include <bits/stdc++.h>
#include "List.h"
using namespace std;

LTNode* LTInit()
{
	Listnode head = (Listnode)malloc(sizeof(LTNode));
	assert(head);//断言,防止开辟不成功
	head->data = -1;
	//初始让头结点的前驱和后继都指向自己
	head->next = head->prev = head;
	return head;
}
void LTPrint(LTNode* phead)
{
	assert(phead);
	printf("guard");
	Listnode p = phead->next;
	while (p!= phead)
	{
		printf("->%d", p->data);
		p = p->next;
	}
	printf("\n");
}

// 在pos之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//断言,判断pos是否合法

	Listnode newnode = new LTNode(x);
	Listnode pre = pos->prev;
	pre->next = newnode;
	newnode->next = pos;
	newnode->prev = pre;
	pos->prev = newnode;

}
bool LTEmpty(LTNode* phead)
{
	return phead->next == phead;
}
void LTPushBack(LTNode* phead, LTDataType x)
{
	Listnode newnode = new LTNode(x);

	//这里最好将两个及以上解引用的操作分开
	LTNode *first = phead->prev;
	first->next = newnode;
	newnode->next = phead;
	newnode->prev = first;
	phead->prev = newnode;

	//或者可以直接调用插入函数
	//LTInsert(phead, x);
}
void LTPushFront(LTNode* phead, LTDataType x)
{
	
	assert(phead);
	LTNode* newnode = new LTNode(x);
	LTNode* first = phead->next;

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

	newnode->next = first;
	first->prev = newnode;
	

	//LTInsert(phead->next, x);
}
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	free(tail);
	tailPrev->next = phead;
	phead->prev = tailPrev;
	

	//LTErase(phead->prev);
}
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	
	LTNode* p = phead->next;
	LTNode* ppnext = p->next;

	free(p);
	phead->next=ppnext;
	ppnext->prev=phead;
	
	//LTErase(phead->next);
}

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	Listnode p = phead->next;
	while (p!= phead)
	{
		if (p->data == x)
			return p;
		p = p->next;
	}
	return NULL;
}


// 删除pos位置的值
void LTErase(LTNode* pos)
{
	assert(pos);
	Listnode front = pos->prev;
	Listnode behind = pos->next;
	front->next = behind;
	behind->prev = front;
	free(pos);
	pos = NULL;
}
void LTDestroy(LTNode* phead)
{
	
	assert(phead);
	LTNode* cur = (phead)->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;//此处传的是一级指针,在外部函数内修改没有作用,应该传二级指针或者在主函数内进行销毁,这里我们选择在主函数内单独进行置空
}

Test.cpp

#include <bits/stdc++.h>
#include "List.h"
using namespace std;

void TestList1()
{
	LTNode* plist = LTInit();
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);

	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	//LTPopBack(plist);
	//LTPrint(plist);

	LTDestroy(plist);
	plist = NULL;
}

void TestList2()
{
	LTNode* plist = LTInit();
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

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

	LTDestroy(plist);
	plist = NULL;
}

void TestList3()
{
	LTNode* plist = LTInit();
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
	LTPrint(plist);

	LTNode* pos = LTFind(plist, 3);
	if (pos)
	{
		LTInsert(pos, 30);
	}
	LTPrint(plist);

	LTDestroy(plist);
	plist = NULL;
}

int main()
{
	TestList1();
	TestList2();
	TestList3();

	return 0;
}

3.金句频道

      最近看到这样一句话:“当我真正开始爱自己,我睡的越来越早,也越来越喜欢锻炼。我不在纠结和焦虑,变得自信满满,去追求有意义的人和事,并为之燃烧自己的热情。我发现,人生才刚刚开始.”

双向链表的功能实现

 文章来源地址https://www.toymoban.com/news/detail-446211.html

到了这里,关于双向链表的功能实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 数据结构——双向链表的实现

    注意: 双向链表又称带头双向循环链表 这⾥的“带头”跟前⾯我们说的“头节点”是两个概念,实际前⾯的在单链表阶段称呼不严 谨,但是为了同学们更好的理解就直接称为单链表的头节点。 带头链表⾥的头节点,实际为“ 哨兵位 ”,哨兵位节点不存储任何有效元素,只

    2024年02月06日
    浏览(35)
  • 数据结构:双向链表的实现(C实现)

    个人主页 : 个人主页 个人专栏 : 《数据结构》 《C语言》 本篇博客,将要实现的是 带头双向循环链表 ,该结构实现尾插,尾删只需要O(1)的时间复杂度。 其结构如下所示: 既然要实现的链表是双向循环的,那么对于指针域,我们就需要 指向节点上一个的指针 和 指向节点

    2024年02月14日
    浏览(35)
  • 北邮22信通:复习补充:双向链表的实现

    北邮22信通一枚~    跟随课程进度每周更新数据结构与算法的代码和文章  持续关注作者  解锁更多邮苑信通专属代码~ 获取更多文章  请访问专栏: 北邮22信通_青山如墨雨如画的博客-CSDN博客 **说明** 最近复习看到书后有双向链表的题目,编出来供大家复习~ ********* 目录

    2024年02月07日
    浏览(44)
  • 基于双向链表的通讯录C语言实现

    关于双向链表的详细了解请见博主的另一篇博客,本文旨在对单链表进行应用,采用C语言编写。

    2024年04月17日
    浏览(37)
  • 【数据结构】—带头双向循环链表的实现(完美链表)

    链表结构一共有八种形式,在前面的文章里已经讲完了不带头单向非循环链表的实现,但是我们发现该链表实现尾插与尾删时比较麻烦,要先从头节点进行遍历,找到尾节点,时间复杂度为O(N),而本次所讲的带头双向循环单链表,则可以直接找到尾节点。 虽然该链表看起来

    2024年01月25日
    浏览(49)
  • 算法竞赛基础:C++双向链表的结构和实现(普通链表、List、静态链表)

    本文将会介绍在算法竞赛中双向链表的几种使用方式,适合有一定基础的人阅读。 一般来说,普通的链表结构是这样的: next指针指向下一个链表,这样的结构只能够支持单向查询。 双向链表,顾名思义,就是可以支持双向的访问和查询。 也就是这样的: 这种链表为访问前

    2024年01月24日
    浏览(31)
  • 【数据结构】双向链表的增删查改(C 代码实现)

    引入双向链表:关于单链表的问题与讨论 单链表存在的毛病: 因为单链表 只能单向 遍历链表, 对于 前插 这个操作,单链表必 须得找到所需前插节点位置的前一个 ,那么这时就得 从头指针重新遍历一次 链表,会造成时间复杂度大大增加。 没有头节点(哨兵位)无法删除

    2024年02月08日
    浏览(33)
  • 数据结构:链表基础OJ练习+带头双向循环链表的实现

    目录 一.leetcode剑指 Offer II 027. 回文链表 1.问题描述 2.问题分析与求解 (1) 快慢指针法定位链表的中间节点 (2) 将链表后半部分进行反转 附:递归法反转链表 (3) 双指针法判断链表是否回文 二.带头双向循环链表的实现 1.头文件 2.节点内存申请接口和链表初始化接口 3.链表的打

    2024年02月02日
    浏览(41)
  • 青岛大学_王卓老师【数据结构与算法】Week04_04_双向链表的插入_学习笔记

    本文是个人学习笔记,素材来自青岛大学王卓老师的教学视频。 一方面用于学习记录与分享,另一方面是想让更多的人看到这么好的《数据结构与算法》的学习视频。 如有侵权,请留言作删文处理。 课程视频链接: 数据结构与算法基础–第04周04–2.5.4双向链表2–双向链表

    2024年02月12日
    浏览(45)
  • 数据结构_链表_双向循环链表的初始化、插入、删除、修改、查询打印(基于C语言实现)

    版本: 2024年4月26日 V1.0 发布于博客园 目录 目录 双向循环链表公式 初始化双向循环链表 构建双向循环链表结点 创建一个空链表(仅头结点) 创建一个新结点 插入数据 头插 中插 尾插 删除数据 头删 中删 尾删 查询打印数据 遍历打印 测试 测试结果: 完整代码 DoubleCirLList.h

    2024年04月27日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包