Posts

Leetcode18及关于算法的一些思索

Manan
题目描述 给你一个由n个整数组成的数组nums,和一个目标值target。请你找出并返回满足下述全部条件且不重复的四元组[nums[a], nums[b], nums[c], nums[d]]: 0 <= a, b, c, d <= n a, b, c, d互不相同 nums[a] + nums[b] + nums[c] + nums[d] == target 你可以按 任意顺序 返回答案。 题目链接:https://leetcode-cn.com/problems/4sum/ 思路分析 本题与leetcode15题三数之和十分相似,对于三数之和问题,我也在之前的文章中进行了叙述https://mananbc1st.github.io/posts/leetcode15/,其主体算法思想为排序+贪心的策略(三指针求解)。 那么对于四数之和可以看作是三数之和问题的推广,理解了其求解的方式,你可以写出N数之和的算法。 如何从已知的三数之和问题为条件去求解四数之和乃至N数之和的问题呢? 主要思想还是讲问题分解为可以求解的子问题,也就是问题归约法 具体的实现逻辑为:递归 + 回溯。 Java AC代码 private final List<List<Integer>> res = new ArrayList<>(); private void doSearchOpt(int[] nums, int start, int target, List<Integer> container) { if (container.size() == 1) { // 只要将问题递归至三数之和问题,就达到了算法的递归基 // 因为三数之和问题有解 for (int i = start; i < nums.

组合算法(Java实现)

