ARTS_WEEK10

ARTS_WEEK10

Posted by CDz on February 9, 2020

这周主要主攻方面:

  • 算法
    • 复习链表LeetCode练习题
    • 排序LeetCode练习
    • O(n^2)排序(冒泡、插入、选择)
    • O(nlogn)排序(快排、归并)
  • 多线程
    • 视频和专栏结合学习
    • 围绕死锁
  • 专栏
    • MySQL的锁问题
      • 数据库锁
      • 表级锁
      • 行级锁
    • 设计模式
      • 跟着专栏优化ID生成器代码
      • 从可用到好用
        • 可读性
        • 可测性
        • 添加单元测试
        • 添加注释

          1. although

          1.1. O(n^2)排序

          1.1.1. 冒泡排序

/**
     * 冒泡排序
     * 核心——只交换相邻两个元素
     * <p>
     * 时间复杂度 O(N^2)
     * 空间复杂度 O(1)
     * <p>
     * 有序性
     *
     * @param arr
     */
    private void bubbleSort(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            boolean flag = false;
            for (int j = 0; j < arr.length - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    swap(arr, j, j + 1);
                    //优化如果在一次遍历中没有进行交换 说明已经是有序数列
                    flag = true;
                }
            }
            if (!flag) break;
        }
    }

1.1.2. 插入排序

/**
     * 插入排序
     * 核心,想象前半部分是有序数列
     * @param arr
     */
    private void insertSort(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            int e = arr[i];
            int j;
            for ( j = i; j > 0 && e < arr[j-1]; j--) {
                //从后往前遍历 有序数组
                //如果 需要插入的数字小于有序数组取出数字,
                //此位置往后挪一位
                arr[j]=arr[j-1];
            }
            arr[j] = e;
        }
    }

1.1.3. 选择排序

 /**
     * 选择排序
     * 最简单实现的一种
     * 不保证有序性,很少使用
     * @param arr
     */
    private void selectSort(int[] arr){
        for (int i = 0; i < arr.length; i++) {
            for (int j = i; j < arr.length; j++) {
                if (arr[i]>arr[j]){
                    swap(arr,i,j);
                }
            }
        }
    }

1.2. O(nlogn)排序

1.2.1. 归并排序

public void mergeSort(int[] arr) {

        mergeSort(arr, 0, arr.length - 1);
    }

    private void mergeSort(int[] arr, int left, int right) {

//        if (left>=right)return ;
//        //这里可能 int溢出
//        int mid = (left+right)/2;
//
//        mergeSort(arr,left,mid);
//        mergeSort(arr,mid+1,right);
//
//        merge(arr,left,mid,right);


        if (left >= right) {
            return;
        }
        int mid = (right + left) / 2;
        //从左边开始分割
        mergeSort(arr, left, mid);
        System.out.println("================");
        //从右边开始分割
        mergeSort(arr, mid + 1, right);
        //归并
        merge(arr, left, mid, right);

    }

/**
     * 合并两个有序数组
     *
     * left : [left,mid]
     * right: [mid+1,right]
     *
     * @param arr
     * @param left
     * @param mid
     * @param right
     */
    private void merge(int[] arr, int left, int mid, int right) {
        //左数组**必须是 left~mid+1**
        //右数组**必须是 mid+1~right + 1**
        //原因与前面分割默认有关
        //从左边开始分割
        // mergeSort(arr, left, mid); 这里的分割左边是 left~mid (包括mid)
        // 从右边开始分割
        // mergeSort(arr, mid + 1, right); mid+1~right 包括right

        //然Arrays.copyOfRange方法拷贝数组 是不包含 右区间的
        int[] leftarr = Arrays.copyOfRange(arr, left, mid+1);
        int[] rightarr = Arrays.copyOfRange(arr, mid+1, right + 1);


        int i = left;
        int l = 0;
        int r = 0;
        while (i-left < leftarr.length && i-left < rightarr.length) {
            if (leftarr[l]<rightarr[r]){
                arr[i] = leftarr[l];
                l++;
            }else {
                arr[i]=rightarr[r];
                r++;
            }
            i++;
        }

        while (l<leftarr.length){
            arr[i]=leftarr[l];
            i++;
            l++;
        }

        while (r<rightarr.length){
            arr[i]=rightarr[r];
            i++;
            r++;
        }
    }

