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

这篇具有很好参考价值的文章主要介绍了从零开始学习数据结构—【链表】—【探索环形链的设计之美】。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

环形链表

1.结构图

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

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

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

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

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

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

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

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

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指向新创建的节点,将哨兵的下一个节点指向新加的节点

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

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);
}

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

2.3.尾部添加数据

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

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

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);
	}

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

2.4.删除头部数据

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

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

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();
}

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

2.5.删除尾部数据

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

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

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();
}

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

2.6.根据内容删除节点

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

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

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);
}

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

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);
		});

	}

从零开始学习数据结构—【链表】—【探索环形链的设计之美】,Java,数据结构,链表,数据结构,java文章来源地址https://www.toymoban.com/news/detail-834519.html

3.具体应用场景

3.1.优点

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

3.2.缺点

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

3.3.应用场景

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

到了这里,关于从零开始学习数据结构—【链表】—【探索环形链的设计之美】的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • [C语言][数据结构][链表] 单链表的从零实现!

    目录 零.必备知识 1.一级指针 二级指针 2. 节点的成员列表     a.数据     b.指向下一个节点的指针. 3. 动态内存空间的开辟 (malloc-calloc-realloc) 一.单链表的实现与销毁          1.1 节点的定义         1.2 单链表的尾插         1.3 单链表的头插         1.4 单链表的尾删  

    2024年04月11日
    浏览(75)
  • 从0开始学C++ 第二十七课 数据结构入门 - 数组与链表

    第二十七课:数据结构入门 - 数组与链表 学习目标: 理解数组的基本概念和操作。 掌握链表的基本结构与特点。 学会在C++中定义和操作数组和链表。 了解数组和链表的基本使用场景。 学习内容: 数组(Array) 概念:数组是一种线性数据结构,用一段连续的内存空间来存储

    2024年01月23日
    浏览(50)
  • 【从零开始拿捏数据结构】 顺序表是什么?它有什么样的特性?结构到底是什么样的?

    🎥 屿小夏 : 个人主页 🔥个人专栏 : 数据结构解析 🌄 莫道桑榆晚,为霞尚满天! ​ 什么是数据结构?我们为什么要学数据结构?数据结构中的顺序表长什么样子?它是怎么运用? ​ 本期我们将对这些一一讲解,彻底明白数据结构的重要性,以及顺序表是一种什么的数据

    2024年02月08日
    浏览(106)
  • 从零开始学数据结构和算法:腾讯Android开发面试记录,已开源_android 开发面试算法

    先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7 深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前! 因此收集整理了一份《2024年最新Android移动开发全套学习资

    2024年04月25日
    浏览(57)
  • Android开发UI新技能,你get这个新技能了吗?(附源码详解),从零开始学数据结构和算法

    2. 文本输入框 val state = +state { “Text Field to input” } TextField( value = state.value, onValueChange = { state.value = it } ) 3. 按钮 Button(text = “咬我啊”, onClick = { Log.v(“test”, “被咬了”) }) 4.弹出框 MaterialTheme { Column { val openDialog = +state { false } Button(“Click me”, onClick = { openDialog.value = true

    2024年04月12日
    浏览(38)
  • 【数据结构】浅谈数据结构-链表【思路+例题学习】

    🏆今日学习目标: 🍀学习算法-数据结构-链表 ✅创作者:贤鱼 ⏰预计时间:30分钟 🎉个人主页:贤鱼的个人主页 🔥专栏系列:算法 🍁贤鱼的个人社区,欢迎你的加入 贤鱼摆烂团 链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中

    2024年01月21日
    浏览(45)
  • 探索树形数据结构,通识树、森林与二叉树的基础知识(专有名词),进一步利用顺序表和链表表示、遍历和线索树形结构

      结点之间有分支,具有层次关系 树的定义 : 树 (tree)是n(n≥0)个有限集。 若n = 0,则称为空树; 若n 0,则它满足如下两个条件: 有且仅有一个特定的称为根(Root)的结点; 其余结点可分为m(m≥0)个互不相交的有限集T1,T2,T3,.....,Tm,其中每一个集合本身又是一棵树,并称为根的

    2024年02月01日
    浏览(53)
  • 从零起步:学习数据结构的完整路径

    🎉欢迎来到数据结构学习专栏~从零起步:学习数据结构的完整路径 ☆* o(≧▽≦)o *☆嗨~我是IT·陈寒🍹 ✨博客主页:IT·陈寒的博客 🎈该系列文章专栏:Java学习路线 📜其他专栏:Java学习路线 Java面试技巧 Java实战项目 AIGC人工智能 🍹文章作者技术和水平有限,如果文中出

    2024年02月11日
    浏览(50)
  • 【c++学习】数据结构中的链表

    链表与线性表相对,链表数据在内存中的存储空间是不连续的,链表每个节点包含数据域和指针域。 下述代码实现了链表及其接口 包括增、删、查、改以及其他一些简单的功能 运行结果: 于 2024-01-23 第一次整理编写 学习时整理,不当之处烦请指正 码字不易,留个赞再走吧

    2024年01月24日
    浏览(55)
  • 数据结构学习分享之双向链表详解

        💓博主CSDN:杭电码农-NEO💓🎉🎉🎉       ⏩专栏分类:数据结构学习分享(持续更新中🫵)⏪🎉🎉🎉       我们上一期说到,两链表中有两个最常用的结构,一个是最简单的无头不循环单向链表,还有一个就是 结构相对比较复杂的带头双向循环链表 ,我们这一章节就来

    2024年02月04日
    浏览(68)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包