从零开始学习数据结构—【链表】—【探索环形链的设计之美】

环形链表

文章目录

  • 环形链表
    • 1.结构图
    • 2.具体实现
      • 2.1.环形链表结构
      • 2.2.头部添加数据
        • 2.2.1.具体实现
        • 2.2.2.测试添加数据
      • 2.3.尾部添加数据
        • 2.3.1.具体实现
        • 2.3.2.添加测试数据
      • 2.4.删除头部数据
        • 2.4.1.具体实现
        • 2.4.2.测试删除数据
      • 2.5.删除尾部数据
        • 2.5.1.具体实现
        • 2.5.2.测试删除数据
      • 2.6.根据内容删除节点
        • 2.6.1.具体实现
      • 2.7.遍历环形链表
        • 2.7.1.迭代器遍历
        • 2.7.2.使用递归进行遍历
      • 2.7.3.测试
    • 3.具体应用场景
      • 3.1.优点
      • 3.2.缺点
      • 3.3.应用场景

1.结构图

双向环形链表带哨兵,这个时候的哨兵可以当头,也可做尾

带哨兵双向循环链表:结构稍微复杂,实现简单。一般用来单独存储数据,实际中使用的链表数据结构都是带头双向链表。另外,这个结构虽然结构复杂,但是使用代码实现后会发现结构会带来很多优势。

双向环形链表是一种链式数据结构,其每个节点包含指向前一个节点和后一个节点的指针,形成了一个闭环。这意味着链表的尾部节点指向头部节点,而头部节点指向尾部节点,形成了一个环状的结构。

带哨兵的双向环形链表在头部和尾部都有一个特殊的哨兵节点,这个哨兵节点不存储任何数据,仅用于简化链表的操作。哨兵节点使得链表中始终存在一个不变的头部和尾部,即使链表为空也如此。具体而言:

  1. 头部哨兵节点: 位于链表的头部,它的前驱节点指向链表的尾部节点,而它的后继节点指向链表的第一个真实节点。头部哨兵节点使得在头部执行操作时变得更加简单,不需要特殊处理链表为空的情况,也不需要区分头部和尾部的操作。
  2. 尾部哨兵节点: 位于链表的尾部,它的后继节点指向链表的头部节点,而它的前驱节点指向链表的最后一个真实节点。尾部哨兵节点同样简化了尾部操作,使得在尾部进行插入、删除等操作更加方便。

带哨兵的双向环形链表在实现时通常会带来一些优势:

  • 简化操作: 哨兵节点的存在使得对链表头部和尾部的操作变得更加统一和简化。不需要特别处理头部或尾部为空的情况,使得代码更加清晰和简洁。
  • 增强鲁棒性: 哨兵节点可以避免出现空指针异常,因为链表中始终存在一个不变的头部和尾部。这增强了代码的鲁棒性和可靠性。
  • 逻辑统一: 哨兵节点的存在使得链表的逻辑更加统一,不需要在特殊情况下单独处理头部或尾部节点,使得代码更加一致性和可读性。

在这里插入图片描述

2.具体实现

2.1.环形链表结构

public class DoubleLinkedListSentinel {
	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

	public DoubleLinkedListSentinel(){
		// 初始化时 环形连链表创建指向自身
		sentinel.next = sentinel;
		sentinel.prev = sentinel;
	}

	/**
	 * 创建哨兵
	 */
	private Node sentinel = new Node(null,-1,null);

	private static class Node{
		Node prev;     // 头指针
		Integer value; // 值
		Node next;     // 尾指针

		public Node(Node prev, Integer value, Node next) {
			this.prev = prev;
			this.value = value;
			this.next = next;
		}
	}
    
   /**
	 * 重写toString 用于Json输出
	 * @return
	 */
	@Override
	public String toString() {
		StringBuffer sb = new StringBuffer();
		Node p = sentinel.next;
		while (p != sentinel){
			sb.append(","+p.value);
			p = p.next;
		}
		//  StringUtils.strip(sb.toString() 去除首位固定字符
		return "Node[ " + StringUtils.strip(sb.toString(), ",") +" ]";
	}
}