1.2.2. 快速排序

关键在于找第K大的数。 注意两点(可优化的两点):

  • 数据接近有序性,算法退化为O(n^2)并且会栈溢出
    • 随机选取一个数作为比对对象
  • 数据重复元素较多时,也会导致栈溢出
    • 三路查找(荷兰旗问题解法)
 	public  void quickSort(int[] arr){
        quickSort(arr,0,arr.length-1);
    }

    private  void quickSort(int[] arr, int left, int right) {
        if (left>=right)return;

        int p = partition(arr,left,right);
        quickSort(arr,left,p);
        quickSort(arr,p+1,right);
    }
 /**
     *
     *
     * @param arr
     * @param left
     * @param right
     * @return
     */
    private int partition(int[] arr, int left, int right) {

        /**
         *     这种可能出现栈溢出的情况
         *     当接近有序时,会退化到O(n^2),调用栈为n
         *
         *     解决办法 随机交换一个 left~right的下标
         */
        //随机交换一个 left~right的下标
        Week10N2Sort.swap(arr,left,left+new Random().nextInt(right-left+1));
        //找出[left,right]中 arr[left]是第几大位置
        int p = arr[left];
        // j 左边所有都小于 p,右边所有都大于p
        int j = left;

        for (int i = left+1; i <= right; i++) {
            if (arr[i]<p){
                j++;
                Week10N2Sort.swap(arr,i,j);
            }
        }
        Week10N2Sort.swap(arr,left,j);
        return j;
    }

三路查找方式:

	public void quickSort3(int[] arr, int left, int right) {
        if (left >= right) return;

        partition3(arr, left, right);
    }

//如果重复的元素比较多
    //
    // 经典荷兰旗问题:
    //中间一路是与p相等,左边小于p,右边大于p
    //
    //
	private void partition3(int[] arr, int left, int right) {
        Week10N2Sort.swap(arr, left, left + new Random().nextInt(right - left + 1));
        int p = arr[left];

        //初始
        int m = left;
        int p0 = left;
        int p2 = right;

        while (p2 >= m) {
            if (arr[m] > p) {
                Week10N2Sort.swap(arr, m, p2);
                p2--;
            } else if (arr[m] < p) {
                Week10N2Sort.swap(arr, m, p0);
                m++;
                p0++;
            } else {
                m++;
            }

        }

        Week10N2Sort.swap(arr, left, p0);
        //直接忽略中间相同部分
        quickSort(arr, left, p0);
        quickSort(arr, p2, right);
    }

1.3. LeetCode排序题目

1.3.1. 数组中的第K个最大元素

两种方式,将每个元素放入最小堆中(最小堆结构放入就是有序的),然后再弹出多于的几个元素,最后最小堆中,最大的那个就是返回的值。

但是这个方法,内存占用比较大。

  /**
     * 最小堆的方式
     * @param nums
     * @param k
     * @return
     */
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
        for (int i = 0; i < nums.length; i++) {
            priorityQueue.add(nums[i]);
            if (priorityQueue.size() > k) {
                priorityQueue.poll();
            }
        }
        return priorityQueue.poll();
    }

JDK的排序,取出倒数第K个值,因为JDK的排序是融合了几种排序的,性能非常好。

/**
     * 快速排序方式
     * @param nums
     * @param k
     * @return
     */
    public int findKthLargest1(int[] nums, int k) {
        Arrays.sort(nums);
        return nums[nums.length-k];
    }

1.3.2. 根据字符出现频率排序

