ARTS打卡第十二周

本文介绍三种检测链表中是否存在环的有效算法:利用哈希集缓存节点、修改链表指向特殊节点及快慢指针法。这些方法分别适用于不同场景,并详细探讨了它们的时间与空间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Algorithm:141. 环形链表
https://leetcode-cn.com/problems/linked-list-cycle/submissions/

给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

进阶:
你能用 O(1)(即,常量)内存解决此问题吗?

解法一:用hashSet缓存遍历过的节点,遇到相同节点表示有环。时间复杂度O(n),空间复杂度O(n)

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        Set<ListNode> set = new HashSet<>();
        
        while(head != null) {
            if(set.contains(head))
                return true;
            
            set.add(head);
            head = head.next;
        }
        return false;
    }
}

解法二(进阶):将遍历过的节点指向一个特殊节点,最后如果又回到这个特殊节点,就表示有环。时间复杂度O(n),空间复杂度O(1).
这个算法的弊端是破话了原链表。

    public boolean hasCycle(ListNode head) {
        ListNode special = new ListNode(-1);
        ListNode p = head;
        ListNode q = head == null ? null : head.next;
        
        while(q != null && q != special)  {
            p.next = special;
            p = q;
            q = q.next;
        }
        
        return q == special;
    }

解法三(进阶):快慢指针,快指针比慢指针多移动一步,如果有环,最终快指针会追上慢指针。时间复杂度O(n),空间复杂度O(1).

    public boolean hasCycle(ListNode head) {
        ListNode fast = head == null ? null : head.next;
        ListNode slow = head;
        
        while(fast != null && fast != slow) {
            if(fast.next == null)
                return false;
            fast = fast.next.next;
            slow = slow.next;
        }
        return fast != null && fast == slow;
    }

Review: What Is MLAG Networking?

原文链接:http://www.fiber-optic-cable-sale.com/mlag-networking-wiki.html

MLAG,全称是Multi-Chassis Link Aggregation Group(多机架链路聚合组),是一种链路聚合技术,Juniper将其称为MC-LAG,Cisco将其称为mLACP 。

MLAG既可以增加带宽,又可以增强可靠性。MLAG从LAG发展而来,但相比于LAG的链路冗余,MLAG提供节点级别的冗余;相比于STP(Spanning Tree Protocol),HA MLAG不需要浪费端口就能防止拓扑环路。

参考资料:
https://en.wikipedia.org/wiki/MC-LAG
详解交换机虚拟化技术VRRP、堆叠、M-LAG及其区别-信锐技术

Tip: 使用Jersey的Response返回JDK自带数据类型的集合

例如,我们想返回一个String类型的list。

List<String> stringList = Arrays.asList("a", "b", "c");

比较方便的做法是用一个POJO类型包装一下:

@Data
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class StringListVO implements Serializable {
    private List<String> data;
}

#然后在代码里直接返回这个POJO对象
StringListVO stringListVO = new StringListVO();
stringListVO.setData(stringList);
return ResponseUtil.success(stringListVO);

这种做法有一个坑要注意,就是POJO类不能定义有参构造方法,否则就会抛出MessageBodyWriter not found 异常

一些尝试

我们无法直接将List<String> 放到Response的entity里,也就是说像下面的写法都是不行的,都会抛出MessageBodyWriter not found 异常:

#写法一:
return Response.ok(stringList).build();
#写法二:
GenericEntity<List<String>> entity = new GenericEntity<List<String>>(stringList){};
return Response.ok(entity).build();

如果是`String[]`数组,将其直接放到Response里,是可以的,只不过显示效果是这样的:
[
	{
		"type": "string",
		"value": "a"
	},
	{
		"type": "string",
		"value": "b"
	},
	{
		"type": "string",
		"value": "c"
	},		
]

如果我们想把StringListVO搞得更通用一些,定义成范型,就像这样:

@Data
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class ListVO<T> implements Serializable {
    private List<T> data;
}

#然后在代码里返回这个POJO对象
ListVO<T> stringListVO = new ListVO<>();
stringListVO.setData(stringList);
GenericEntity<ListVO<String>> entity = new GenericEntity<ListVO<String>>(stringListVO){};
return Response.ok(entity).build();

这样也是不行的,而且没有异常输出。

Share: 分析源码,学会正确使用 Java 线程池

https://my.oschina.net/editorial-story/blog/3107684

异常处理

  1. 通过new Thread 方式创建的线程,Runnable 接口不允许抛出异常,异常只会发送到 System.err 。可以通过设置 UncaughtExceptionHandler 来捕获异常。
  2. 通过 execute 方法提交到线程池,可以继承 ThreadPoolExecutor 重写其 afterExecute 方法处理异常。
  3. 通过 submit 方法提交到线程池,其异常只有在调用返回的 Futureget 方法的时候,异常才会抛出来,抛出ExecutionException异常,那么调用它的getCause方法就可以拿到最原始的异常对象了。

线程数设置

  1. CPU密集型,建议核心线程数设为CPU核数+1,最大线程数设为CPU核数*2;
  2. IO密集型,建议核心线程数设为CPU核数4,最大线程数设为CPU核数5;

如何正确关闭一个线程池

shutdown 方法优雅关闭,会拒绝新任务,但已添加到线程池的任务仍然会执行;
shutdownNow 方法立即关闭,会拒绝新任务,并丢弃已经在线程池队列中的任务,同时设置每个线程的中断标记;