2.2.头部添加数据

# 思路
	找到哨兵,找到哨兵的下一个节点,创建新的对象(指定新节点的前后节点),将哨兵next指向新创建的节点,将哨兵的下一个节点指向新加的节点

在这里插入图片描述

2.2.1.具体实现
	/**
	 * 在头部添加值
	 * @param value 待添加的元素
	 */
	public void addFirst(int value){
		// 找到哨兵
		Node head = sentinel;
		// 找到哨兵的下一个节点
		Node next = sentinel.next;
		// 创建新的对象
		Node addNode = new Node(head, value, next);
		// 将哨兵next指向新创建的节点
		head.next = addNode;
		//将哨兵的下一个节点的头节点指向新加的节点
		next.prev = addNode;
	}
2.2.2.测试添加数据
@Test
@DisplayName("测试双向环形链表")
public void test(){
    DoubleLinkedListSentinel node = new DoubleLinkedListSentinel();
    logger.error("add after node :{}",node);
    node.addFirst(1);
    node.addFirst(2);
    node.addFirst(3);
    logger.error("add after node :{}",node);
}

在这里插入图片描述

2.3.尾部添加数据

# 思路
	找到最后一个节点,找到头节点,创建新的节点(指明前后节点),将最后一个节点的next指向新创建的节点,将头节点的prev指向新创建的节点

在这里插入图片描述

2.3.1.具体实现
/**
 * 向链表的最后一个节点添元素
 * @param value 需要添加元素的值
 */
public void addLast(int value){
    // 找到最后一个节点
    Node next = sentinel.prev;
    // 找到头节点
    Node head = sentinel;
    // 创建新的节点
    Node node = new Node(next, value, head);
    // 将最后一个节点的next指向新创建的节点
    next.next = node;
    // 将头节点的prev指向新创建的节点
    head.prev = node;
}
2.3.2.添加测试数据
	@Test
	@DisplayName("测试-双向环形链表-尾部添加元素")
	public void tes2(){
		DoubleLinkedListSentinel node = new DoubleLinkedListSentinel();
		node.addLast(1);
		node.addLast(2);
		node.addLast(3);
		node.addLast(4);
		logger.error("linked list is: :{}",node);
	}

在这里插入图片描述

2.4.删除头部数据

# 思路 
	找到需要删除的节点,找到上一个节点,找到删除节点的下一个节点,将头节点的next指向删除节点的下一个节点,将删除节点的prev指向head

在这里插入图片描述

2.4.1.具体实现
/**
 * 删除第一个节点
 */
public void removedFirst(){
    // 先找到需要删除的节点
    Node deleteNode = sentinel.next;
    // 如果删除的节点等于哨兵 那么不能删除
    if (deleteNode == sentinel){
        throw new IllegalArgumentException("delete node is null!");
    }
    // 找到上一个节点
    Node head = sentinel;
    // 找到删除节点的下一个节点
    Node next = deleteNode.next;
    // 将头节点的next指向删除节点的下一个节点
    head.next = next;
    // 将删除节点的prev指向head
    next.prev = head;
}
2.4.2.测试删除数据
@Test
@DisplayName("测试-双向环形链表-删除第一个数据")
public void tes2(){
    DoubleLinkedListSentinel node = new DoubleLinkedListSentinel();
    node.addLast(1);
    node.addLast(2);
    node.addLast(3);
    node.addLast(4);
    logger.error("remove first node :{},size :{}",node,node.size());
    logger.error("------------------ remove ----------------");
    node.removedFirst();
    logger.error("remove first node :{},size :{}",node,node.size());
    node.removedFirst();
    logger.error("remove first node :{},size :{}",node,node.size());
    node.removedFirst();
    logger.error("remove first node :{},size :{}",node,node.size());
    node.removedFirst();
    logger.error("remove first node :{},size :{}",node,node.size());
    node.removedFirst();
}

