Questions 02 (算法题总结、JVM问题总结)

Questions 02

一、算法题总结

1. 两数之和

  • LeetCode1 (https://leetcode-cn.com/problems/two-sum/)

  • 方式1:暴力枚举

    思路:指定第一个数对应的数组索引为i,第二个数对应的数组索引为k,i按数组元素顺序依次遍历时k也同时枚举出后面的所有情况的索引值,当两个索引对应的数组值的和为target时,就将这两个索引放入新数组中并return这个新数组。

    public int[] twoSum(int[] nums, int target) {
        int[] arr = new int[2];
        for (int i = 0; i < nums.length - 1; i++) {
            for (int k = i + 1; k < nums.length; k++) {
                if (nums[i] + nums[k] == target) {
                    arr[0] = i;
                    arr[1] = k;
                    return arr;
                }
            }
        }
        return new int[0];
    }
    
  • 方式2:哈希表

    思路:遍历数组时当前索引对应的数为x,另一个数则为target-x。首先判断map中是否有target-x,若没有就将x对应的数组值作为key同时索引作为value存入map中,若map中有target-x,就将target-x对应的索引和x对应的索引放入新数组中并return这个新数组。

    public int[] twoSum(int[] nums, int target) {
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            if (map.containsKey(target - nums[i])) {
                return new int[]{map.get(target - nums[i]), i};
            }
            map.put(nums[i], i);
        }
        return new int[0];
    }
    

