这周主要主攻方面:
- 算法
- 复习链表LeetCode练习题
- 排序LeetCode练习
- O(n^2)排序(冒泡、插入、选择)
- O(nlogn)排序(快排、归并)
- 多线程
- 视频和专栏结合学习
- 围绕死锁
- 专栏
- MySQL的锁问题
- 数据库锁
- 表级锁
- 行级锁
- 设计模式
- 跟着专栏优化ID生成器代码
- 从可用到好用
- 可读性
- 可测性
- 添加单元测试
- 添加注释
1. although
1.1. O(n^2)排序
1.1.1. 冒泡排序
- MySQL的锁问题
/**
* 冒泡排序
* 核心——只交换相邻两个元素
* <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策略是:
- 在RedisObject类中定义访问时间
- RedisObject类:在Redis中所有的类型都被包装成RedisObject对象
- 比如所有的key都是string类型的RedisObject对象
- RedisObject对象里定义有类型和长度等等,是底层定义对象
- 随机取出5个key,然后空转最长的key
- 从什么地方取出,是可以设置不同的策略
- noeviction:直接抛出异常
- allkeys-lru:从所有key中
- volatile-lru:所有设置超时时间key
- allkeys-random: 所有key随机删除
- volatile-random: 所有设置超时时间key随机删除
- 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. 死锁的必要条件
- 互斥,共享资源X与Y只能被一个线程占用
- 占用且等待,当线程T1获取到共享资源X锁时,在等待锁Y时,不释放锁X。
- 不可抢占,其他线程不能强行抢占线程T1占有的锁
- 循环等待,线程T1等待线程T2占有的锁,线程T2等待T1占有的锁
这四个条件必须同时满足时,才会出现死锁。
也就是说,只要破解一个条件就可以避免死锁的发生。
3.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设备的综合利用率