在这里插入图片描述

2.5.删除尾部数据

# 思路
	找到最后一个节点,找到删除节点的上一个节点,找到删除节点的下一个节点,将删除节点的上一个节点 next指向头部,将哨兵执行最后一个节点

在这里插入图片描述

2.5.1.具体实现
/**
	 * 删除最后一个节点
	 */
	public void removeLast(){
		// 找到最后一个节点
		Node deleteNode = sentinel.prev;
		// 如果删除的节点等于哨兵 那么不能删除
		if (deleteNode == sentinel){
			throw new IllegalArgumentException("delete node is null!");
		}
		// 找到删除节点的上一个节点
		Node head = deleteNode.prev;
		// 将删除节点的下一个节点
		Node next = sentinel;
		// 将删除的节点的上一个节点 next指向头部
		head.next = next;
		// 将哨兵执行最后一个节点
		next.prev = head;
	}

2.5.2.测试删除数据
@Test
@DisplayName("测试-双向环形链表-删除最后一个数据")
public void tes2(){
    DoubleLinkedListSentinel node = new DoubleLinkedListSentinel();
    node.addLast(1);
    node.addLast(2);
    node.addLast(3);
    node.addLast(4);
    logger.error("remove last node :{},size :{}",node,node.size());
    logger.error("------------------ remove ----------------");
    node.removeLast();
    logger.error("remove last node :{},size :{}",node,node.size());
    node.removeLast();
    logger.error("remove last node :{},size :{}",node,node.size());
    node.removeLast();
    logger.error("remove last node :{},size :{}",node,node.size());
    node.removeLast();
    logger.error("remove last node :{},size :{}",node,node.size());
    node.removeLast();
}

在这里插入图片描述

2.6.根据内容删除节点

# 思路
	找到需要删除的节点,找到删除节点的上一个节点,找到删除节点的下一个节点,将删除节点的上一个节点 next指向删除节点的下一个节点,将删除节点的下一个节点 prev指向删除节点的上一个节点

在这里插入图片描述

2.6.1.具体实现
@Test
@DisplayName("测试-双向环形链表-根据内容删除元素")
public void tes2(){
    DoubleLinkedListSentinel node = new DoubleLinkedListSentinel();
    node.addLast(1);
    node.addLast(2);
    node.addLast(3);
    node.addLast(4);
    int r1 = RandomUtils.nextInt(1, 5);
    int r2 = RandomUtils.nextInt(1, 10);
    logger.error("linked list :{}",node);
    int i = node.removeByIndex(r1);
    if (i == -1){
        logger.error("未找到需要删除的元素,{}",r1);
    }else {
        logger.error("删除成功,{}",r1);
    }
    int j = node.removeByIndex(r2);
    if (j == -1){
        logger.error("未找到需要删除的元素,{}",r2);
    }else {
        logger.error("删除成功,{}",r2);
    }
    logger.error("find linked list :{}",node);
}

在这里插入图片描述

2.7.遍历环形链表

2.7.1.迭代器遍历
// 实现 public class DoubleLinkedListSentinel implements Iterable<Integer> 接口  重写

/**
 * 通过实现迭代器 进行循环遍历
 */
@Override
public Iterator<Integer> iterator() {
    return new Iterator<Integer>() {
        Node p = sentinel.next;
        @Override
        public boolean hasNext() {
            return p != sentinel;
        }

        @Override
        public Integer next() {
            Integer value = p.value;
            p = p.next;
            return value;
        }
    };
}
2.7.2.使用递归进行遍历
/**
 * 递归遍历(递归遍历)
 */
public void loop(Consumer<Integer> before,Consumer<Integer> after){
    recursion(sentinel.next,before,after);
}

/**
 * 递归进行遍历
 * @param node   下一个节点
 * @param before 遍历前执行的方法
 * @param after  遍历后执行的方法
 * @deprecated  递归遍历,不建议使用,递归深度过大会导致栈溢出。建议使用迭代器,或者循环遍历,或者使用尾递归,或者使用栈
 * @see #loop(Consumer, Consumer)
 */
