这周开始有工作了,所以开始学习时间上有所挤压,内容上主要还是围绕专栏与视频课展开。最主要的还是二分查找的算法练习。在写的过程中,发现二分查找思想很简单,但是真正写出一个没有BUG的程序却并不容易(推荐一篇LeetCode上讲解二分查找的文章写得很详细)。
- 专栏
- 数据结构与算法
- 二分查找
- 调表
- Java并发实战
- MySQL实战
- 架构课
- 数据结构与算法
- 视频
- 多线程核心
- LeetCode
- 二分查找
1. although
二分查找,要写出正确的程序,这一周经验总结,确定边界范围、停止条件,画图理清思路。
1.1. 二分查找基础
特别注意: 终止条件、区间上下界更新方法、返回值选择。
1.1.1. 循环求解二分查找
/**
* 循环求解二分查找
* @param arr 查找数组
* @param n 数组长度
* @param val 查找元素
* @return
*/
public int bsearch(int[] arr,int n,int val){
int low = 0;
int high = n-1;
//返回下标
int index = -1;
while (low<=high){
//int mid = low+(high-low)/2;
//更有效率写法
//注意:位运算的优先级最低
int mid = low+((high-low)>>2);
if (val>arr[mid]){
low = mid+1;
}else if (val<arr[mid]){
high = mid-1;
}else if (val==arr[mid]){
index = mid;
break;
}
}
return index;
}
1.1.2. 使用递归
/**
* 使用递归
* @param arr 查找数组
* @param n 数组长度
* @param val 查找元素
* @return
*/
public int bsearch2(int[] arr,int n,int val){
return bsearch(arr,0,n-1,val);
}
private int bsearch(int[] arr, int low, int high, int val) {
if (low>high)return -1;
int mid = low + ((high-low)>>2);
if (val>arr[mid]){
low = mid+1;
}else if (val<arr[mid]){
high = mid -1;
}else if (val==arr[mid]){
return mid;
}
return bsearch(arr,low,high,val);
}
1.1.3. 查找第一个值等于给定值的元素
关键在相等的时候, 如果相等 判断是否是第一个。
//查找第一个值等于给定值的元素
public int bsearch3(int[] arr,int n,int val){
int low = 0;
int high = n-1;
//返回下标
int index = -1;
while (low<=high){
//int mid = low+(high-low)/2;
//更有效率写法
//注意:位运算的优先级最低
int mid = low+((high-low)>>2);
if (val>arr[mid]){
low = mid+1;
}else if (val<arr[mid]){
high = mid-1;
}else if (val==arr[mid]){
//关键在相等的时候
//如果相等 判断是否是第一个
if (mid==0||arr[mid-1]!=val){
index = mid;
break;
}
high = mid -1;
}
}
return index;
}
1.1.4. 查找最后一个值等于给定值的元素
注意:位运算的优先级最低
//查找最后一个值等于给定值的元素
public int bsearch4(int[] arr,int n,int val){
int low = 0;
int high = n-1;
//返回下标
int index = -1;
while (low<=high){
//int mid = low+(high-low)/2;
//更有效率写法
//注意:位运算的优先级最低
int mid = low+((high-low)>>2);
if (val>arr[mid]){
low = mid+1;
}else if (val<arr[mid]){
high = mid-1;
}else if (val==arr[mid]){
//关键在相等的时候
//如果相等 判断是否是最后一个
if (mid==arr.length-1||arr[mid+1]!=val){
index = mid;
break;
}
low = mid +1;
}
}
return index;
}
1.1.5. 查找第一个大于等于给定值的元素
//查找第一个大于等于给定值的元素
public int bsearch5(int[] arr,int n,int val){
int low = 0;
int high = n-1;
//返回下标
int index = -1;
while (low<=high){
//int mid = low+(high-low)/2;
//更有效率写法
//注意:位运算的优先级最低
int mid = low+((high-low)>>2);
if (val>arr[mid]){
low = mid+1;
}else if (val<=arr[mid]){
if (mid==0||arr[mid-1]<val){
return mid;
}
high = mid-1;
}
}
return index;
}
1.1.6. 查找最后一个小于等于给定值的元素
//查找最后一个小于等于给定值的元素
public int bsearch6(int[] arr,int n,int val){
int low = 0;
int high = n-1;
//返回下标
int index = -1;
while (low<=high){
//int mid = low+(high-low)/2;
//更有效率写法
//注意:位运算的优先级最低
int mid = low+((high-low)>>2);
if (val>=arr[mid]){
if (mid==arr.length-1||arr[mid+1]>val){
return mid;
}
low = mid+1;
}else if (val<arr[mid]){
high = mid-1;
}
}
return index;
}
1.2. LeetCode
LeetCode上的通过率也可以看出,就算是简单的题目,也是基本上很多题目都在50%以下,说明二分查找思想简单,但是实现起来有很多需要注意的地方,几次将人写崩溃。
总结:注意二分切割区间
1.2.1. 第一个错误的版本
package com.myserious.code.cdz.week20;
/**
* 278. 第一个错误的版本
* 你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
* <p>
* 假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
* <p>
* 你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
* <p>
* 示例:
* <p>
* 给定 n = 5,并且 version = 4 是第一个错误的版本。
* <p>
* 调用 isBadVersion(3) -> false
* 调用 isBadVersion(5) -> true
* 调用 isBadVersion(4) -> true
* <p>
* 所以,4 是第一个错误的版本。
* <p>
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/first-bad-version
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Week11EFirstBadVersion278 extends VersionControl {
public int firstBadVersion(int n) {
int low = 1;
int high = n;
while (high > low) {
int mid = low + ((high - low) >> 2);
if (isBadVersion(mid)) {
//true 说明在 左边
high = mid;
} else {
low = mid+1;
}
}
return low;
}
}
class VersionControl{
boolean isBadVersion(int version){
return false;
}
}
1.2.2. x 的平方根
一个>2
的数的平方根一定在2~n/2
之间。
package com.myserious.code.cdz.week20;
/**
* 69. x 的平方根
* 实现 int sqrt(int x) 函数。
* <p>
* 计算并返回 x 的平方根,其中 x 是非负整数。
* <p>
* 由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
* <p>
* 示例 1:
* <p>
* 输入: 4
* 输出: 2
* 示例 2:
* <p>
* 输入: 8
* 输出: 2
* 说明: 8 的平方根是 2.82842...,
* 由于返回类型是整数,小数部分将被舍去。
* <p>
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/sqrtx
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*
*
* 进阶:如何编程实现“求一个数的平方根”?要求精确到小数点后 6 位。
*/
public class Week11EMySqrt69 {
/**
* 一个数的平方根
*
* 在 0~x/2 之间
* @param x
* @return
*/
public int mySqrt(int x) {
if (x<2){
return x;
}
long low = 2;
long high = x;
while (high>=low){
long mid = low + ((high-low)>>2);
long q = mid*mid;
if (q==x){
return (int) mid;
}else if (q>x){
high = mid-1;
}else {
low = mid + 1;
}
}
return (int) high;
}
}
1.2.3. 寻找比目标字母大的最小字母
主要当需要查找的元素大于数组的最大元素时,取第一个。
package com.myserious.code.cdz.week20;
/**
* 744. 寻找比目标字母大的最小字母
* <p>
* 给定一个只包含小写字母的有序数组letters 和一个目标字母 target,寻找有序数组里面比目标字母大的最小字母。
* <p>
* 数组里字母的顺序是循环的。举个例子,如果目标字母target = 'z' 并且有序数组为 letters = ['a', 'b'],则答案返回 'a'。
* <p>
* 示例:
* <p>
* 输入:
* letters = ["c", "f", "j"]
* target = "a"
* 输出: "c"
* <p>
* 输入:
* letters = ["c", "f", "j"]
* target = "c"
* 输出: "f"
* <p>
* 输入:
* letters = ["c", "f", "j"]
* target = "d"
* 输出: "f"
* <p>
* 输入:
* letters = ["c", "f", "j"]
* target = "g"
* 输出: "j"
* <p>
* 输入:
* letters = ["c", "f", "j"]
* target = "j"
* 输出: "c"
* <p>
* 输入:
* letters = ["c", "f", "j"]
* target = "k"
* 输出: "c"
* 注:
* <p>
* letters长度范围在[2, 10000]区间内。
* letters 仅由小写字母组成,最少包含两个不同的字母。
* 目标字母target 是一个小写字母。
* <p>
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/find-smallest-letter-greater-than-target
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Week11ENextGreatestLetter744 {
public char nextGreatestLetter(char[] letters, char target) {
//分割的区间 是左闭右开 [0,n)
// 切割方式 [0,mid) [mid+1,n)
//原因:这里需要查找的是比target第一大的字母,而不是找其本身
int low = 0;
int high = letters.length;
while (high>low){
int mid = low + ((high-low)>>2);
if (letters[mid]<=target){
low = mid+1;
}else {
high = mid;
}
}
return letters[high%letters.length];
}
public static void main(String[] args) {
Week11ENextGreatestLetter744 letter = new Week11ENextGreatestLetter744();
char[] arr = new char[]{'c','f','j'};
System.out.println(letter.nextGreatestLetter(arr, 'j'));
}
}
1.2.4. 寻找旋转排序数组中的最小值
一个有序数组旋转之后,使用二分法,切分成两个数组,左边与右边,其中必定有一边为有序,另一边为无序。
package com.myserious.code.cdz.week20;
/**
* 153. 寻找旋转排序数组中的最小值
* <p>
* 假设按照升序排序的数组在预先未知的某个点上进行了旋转。
* <p>
* ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
* <p>
* 请找出其中最小的元素。
* <p>
* 你可以假设数组中不存在重复元素。
* <p>
* 示例 1:
* <p>
* 输入: [3,4,5,1,2]
* 输出: 1
* 示例 2:
* <p>
* 输入: [4,5,6,7,0,1,2]
* 输出: 0
* <p>
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Week11MFindMin153 {
public int findMin(int[] nums) {
if (nums.length==1){
return nums[0];
}
int low = 0;
int high = nums.length-1;
while (high>low){
int mid = low + ((high-low)>>2);
// [low,mid] [mid+1,high]
//判断两边有序度
if (nums[mid]-nums[low]>=0&&nums[high]-nums[mid+1]>=0){
//两边都有序
//比较两边 最小值
if (nums[low]>nums[mid+1]){
return nums[mid +1];
}else {
return nums[low];
}
}else if (nums[mid]-nums[low]<0){
//左边无序
high = mid;
}else {
low = mid +1;
}
}
return low;
}
public static void main(String[] args) {
Week11MFindMin153 findMin = new Week11MFindMin153();
int[] arr = new int[]{2,3,1};
System.out.println(findMin.findMin(arr));
}
}
1.2.5. 搜索旋转排序数组
package com.myserious.code.cdz.week20;
/**
* 33. 搜索旋转排序数组
* 假设按照升序排序的数组在预先未知的某个点上进行了旋转。
* <p>
* ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
* <p>
* 搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
* <p>
* 你可以假设数组中不存在重复的元素。
* <p>
* 你的算法时间复杂度必须是 O(log n) 级别。
* <p>
* 示例 1:
* <p>
* 输入: nums = [4,5,6,7,0,1,2], target = 0
* 输出: 4
* 示例 2:
* <p>
* 输入: nums = [4,5,6,7,0,1,2], target = 3
* 输出: -1
* <p>
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Week11MSearch33 {
/**
* 重点理解
* 折叠的有序数组
* 其实二分法 找到中间位置
* 如果左边有序 那么右边一定无序,反之
*
* @param nums
* @param target
* @return
*/
public int search(int[] nums, int target) {
int low = 0;
int high = nums.length - 1;
while (high >= low) {
int mid = low + ((high - low) >> 2);
//判断那边有序
if (target == nums[mid]) {
return mid;
}else if (nums[mid] >= nums[low]) {
//这里只能切割 [low,mid] (mid,high] 这样的区间(当只有两个元素时并且 两个元素为倒序情况,只有这样才能满足)
//左边有序
//判断 target 是否在 左边区间内
if (target >= nums[low] && target < nums[mid]) {
// 在左边区间内
high = mid - 1;
} else {
//右边区间内
low = mid + 1;
}
} else {
//右边有序
if (target > nums[mid] && target <= nums[high]) {
//在右边区间内
low = mid + 1;
} else {
high = mid - 1;
}
}
}
return -1;
}
public static void main(String[] args) {
Week11MSearch33 search = new Week11MSearch33();
int[] ints = {3, 1};
System.out.println(search.search(ints, 1));
}
}
1.2.6. 在排序数组中查找元素的第一个和最后一个位置
- 思路找到第一个 target相等位置
- 然后一直加加直到不相等
package com.myserious.code.cdz.week20;
import java.util.Arrays;
/**
* 34. 在排序数组中查找元素的第一个和最后一个位置
* 给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
* <p>
* 你的算法时间复杂度必须是 O(log n) 级别。
* <p>
* 如果数组中不存在目标值,返回 [-1, -1]。
* <p>
* 示例 1:
* <p>
* 输入: nums = [5,7,7,8,8,10], target = 8
* 输出: [3,4]
* 示例 2:
* <p>
* 输入: nums = [5,7,7,8,8,10], target = 6
* 输出: [-1,-1]
* <p>
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Week11MSearchRange34 {
public int[] searchRange(int[] nums, int target) {
int firstInx = -1;
int lastInx = -1;
int low = 0;
int high = nums.length-1;
/**
* 思路找到第一个 target相等位置
* 然后一直加加直到不相等
*/
while (high>=low){
int mid = low + ((high-low)/2);
if (nums[mid]>target){
high = mid -1;
}else if (nums[mid]<target){
low = mid +1;
}else {
if (mid==0||nums[mid-1]!=target){
//找到了
firstInx = mid;
lastInx = mid;
while (nums.length>lastInx+1&&nums[lastInx+1]==target){
lastInx++;
}
break;
}else {
high = mid -1;
}
}
}
return new int[]{firstInx,lastInx};
}
public static void main(String[] args) {
Week11MSearchRange34 searchRange = new Week11MSearchRange34();
int[] ints = {5, 7, 7, 8, 8, 10};
int[] ints1 = searchRange.searchRange(ints, 3);
System.out.println(Arrays.toString(ints1));
}
}
1.2.7. 有序数组中的单一元素
这一题的注意点是,切割两数组时,分为两种情况,查找的元素相同两元素的第一个或者第二个。
- 第二个
- b为true时表示的是右边为偶数个
- 第一个
- b为true时 表示的是右边为奇数个
package com.myserious.code.cdz.week20;
/**
* 540. 有序数组中的单一元素
* 给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
* <p>
* 示例 1:
* <p>
* 输入: [1,1,2,3,3,4,4,8,8]
* 输出: 2
* 示例 2:
* <p>
* 输入: [3,3,7,7,10,11,11]
* 输出: 10
* 注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。
* <p>
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/single-element-in-a-sorted-array
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Week11MSingleNonDuplicate540 {
public int singleNonDuplicate(int[] nums) {
//因为其他元素都是成对出现
//只有一个元素是单个的
//所以 数组必定是奇数个
//二分法,排除中间的两个,
//再判断 左右两边,那边是奇数个,继续找奇数个的
// [3,3,7,7 ,10,11,11] mid=3
// [1,3,3,4,4] mid=3
// 并且 中间的数字必须等于后一个数字,奇数个数组的特性
// [low,mid-1] [mid+2,high]
int low = 0;
int high = nums.length-1;
while (low<high){
int mid = low + ((high-low)/2);
//关键在这里
//当 nums[mid]==nums[mid+1] 与 nums[mid-1]==nums[mid]
// 他们表示的含义是不一样的
//当nums[mid]==nums[mid+1] 标点mid b为true时表示的是右边为偶数个
//当nums[mid-1]==nums[mid] 标点mid-1 b为true时 表示的是右边为奇数个
//这是因为标点变了
boolean b = (high - mid+1) % 2 == 0;
if (nums[mid]==nums[mid+1]){
if (b){
//在左边
high = mid -1;
}else {
//在右边
low = mid +2;
}
}else if (nums[mid-1]==nums[mid]){
if (b){
//在右边
low = mid +1;
}else {
//在左边
high = mid - 2;
}
}else {
return nums[mid];
}
}
return nums[low];
}
public int singleNonDuplicate1(int[] nums) {
int lo = 0;
int hi = nums.length - 1;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
boolean halvesAreEven = (hi - mid) % 2 == 0;
if (nums[mid + 1] == nums[mid]) {
if (halvesAreEven) {
lo = mid + 2;
} else {
hi = mid - 1;
}
} else if (nums[mid - 1] == nums[mid]) {
if (halvesAreEven) {
hi = mid - 2;
} else {
lo = mid + 1;
}
} else {
return nums[mid];
}
}
return nums[lo];
}
public static void main(String[] args) {
Week11MSingleNonDuplicate540 duplicate = new Week11MSingleNonDuplicate540();
int[] ints = new int[]{3,7,7} ;
System.out.println(duplicate.singleNonDuplicate(ints));
}
}
2. review
- 积极参与开源项目
- 也是这个小哥的转折点
- 英文很重要
- 实战才能有真提升
- 演讲能力
- 预备演讲
- Like most important things in my life, it happened randomly.
- 预备演讲
3. tip
3.1. 数据结构与算法
3.1.1. 二分查找适用场景
- 数组(可随机访问)
- 插入、删除操作不频繁(一次排序,多次查找)
- 有序
- 数组容量不易过小
- 例外比较操作耗时时,二分查找比较次数最低
- 数据量过大也不适合
- 因为数组需要一篇连续的内存在存储,多大的话可能内存不够。
3.1.2. 二分查找程序注意
注意二分切割区间
- 因为数组需要一篇连续的内存在存储,多大的话可能内存不够。
3.2. 多线程
3.2.1. RPC其实是异步
RPC其实是异步的,RPC框架通过编程的方式,将异步转化为了同步,方便代码编写,如dubbo
3.2.2. 生产中什么场景下会发生死锁?
- 同一个方法中获取多个锁
- 一个方法中获取一个锁,但是调用的其他方法也会获取其他锁,导致循环锁
3.2.3. ReadWriteLock读写锁
特点:
- 读可以重入
- 写时互斥 使用注意事项:
- 锁不能升级
- 读锁内部 不能使用写锁
- 锁可以降级
- 写锁内部可以使用读锁
https://time.geekbang.org/column/article/88909
4. share
4.1. 数据结构与算法
4.1.1. 跳表的结构
4.2. 多线程
4.2.1. 创建多少线程才合适?
CPU密集型:线程数=CPU核心数(逻辑核心数)+1 I/O密集型:线程数=CPU核心数*(1+I/O耗时/CPU耗时)
实际开发中最难确定的是I/O耗时/CPU耗时这个参数,
一般使用apm压测的方式,来主要观察I/O耗时与CPU耗时。
部署分容器与物理机,
容器部署一般一个容器中只有一个Java程序在跑,可以使用Runtime.getRuntime().availableProcessors()
来获取容器中的线程数,新版本Java才支持。
而在机器上跑,很多时候并不只有一个Java程序,所以还需要根据实际的情况从理论值开始调节。
4.2.2. 活锁是什么?
- 一直尝试获取锁,却一直获取不到,一直进行循环执行
- 效果与死锁一致,但是危害比死锁要大,因为一直循环占用CPU资源
- 结果和活锁一样,不同的是,活锁没有堵塞,顾名思义,一直在运行,但是没有执行有效代码。
这里为什么会出现活锁? 可能导致活锁 ,并不一定会活锁。 原因:双方同时为对方转账时,同时进入第一个锁,但是发现获取不到第二个锁,于是释放,进行下一次循环。 下一次循环同样的结果,这就是出现了活锁。
解决办法:加入随机因素
消息队列中,活锁的出现:
- 依赖服务出现问题(宕机),消息消费失败
- 导致消息队列一直重试发送消息
解决办法:
- 放到队列尾部
- 重试限制
4.2.3. 饥饿是什么意思?
线程等待得不到资源执行。
饥饿的一个场景: 浏览器的设计,也有后台程序运行,与前台程序运行。 后台程序部分:下载文件 前台显示部分:浏览网页,打开设置
如果后台程序沾满了CPU,那么前台程序就得不到运行,点击就会导致无响应的情况。
4.3. 架构
4.3.1. MySQL主从读写分离解决方案
数据库读写分离一般应用于什么场景?能支撑多大的业务规模?
在单机MySQL在,缓存和索引优化后,已经支撑不了的情况下再进行读写分离。 一般有些慢的情况,其实优化慢查询,和添加缓存机制,基本上都可以解决。
读写分离最大的问题:
主从复制延迟,有时候会延迟1秒,有时候可能会延迟1分钟。
解决办法:
- 写操作后,读操作指定发送给主库(不可取,容易出BUG)
- 两次读取,当读取不到内容,去主库读取
- 缺点:可能黑客暴力破解账户,导致主库承受巨大压力
- 关键业务操作指向主机
- 比如用户注册业务,所有操作都在主库中完成,这样就不会有延迟
- 而用户修改个人资料这样的信息,可以使用读写分离
- 这也就是很多修改完了资料信息,而没有立马展示的原因
读写分离实现:
- 代码层面(代码程序封装)
- 实现简单,根据业务制定化
- 每个编程语言都要实现一遍,无法通用
- 故障下,切换主从,需要修改代码配置,重启服务
- 开源实现淘宝的:DTTL
- 中间件封装
- 支持多语言,标准的SQL入口
- 实现复杂度高,长时间才能稳定
- 所有请求都在中间件处理,并实现读写分离,性能要求高
- 主从切换无感知
- 可选:
- MySQL Proxy一直没有正式 GA(稳定版本)
- 现在 MySQL 官方推荐 MySQL Router
- 360开源Atlas,基于MySQL Proxy
4.4. 疫情下做饭感悟