当前位置: 首页 > news >正文

【初探数据结构】线性表——链表(二)带头双向循环链表(详解与实现)

文章目录

        • 1. **什么是带头双向循环链表?**
        • 2. **带头双向循环链表的结构**
        • 3. **带头双向循环链表的操作**
          • 3.1 **初始化链表**
          • 3.2 **插入节点**
          • 3.3 **删除节点**
          • 3.4 **查找节点**
          • 3.5 **删除指定节点**
        • 4. **带头双向循环链表的优势**
        • 5. **完整代码**
          • `List.h`
          • `List.c`
          • `Test.c`

1. 什么是带头双向循环链表?

带头双向循环链表,顾名思义,包含以下几个重要特点:

  • 双向性:每个节点不仅有指向下一个节点的next指针,还有一个指向前一个节点的prev指针。这使得遍历链表时可以从任意一个节点开始,向前或向后都可以。
  • 循环性:链表的最后一个节点的next指针指向头节点,而头节点的prev指针指向最后一个节点,形成一个闭环。即链表没有尾节点,头节点和尾节点连接起来,方便链表的尾插尾删等操作。
  • 头节点(哨兵节点):链表通过引入一个特殊的头节点(哨兵节点)来简化插入和删除操作。这个头节点本身不存储实际数据,只是作为一个占位符,避免了空链表或单一节点链表的特殊情况。(我的上一篇文章讲解过,欢迎来学习哦【初探数据结构】链表OJ算法——哨兵位(合并两个有序链表详解))
2. 带头双向循环链表的结构
typedef int LTDataType;

typedef struct ListNode
{
    LTDataType data;       // 存储节点数据
    struct ListNode* next; // 指向下一个节点
    struct ListNode* prev; // 指向前一个节点
} ListNode;

每个ListNode结构体包含:

  • data:保存节点的数据。
  • next:指向下一个节点。
  • prev:指向前一个节点。
3. 带头双向循环链表的操作

带头双向循环链表支持常见的增删查改操作,以下是常用操作的实现。

3.1 初始化链表
ListNode* ListInit()
{
    ListNode* head = (ListNode*)malloc(sizeof(ListNode));
    if (head == NULL) {
        perror("malloc fail");
        exit(-1);
    }
    head->next = head;  // 头节点的next指向自己,形成循环
    head->prev = head;  // 头节点的prev也指向自己,形成循环
    return head;
}

在初始化时,创建一个头节点,并将其nextprev指针都指向自身,这样链表初始时是空的,并且形成了一个循环结构。

3.2 插入节点
  • 尾插操作:在链表尾部插入新节点。
void ListPushBack(ListNode* pHead, LTDataType x)
{
    assert(pHead);
    ListInsert(pHead, x);
}

ListInsert用于执行具体的插入操作,它将新节点插入到链表末尾。

  • 头插操作:在链表头部插入新节点。
void ListPushFront(ListNode* pHead, LTDataType x)
{
    assert(pHead);
    ListInsert(pHead->next, x);
}
3.3 删除节点
  • 尾删操作:删除链表尾部的节点。
void ListPopBack(ListNode* pHead)
{
    assert(pHead);
    ListNode* tail = pHead->prev;
    ListErase(tail);
}
  • 头删操作:删除链表头部的节点。
void ListPopFront(ListNode* pHead)
{
    assert(pHead);
    ListErase(pHead->next);
}
3.4 查找节点

通过值查找链表中的节点。

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
    assert(pHead);
    ListNode* cur = pHead->next;
    while (cur != pHead) {
        if (cur->data == x)
            return cur;
        cur = cur->next;
    }
    return NULL;
}
3.5 删除指定节点

通过pos指针删除指定位置的节点。

void ListErase(ListNode* pos)
{
    assert(pos);
    ListNode* after = pos->next;
    ListNode* behind = pos->prev;
    behind->next = pos->next;
    after->prev = pos->prev;
    free(pos);
    pos = NULL;
}
4. 带头双向循环链表的优势
  1. 简化链表操作:带头节点和双向指针的结合使得在链表的任意位置插入和删除操作更加简便。特别是通过哨兵节点,可以避免单独处理空链表、头节点和尾节点的特殊情况。

  2. 双向遍历:通过prevnext指针,我们可以实现从链表任意节点出发,向前或向后遍历。这为一些复杂操作提供了极大的灵活性。

  3. 内存管理:链表在操作时可以动态地分配和释放节点内存,减少内存浪费。