桶排序

public static String frequencySort(String s) {

        Map<Character, Integer> counts = new HashMap<>();

        for (int i = 0; i < s.length(); i++) {
            counts.put(s.charAt(i),counts.getOrDefault(s.charAt(i),0)+1);
        }

        List[] lists = new List[s.length()+1];

        Set<Map.Entry<Character, Integer>> entries = counts.entrySet();

        for (Map.Entry<Character, Integer> entry : entries) {
            //字符
            Character key = entry.getKey();
            //频率
            Integer value = entry.getValue();

            if (lists[value]==null){
                lists[value]=new ArrayList();
            }
            lists[value].add(key);
        }

        StringBuilder sb = new StringBuilder();
        for (int i = lists.length-1; i > 0; i--) {
            if (lists[i]==null)continue;
            List<Character> list = lists[i];
            for (Character o : list) {
                for (int j = 0; j < i; j++) {
                    sb.append(o);
                }
            }
        }

        return sb.toString();
    }

1.3.3. 颜色分类

经典荷兰旗问题,使用快速排序思想,三路寻址。

package com.myserious.code.cdz;

/**
 * 75. 颜色分类
 * 给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
 * <p>
 * 此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
 * <p>
 * 注意:
 * 不能使用代码库中的排序函数来解决这道题。
 * <p>
 * 示例:
 * <p>
 * 输入: [2,0,2,1,1,0]
 * 输出: [0,0,1,1,2,2]
 * 进阶:
 * <p>
 * 一个直观的解决方案是使用计数排序的两趟扫描算法。
 * 首先,迭代计算出0、1 和 2 元素的个数,然后按照0、1、2的排序,重写当前数组。
 * 你能想出一个仅使用常数空间的一趟扫描算法吗?
 * <p>
 * 来源:力扣(LeetCode)
 * 链接:https://leetcode-cn.com/problems/sort-colors
 * 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
 */
public class Week10MSortColors75 {

    //荷兰旗问题
    //维护三个指针
    //初始值:
    //p0=0 p2=len-1
    //curr=0

    /**
     * 当 curr = 0,curr与p0交换,curr++,p0++
     * 当 curr = 1,curr++
     * 当 curr = 2,curr与p2交换, p2--
     *      这里curr不需要++,因为交换过来之后curr当前的元素没有被扫描过
     * <p>
     * 当curr<=p2时退出(当curr==p2是可以进入循环的)
     *
     * @param nums
     */
    public void sortColors(int[] nums) {

        int curr = 0;
        int p0 = 0;
        int p2 = nums.length - 1;

        /**
         * 当curr==p2是可以进入循环的
         */
        while (curr<=p2){
            if (nums[curr]==0){
                swap(nums,curr,p0);
                curr++;
                p0++;
            }else if (nums[curr]==1){
                curr++;
            }else if (nums[curr]==2){
                swap(nums,curr,p2);
                p2--;
            }
        }
    }
    public static void swap(int[] arr, int i, int j) {
        int temp;
        temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

1.3.4. 前 K 个高频元素

两种方式,一种最小堆方式。一种使用桶排序思想。

package com.myserious.code.cdz;

import java.util.*;

/**
 * 347. 前 K 个高频元素
 * 给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
 *
 * 示例 1:
 *
 * 输入: nums = [1,1,1,2,2,3], k = 2
 * 输出: [1,2]
 * 示例 2:
 *
 * 输入: nums = [1], k = 1
 * 输出: [1]
 * 说明:
 *
 * 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
 * 你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。
 *
 * 来源:力扣(LeetCode)
 * 链接:https://leetcode-cn.com/problems/top-k-frequent-elements
 * 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
 */
public class Week10MTopKFrequent215 {

    public List<Integer> topKFrequent(int[] nums, int k) {

        //hash表统计 频率
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            map.put(nums[i],map.getOrDefault(nums[i],0)+1);
        }

        //对频率排序
        //第一种方法
        //最小堆
        //堆的使用地方:
        //需要一个个放入元素,并且放置后就是有序的情况
        PriorityQueue<Integer> queue = new PriorityQueue<>((n1,n2)->map.get(n1)-map.get(n2));

        Set<Integer> keyset = map.keySet();
        for (Integer key : keyset) {
            queue.add(key);
            if (queue.size()>k){
                queue.poll();
            }
        }

        ArrayList<Integer> list = new ArrayList<>();

        while (!queue.isEmpty()){
            list.add(queue.remove());
        }

        Collections.reverse(list);
        return list;
    }

    /**
     * 桶排序思想
     * @param nums
     * @param k
     * @return
     */
    public List<Integer> topKFrequent1(int[] nums, int k) {
        //hash表统计 频率
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            map.put(nums[i],map.getOrDefault(nums[i],0)+1);
        }
        //桶
        //将频率作为下标
        //使用 List数组 原因是,频率可能相同的元素
        List[] bucket = new List[nums.length+1];

        Set<Integer> keyset = map.keySet();
        for (Integer key : keyset) {
            Integer index = map.get(key);
            if (bucket[index]==null){
                bucket[index] = new ArrayList<Integer>();
            }
            bucket[index].add(key);
        }
        ArrayList<Integer> res = new ArrayList<>();
        //倒着遍历桶
        for (int i = bucket.length-1; i >0&& res.size()<k; i--) {
            if (bucket[i]==null)continue;
            res.addAll(bucket[i]);
        }
        return res;
    }

}