2. 环形链表

  • LeetCode141 (https://leetcode-cn.com/problems/linked-list-cycle/)

  • 方式1:哈希表

    思路:遍历链表的每个节点,每次将节点地址放入HashSet中,利用HashSet的特性(放入的元素不允许重复),当遇到有相同的节点地址值放入HashSet时则该链表为环形链表。

    public boolean hasCycle(ListNode head) {
        HashSet<ListNode> set = new HashSet<>();
        while (head != null) {
            if (!set.add(head)) {
                return true;
            }
            head = head.next;
        }
        return false;
    }
    
  • 方式2:快慢指针

    思路:假定两个指向链表的指针,快指针在前每次移动两个节点,慢指针在后每次移动一个节点,两个指针总会有重合的时候(指向的地址相同时),所以设定循环条件为两个指针地址不同时继续循环,若顺利完成此循环(快慢指针重合时)则链表是环形链表,若中途有指针指向null则不是环形链表。

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

3. 三数之和

  • LeetCode15 (https://leetcode-cn.com/problems/3sum/)

  • 方式1:暴力枚举(Time Limit Exceeded)

    思路:嵌套遍历数组,判断后将符合条件的List放入List中,需要去除内容重复的三元组。

    public List<List<Integer>> removeDuplicate(List<List<Integer>> srcList) {
        for (List<Integer> list : srcList) {
            Collections.sort(list);
        }
        HashSet<List<Integer>> set = new HashSet<>();
        for (List<Integer> list : srcList) {
            set.add(list);
        }
        List<List<Integer>> destList = new LinkedList<>();
        destList.addAll(set);
        return destList;
    }
    
    public List<List<Integer>> threeSum(int[] nums) {
        LinkedList<List<Integer>> linkedList = new LinkedList<>();
        for (int i = 0; i < nums.length - 2; i++) {
            for (int j = i + 1; j < nums.length - 1; j++) {
                for (int k = j + 1; k < nums.length; k++) {
                    if (nums[i] + nums[j] + nums[k] == 0) {
                        List list = Arrays.asList(nums[i], nums[j], nums[k]);
                        linkedList.add(list);
                    }
                }
            }
        }
        return removeDuplicate(linkedList);
    }
    
  • 方式2:双指针

    思路:

    1. 先将给定的数组进行从小到大排序,根据题目nums[i]+nums[j]+nums[k]=0(nums[i]+nums[j]=-nums[k]),选择k作为数字最小的指针,i和j分别在k的右端。

    2. 当索引为k的数组值大于0时(i和j也就大于0),3个数都大于0,这种情况直接跳过。

    3. 当k>0时且索引为k的数组值和索引为k-1的数组值相同时,此时跳过索引为k的数组值,避免得到重复的结果。

    4. 当i<j且nums[i]+nums[j]<-nums[k]时,nums[i]+nums[j]的和需要增大,此时i需要向右移动(i+=1)并跳过重复的元素;

      当i<j且nums[i]+nums[j]>-nums[k]时,nums[i]+nums[j]的和需要减小,此时j需要向左移动(j-=1)并表过重复的元素;

      当i<j且nums[i]+nums[j]=-nums[k]时,将三个索引值存入数组中,执行i+=1和j-=1并跳过重复的nums[i]和nums[j]。

    public List<List<Integer>> threeSum(int[] nums) {
        Arrays.sort(nums);
    
        ArrayList<List<Integer>> res = new ArrayList<>();
    
        for (int k = 0; k < nums.length - 2; k++) {
            if (nums[k] > 0) {
                break;
            }
            if (k > 0 && nums[k] == nums[k-1]) {
                continue;
            }
            int i = k + 1, j = nums.length - 1;
            while (i < j) {
                int sum = nums[k] + nums[i] + nums[j];
                if (sum < 0) {
                    while(i < j && nums[i] == nums[++i]);
                } else if (sum > 0) {
                    while(i < j && nums[j] == nums[--j]);
                } else {
                    res.add(new ArrayList<>(Arrays.asList(nums[k], nums[i], nums[j])));
                    while(i < j && nums[i] == nums[++i]);
                    while(i < j && nums[j] == nums[--j]);
                }
            }
        }
        return res;
    }
    

4. 环形链表 II

  • LeetCode142 (https://leetcode-cn.com/problems/linked-list-cycle-ii/)

  • 方式1:哈希表

    思路:遍历链表的每个节点,每次将节点地址放入HashSet中,利用HashSet的特性(放入的元素不允许重复),当遇到有相同的节点地址值放入HashSet时则该链表为环形链表,然后返回这个节点。

    public ListNode detectCycle(ListNode head) {
        HashSet<ListNode> set = new HashSet<>();
        while (head != null) {
            if (!set.add(head)) {
                return head;
            }
            head = head.next;
        }
        return null;
    }
    
  • 方式2:快慢指针

    思路:假设链表头节点到环入口的距离为e,快慢指针相遇时,距离环入口r,下次到环入口的距离为g,

    可以证明出e = n(r+g)-r,同时n(r+g)代表环n圈的长度。此时设定一个指针指向头节点,这个指针同时和慢指针一起运动,

    慢指针再走e = n(r+g)-r就是n(r+g) -r+r = n(r+g),此时恰好和刚才指向头节点的指针在环入口处相遇,返回此时的这个节点。

    快慢相遇时,慢指针的路径e+r = x

    快慢相遇时,快指针的路径e+n(r+g)+r = 2x

    可得n(r+g) = e+r,也就是e = n(r+g)-r

    public ListNode detectCycle(ListNode head) {
        // 若该链表是空链表或一个节点的链表,直接返回 null
        if(head == null || head.next == null){
            return null;
        }
        // 定义两个快慢指针,指向链表头节点
        ListNode slow = head;
        ListNode fast = head;
        // 只要有指针不为 null ,两个指针就按不同速度移动下去
        while(fast != null && fast.next != null){
            // 慢指针每次移动一个节点
            slow = slow.next;
            // 快指针每次移动两个节点
            fast = fast.next.next;
            // 链表有环,将会在某处重合此时退出循环
            if(slow == fast){
                break;
            }
        }
        // 根据上一步,若退出循环时,有指针指向 null ,则表示链表无环状,直接返回 null
        if(fast == null || fast.next == null){
            return null;
        }
        // 定义一个指针指向链表头节点
        ListNode pre = head;
        // pre 从头节点开始移动,slow 从重合点开始移动
        while(pre != slow){
            pre = pre.next;
            slow = slow.next;
        }
        // 当 pre 和 slow 相遇时,返回 pre
        return pre;
    }
    

二、JVM问题总结

Reference: JavaGuide

1. 创建一个对象的详细流程

  • 对象创建具体步骤

    1. 类加载检查

      虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

    2. 分配内存

      类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式“指针碰撞”“空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

    3. 初始化零值

      内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

    4. 设置对象头

      初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

    5. 执行init方法

      在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

posted @ 2022-04-11 10:17  Ramentherapy  阅读(105)  评论(0)    收藏  举报