public void recursion(Node node, Consumer<Integer> before, Consumer<Integer> after){
    // 表示链表没有节点了,那么就退出(注意 环形链表的 末尾 不是null 而是头节点)
    if (node == sentinel){
        return;
    }
    // 反转位置就是逆序了
    before.accept(node.value);
    recursion(node.next, before, after);
    after.accept(node.value);
}

2.7.3.测试

	@Test
	@DisplayName("测试-双向环形链表-遍历")
	public void tes3(){
		DoubleLinkedListSentinel node = new DoubleLinkedListSentinel();
		node.addLast(1);
		node.addLast(2);
		node.addLast(3);
		node.addLast(4);
		logger.error("=========== 迭代器遍历链表 ===========");
		for (Integer i : node) {
			logger.error("迭代器遍历链表 :{}",i);
		}

		logger.error("=========== 递归遍历链表 ===========");
		node.loop(it->{
			logger.error("从头部开始遍历 :{}",it);
		},it ->{
			logger.error("从尾部开始遍历 :{}",it);
		});

	}

在这里插入图片描述

3.具体应用场景

3.1.优点

  1. 循环遍历简便: 由于双向环形链表形成了一个闭环,因此在需要循环遍历链表时,可以更加简便地实现,不需要额外的指针变量来记录链表的尾部或头部。
  2. 高效的插入和删除操作: 双向环形链表的节点结构允许在任意位置进行节点的插入和删除操作,并且这些操作通常比较高效,尤其是在头部和尾部进行操作时。
  3. 适用于循环结构数据: 对于需要处理循环结构的数据或需要实现环形队列等特定功能的场景,双向环形链表是一种很自然的数据结构选择。

3.2.缺点

  1. 强引用导致的内存泄漏: 如果双向环形链表中的节点持有对外部对象的强引用,并且这些外部对象的生命周期比链表更长,那么即使链表中的节点不再被使用,这些节点仍然被链表中的引用所持有,从而无法被垃圾回收器回收,导致内存泄漏。
  2. 未正确处理节点的引用关系: 在双向环形链表中,节点之间相互引用,如果在节点删除或者替换的过程中未正确地处理节点之间的引用关系,可能会导致链表中的节点无法被回收,从而引发内存泄漏。
  3. 长期持有迭代器: 如果在遍历双向环形链表的过程中长期持有迭代器对象,而没有正确地释放迭代器对象,可能会导致链表中的节点无法被回收,造成内存泄漏。
  4. 容易产生死循环: 由于环形链表的特性,编写循环遍历的代码时需要特别小心,如果没有正确地处理循环结束的条件,可能会产生死循环,导致程序崩溃或陷入无限循环。
  5. 实现复杂度较高: 相比于普通的单向链表,双向环形链表的实现复杂度较高,需要更多的代码来维护节点之间的引用关系,尤其是在节点的插入和删除操作时需要考虑更多的边界条件。

3.3.应用场景

  1. LRU Cache(最近最少使用缓存): 在LRU缓存中,双向环形链表可以用于维护最近使用的数据项的顺序。每次访问缓存中的数据时,可以将该数据项移到链表的头部,而最少使用的数据项则会被移动到链表的尾部,当缓存空间不足时,可以删除链表尾部的数据项。双向环形链表使得在链表头尾进行插入和删除操作更加高效。
  2. 循环队列: 在某些情况下,需要实现循环队列以存储和处理数据,比如在生产者-消费者模型中。双向环形链表可以用作循环队列的基础数据结构,使得在队列头尾进行数据插入和删除操作更加高效。
  3. 哈希表的冲突解决: 在哈希表中,如果多个键散列到相同的槽位上,就会发生冲突。双向环形链表可以用作哈希表中槽位的链表,用于解决冲突,实现链地址法(Separate Chaining)的哈希表。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/393111.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PFA洗气瓶配空气采样泵用PFA气体吸收瓶的特点