2. review

2.1. RedisLRU与LFU策略

 redis中LRU与LFU算法解释   redis中LRU与LFU官方文档 

Redis的LRU策略并不是严格意义上的LRU,而是近似LRU算法。

原因:LRU会消耗大量内存,并且还需要进行排序消耗资源。

Redis的近似LRU策略是:

  1. 在RedisObject类中定义访问时间
    1. RedisObject类:在Redis中所有的类型都被包装成RedisObject对象
    2. 比如所有的key都是string类型的RedisObject对象
    3. RedisObject对象里定义有类型和长度等等,是底层定义对象
  2. 随机取出5个key,然后空转最长的key
    1. 从什么地方取出,是可以设置不同的策略
      1. noeviction:直接抛出异常
      2. allkeys-lru:从所有key中
      3. volatile-lru:所有设置超时时间key
      4. allkeys-random: 所有key随机删除
      5. volatile-random: 所有设置超时时间key随机删除
      6. volatile-ttl:设置超时时间中最快到期的key 这样的策略可以很大效率的提升性能,也能够近似LRU算法。

LRU算法,只是在时间间隔上做了处理,但是其实更准确的可以加上一个访问频率,可以更有效的提升命中率。 加上访问频率,就是LFU算法。

Redis的近似LFU策略: Redis一样在LFU上做了近似,原因还是性能要求。

两个关键参数

lfu-log-factor 10 //对数因子 越高分辨率越高
lfu-decay-time 1  //半衰期时间 单位分钟

对数因子数字与分辨率关系:

+--------+------------+------------+------------+------------+------------+
| factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |
+--------+------------+------------+------------+------------+------------+
| 0      | 104        | 255        | 255        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 1      | 18         | 49         | 255        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 10     | 10         | 18         | 142        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 100    | 8          | 11         | 49         | 143        | 255        |
+--------+------------+------------+------------+------------+------------+
  • 只使用0255数字来计数
  • 默认一分钟为一个半衰期
    • 一分钟没有访问就降级
  • 使用对数因子来控制统计分辨率
    • 数字越高分辨率越大

通过此方式,巧妙实现能近似的功能,还保证了性能。

Redis在底层所有的算法与数据结构,都是一个目标为了提高性能,是一个值得研究的好框架。

3. tip

3.1. 算法

3.2. 多线程

3.2.1. 锁的可见性