建议:先调用shutdown 方法拒绝新任务,然后调用awaitTermination方法设置超时时间。当awaitTermination方法返回false时,表示超时了,此时可以尝试调用 shutdownNow 方法,这就要求提交到线程池的任务能够响应中断,做一些资源回收的工作。

线程池中的其他有用方法

prestartAllCoreThreadsThreadPoolExecutor 类的方法,用来预先创建所有的核心线程,避免第一次调用时创建线程的开销。
setCorePoolSize方法和setMaximumPoolSize方法,在线程池创建完毕之后更改其线程数。建议的一种临时扩容的方法:

  1. 起一个定时轮询线程(守护类型),定时检测线程池中的线程数,具体来说就是调用getActiveCount方法。
  2. 当发现线程数超过了核心线程数大小时,可以考虑将CorePoolSize和MaximumPoolSize的数值同时乘以2,当然这里不建议设置很大的线程数,因为并不是线程越多越好的,可以考虑设置一个上限值,比如50、100之类的。
  3. 同时,去获取队列中的任务数,具体来说是调用getQueue方法再调用size方法。当队列中的任务数少于队列大小的二分之一时,我们可以认为现在线程池的负载没有那么高了,因此可以考虑在线程池先前有扩容过的情况下,将CorePoolSize和MaximumPoolSize还原回去,也就是除以2。

其他注意事项

  • 线程池中的队列是共享的,所以会有很多锁。如果想提高性能,可以创建一个单线程的线程池列表,用这个线程池列表来实现多线程,这就是Netty EventLoop的思路。也可以使用Disruptor。

  • 任何情况下都不应该使用可伸缩线程池(线程的创建和销毁开销是很大的),这个似乎有些绝对,但绝大多数情况下是不应该用的。典型例子就是 Executors.newCachedThreadPool,线程数量不可控,创建和销毁开销大,这种线程池应该用在使用频率很低并且性能不敏感的场景,且最好自定义线程数量。

  • 任何情况下都不应该使用无界队列,单测除外。有界队列常用的有ArrayBlockingQueue和LinkedBlockingQueue,前者基于数组实现,后者基于链表。从性能表现上来看,LinkedBlockingQueue的吞吐量更高但是性能并不稳定,实际情况下应当使用哪一种建议自行测试之后决定。顺便说一句,Executors的newFixedThreadPool采用的是LinkedBlockingQueue。

  • 推荐自行实现RejectedExecutionHandler,JDK自带的都不是很好用,你可以在里面实现自己的逻辑。如果需要一些特定的上下文信息,可以在Runnable实现类中添加一些自己的东西,这样在RejectedExecutionHandler中就可以直接使用了

### ARTS打卡 Java 学习或项目进展 #### 一、Algorithm 算法练习 在算法方面,最近研究了回文验证问题中的双指针方法。通过实现 `validPalindrome` 函数来判断给定字符串是否可以通过删除最多一个字符形成回文串[^2]。 ```cpp class Solution { public: bool validPalindrome(string s) { int i = 0; int j = s.size() - 1; int diffCount = 0; while (i < j) { if (s[i] == s[j]) { ++i; --j; } else { if (diffCount > 0) return false; // 尝试移除左边或右边的一个字符并继续比较剩余部分 string sub1 = s.substr(i + 1, j - i); string sub2 = s.substr(i, j - i); return is_palindrome(sub1) || is_palindrome(sub2); } } return true; // 辅助函数用于检测子串是否为回文 auto is_palindrome = [](const std::string& str){ int l = 0, r = str.length() - 1; while(l<r && str[l]==str[r]){ ++l;--r; } return l>=r; }; } }; ``` 此版本改进了原始逻辑,在遇到不匹配的情况时不再直接修改原字符串而是创建两个新的子串分别测试其合法性,从而提高了代码可读性和效率。 #### Review 技术文章阅读心得 关于数据库操作的学习笔记中提到 MySQL 支持四种不同的事务隔离级别:未提交读(Read Uncommitted),已提交读(Read Committed),可重复读(Repeatable Read),序列化(Serializable)[^1]。每种级别的特性决定了并发环境下数据的一致性程度以及性能表现之间的权衡关系。 另外还探讨了两种常见的锁机制——悲观锁(Pessimistic Locking) 和乐观锁(Optimistic Locking) 的原理及其适用场景: - **悲观锁** 假设冲突不可避免,因此总是先锁定资源再执行更新动作; - **乐观锁** 则认为大多数情况下不会发生竞争,仅当实际发生写入时才检查是否有其他更改影响到目标对象。 这两种策略各有优劣,具体选择取决于应用的具体需求和环境特点。 #### Tip 技巧总结 对于上述提及的内容,建议开发者们理解各自系统的默认配置,并根据业务逻辑调整合适的参数设置;同时也要熟悉如何利用编程语言提供的工具去处理并发控制问题,比如 Java 中可以借助框架如 Spring 提供的支持简化分布式事务管理过程。 #### Share 经验交流 分享过程中发现很多同学对多线程下的共享变量可见性和原子性的概念存在误解。实际上,Java 内存模型规定了 volatile 关键字能保证变量的即时可见性但不具备原子性保障,而 synchronized 或者 ReentrantLock 可以提供更强大的同步功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值