Manan
组合算法 排列组合是一个重要的数学概念,组合通常用来枚举从一个集合中选择特定个数元素的所有结果。 实现与思路阐述 实现方式:递归 + 回溯法。 时间复杂度: O(n ^ x),其中x代表选择元素的个数。 import java.util.ArrayList; import java.util.List; public final class CombinationAlgorithm<ElementType> { private final List<List<ElementType>> res = new ArrayList<>(); private void doSelect(List<ElementType> collection, int start, int selectionNumber, List<ElementType> container) { if (selectionNumber == 0) { res.add(new ArrayList<>(container)); return; } for (int i = start, len = collection.size(); i < len; ++i) { container.add(collection.get(i)); doSelect(collection, i + 1, selectionNumber - 1, container); // 回溯 container.

Leetcode15

Manan
题目描述 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组 注意:答案中不可以包含重复的三元组 题目链接:https://leetcode-cn.com/problems/3sum/ 思路阐述 显然此题可以通过暴力求解的方式,使用三个循环依次遍历数组求解。 暴力的方式不是最优的方式,更为简单直观的方式是:排序 + 三指针。 题目给出的数组是无序的,无序数组的可操作性比较低,因为元素的随机排列使得算法求解失去目的性。在求解之前需要对无序的数组进行排序,然后采用固定一个指针,剩下的两个指针需要根据当前的结果选择贪心的策略进行移动。 首先最外层循环变量front是我们需要固定下来的指针。一旦front固定下来,只需考虑front之后的子序列。 在子序列中,一个指针定义为mid位于子序列的最开头,另一个指针定义为rear位于子序列的最末尾。mid指针需要位于rear指针的左侧。因为整个序列有序,所以子序列也有序(假设序列递增) 所以由当前三个指针front, mid和rear便可以确定序列中的三个数的组合。 $ nums[front] + nums[mid] + nums[rear] == 0 $ $ nums[front] + nums[mid] + nums[rear] < 0 $ $ nums[front] + nums[mid] + nums[rear] > 0 $ case 1: 符合题目要求的情况 case 2: 需要将mid指针向后移动(序列递增)找到一个更大的数。 case 3: 需要将rear指针向前移(序列递增)找一个更小的数。 题目要求,不可以出现重复的元组组合,所以需要进行去重操作。

Java volatile关键字作用

Manan
概述 volatile是java中的一个关键字,用于修饰变量。被此关键修饰的变量可以禁止对此变量操作的指令进行重排,还有保持内存的可见性。 简言之它的作用就是: 禁止指令重排 保持内存的可见性 禁止指令重排 CPU在执行代码时,为了提高执行效率,有时会将代码乱序执行。但是乱序也不是随随便便的乱序,而是在一定规则下,对指令进行重排然后执行。指令重排在单线程下没有什么问题,但是在多线程环境下容易造成并发安全问题。 保持内存的可见性 何谓之内存的可见性,其实笔者在Java线程安全问题一文中对此问题进行过阐述。线程是一种资源,线程在执行代码时有自己的工作内存(线程执行的堆栈)。一般来说,一些共享变量存在于堆内存中,线程对于共享变量的操作实际上对自己工作内存中共享变量的副本进行操作,线程并不会直接操作堆内存的中的共享变量。 这里我们可以通过字节码进行验证,首先我们的java源代码是对变量a进行一个自增操作 => a++,而其对应的字节码为: getstatic #2 iconst_1 iadd putstatic #2 return 从字节码层面可以体现出工作内存与主存。但是,Java内存可见性的控制并不是在字节码层面,而是在JVM层面。即使你对变量a使用volatile修饰,那么编译之后的字节码也没有变化。 JVM定义了对于内存的8种操作: 这些操作是JVM层面的操作,在Java源代码中并不能感受到。线程在所操作的共享变量,其实是存在于自己线程栈中的变量副本。在多线程并发的情况下,如果有其他线程修改了主存中的值,那么其他线程无法感知这种修改。因为线程在通过READ-LOAD操作拷贝完副本之后,之后线程对于数据的操作都是对于副本进行的。这也就是内存可见性的问题的来源,简言之就是线程之间无法感知对于主存共享变量的修改。 volatile关键字就是解决了这样的问题,使得线程对于主存具有一定的可见性。其解决方案是对于volatile标注的变量,每次在使用之前都要重新从主存中加载,同时每次对于变量完成修改后,要及时的将变量写回主存。 通过这样的操作,每个线程就能感知到内存中变量的变化,并及时更新自己副本的值。不过需要注意的是,volatile关键字并不能保障并发的安全性。尽管每次在使用之前都会更新值,但是这并没有解决变量访问的有序性。所以在高并发场景下依然会出现问题。

Java线程池内部原理

Manan
概述 ThreadPoolExecutor是Java语言对于线程池的实现。池化技术是一种复用资源,减少开销的技术。线程是操作系统的资源,线程的创建与调度由操作系统负责,线程的创建与调度都要耗费大量的资源,其中线程创建需要占用一定的内存,而线程的调度需要不断的切换线程上下文造成一定的开销。同时线程执行完毕之后就会被操作系统回收,这样在高并发情况下就会造成系统频繁创建线程。 为此线程池技术为了解决上述问题,使线程在使用完毕后不回收而是重复利用。如果线程能够复用,那么我们就可以使用固定数量的线程来解决并发问题,这样一来不仅节约了系统资源,而且也会减少线程上下文切换的开销。 参数 ThreadPoolExecutor的构造函数有7个,它们分别是: corePoolSize(int):线程池的核心线程数量 maximumPoolSize(int):线程池最大线程数量 keepAliveTime(long):保持线程存活的时间 unit(TimeUnit):线程存活时间单位 workQueue(BlockingQueue):工作队列,用于临时存放提交的任务 threadFactory(ThreadFactory):线程工厂,用于创建线程 handler(RejectedExecutionHandler):任务拒绝处理器,当线程池无法再接受新的任务时,会交给它处理 一般情况下,我们只使用前五个参数,剩余两个我们使用默认参数即可。 任务提交逻辑 其实,线程池创建参数都与线程池的任务提交逻辑密切相关。根据源码描述可以得知:当提交一个新任务时(执行线程池的execute方法)会经过三个步骤的处理。 当任务数量小于corePoolSize时,线程池会创建一个新的线程(创建新线程由传入参数threadFactory完成)来处理任务,哪怕线程池中有空闲线程,依然会选择创建新线程来处理。 当任务数量大于corePoolSize时,线程池会将新任务压入工作队列(参数中传递的workQueue)等待调度。 当新提交的任务无法压入工作队列时,会检查当前任务数量是否大于maximumPoolSize。如果小于maximunPoolSize则会新建线程来处理任务(这时我们的keepAliveTime参数就起作用了,它主要作用于这种情况下创建的线程,如果任务数量减小,这些线程闲置了,那么在超过keepAliveTime时间后就会被回收)。如果大于了maximumPoolSize就会交由任务拒绝处理器handler处理。 线程池状态 正如线程有不同的状态一样,线程池也拥有不同的运行状态。源码中提出,线程池有五种状态,分别为: RUNNING:运行状态,不断接收任务并处理它们。 SHUTDOWN:关闭状态,不接收新任务,但是会处理工作队列中排队的任务。 STOP:停止状态,不接收新任务,清空工作队列且不会处理工作队列的任务。 TIDYING:待终止状态,此状态下,任务队列和线程池都为空。 TERMINATED:终止状态,线程池关闭。 如何让线程不被销毁 文章开头说到,线程在执行完毕之后会被操作系统回收销毁,那么线程池时如何保障线程不被销毁?首先看一个测试用例: public static void testThreadState() { Thread thread = new Thread(() -> System.out.println("Hello world")); // 创建一个线程 System.out.println(thread.getState()); // 此时线程的状态为NEW thread.start(); // 启动线程,状态为RUNNING System.out.println(thread.getState()); try { thread.join(); System.out.println(thread.getState()); // 线程运行结束,状态为TERMINATED thread.start(); // 此时再启动线程会发生什么呢? } catch (InterruptedException e) { e.

leetcode12

Manan
题目描述 题目链接:https://leetcode-cn.com/problems/integer-to-roman/ 思路 首先罗马数字是一种数字符号表现形式,罗马数字对于数的表示法依然采用了十进制的形式,这一点很重要,尽管会有单独的符号来表示5,50, 500等,但是这些字符仅仅是符号表示的辅助符号。 数位表示符: 个位 十位 百位 千位 I X C M 辅助表示符: 个位 十位 百位 V L D 罗马数字表示规则如下: 若当前数位上的数字小于4,直接用当前数位表示符表示。 若当前数位上的数字等于4, 用当前数位表示符 + 当前数位辅助表示符进行表示。 若当前数位上的数字大于等于5, 用当前数位辅助表示符 + 当前数位表示符进行表示。 若当前数位上的数字等于9,用当前数位表示符 + 下一数位表示符进行表示。 Java AC代码 var stack = new Stack<Character>(); // 1 10 100 1000 var bitSymbol = new char[] {'I', 'X', 'C', 'M'}; // 5 50 500 var assistantBitSymbol = new char[] {'V', 'L', 'D'}; var bit = 0; for(;;) { var bitNumber = num % 10; var symbol = bitSymbol[bit]; if (bitNumber < 4) { for (int i = 0; i < bitNumber; ++i) { stack.