class SafeCalc {
  long value = 0L;
  long get() {
    return value;
  }
  synchronized void addOne() {
    value += 1;
  }
}

此写法不具备可见性。 正确写法:

class SafeCalc {
  long value = 0L;
  synchronized long get() {
    return value;
  }
  synchronized void addOne() {
    value += 1;
  }
}

3.2.2. 死锁的必要条件

  1. 互斥,共享资源X与Y只能被一个线程占用
  2. 占用且等待,当线程T1获取到共享资源X锁时,在等待锁Y时,不释放锁X。
  3. 不可抢占,其他线程不能强行抢占线程T1占有的锁
  4. 循环等待,线程T1等待线程T2占有的锁,线程T2等待T1占有的锁

这四个条件必须同时满足时,才会出现死锁。

也就是说,只要破解一个条件就可以避免死锁的发生。

3.2.3. 破解死锁的方法

  1. 对于“占有且等待”的条件,可以一次性申请所有的锁(也就是说申请多个锁封装成一个原子操作)
  2. 对于“不可抢占”这个条件,当占用部分锁,并且进一步申请其他锁资源时,设置超时时间,一旦获取不到,释放已持有的锁
  3. 对于“循环等待”,可以按照规定的顺序获取锁(数据库ID),或者可以使用对象的Hash值

3.2.4. 定位死锁的方式

  • jstack后跟Java线程PID,可以拿出当时的栈信息(保存事故现场)
  • ThreadMXBean类,代码方式检测死锁

    3.2.5. 枚举类型单列模式为什么是最佳方法?

  • 《effective Java》中明确表达观点:虽然枚举实现单例模式的方法没有被广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方式
  • 写法简单
  • 线程安全有保障
  • 避免反序列化破坏单例
3.2.5.1. 静态内部类单例模式

JVM类加载原则,静态内部只有在使用的时候才会去加载,不会一开始就加载。

package com.myserious.code.cdz.thread.singleton;

/**
 * 饿汉式变种 静态内部类的方式
 */
public class Singleton5 {


    private Singleton5() {
    }

    /**
     * JVM类加载原则
     * 静态内部只有在使用的时候才会去加载
     * 不会一开始就加载
     */
    private static class SingletonInstance{
        private static Singleton5 INSTANCE = new Singleton5();
    }

    public static Singleton5 getInstance(){

        return SingletonInstance.INSTANCE;
    }
}

3.2.5.2. double check
package com.myserious.code.cdz.thread.singleton;

/**
 * 懒汉式 无bug的单例模式
 */
public class Singleton1 {

    //防止new 对象时指令重拍
    private static volatile Singleton1 singleton;

    private Singleton1() {
    }

    public static Singleton1 getInstance(){
        if (singleton==null){
            synchronized (Singleton1.class){
                //双重check
                //原因:第一个判断可能两个线程同时进入
                if (singleton==null){
                    singleton = new Singleton1();
                }
            }
        }
        return singleton;
    }


}

3.2.5.3. 枚举模式
package com.myserious.code.cdz.thread.singleton;

/**
 * 饿汉式变种 枚举
 */
public enum  SingletonEnum6 {

    INSTANCE;

    /**
     * 枚举中的方法
     */
    public void whatever(){

    }

    public static void main(String[] args) {
        //使用方法
        SingletonEnum6.INSTANCE.whatever();
    }

}

3.3. MySql

3.3.1. Mysql Update原理

更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。

3.3.2. mysql行级锁

mysql的行级锁,添加与释放节点:

添加行级锁: 当执行时。

释放(结束)行级锁:

  • 如果没有在另外的事务中,执行完立即释放
  • 如果在大事务中,需要等待大事务结束后才释放

4. share

4.1. 算法与数据结构

4.1.1. 链表倒置(动图)

因为链表倒置复习的时候,还是有些不够理解,通过自己制作动图来帮助理解。