PFA洗气瓶是一种洗去气体中杂质的器皿&#xff0c;是将不纯气体通过选定的适宜液体介质鼓泡吸收&#xff08;溶解或由于发生化学反应&#xff09;&#xff0c;从而洗去杂质气体&#xff0c;以达净化气体的目的。在设计时&#xff0c;四氟球的周围都布满小孔。一般情况下&#x…

【教学类-19-10】20240214《ABAB式-规律黏贴18格-手工纸15*15CM-一页3种图案,AB一组样板,纵向、有边框》(中班)

背景需求 利用15*15CM手工纸制作AB色块手环&#xff08;手工纸自带色彩&#xff09;&#xff0c;一页3个图案&#xff0c;2条为一组&#xff0c;画图案&#xff0c;黏贴成一个手环。 素材准备 代码展示 # # 作者&#xff1a;阿夏 # 时间&#xff1a;2024年2月14日 # 名称&…

LeetCode刷题计划---day2

07 #include <iostream> #include <iomanip> // 头文件用于控制输出格式 using namespace std;int main() {const int n 5; // 等级个数double grade[n] {4.0, 3.0, 2.0, 1.0, 0.0}; // 每个等级对应的分数string input;while (getline(cin, input)) { // 读入一…

我国纯自研水陆两栖大飞机,鲲龙AG600M完成高寒试飞任务

据航空工业官微介绍&#xff0c;近期我国自主研制的大型水陆两栖飞机“鲲龙”AG600M在海拉尔完成最后一项高寒试飞任务。 其动力装置系统、燃油系统、液压系统、飞控系统、航电系统、起落架系统等关键系统通过了高寒地面试验和试飞验证&#xff0c;可满足我国全疆域范围内的森…

如何将阿里云服务器迁移

&#x1f4d1;前言 本文主要是如何将阿里云服务器迁移实现数据转移的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️** &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是青衿&#x1f947; ☁️博客首页&#xff1a;CSDN主页放风讲故事 &#x1f304;每日…

【hcie-cloud】【30】华为云Stack应用安全于防护

文章目录 前言Web技术基础和常见Web漏洞Web技术Web系统组成URL结构Web后端技术HTTP/HTTPS协议Cookie/Session简介OWASP TOP 10OWASP TOP 10 2021年版访问控制失效 - 越权访问控制失效 - 跨站请求伪造&#xff08;CSRF&#xff09;URL不安全跳转应用安全法律法规及行业规范 Web应…

React 更改程序入口点(index.js文件位置变更)

食用前提示&#xff1a;本文基于已经快速配置好的React环境而作&#xff0c;配置React环境详见拙作&#xff1a;React环境配置-CSDN博客~ 一、了解默认入口点 使用create-react-app快速搭建react环境后&#xff0c;npm start启动程序的默认入口点为/src/index(即src目录下的ind…

【JavaEE】_HTTP请求首行

目录 1. URL 2. 方法 2.1 GET方法 2.2 POST方法 2.3 GET与POST的区别 2.4 低频使用方法 1. URL 在mysql JDBC中已经提到过URL的相关概念&#xff1a; 如需查看有关JDBC更多内容&#xff0c;原文链接如下&#xff1a; 【MySQL】_JDBC编程-CSDN博客 URL用于描述某个资源…

揭秘京东商品背后的秘密:一键获取详细数据,打造全新购物体验

京东商品详情原数据API接口技术详解 一、概述 京东商品详情原数据API接口是京东开放平台提供的一套用于获取商品详细信息的接口。通过调用该接口&#xff0c;第三方开发者可以获取包括商品描述、价格、图片、评价等详细信息&#xff0c;进而在自己的应用或网站中展示给用户&a…

听说解锁字节扣子,能轻松搭建你的私人AI助手!