5. 完整代码
List.h
#pragma once

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


// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

// 创建返回链表的头结点.
ListNode* ListInit();
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

List.c
#define  _CRT_SECURE_NO_WARNINGS 1

#include"List.h"

ListNode* ListInit()
{
	//设置哨兵位头结点
	ListNode* head = (ListNode*)malloc(sizeof(ListNode));
	if (head == NULL) {
		perror("malloc fail");
		exit(-1);
	}
	head->next = head;
	head->prev = head;
	return head;
}

void ListDestory(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	while (cur != pHead) {
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(pHead);
	pHead = NULL;
}

void ListPrint(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	printf("HEAD=>");
	while (cur != pHead) {
		printf("%d<=>",cur->data);
		cur = cur->next;
	}
	printf("\n");
}

ListNode* CreateNewNode(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL) {
		perror("malloc fail");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListInsert(pHead, x);
}

void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	ListNode* tail = pHead->prev;
	ListErase(tail);
}

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListInsert(pHead->next, x);
}

void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	ListErase(pHead->next);
}

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	while (cur != pHead) {
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* newnode = CreateNewNode(x);
	ListNode* cur = pos->prev;
	pos->prev = newnode;
	newnode->next = pos;
	cur->next = newnode;
	newnode->prev = cur;
}

void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* after = pos->next;
	ListNode* behind = pos->prev;
	behind->next = pos->next;
	after->prev = pos->prev;
	free(pos);
	pos = NULL;
}
Test.c

在实际使用时,我们可以通过以下代码进行链表的操作:

void test()
{
    ListNode* plist;
    plist = ListInit();  // 初始化链表

    // 插入元素
    ListPushBack(plist, 1);
    ListPushBack(plist, 2);
    ListPushBack(plist, 3);
    ListPushBack(plist, 4);
    ListPrint(plist);  // 打印链表

    // 删除元素
    ListPopBack(plist);
    ListPopBack(plist);
    ListPrint(plist);  // 打印链表

    // 头部插入
    ListPushFront(plist, 0);
    ListPrint(plist);  // 打印链表
    ListPopFront(plist);
    ListPrint(plist);  // 打印链表

    // 查找元素并插入
    ListNode* pos = ListFind(plist, 2);
    ListInsert(pos, 20);
    ListPrint(plist);  // 打印链表
}

相关文章:

  • MySQL 架构、索引优化、DDL解析、死锁排查
  • 在ubuntu20.4中如何创建一个虚拟环境(亲测有效)
  • ubuntu20.04已安装 11.6版本 cuda,现需要通过源码编译方式安装使用 cuda 加速的 ffmpeg 步骤
  • 有效的括号(栈)
  • 【论文阅读笔记】ALSS-YOLO | 无人机(UAVs)、热红外(TIR)、野生动物探测、小目标、轻量级探测器
  • 【Node.js入门笔记1---初始Node.js)】
  • 笔记:代码随想录算法训练营day37:完全背包、518. 零钱兑换 II、377. 组合总和 Ⅳ、70. 爬楼梯 (进阶)
  • Python--网络编程(下)
  • GPTQ - 生成式预训练 Transformer 的精确训练后压缩
  • C++编程指南23 - 在无关线程之间共享资源时应使用shared_ptr
  • IntelliJ IDEA 华为云远程开发配置步骤
  • 通用文件模型
  • Axure设计之数据列表动态列设置/列筛选案例
  • 设计模式Python版 状态模式
  • 开发者社区测试报告(功能测试+性能测试)
  • 尚硅谷爬虫note14
  • ZooKeeper 基本概述
  • 意向锁的目的
  • 探索数据仓库自动化:ETL流程设计与实践
  • 【Pandas】pandas Series swaplevel
  • 贵阳快速建站模板/最新搜索关键词
  • 创业网站建设政策/app推广软文范文
  • 纯 flash 网站/网页推广怎么收取费用
  • 网站定位案例/seo短视频保密路线
  • 网站广告位制作/扬州seo优化
  • 域名网址/超级seo外链