4.1.2. 使用链表实现排序问题

使用链表来实现排序问题,是非常麻烦的一件事情。

原因是因为链表本身只是一个指针(引用)。

例如: A : 2->1->10->6;

A是这样的一个链表

A 的指针指向的就是 2 这个节点

然而排序后: 1->2->6->10

A的指针还是指向的2节点,于是就成了2->10,因为在原有基础上做的修改。

也是这个原因,Java在任何使用迭代器中,对迭代的集合进行增删都是违法的。

ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(1);
list.add(1);
list.add(1);
list.add(1);
for (Integer integer : list) {
    list.remove(integer);
}

报错:
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
    at java.util.ArrayList$Itr.next(ArrayList.java:859)

4.1.3.  非线性排序——桶排序极其衍生 

4.2. 多线程

4.2.1.  多线程三大核心——原子性、可见性、有序性 

4.2.2.  哲学家问题 

代码演示哲学家问题

package com.myserious.code.cdz.thread;

/**
 * 哲学家问题
 */
public class DiningPhilosophers {
    static class Philosopher implements Runnable {
        private Object left;
        private Object right;

        public Philosopher(Object left, Object right) {
            this.left = left;
            this.right = right;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    doAction("thinking");
                    synchronized (left) {
                        doAction("get left");
                        synchronized (right){
                            doAction("get right");
                            doAction("get all");

                            doAction("put down");
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) Math.random() * 10);
        }
    }

    public static void main(String[] args) {

        Philosopher[] philosophers = new Philosopher[5];
        Object[] objects = new Object[philosophers.length];
        //初始化筷子
        for (int i = 0; i < objects.length; i++) {
            objects[i] = new Object();
        }
        //初始化哲学家
        for (int i = 0; i < philosophers.length; i++) {
            Object left = objects[i];
            Object right = objects[(i+1)%objects.length];

            //原因在这里形成了一个锁的环路
            //调换一个哲学家的拿餐具顺序即可
            //philosophers[i]=new Philosopher(left,right);

            if (i==philosophers.length-1){
                philosophers[i]=new Philosopher(right,left);
            }else {
                philosophers[i]=new Philosopher(left,right);
            }

        }

        for (int i = 0; i < philosophers.length; i++) {
            new Thread(philosophers[i],"哲学家"+(i+1)).start();
        }
    }

}

4.2.3. JVM线程状态RUNNABLE,执行IO堵塞时是什么状态?

答案:还是RUNNABLE状态。

线程状态是JVM层面定义,与操作系统调度无关

4.2.4. volatile与synchronized关系与区别是什么?

volatile:

  • 可见性
  • 有序性

Synchronized:

  • 可见性
  • 有序性
  • 原子性

他们根本的差别就在原子性上。

volatile是无锁的,不能替代synchronized,没有提供互斥性与原子性

所以volatile是轻量级的synchronized

  • 充当触发器(happens-before与volatile)
  • 禁止重排序(双check单例模式)
  • 只赋值,不运算的多线程共享资源

4.2.5. Java中有原子操作有哪些?

  • 除了long和double之外的基本类型(int,byte,boolean,short,char,float)的赋值操作
    • 注意这是JVM的定义,所有JVM商用实现都支持long和double的原子操作
  • 所有引用reference的赋值操作,不管是32位机器还是64位机器
  • java.concurrent.Atomic.包中所有类的原子操作

4.2.6. 计算机操作系统已经将硬件优化到极致了,为什么还需要并发编程?

操作系统确实已经将硬件优化到了极致,但是操作系统优化的是单个硬件的效率。

比如:CPU的使用上,IO的处理上。

而并发往往是需要结合两个硬件一起,比如在IO堵塞时,CPU去执行其他任务。

总结:

  • 计算机操作系统优化的是单个硬件的使用效率(CPU/IO)
  • 并发编程,优化的是CPU与I/O设备的综合利用率

4.3.  如何驱动自己学习?