一、引子 几年前低代码平台推出了&#xff0c;这种概念应该是未来的一种趋势&#xff0c;不过一直没有被大面积推广起来&#xff0c;或许技术方面还不算成熟。不过随着科技的发展&#xff0c;区块链技术、元宇宙技术、AI技术这些对我们来说触不可及的技术也已经走进大家的视线…

自定义类型详解 ----结构体,位段,枚举,联合

目录 结构体 1.不完全声明 2.结构体的自引用 3.定义与初始化 4.结构体内存对齐与结构体类型的大小 结构体嵌套问题 位段 1.什么是位段&#xff1f; 2.位段的内存分配 枚举 1.枚举类型的定义 2.枚举的优点 联合&#xff08;共同体&#xff09; 1.联合体类型的声明以…

PyCharm 自动添加文件头注释

PyCharm 自动添加文件头注释 1. File and Code Templates2. Python FileReferences 1. File and Code Templates File -> Settings -> Editor -> File and Code Templates -> Python Script Reformat according to style & Enable Live Templates Created by…

UE4 C++联网RPC教程笔记(一)(第1~4集)

UE4 C联网RPC教程笔记&#xff08;一&#xff09;&#xff08;第1~4集&#xff09; 前言1. 教程介绍与资源2. 自定义 Debug 功能3. Actor 的复制4. 联网状态判断 前言 本系列笔记将会对梁迪老师的《UE4C联网RPC框架开发吃鸡》教程进行个人的知识点梳理与总结&#xff0c;此课程…

第三十五回 梁山泊吴用举戴宗 揭阳岭宋江逢李俊-python中用Shell通配符匹配字符串

宋江被抓住&#xff0c;判脊杖二十&#xff0c;刺配江州牢城。临走时宋太公专门叮嘱他不要入伙梁山。 宋江和差人专门挑小路走&#xff0c;想避开梁山&#xff0c;结果还是被赤发鬼刘唐守到了。大家把宋江请上山&#xff0c;都参拜了宋江。看宋江执意要走&#xff0c;吴用说自…

Python学习路线图

防止忘记&#xff0c;温故知新 进阶路线

思迈特再获国家权威认证:代码自主率98.78%

日前&#xff0c;思迈特软件自主研发的商业智能与数据分析软件&#xff08;Smartbi Insight&#xff09;通过中国赛宝实验室&#xff08;工业和信息化部电子第五研究所&#xff09;代码扫描测试&#xff0c;Smartbi Insight V11版本扫描测得代码自主率为98.78%的好成绩&#xf…

Json格式文件

1.把Java对象转换成Json格式 1.1.导入依赖 这里推荐一个插件Jackson&#xff0c;其提供的类可以让Java的类转换成Jason格式文件 <dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><vers…

Aquarius Fantasy Series Orcs

使用标准管道创建。目前不支持URP或HDRP。 - 如果想将其转换为URP或类似材质。90%的材质可以完美转换。但是树叶材质和岩石顶盖材质无法转换,除非有自己的材质,无论是自己制作的,还是其他资源包。布料也是如此,每块布料都是单面的,使用简单的材质来达到双面效果。所有其他…

【简洁的代码永远不会掩盖设计者的意图】如何写出规范整洁的代码

个人名片&#xff1a; &#x1f981;作者简介&#xff1a;学生 &#x1f42f;个人主页&#xff1a;妄北y &#x1f427;个人QQ&#xff1a;2061314755 &#x1f43b;个人邮箱&#xff1a;2061314755qq.com &#x1f989;个人WeChat&#xff1a;Vir2021GKBS &#x1f43c;本文由…

ChatGPT的大致原理

国外有个博主写了一篇博文&#xff0c;名字叫TChatGPT: Explained to KidsQ」&#xff0c; 直译过来就是&#xff0c;给小孩子解释什么是ChatGPT。 因为现实是很多的小孩子已经可以用父母的手机版ChatGPT玩了 &#xff0c;ChatGPT几乎可以算得上无所不知&#xff0c;起码给小孩…