Test:angel:

技术学习指导思想

1
2
3
4
这项技术诞生的是为什么? 解决了什么问题吗?
它的这个技术的主要原理是什么?有什么骚操作?
这个技术的缺点是什么? 如何解决这个技术的问题?
有没有其他相同的技术做对比?

Spring:airplane:

为什么选用Spring?

1
为什么选用Spring,Spring提供了IOC以及AOP的核心,IOC能够把对象创建交给容器来处理,而不需要通过手写new 一个对象,这样就降低了代码的冗余性,并且实现了对象之间的松耦合,并且Spring能够通过AOP实现事务管理,Spring从1.0版本到现在,它的生态已经非常的庞大,而且Spring是一个轻量级的,只有2MB大小。Spring由AOP和IOC以及Test,WEB,JDBC,

SpringBoot的启动过程?

1
SpringBoot启动的接口有SpringBootApplication注解里面有多个注解,SpringBootConfiguration、EnabletAutoConfiguration、ComponeScan等注解,然后我们从pom.xml文件加载组件,再从配置文件加载配置,再初始化Web容器,此时请求控制权限交给Web容器(Tomcat或者Jetty都行)

SpringBoot自动配置源码分析

1
2
3
@SpringBootApplication 这个注解里面有ComponeScan我们可以通过这种方式扫描Controller、Service、Configuration等注解Bean
SpringApplication.run(BootApplication.class, args);这行代码里面的BootApplication其实也可以是别的,自己写一个类就行,这个类的作用就相当于一个配置类,这个run方法里面的逻辑代码主要是: 创建Spring容器,然后Register(配置类),然后再选择一个Web容器(Tomcat或者Jetty),Tomcat里面得加一个Servlet(具体是加DispacthServlet),这样就是SpringMVC将Controller分解成了前端的Controller(DispatcherServlet)和后端的Controller,当前端发送请求的时候,DispatcherServlet接受到请求之后去找HandleMapping找对应的Controller,然后再通过Controller到Service返回ModeAndView,DispatcherServlet接受到ModeAndView之后传给视图解析器(ViewSolver),再传给视图(HTML,JSP).
@SpringBootApplication 下面还有EnableAutoConfiguration注解,这个就是Spring的自动配置,注意不是自动装配哈,EnableAutoConfiguration下面有@Import({AutoConfigurationImportSelector.class}),这个用来加载AutoConfiguration类,比如SqlSessionFactory,我们加载的时候,怎么去识别某个类是AutoConfiguration呢? 我们可以通过spring.factories来得到需要引入的自动配置类,这些在第三方jar包里面的自动配置类全部要加载过来吗? 当然不需要了,有些没有用到的自动配置类你加载干嘛,当然是加载需要用到的,怎么判断它是否需要用到呢? 里面有

Spring中的Bean的生命周期

5步骤看
1
2
3
4
5
1. 实例化 也就是构造方法
2. 依赖注入 也就是set属性名
3. 初始化
4. 使用Bean
5. 销毁Bean
7步骤看
1
2
3
4
5
6
7
1. 实例化 
2. 依赖注入
3. 初始化Before 有点像AOP
4. 初始化Bean
5. 初始化After
6. 使用Bean
7. 销毁Bean

设计模式

1
2
3
4
5
6
1. 工厂模式: 其实这个分两种实现方式,一种是抽线工程通过Spring当中的顶级接口BeanFactory作为容器来getBean方法获得Bean,还有一种是方法工厂通过一个FactoryBean接口来获得Bean,主要是能通过对这个Bean的一些初始化配置。
2. 单例模式: 其实Spring就是对整个Bean的生命周期的一个管理,单例模式就是说需要用到某个bean的时候是同一个bean,不会去新建一个bean,也就是说这个bean是全局性的,涉及到一些懒汉和饿汉模式实现
3. 代理模式: 这个主要就是通过动态代理的方式创建bean,主要实现就是AOP,用于增强某些功能
4. 策略模式: 这个就是通过提供一个接口,有不同的实现类,比如说数据库的驱动加载,数据库来自不同厂家,只需要切换不同的数据源就能实现接口,能够自由切换
5. 观察者模式: Spring当中的applicatoncontext接口作为上下文容器,里面实现了一个接口实现了订阅/发布功能,有点类似于MQ,那边动了,我这边也得做出改变
6. 模板方法模式: RedisTemplete和JDBCTemplete这些,已经有很多类的框架已经写好了

计算机网络:alien:

当你在浏览器中输入URL网址的时候发生了什么?

1
2
3
4
5
6
7
8
9
10
11
DNS解析:当我们在浏览器中输入URL时,浏览器会首先将该URL发送给本地DNS服务器进行解析,以获取该URL所对应的IP地址。

建立TCP连接:一旦浏览器获取到URL所对应的IP地址,浏览器会通过TCP/IP协议向该IP地址发起连接请求,以建立与Web服务器的连接。

发送HTTP请求:一旦TCP连接建立成功,浏览器会通过该连接向Web服务器发送HTTP请求,请求服务器返回对应的HTML文件。

服务器响应:Web服务器接收到浏览器的HTTP请求后,会查找请求的资源并返回HTTP响应。

接收响应数据:浏览器接收到服务器返回的HTTP响应后,会解析响应数据,包括HTML、CSS和JavaScript等资源,并对其进行加载和渲染。

显示网页:一旦浏览器完成了HTML、CSS和JavaScript等资源的加载和渲染工作,它就会将这些资源以及浏览器自身的UI元素结合起来,最终呈现出我们所看到的网页。

对于Unicode的理解是什么?:

1
Unicode字符集有世界上大部分的语言字符构成,Unicode有17个面板,每个面板有65536个字符组成(即2的16次方),总共有17个面,其中有个BMP面,表示的就是常用的语言,比如说英文字母以及常见的数字符号,有16个辅助面板,里面有emoij这种表情符号组成(一个表情符号字符由2个char组成,常见的字符只有一个char组成),可以将Unicode字符集当成一个字典来看每个字符有对应的码点以及Unicode字符,每一个字符对应着一个,首先我们搞清楚什么是UTF-16编码,就是通过将一个码点转化为二进制数字的过程,一个char对应的是一个UTF-16代码单元

为什么TCP需要三次握手?

1
TCP是一个可靠的基于字节流的面向连接的协议,需要三次握手的原因有三点
三次握手的流程是什么?
1
2
3
客户端向服务端发送请求携带一个SYN
服务端接受到请求后向客户端发送SYN(这个是服务端的同步序列)和一个ACK(这个是回应有没有收到客户端的同步序列的)
客户端接受到服务端的请求后,向服务端发送ACK。
三次握手的具体原因是什么?
1
2
3
首先是因为TCP需要在不可靠的网络环境下完成可靠传输,所以这样就需要用到SYN
其次是因为如果只有两次握手,那么如果因为网络原因导致的之前的请求阻塞后面服务端看到之后有两种选择接受和不接受,不知道这个请求是有效还是无效的
最后其实四次、五次也能够保证安全只是没有必要
TCP三次握手如何数据丢失怎么办?
1
2
3
4
如果第一次握手丢失,那么C端会不断发送SYN,直至默认最大值5次之后就不发了
如果第二次握手丢失,那么C端会不断发送SYN,直至默认最大值5次之后就不发了,S端会不断发送SYN,ACK,直至默认最大值5次之后就不发了
如果第三次握手丢失,S端会不断发送SYN,ACK,直至默认最大值5次之后就不发了
第三次握手丢失时,C端处于ESTABLELIST状态,如果不发送数据,可能会触发保活机制,看tcp连接是否还在,如果发送数据,默认发送超过15次没有响应,那么也不会在发了

CSDN的三次握手丢失处理方法

OS:woman_astronaut:

数据结构与算法:astonished:

排序

冒泡排序

思路:
1
需要两层for循环,第一层for循环从后开始,第二层从第一个元素到第一层提供的元素
代码:
1
2
3
4
5
6
7
8
9
10
11
12
public int[] sortArray(int[] nums) {
for(int i = nums.length - 1; i > 0; i--) {
for(int j = 0; j < i; j++) {
if(nums[j] > nums[j+1]) {
int tmp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = tmp;
}
}
}
return nums;
}

选择排序

思路:
1
需要两层for循环,第一层从前向后,第二层从第一层提供元素开始,到数组的末尾结束,找到第二层的最小下标,拿到最小值就行。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
public int[] sortArray(int[] nums) {
for(int i = 0; i < nums.length - 1; i++) {
int minIndex = i;
for(int j = i+1; j < nums.length; j++) {
if(nums[j] < nums[minIndex]) minIndex = j;
}
int tmp = nums[i];
nums[i] = nums[minIndex];
nums[minIndex] = tmp;
}
return nums;
}

插入排序

思路:
1
把数组分成两类,一类是排序的一类是非排序的,我们先把要插入的元素给存取出来,每次从有序的数组倒序遍历,只要元素大于要插入的元素,就移动一下元素,小于等于就弹出,并且填充要插入的元素
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int[] sortArray(int[] nums) {
for(int i = 1; i < nums.length; i++) {
int tmp = nums[i];
int j;
for(j = i - 1; j >= 0; j--) {
if(nums[j] > tmp) {
nums[j+1] = nums[j];
} else {
break;
}
}
nums[j+1] = tmp;
}
return nums;
}

快速排序

思路:
1
从最左边选取一个key,start为key+1,end为数组最右边的一个元素,先从end开始,直到一个小于key的就停止,此时start开始向前移动直到大于key的就停止,此时end和start开始交换元素内容,直到end和start相遇,那么key和这个相遇的地方开始交换数据
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public int[] sortArray(int[] nums) {
if(nums.length == 0 || nums.length == 1) return nums;
QuickSort(nums,0,nums.length - 1);
return nums;
}
public void QuickSort(int[] nums,start,end) {
if(start >= end) return;

int left = start;
int right = end;
int key = nums[start];
while(start < end) {
while(nums[end] >= key && start < end) {
end--;
}
while(nums[start] <= key && start < end) {
start++;
}
int tmp = nums[start];
nums[start] = nums[end];
nums[end] = tmp;
}
int tmp = nums[end];
nums[end] = key;
nums[left] = tmp;
QuickSort(nums,left,end-1);
QuickSort(nums,end+1,right);
}

哈希表

有效的字母异位词

思路:
1
因为是26位字母且都是小写,字母异位词的意思就是相同大小的字符串中所有的字母出现次数都是一样的,因为字母只有26位,那么这样的话我们可以设一个26个大小的数组,怎么得到数组的下标,都是通过element - 'a'得到
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean isAnagram(String s, String t) 	{
if(s.length() != t.length()) return false;

int[] record = new int[26];

for(int i = 0;i < s.length(); i++) {
record[s.charAt(i) - 'a']++;
}
for(int j = 0;j < t.length(); j++) {
record[s.charAt(j) - 'a']--;
}
for(int count:record) {
if(count != 0) return false;
}
return true;

}

两个数组的交集

思路:
1
先通过HashSet一个数组的元素,在遍历另外一个数组并且创建一个新的Set,遍历的时候如果之前的HashSet里面包含了另外一个数组的元素,则放入新创建的Set里面。在把Set转化为数组返回即可
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set01 = new HashSet<>();
Set<Integer> set02 = new HashSet<>();
for(int i: nums1) {
set01.add(i);
}
for(int j:nums2) {
if(set01.contains(j)) {
set02.add(j);
}
}
int[] arr = new int[set02.size()];
// 比较low的写法
int index = 0;
for(int num:set02) {
arr[index++] = num;
}
return arr;
// 比较high的写法

return set02.stream().mapToInt(x -> x).toArray();
}

快乐数

思路:
1
快乐数就是把某个整数上面的所有位数都开平方的和,得到一个整数,一直开平方直到为1就证明此数为快乐数,但是也可能存在无限循环,意思就是开平方得到的和可能存在重复,那么就要使用到Hash表来解决这个问题
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public boolean isHappy(int n) {
int sum = 0;
Set<Integer> set01 = new HashSet();
while(sum != 1) {
sum = getSum(n);
if(set01.contains(sum)) return false;
set01.add(sum);
n = sum;
}
return true;

}

public int getSum(int n) {
int sum = 0;
// 只要判断n / 10之后 这个n是个位数就可以
while(n >= 10) {
sum = sum + (int)Math.pow(n % 10,2);
n = n / 10;
}
sum = sum + (int)Math.pow(n % 10,2);
return sum;
}

两数之和

思路:
1
两数之和这个题目里面提供了target,那么我们可以创建一个Map来存储元素,key存储元素值,value存储元素下标,这样需要先遍历数组通过Map存储元素,因为Target我们是知道的,a+b = target,那么我们先遍历数组,在通过Map.contains方法判断需要补充的元素,这样就能取出两个下标。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> map = new HashMap<>();

for(int i = 0 ; i < nums.length; i++) {
int need = target - nums[i];
if(map.containsKey(need)) {
return new int[] {i,map.get(need)};
}
map.put(nums[i],i);
}
return null;
}

四数之和

思路:
1
这个题目就是通过Map先取出两个集合当中的和就可以了,然后再去找另外两个集合里面的和是不是和之前的相等即可,这样的话又变成了两个数相加减
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
int n = nums1.length;
Map<Integer,Integer> map = new HashMap<>();
for(int i = 0;i < n;i++) {
for(int j = 0; j < n; j++) {
int tmp = nums1[i]+nums2[j];
map.put(tmp,map.getOrDefault(key, 0)+1);
}
}
int count = 0;
for(int i = 0;i < n; i++) {
for(int j = 0; j < n; j++) {
int tmp = nums3[i]+nums4[j];
if(map.containsKey(0 - tmp)) {
count += map.get(0 - tmp);
}
}
}
return count;

}

赎金条

思路:
1
这个题目给我的感觉就是很简单,一看就是用的数组索引,26个字母,那么就可以用new int[]的方式来获得各种字母出现的次数,先用magazine去统计字符出现次数再用ransomNote去减,这样最后数组遍历的时候看看是否存在0即可
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean canConstruct(String ransomNote, String magazine) {
int[] res = new int[26];
for(int i = 0; i < magazine.length(); i++) {
res[magazine.charAt(i) - 'a']++;
}
for(int j = 0; j < ransomNote.length(); j++) {
res[ransomNote.charAt(j) - 'a']--;
}
for(int i : res) {
if(i < 0) return false;
}
return true;
}

三数之和

思路:
1
这个题目最好是用双指针来写,为什么呢? 因为用哈希表会超时,首先需要明白题目的意思就是,不同的三元组,意味着三个元素的值&&位置是唯一的。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(res);
for(int i = 0 ; i < nums.length; i++) {
if(nums[i] > 0) return res;

if(i > 0 && nums[i] == nums[i-1]) continue;

int left = i + 1;
int right = nums.length - 1;
while(left < right) {
int sum = nums[i] + nums[left] + nums[right];
if(sum < 0) left++;
else if(sum > 0) right--;
else {
res.add(Arrays.asList(nums[i],nums[left],nums[right]));
while(left < right && nums[left] == nums[left + 1]) left++;
while(left < right && nums[right] == nums[right-1]) right--;
left++;
right--;
}
}
}
return res;
}

字符串

反转字符串

思路:
1
这个反转字符串可以通过函数库来写,如果这样写的话就不道德了,那么我们可以通过双指针的方式来写,两个指针分别在头或者在尾,这样头尾指针交换值就可以了,这样就可以不用创建新的数组,非常Nice
代码:
1
2
3
4
5
6
7
8
9
10
11
12
 public void reverseString(char[] s) {
int i = 0;
int j = s.length -1;
while(i < j) {
char tmp = s[i];
s[i] = s[j];
s[j] = tmp;
i++;
j--;
}

}

反转字符串2

思路:
1
给定一段字符串,每次取2k,前k反转,如果剩余的字符不超过k那么就全部反转,如果大于等于k,但小于2k,那么就反转前k个,问题是如果判断剩余有多少个?
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static void main(String[] args) {
Test test = new Test();
System.out.println(test.reverseStr("abcd", 2));
}
public String reverseStr(String s, int k) {
char[] sChar = s.toCharArray();
int len = sChar.length;
int alloc = len - 2*k;
int start = 0;
while (alloc >= 0) {
reverse01(sChar, start, k);
alloc -= 2*k;
start += 2*k;
}
alloc += 2*k;
if (alloc < 2*k && alloc >= k){
reverse01(sChar, start, k);
} else if (alloc < k) {
reverse02(sChar, start, len - 1);
}
return String.valueOf(sChar);
}

public void reverse01(char[] sChar, int start, int k) {
int i = start;
int j = start + k - 1;
while (i < j) {
char tmp = sChar[i];
sChar[i] = sChar[j];
sChar[j] = tmp;
i++;
j--;
}
}

public void reverse02(char[] sChar, int start, int end) {
int i = start;
int j = end;
while (i < j) {
char tmp = sChar[i];
sChar[i] = sChar[j];
sChar[j] = tmp;
i++;
j--;
}
}

替换空格

思路:
1
这个题目的思路使用双指针法来写,先用StringBuilder来计算需要填充的字符数量,这样得到了right指针下标,从右往左填,当left指到最左边的时候就出可以结束了。
思路1:
1
这个其实比较的简单,一个字符一个字符的去查,如果遇到了' ',那么就加%20
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public String replaceSpace(String s) {
StringBuilder sb = new StringBuilder();
for(int i = 0; i < s.length(); i++) {
if(s.charAt(i) == ' ') {
sb.append(' ');
}
}
if(sb.length() == 0) return s;
// 此时说明有空格
int left = s.length() - 1;
s += sb.toString();
int right= s.length() - 1;
char[] sChar = s.toCharArray();

while(left >= 0) {
if(sChar[left] == ' ') {
sChar[right--] = '0';
sChar[right--] = '2';
sChar[right--] = '%';
} else {
sChar[right--] = sChar[left];
}
left--;
}
return String.valueOf(sChar);
}

反转字符串里的单词

思路:
1
这个题目就是把字符串里面的单词给反转过来,去除掉前面和后面的空格,再把字符串整体进行反转,再把单个单词进行反转,这样也同样实现了目的,当然也可以通过,创建一个新的数组来存储反转后的字符串,再通过倒序相加就得到了反转和的字符串,但这样也同样失去了这道题的意义。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public String reverseWords(String s) {
// 去除字符串当中的前后的空格以及中间的空格
String sb = removeSpace(s);
// 反转整个字符串
char[] sChar = sb.toCharArray();
reverseWhole(sChar,0,sChar.length);
// 反转单个单词
int start = 0;
int end = 0;
for(int i = 0; i < sChar.length; i++) {
if(sChar[i] == ' ') {
end = i - 1;
reverseWords(sChar,start,end);
start = i + 1;
}
}
return String.valueOf(sChar);
}
public String removeSpace(String s) {
StringBuilder sb = new StringBuilder();
int left = 0;
int right = s.length() - 1;
// 先去除掉尾部
while(left < s.length()) {
if(s.charAt(left) != ' ') break;
left++;
}
while(0 < right) {
if(s.charAt(right) != ' ') break;
right--;
}
// 除去中间
for(int i = left; i <= right; i++) {
char c = s.chatAt(i);
if(c != ' ' || sb.charAt(sb.length() - 1) != ' ') {
sb.append(c);
}
}
return sb.toString();
}

public void reverseWhole(char[] sChar,int start,int end) {
while(start < end) {
char tmp = sChar[start];
sChar[start] = sChar[end];
sChar[end] = tmp;
start++;
end--;
}
}

public void reverseWords(char[] sChar) {
int start = 0;
int end = 1;
int n = sChar.length;
while(start < n) {
while(end < n && sChar[end] != ' ') {
end++;
}
reverseWhole(sChar,start,end);
start = end + 1;
end = start + 1;
}

}

左旋转字符串

思路:
1
左旋转字符串的做题思路就是,先通过把前n个旋转,再通过把后n个旋转,再把整个字符串选择,如果用subStr,着当然可以一把梭哈,但会申请额外的空间O(n)
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public String reverseLeftWords(String s, int n) {
char[] chars = s.toCharArray();
// 先旋转左边
reverse(chars,0,n - 1);
// 再旋转右边
reverse(chars,n,chars.length - 1);
// 整体进行旋转
reverse(chars,0,chars.length - 1);
return String.valueOf(chars);
}
public String reverse(char[] chars, int start, int end) {
while(start < end) {
char tmp = chars[start];
chars[start] = chars[end];
chars[end] = tmp;
start++;
end--;
}
}

找出字符串第一个匹配项的下标

思路:
1
这是一个经典的KMP算法,难点主要就是构建前缀表,给我的感觉就是,前缀表构建过程中的每个元素值对后面的一个元素值有影响,前缀意味着以第一个元素开始但是不以最后最后一位结束,后缀意味着以最后一个元素结尾但是开始不能到第一个元素,next[j],j代表着元素下标,next[j]代表着j元素结尾它的前缀最后一个元素索引位置。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public int strStr(String haystack, String needle) {
int len_needle = needle.length();
int[] preArray = new int[len_needle];
generatePreArray(preArray,needle);
int j = -1;
for(int i = 0; i < haystack.length(); i++) {
while(j >= 0 && haystack.charAt(i) == needle.charAt(j+1)) {
j = next[j];
}
if(haystack.charAt(i) == needle.charAt(j+1)) {
j++;
}
if(j == needle.length() - 1) return (i - needle.length() + 1);
}
return -1;
}
public void generatePreArray(int[] preArray,String needle) {
int j = -1;
preArray[0] = j;
for(int i = 1; i < preArray.length; i++) {
while(j >= 0 && needle.charAt(i) != needle.charAt(j+1)) {
j = next[j];
}
if(needle.charAt(i) == needle.charAt(j+1)) {
j++;
}
next[i] = j;
}


}

重复的子字符串

思路:
1
这个用的还是KMP,刚刚看完还是有点懵的,用s+s,取除掉前后子串,就能证明该s有多个相同的子字符串组成,然后又说用KMP来解这道题目,前后缀这个问题要理解清楚,前缀就是以第一个字符开始不能以最后一个字符结尾,后缀反之。什么是最小的子字符串? 就是通过s - 最长公共前后缀得到,怎么判断这个是符合我们目标的子字符串? len % 子串长度  == 0;哪个是最长的公共前后缀? next[len - 1];
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public boolean repeatedSubstringPattern(String s) {
char[] chars = s.toCharArray();
int len = chars.length;
int[] next = new int[len];
int j = -1;
int next[0] = j;
for(int i = 1; i < len; i++) {
while(j >= 0 && s.charAt(i) != s.charAt(j + 1)) {
j = next[j];
}
if(s.charAt(i) == s.charAt(j + 1)) {
j++;
}
next[i] = j;
}
int maxCommonLen = next[len - 1];
if(maxCommonLen == -1) return false;
// 下面是子串长度
int subLen = len - maxCommonLen - 1;
if(len % subLen == 0) return true;
else return false;

}
总结:
1
这个题目给我的感受就是不是个简单题,这个题目需要一定的数学基础,怎么判断哪个是最小子字符串(当然这个题目没有要求给出具体子字符串,但是也是可以实现的),如何判断该子字符是否满足组成s字符串的要求? 构建一个next数组,并且取最后一个next[len - 1]的大小,得到子字符串的长度,总字符串%子字符串==0就说明能满足。

栈与队列

[栈实现队列](232. 用栈实现队列 - 力扣(LeetCode))

思路:
1
通过用两个栈来实现队列的所有操作,一个是进栈一个是出栈,添加元素时就往进栈当中加就可以了,如果是pop操作那么先判断出栈里面有没有元素,有则从出栈里面去出元素,否则,先把进栈里面的元素全部导入到出栈里面,然后再在出栈里面出一个元素,如何判断队列为空,就看出栈进栈是否都为空,如果都为空说明队列为空,如何获得第一个元素peek(),如何得到呢,其实就和前面的pop逻辑一样,先判断出栈有没有,有出栈栈顶的第一个元素就是的,出栈没有则需要把进栈的元素栈底那个就是的
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Stack<Integer> stackIn;
Stack<Integer> stackOut;
public MyQueue() {
stackIn = new Stack();
stackOut = new Stack();
}

public void push(int x) {
stackIn.push(x);
}

public int pop() {
InsertIntoOut();
return stackOut.pop();

}

public int peek() {
InsertIntoOut();
return stackOut.peek();
}

public boolean empty() {
if(stackOut.isEmpty() && stackIn.isEmpty()) return true;
else return false;
}
public void InsertIntoOut() {
if(!stackOut.isEmpty()) {
return;
}
while(!stackIn.isEmpty()) {
stackOut.push(stackIn.pop());
}
}

用队列实现栈

思路:
1
此时我们需要两个队列来实现栈,一个queue1,一个queue2,queue2完全是用来备份的,push操作直接往queue2当中放,然后再把queue1里面的元素放到queue2里面去, 然后再把queue2里面的元素放到quque1
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Queue<Integer> queue1;
Queue<Integer> queue2;
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}

public void push(int x) {
queue2.offer(x);
while(!queue1.isEmpty()) {
queue2.offer(queue1.poll());
}
Queue tmp = new LinkedList();
tmp = queue1;
queue1 = queue2;
queue2 = tmp;
}

public int pop() {
return queue1.poll();
}

public int top() {
return queue1.peek();
}

public boolean empty() {
return queue1.isEmpty();
}

有效的括号

思路:
1
必须满足三个条件,每个左边的符号要和右边的符号匹配且同类型,左边的符号要以正确的顺序闭合,这个我觉得可以用栈来解决,当遍历到左符号了,就直接往里面压就行,当遍历到右边符号了,就直接和栈顶元素进行匹配,如果一样栈顶元素弹出,且遍历下一个元素,元素遍历完之后,判断栈是否为空。刚刚测试完发现,有没有注意到的点就是遍历到右边符号时要判断一个栈是否为空。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
Map<Character,Character> map = new HashMap<>();
map.put('[',']');
map.put('(',')');
map.put('{','}');
char[] chars = s.toCharArray();
for(int i = 0; i < chars.length; i++) {
if(chars[i] == '[' || chars[i] == '(' || chars[i] == '{') {
stack.push(chars[i]);
} else {
if(!stack.isEmpty() && map.get(stack.pop()) == chars[i]) {

} else {
return false;
}
}
}
if(stack.isEmpty()) return true;
return false;
}

逆波兰表达式求值

思路:
1
逆波兰表达式的思路就是,把每个字符串数组的tokens,如果是数字就往栈里面压,如果不是就把后面两个pop出来并进行计算,再把计算结果压入栈里面,继续遍历字符
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for(String str : tokens) {
if(str.equals("+")) {
stack.push(stack.pop() + stack.pop());
} else if(str.equals("-")) {
int num1 = stack.pop();
int num2 = stack.pop();
stack.push(num2 - num1);
} else if(str.equals("*")) {
stack.push(stack.pop() * stack.pop());
} else if(str.equals("/")) {
int num1 = stack.pop();
int num2 = stack.pop();
stack.push(num2 / num1);
} else {
stack.push(Integer.valueOf(str));
}
}
return Integer.valueOf(stack.pop());
}

滑动窗口最大值

思路:
1
题目的需求就是给定一个数组nums和一个k大小的滑动窗口,每次移动的时候给出滑动窗口里的最大值。我们需要一个队列queue来做滑动窗口,每次pop()的时候,队列出口的元素就是最大值,pop()出去即可。每次push的时候,push进去的元素要把小于它的元素给去除掉,直到遇到大于等于它的就停止。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Class MyQueue {
Queue<Integer> que = new LinkedList<>();

public void poll(int val) {
if(!que.isEmpty() && que.peek() == val) {
que.poll();
}

}

public void push(int val) {
while(!que.isEmpty() && que.getLast() < val) {
que.removeLast();
}
que.push(val);
}

public Integer front() {
return que.peek();
}
}


public int[] maxSlidingWindow(int[] nums, int k) {
int len = nums.length - k + 1;
int[] res = new int[len + 1];
int num = 0;
MyQueue myQue = new MyQueue();
// 先把K个元素放进去
for(int i = 0; i < k; i++) {
myQue.push(nums[i]);
}
res[num++] = myQue.peek();
for(int j = k; j < nums.length; j++) {
myQue.poll(nums[j-k]);
myQue.push(nums[j]);
res[num++] = myQueue.peek();
}
return res;
}

前k个高频元素

思路:
1
这个题目给一个数组nums和k,先通过Map把num中元素出现的频率进行统计一波,再把PriorityQueue放入二元组,对其pair[1] - pair[2]进行小堆顶进行排序,再通过创建一个k大小的数组存储前k个元素;
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int[] topKFrequent(int[] nums, int k) {
Map<Integer,Integer> map = new HashMap<>();
for(int num : nums) {
map.put(num,map.getOrDefault(num,0) + 1);
}
PriorityQueue<int[]> priorityQue = new PriorityQueue<>((pair1,pair2)->pair2[1] - pair1[1]);
for(Map.Entry<Integer,Integer> entry:map.entrySet()) {
priorityQue.add(new int[]{entry.getKey(),entry.getValue()});
}
int[] res = new int[k];
for(int i = 0; i < k; i++) {
res[i] = priorityQue.poll()[0];
}
return res;
}

二叉树

递归遍历

思路1:
1
前中后序遍历,我们这次主打的递归的方式,递归那么就需要确定递归的参数以及返回值,递归的终止条件,递归的逻辑,咱们这个递归的终止条件就是当遍历的节点为null时就是终止条件了,返回值是List<Integer>,所以我们需要创建一个List(result)来存储值,参数就是TreeNode以及result,如果是前序遍历那么递归的逻辑就是中左右,其他遍历也是如此
代码1:
1
2
3
4
5
6
7
8
9
10
11
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Traveral(root, result);
return result;
}
public void Traversal(TreeNode root, List<Integer> result) {
if(root == null) return;
result.add(root.val);
Traversal(root.left,result);
Traversal(root.right,result);
}
思路2:
1
用迭代法来实现前序遍历的原理很简单,就是通过栈来实现,先把根节点压入栈当中,在把根节点给pop出来,这样就能得到根节点的左右子节点,记得是先压入右子节点,再是左子节点,因为栈是先进后出的
代码2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
前序
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()) {
TreeNode root1 = stack.pop();
res.add(root1.val);
if(root1.right != null) stack.push(root1.right);
if(root1.left != null) stack.push(root1.left);
}
return res;
}
代码3:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
后序
public List<Integer> backorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()) {
TreeNode node = stack.pop();
res.add(node.val);
if(node.left != null) stack.push(node.left);
if(node.right != null) stack.push(node.right);
}
Collections.reverse(res);
return res;
}
思路4:
1
迭代法的中序遍历不太好整,遍历的顺序是左中右,那么我们可以先把元素存储到栈当中,先把最左边的,最左边为空了,再放最右边的,可以想象一个三层的二叉树,一个根节点,根节点接着左节点,但没有右节点,左节点跟着一个左节点+右节点,那么在遍历的时候,我们先把根节点压入栈当中,再把根节点的左孩子节点压入栈中,再把左孩子的左孩子压入栈中,此事孙左节点没有左右儿子了,那么就把它从栈里面弹出来,指针cur指向弹出的左子孙的右节点,判断右节点为空,那么此时就把左孩子给弹出来,再把cur指向右子孙,听起来挺复杂,咱们只要把它实例化就行了。去想想它的构造过程,Though it is painful.
代码4:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null){
return res;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while(cur != null || !stack.isEmpty()) {
if(cur != null) {
stack.push(cur);
cur = cur.left;
} else {
cur = stack.pop();
res.add(cur.val);
cur = cur.right;
}
}
return res;
}

层序遍历

思路:
1
我们使用队列Queue来存储二叉树的节点,从左到右的一层一层的遍历,怎么一层一层的遍历? 我们通过队列里面的元素大小来确定每次每一层的元素大小。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
Queue<TreeNode> que = new LinkedList<>();
if(root != null) que.offer(root);
while(!que.isEmpty()) {
int size = que.size();
List<Integer> list = new ArrayList<>();
for(int i = 0 ; i < size; i++) {
TreeNode node = que.poll();
list.add(node.val);
if(node.left != null) que.offer(node.left);
if(node.right != null) que.offer(node.right);
}
res.add(list);
}
return res;
}

翻转二叉树

思路:
1
这个题目只要把左右子节点进行调换即可,前后序的递归,前序的迭代,层序遍历都可以实现
代码1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 DFS
public TreeNode invertTree(TreeNode root) {
if(root == null) return null;
swap(root);
invertTree(root.left);
invertTree(root.right);
return root;
}
public TreeNode invertTree(TreeNode root) {
if(root == null) return null;
invertTree(root.left);
invertTree(root.right);
swap(root);
return root;
}
public void swap(TreeNode root) {
TreeNode node = root.left;
root.left = root.right;
root.right = node;
}
代码2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 
public TreeNode invertTree(TreeNode root) {
Stack<TreeNode> stack = new Stack();
if(root == null) return root;
stack.push(root);
while(!stack.isEmpty()) {
TreeNode node = stack.pop();
swap(node);
if(node.right != null) stack.push(node.right);
if(node.left != null) stack.push(node.left);

}
return root;
}
public void swap(TreeNode root) {
TreeNode node = root.left;
root.left = root.right;
root.right = node;
}
代码3:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  public TreeNode invertTree(TreeNode root) {
Queue<TreeNode> que = new LinkedList<>();
que.offer(root);
while(!que.isEmpty()) {
int size = que.size();
for(int i = 0;i < size; i++) {
TreeNode node = que.pop();
swap(node);
if(node.left != null) que.offer(node.left);
if(node.right != null) que.offer(node.right);
}
}
return rootl
}
public void swap(TreeNode root) {
TreeNode node = root.left;
root.left = root.right;
root.right = node;
}

对称二叉树

思路:
1
这个可以用递归和迭代两种写法,递归我们先要确定终止条件,左右子节点只有一个为空,返回false,都为空返回false,左右都不空但值不同返回false,递归的参数就是左右子节点,递归的单层逻辑就是,左孩子的左孩子与右孩子的右孩子相比较。左孩子的右孩子和右孩子的左孩子相互比较,比较完之后看是否都相等,返回比较结果。
代码1:
1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return isCompare(root.left,root.right);
}
public boolean isCompare(TreeNode left,TreeNode right) {
if(left == null && right != null) return false;
else if(left != null && right == null) return false;
else if(left == null && right == null) return true;
else if(left.val != right.val) return false;
boolean L = isCompare(left.left,right.right);
boolean R = isCompare(left.right,right.left);
return L&&R;
}
代码2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public boolean isSymmetric(TreeNode root) {
Queue<TreeNode> que = new LinkedList<>();
que.offer(root.left);
que.offer(root.right);
while(!que.isEmpty()) {
TreeNode left = que.poll();
TreeNode right = que.poll();
if(left == null && right == null) continue;

if(left == null && right !=null) return false;
else if(left != null && right ==null) return false;
else if(left.val != right.val) return false;

que.offer(left.left);
que.offer(right.right);
que.offer(left.right);
que.offer(right.left);
}
return true;
}

最大深度

思路:
1
这个题目最好是用前序遍历来写这样才能让你弄懂这个题目,因为用前序的方式来写的话你就能清楚什么叫回溯了,当然你也可以用后序遍历的方式来写,但这样感受不到回溯的美,也可以用迭代法来写,这样写其实其实就是层序遍历了。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
后序遍历
public int maxDepth(TreeNode root) {
if(root == null) return;
int L = maxDepth(root.left); // 左
int R = maxDepth(root.right); // 右
int depth = 1 + Math.max(L,R); // 中
return depth;
}
public int maxDepth(TreeNode root) {
int result = 0;
maxD(root,1);
return result;

}
public void maxD(TreeNode root,int depth) {
result = result > depth ? result:depth;
if(root.left == null && root.right == null) return;
if(root.left != null) {
depth++
maxD(root.left,depth);
depth--
}
if(root.right != null) {
depth++;
maxD(root.right,depth);
depth--;
}


}
代码1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int maxDepth(TreeNode root) {
Queue<TreeNode> que = new LinkedList<>();
if(root == null) return 0;
que.offer(root);
int res = 0;
while(!que.isEmpty()) {
res++;
int size = que.size();
for(int i = 0; i < size; i++) {
TreeNode node = que.pop();
que.offer(node.left);
que.offer(node.right);
}
}
return res;
}

最小深度

思路:
1
首先清楚什么是最小深度,从根节点到最短路程的叶子节点,那么我们可能会遇到的情况就是左子树为空,那么最短距离就是1+右子树最短深度, 右子树为空,那么最短距离就是1+左子树最短深度,递归的写法就是,停止条件就是碰到左右节点为空,单层逻辑就是先判断得到左右字子树高度,再给出最低的。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 public int minDepth(TreeNode root) {
if(root == null) return 0;
if(root.left == null && root.right != null) {
return 1 + minDepth(root.right);
}
if(root.left != null && root.right == null) {
return 1 + minDepth(root.left);
}
int L = minDepth(root.left);
int R = minDepth(root.right);
int minD = 1 + Math.min(L,R);
return minD;
}
//迭代法
public int minDepth(TreeNode root) {
Queue<TreeNode> que = new LinkedList<>();
if(root == null) return 0;
que.offer(root);
int depth = 0;
while(!que.isEmpty()) {
int size = que.size();
depth++;
for(int i = 0; i < size; i++) {
TreeNode node = que.pop();
if(node.left != null)que.offer(node.left);
if(node.right != null)que.offer(node.right);
if(node.left == null && node.right == null) return depth;
}
}
return depth;
}

完全二叉树

思路:
1
对于普通的二叉树直接用递归的方式就能解决,对于完全二叉树,咱们有特殊的办法,什么特殊的办法呢? 完全二叉树里面肯定都会有子的满二叉树,怎么判断是否是满二叉树呢? 最左边的深度和最右边的深度是一样长说明是满二叉树,你怎么这么确定呢? 因为完全二叉树的原因,完全最后一层的叶子节点都是靠左的。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public int countNodes(TreeNode root) {
if(root == null) return 0;
TreeNode left = root.left;
TreeNode right = root.right;
int L = 0;
while(left != null) {
L++;
left = left.left;
}
int R = 0;
while(right != null) {
R++;
right = right.right;
}
if(L == R) {
return (2 << L) - 1;
}
return 1 + countNodes(root.left) + countNodes(root.right);
}

平衡二叉树

思路:
1
这个题目最好是用递归来写,因为是求高度,咱们用后序遍历来写这道题目。平衡二叉树就是左右子树的高度差小于1
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public boolean isBalanced(TreeNode root) {
if(root == null) return true;
return getHeight(root) == -1 ? false:true;
}
public int getHeight(TreeNode root) {
if(root == null) {
return 0;
}

int L = getHeight(root.left);
int R = getHeight(root.right);

if(Math.abs(L-R) > 1) {
return -1;
}
int res = Math.max(L,R) + 1;
return res;

}

二叉树所有的路径

思路:
1
这个直接用递归去写就好,确定好终止条件是什么,单层逻辑是什么,递归函数的参数是什么,这样就可以了,听说迭代也可以写,本题用到了回溯的方法。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public List<String> binaryTreePaths(TreeNode root) {
List<String> res = new ArrayList<>();
List<Integer> list = new ArrayList<>();
list.add(root);
getPath(root,list,res);
return res;
}
public void getPath(TreeNode root, List<Integer> list, List<String> res) {
if(root.left == null && root.right == null) {
int size = list.size();
StringBuilder sb = new StringBuilder();
for(int i = 0; i < size - 1; i ++) {
sb.append(list.get(i));
sb.append("->");
}
sb.append(list.get(size - 1));
res.add(sb.toString());
}
if(root.left != null) {
list.add(root.left);
getPath(root.left,list,res);
list.remove(list.size() -1);
}
if(root.right != null) {
list.add(root.right);
getPath(root.right,list,res);
list.remove(list.size() -1);
}
}

贪心

分发饼干

思路:
1
这是一个贪心算法,也就是通过局部最优得到全局最优。我们需要能喂足尽可能多的孩子,那么最大孩子数是饼干数,g[i] >= s[j],需要先给这两个数组排序,用最大的饼喂给胃口最大的
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
public int findContentChildren(int[] g, int[] s) {
Collections.sort(g);
Collections.sort(s);
int res = 0;
int index = s.length - 1;
for(int i = g.length - 1; i >= 0; i--) {
if(index >=0 && s[index] >= g[i]) {
res++;
index++;
}
}
return res;
}

摇摆序列

思路:
1
前后的差值是不一样的,那么我们就可以定义两个参数了,一个preDiff,一个curDiff,一个代表当前元素和前面的元素差距,一个代表当前的元素和下一个元素的差距,我们需要去维护有多少个峰值。我们会遇到两种特殊的情况,第一种就平坡,比如 1,2,2,2,1.其实这个序列只有2个峰值,那么我们可以舍去左边的两个2,也就是当preDiff = 0,curDiff > 0的时候算一个峰值,第二种特殊情况就是出现单调递增1,2,2,2,2,3,4.如果我们还是按遍历的顺序来实时维护preDiff的话,也就是(preDiff >=0 && curDiff < 0)就算峰值的话,再单调的坡上面是不行的,那么我们需要在遇到峰值的时候再改变preDiff.最后的一个特殊情况就是只有两个元素,也就是不能满足preDiff,那么该怎么办呢?我们都是先假设第一个元素前面会有一个和第一个元素值相同的元素,这样就有了preDiff = 0,并且与此同时我们默认最右边的元素是一个峰值。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
public int wiggleMaxLength(int[] nums) {
int curDiff;
int preDiff = 0;
int res = 1; // 默认最右边的元素为一个峰值
for(int i = 0; i < nums.length - 1; i++) {
curDiff = nums[i+1] - nums[i];
if((preDiff >= 0 && curDiff < 0) || (preDiff <= 0 && curDiff > 0)) {
res++;
preDiff = curDiff;
}
}
return res;
}

最大子序和

思路:
1
主要贪心贪的地方就是当出现和为<=0的时候那么咱们就可以停止了,每到连续最大值的时候就记录以下。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
public int maxSubArray(int[] nums) {
int res = Integer.MIN_VALUE;
int sum = 0;
for(int i : nums) {
sum += i;
if(sum > res) res = sum;
if(sum <= 0) {
sum = 0;
}
}
return res;
}

买卖股票

思路:
1
局部最优获得整体最优,我们把股票价格数组prices进行拆分,只要相邻两天为正数,那么这笔买卖咱们就做
代码:
1
2
3
4
5
6
7
8
public int maxProfit(int[] prices) {
int[] value = new int[prices.length - 1];
for(int i = 0; i < prices.length - 1; i++) {
if((prices[i+1] - prices[i]) > 0) sum += (prices[i+1] - prices[i]);

}
return sum;
}

跳跃游戏

思路:
1
在每个起跳点values[i]上力所能及的所有点位上选择能条最远的。
代码:
1
2
3
4
5
6
7
8
public boolean canJump(int[] nums) {
int cover = 0;
for(int i = 0; i <= cover; i++) {
cover = Math.max(i + nums[i],cover);
if(cover >= (nums.length - 1)) return true;
}
return false;
}
代码2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public int jump(int[] nums) {
int cover = 0;
int moreCover = 0;
int res = 1;
int startIndex = 0;
int highIndex = 0;
while(moreCover < (nums.length - 1)) {
cover = moreCover;
startIndex = highIndex;
for(int i = startIndex; i <= cover; i++) {
if((i+nums[i]) > moreCover) {
moreCover = i+nums[i];
highIndex = i;
}
}
res++;
}
return res;
}

k次取反

思路:
1
k次取反可以对同一个元素重复取,我们先通过从小到大进行排序,遇到K小于0的取反,遇到大于0的直接来回取反
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
public int largestSumAfterKNegations(int[] nums, int k) {
nums = IntStream.of(nums).boxed().sorted((o1,o2) -> Math.abs(o2) - Math.abs(o1)).mapToInt(Integer::intValue).toArray();
for(int i = 0; i < nums.length && k > 0; i++) {
if(nums[i] < 0) {
nums[i] = -nums[i];
k--;
}
}
if(k % 2 == 1) {
nums[nums.length -1] = -nums[nums.length -1];
}
return Arrays.stream(nums).sum();
}

汽车加油

思路:
1
用暴力的解法很简单,循环每个加油站,每个加油站i到下一个加油站i+1需要cos[i]升汽油,
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
public int canCompleteCircuit(int[] gas, int[] cost) {
for(int i = 0; i < gas.length; i++) {
int res = 0;
res = gas[i] - cos[i];
int index = (i+1)%gas.length;
while(res < 0 && index != i) {
res += gas[index] - cos[index];
index = (1+index)%gas.length;
}
if(res >=0 && index == i) return i;
}
return -1;
}
代码1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int canCompleteCircuit(int[] gas, int[] cost) {
int curSum = 0;
int totalSum = 0;
int start = 0;
int i;
for(i = 0; i < gas.length; i++) {
curSum += gas[i] - cost[i];
totalSum += gas[i] - cost[i];
if(curSum < 0) {
start = i + 1;
curSum = 0;
}
}
if(totalSum < 0) return -1;
return start;
}

分发糖果

思路:
1
先从左到右前序遍历,判断右边大于左边的情况,再从右到左的后序遍历,判断左边大于右边的情况,这个是时候要非常注意的点就是当左边大于右边时,得选一个(candy[i],candy[i+1] + 1)中最大的,candy[i]是i-1和i相比较得到的,而后者是i和i+1相互比较得到的
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int candy(int[] ratings) {
int[] candys = new int[ratings.length];
for(int i = 0; i < candys.length; i++) {
candys[i] = 1;
}
for(int i = 0; i < ratings.length - 1; i++) {
if(ratings[i+1] > rating[i]) {
candys[i+1]++;
}
}
for(int i = ratings.length - 2; i >= 0; i--) {
if(ratings[i] > ratings[i+1]) {
candys[i] = Math.max(candys[i],candys[i+1] + 1);
}
}
return Arrays.stream(candys).sum();
}

柠檬水找零

思路:
1
5,10,20这三个元素是固定的,当我们收到5元时直接收入,10元就是返五元,20就先放10再返5,或者先返10再返10
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public boolean lemonadeChange(int[] bills) {
int[] moneys = new int[3];
for(int i:bills) {
if(i == 5) {
moneys[0]++;
}
if(i == 10) {
if(moneys[0] <=0 ) return false;
moneys[1]++;
moneys[0]--;
}
if(i == 20) {
if(moneys[1] >=1 && moneys[0] >= 1) {
moneys[1]--;
moneys[0]--;
moneys[2]++;
} else {
moneys[0] -= 3;
if(moneys[0] < 0) return false;
}
}
}
return true;
}

弓箭引爆气球

思路:
1
先对数组元素的起始下标进行排序,找到右边界最小值,再去判断左边界是否超出最小右边界,若有超出则给弓箭加1,重新更换最小右边界,其实右边界是随着遍历变化的,我们需要判断下一个元素的左边界是否大于最小右边界,这样我们就能得到全局最优,res记得遍历之后要加1,因为不管最后是否有重叠都是一样的要加1。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int findMinArrowShots(int[][] points) {
Arrays.sort(points,(o1,o2) -> Integer.compare(o1[0] - o2[0]));
int res = 0;
int minRightValue = Integer.MAX_VALUE;
for(int i = 0; i < points.length; i++) {
if(points[i][1] < minRightValue) {
minRightValue = points[i][1];
continue;
}
if(points[i][0] > minRightValue) {
minRightValue = points[i][1];
res++;
}
}
return res+1;
}

无重复区间

思路:
1
总数组个数减去不相交的区间,我们先对数组进行右排序,每当下一个元素的左边的值大于等于上一次的元素的右边值,那么就将结果加1,并且将minRight更新为此元素的右边值
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort((o1,o2) -> Integer.compare(o1[1],o2[1]));
int remove = 0;
int pre = intervals[0][1];
for(int i = 1; i < intervals.length; i++) {
if(intervals[i][0] < pre) {
remove++;
pre = Math.min(pre,intervals[i][1]);
} else {
pre = intervals[i][1];
}
return remove;
}
}

划分子区间

思路:
1
先是对每个小字母出现的最大值进行记录,然后我们再遍历数组,当最大值下标和正在遍历的值相等时就是最大的区间了。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public List<Integer> partitionLabels(String s) {
int[] edge = new int[26];
char[] chars = s.toCharArray();
for(int i = 0 ; i < chars.length; i++) {
edge[chars[i] - 'a'] = i;
}
int maxIndex = 0;
int last = 0;
List<Integer> res = new ArrayList<>();
for(int i = 0; i < chars.length; i++) {
maxIndex = Math.max(maxIndex,edge[chars[i] - 'a']);
if(i == maxIndex) {
res.add(maxIndex - last + 1);
last = maxIndex + 1;
}
}
return res;
}

合并区间

思路:
1
咱们先对数组进行左排序,遍历数组,动态更新最右边的值,每次取最大的,前提是下一个元素的最左边会小于之前的右边值,如果左边值大于之前的右边值那么就算了。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public int[][] merge(int[][] intervals) {
List<Int[]> res = new ArrayList<>();
Arrays.sort(intervals,(o1,o2) -> Integer.compare(o1[0],o2[0]));
int maxRightIndex = intervals[0][1];
int last = intervals[0][0];
for(int i = 1; i < intervals.length; i++) {
if(maxRightIndex >= intervals[i][0]) {
maxRightIndex = Math.max(maxRightIndex,intervals[i][1]) // 其实这一步可以不比较
continue;
} else {
res.add(new int[] {last,maxRightIndex});
last = intervals[i][0];
maxRightIndex = intervals[i][1];
}
}
int[][] arr = new int[list.size()][2];

// 遍历 List,并将每个元素转换为 int 型并保存到数组中
for (int i = 0; i < list.size(); i++) {
arr[i][0] = list.get(i)[0].intValue();
arr[i][1] = list.get(i)[1].intValue();
}
return arr;
}

递增数字

思路:
1
暴力解法理论上是可以的,但是在力扣上面会超时,首先要意识到一点就是当遇到StrNum[i] < StrNum[i-1],此时strNum[i-1]要--。比如说98这个,变成89.那么我们如果从前向后遍历的话会不会出问题? 当然会了,如果StrNum[i-1] < StrNum[i]时,StrNum[i-1]--,但是此时StrNum[i-2]会大于StrNum[i-1]该咋整。就比如说335,变成了329这好吗? 这不好, 那么我们就向后遍历,这不美滋滋
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int monotoneIncreasingDigits(int n) {
String[] nums = (n + "").split("");
int start = nums.length;
for(int i = nums.length-1; i > 0; i--) {
if(Integer.parseInt(nums[i-1]) > Integer.parseInt(nums[i])) {
nums[i-1] = String.valueOf(Integer.valueOf(nums[i-1]) -1);
start = i;
}
}
for(int i = start; i < nums.length; i++) {
nums[i] = "9";
}
return Integer.parseInt(String.join("",strings));
}

MyBatis:ambulance:

Redis:aerial_tramway:

如何保证Redis与Mysql数据库的一致性?

1
保证Redis以及MySQL的数据一致性问题,这里有两种方案,首先就是先更新MySQL数据库再更新Redis,其次就是先删除Redis缓存再通过更新MySQL数据库来更新Redis缓存来达到数据一致性,其次可以通过消息通信,还有就是cannel

Redis集群如何工作?

什么是Redis?
1
Redis是一个基于内存的key-value的非关系数据库,它支持5种数据类型,但不是所有数据类型都支持分布式例如:Sorted set.
Redis的集群组成是什么样的?
1
2
3
4
路由器: 负责把请求分发到不同的Redis节点上面
主节点: 主要负责读写的节点
从节点: 必要时可以为主节点分担读的压力
集群总线: 用来探测集群里某些节点是正常
工作流程是什么样的?
1
当请求发送过来的时候,路由器负责把请求分发到指定的节点上面,如果请求落到主节点上面直接进行读写操作,如果是丛节点将请求转移给主节点。集群里面的节点会定时发送心跳检测,如果某个节点长时间没有进行心跳发送,那么这个节点可以认为是宕机了(哨兵模式),如果主节点宕机了,从节点顶上,如果主节点恢复不了了,将数据迁移到某个节点,并讲某个节点升级为主节点。当需要新增加一个Redis节点时,需要这个Redis节点与集群中的一个主节点发生三次握手(类似TCP连接),如果加入那么就通知其他集群中的节点,此时组成了一个更加庞大的集群。
路由器是怎么找节点的?
1
根据发送请求的键值对来判断要存放的地方,存放的地方就是hash槽(16384)
Redis的应用场景是什么?
1
排行榜需要用到Sorted set,共同好友、共同爱好用到Set,Session登录如果遇到集群,那么落地到不同机器的时候需要频繁登录,那么此时可以用String,如果要限制访问次数,点赞数,计数器的功能我们就需要String.Hash可以存储对象信息。
Redis不适用的场景有哪些?
1
数据量大,且访不频繁的数据,这简直纯属浪费内存资源

AOF的工作原理是什么?

1
Redis中的AOF能够实现数据的实时持久化操作,但是这样会造成一个问题就是AOF的存储文件过大,这样IO的性能就会降低,读取内存当中所有键值对的时候时间过长
如何解决AOF的IO性能?
1
AOF的重写功能能够挺高IO性能也就是减小AOF文件大小,AOF的重写通过一个子进程同步Redis内存当中的所有键值对,AOF重写说白了就是把相同操作相同的Key的不同命令压缩,还有个问题是可能会造成Redis内存与AOF的数据不一致
如何解决AOF重写不一致?
1
加入一个AOF缓冲区文件,先通过新的AOF文件读取Redis当中的键值对,重写完后,再把AOF缓冲区文件当中的数据复制到新的AOF文件当中

Redis当中SDS的原理是什么?

1
Redis当中的SDS(Simple Synmatic String), 这个对象里面有len,alloc,buf[],flags(类型)
SDS的相比C语言的字符串好在哪里?
1
2
3
能够动态分配String的容量,比如String的拼接或者删除
不需要多次的修改字符串的内存,其中使用到内存预分配
二进制安全传输

如何保持Redis的数据一致性?

1
2
3
首先可以通过主动更新的方式(这里涉及到先删除缓存还是先更新数据库的问题),当需要写数据库的时候,其实无论是先删除缓存还是先更新数据库都会出现数据不一致的问题,在发生数据不一致性概率的高低程度来看我们更偏向于先更新数据库再删除缓存。
其次可以通过Redis自带的内存淘汰机制
最后可以通过给key设置有效时间TTL

缓存会遇到哪些问题?

概念描述:
1
2
3
缓存穿透: 当数据库和缓存都没有某个key的数据的时候,用户一直访问会导致数据库崩溃
缓存雪崩: 在某个时间段大量的key同时失效
缓存击穿: 当某个key一直处于高并发访问的时候突然失效了,那么数据库会崩溃
解决方法:
1
2
3
缓存穿透的解决方案: 访问某个key的时候,如果缓存没有命中,数据库当中同样也没有,那么我们就在缓存把key的值设置为null,set(key,null)。
缓存雪崩的解决方案: 给不同的key设置随机的TTL
缓存击穿的解决方案: 通过互斥锁的方式来获取操作某个key的缓存重建对象锁。当没有获取到锁的线程去访问的时候,正好有一个线程在访问互斥锁,那么其他的线程需要休息一段时间然后重新从Redis当中去访问。

image-20230710093712334

image-20230710094631453

MySQL:apple:

InnoDB的原理是什么?

声明: a是索引字段
1
InnoDB中每一页16kb大小,默认是10行数据,页目录和用户数据区域是主要构成成分,用户数据区域的数据插入是按顺序插入的(索引),这样查询的时候可以提高性能; Innodb有三大优点: 1.支持高并发(多版本并发控制,某个一行数据更新的时候有个版本数据) 2.支持事务 3.支持外键和聚族索引(也就是说删除某个表的数据那么有这个表的外键索引的外表同时也会删除此行数据);同时Innodb也有缺点,缺点就是存储的时候的内存这块需求较大,可以通过压缩表的方式以及选择合适的数据类型和数据大小来减少内存的使用。对比MylSAM的话,MylSAM不支持事务和高并发,MyLSAM更适合用于读不适用写。
如果在插入某个数据的时候当前这一页数据满了怎么办?
1
新创建一页,并且要判断这条数据的索引值在当前页中是否满足插入条件,并且把最后的一条满足条件的数据给放到新的一页当中,相邻页用指针相连接,其实这样还是非常的难受的,这也是为什么我建议使用自增的索引

请看下图

image-20230424155002346

再建立一级索引,二级索引,主要就是通过范围查找来减少IO。

什么情况会是全表扫描?
1
select * from table_name where b = 2,这个b不是索引,那么就需要全表扫描
哪些常见的sql语句会用到索引?
1
select * from a > 6; 这个会先去找a=6的索引, 底层的数据区域也都是两个指针相互连接并且有顺序的,那么只需要a后面的数据就可以了; select * from a <6 同样如此
什么是联合索引?
1
就是多个字段一起作为索引(b,c,d)三个字段都作为索引,这样会有一个索引树依据b,c,d来建立的,并且遵从最左前缀的规律,也就是说以*cd,**d这种就不可以作为索引进行查找,写成sql语句就是: select * from where c = 1 and d = 1;
什么是索引覆盖?
1
索引覆盖就是当你要查询的字段都在建立好的索引表里面都有那么你就不需要再去通过主键索引去找了。
为什么不通过主键索引去找?
1
因为主键索引去找的话,可以想想就是主键索引的数据页(16kb)每一页能存储2条,而联合索引树的数据页可以存储三条,这样就减少了页数也就减少了IO操作,这样就能提高IO的性能,这样就不用回表了,因为联合索引下面的叶子节点本来就有你想要查找的数据。
再想想为什么只能存储三条呢?
1
原因很简单就是因为,主键索引下面每条记录存储的是完整的一条记录,而联合索引下面存储的字段要少很多,主要就是联合索引的几个字段加上主键索引的字段。
能用sql语句展示吗?
1
2
select b,c,d,a from table_name where b > 1,此时此刻就能使用联合索引,因为要查询的字段联合索引里面都有。
select * from table_name where b > 1这样的话就全表扫描比索引强多了,因为这样还要回表。

请看下图

image-20230424175008539

什么情况下会导致索引失效?

只要对字段的值有所改变那么索引就会失效

1
select e from table_name where e = 1,e是varchar类型(255),SQL除了数字之外,其他的英文什么的都转化位0,而数字任然是数字,这段索引为什么会失效原因就是,e字段是varchar类型,如果要和1比较,那么自生就得先转化为数字,这样消耗的成本实在太大,那就只能走全表扫描了。

请看下图

image-20230424203138333

也有可能破坏一个完整的B+树:本来1是小于b的,结果转化为Number类型之后b变成了0。

image-20230424225135663

Innodb是如何实现事务的?
1
四种元素:Buffer Pool,LogBuffer,Redo Log,Undo Log。在一次修改数据的事务当中,Innodb收到update请求之后,将根据要修改的数据查找到某一页并使用Buffer Pool存储这一页的数据,再对数据进行修改生成Redo Log对象存储再LogBuffer当中,如果你突然不想修改了那么你就执行Undo Log。
主键索引的图可以优化成什么样?

image-20230426114041672

为什么Mysql单表最大两千万?

1
在索引页中,假设主键a大小为8个bit,页号大小为4个bit,那么每页15k那么就能存储15k/12 = 1280,在叶子节点(也就是数据页当中)每一条1k,那么15k/1k = 15,根据数据量计算公式1280^(z-1)* 15,其中z为B+数的层数。
为什么是15k而不是16k?
1
一页当中包含页目录以及页头,去掉之后就剩下15k
为什么不可以是1亿?
1
答案是当然可以, 假设数据页是250byte每条,那么如果是3层,N = 1280^2 * 60 = 1个亿

MVCC是什么?

1
MVCC是MYSQL中的innodb引擎在RC,RR事务且快照读当中实现的多版本并发控制,这个版本下面有隐藏字段,arx_id 和 rollPointer分表代表着某个版本的记录id和指向当前版本的前一个版本,Undo版本链代表着多个版本形成的链表,ReadView代表着你在多个版本下面选出一个版本出来,选哪个版本的时候会伴随着某个选择版本的算法

image-20230517113457231

MVCC在RC、RR场景的区别?
1
如果读取某条记录两次,那么在RC场景下面,每次返回的是不同的版本,而在RR下面返回的是同一个版本,这也天然的方式解释了什么是不可重复读和可重复读
MVCC有什么用?
1
避免了被阻塞,虽然可能读取的数据不是最新的数据
1
在RR下避免了幻读

事务隔离级别

1
2
3
4
1.read uncommited
2.read commited
3.Repeatable read
4.Serilizable
脏读
1
2
3
1.事务A读取数据
2.事务B insert数据并未提交
3.事务A读取数据发现数据有变化
不可重复读
1
2
3
1.事务A读取数据
2.事务B insert数据并提交
3.事务A读取数据发现数据有变化
幻读
1
2
3
4
5
1.事务A读取数据
2.事务B insert数据(1,老王)
3.事务A读取数据发现没有变化
4.事务A insert数据(1,老哥)
5.事务A爆出错误 Duplicate 意味着主键重复

侧重点

1
2
脏读和不可重复读侧重于读-读的不同
幻读侧重于读-读-写,通过写来验证读的幻觉

image-20230727171413977

快照读和当前读

image-20230727172112084

表锁和意向锁的缘分
1
表锁是否加成功得看意向锁的脸色,意向锁相当于整个表的一个标识,当执行DML的时候会加意向锁,那么表锁来加锁的时候得看意向锁,意向锁标识当前表是否有数据行有加锁,这样就不用在加表锁的时候一行一行的查看是否要加锁了

事务是怎么实现的?

1
首先事务主要是有四个特性,ACID,A就是Atomic原子性,原子性其实是基于这个undolog日志来实现的,就是说在执行DML语句的时候,执行的数据会先存储在undolog版本链里面,不是真正的数据库文件里,如果事务执行失败就可以撤销操作,如果事务成功,commit提交之后它可以把数据真正的放到数据库文件里面,C就是Consistency一致性,这个一致性其实是基于其他三个属性实现的,I就是Isolation,就是隔离性,这个隔离性有四个级别,都是基于这个MVCC和锁来实现的,MVCC层面比如说快照读就是在RR和Serilaze实现,读取的是之前读取的undolog版本,就是在每次读取的时候会服用之前的ReadView,但是在RC级别下,当前读是读取的最新的一个undolog版本日志,也就是每次查询都是新建的一个readview,从锁的层面来讲的话,就是RC级别下就是行锁,RR级别是在行锁的粒度基础上面加了间隙锁,就是会锁住某个范围的数据,当某个事务读取数据时,其他不允许插入数据,锁的性质来讲的话就是,在RR级别下,A事务读取数据时,其他事务可以得到了共享锁和排它锁,B事务写入数据时,其他事务不能得到共享锁和排它锁。

Java基础:anchor:

Java反射

反射实现有哪些?
1
2
3
Class.forName("com.jdbc.cj.Driver.mysql")
类名.class
对象名.getClass()
反射优缺点有哪些?
1
2
优点: 能够动态的获取类的实例,提高灵活性
缺点: 会降低性能,解决办法: 1. 如果多次创建某个对象的实例,使用缓存 2. 通过SetAccessible关闭JDK的安全检查来提高提升反射速度。

访问修饰符

image-20230707135655807

1
这个本类指的是自己创建的这个类里面的范围,而不包括本类下面的一个默认修饰符修饰的类
修饰符使用范围?
1
2
四种修饰符可以用在属性和方法
类和接口只能用public 和 默认
为什么子类的访问修饰符得大于父类?
1
2
private修饰的属性和方法子类是不能继承的。
当子类对象当作父类对象使用的时候,如果父类是public 而子类是private,那么想要调用子类的方法就不行了,这不符合继承的特性。

类相关

super关键字
1
2
3
super关键字是指调用父类的当中的一些特征,super()方法必然会执行,通常来构造方法的第一行,所以在初始化子类的实例对象的时候先调用父类的初始化特征,注意: new 照样还是子类的对象,super只是给子类扩充属性
super关键字通常能够省略,有些情况不能省略:1.需要调用父类的属性,但是子类和父类有相同的属性名和方法名,如果子类想要调用父类的属性或者方法那么必须使用super来访问
super可以看作是对new 对象的属性的一个扩充,这也是为什么每个对象都有一个wait()和notify()方法

image-20230622102057654

String类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StringTest02 {
public static void main(String[] args) {
String s1 = "hello";
// "hello"是存储在方法区的字符串常量池当中
// 所以这个"hello"不会新建。(因为这个对象已经存在了!)
String s2 = "hello";

// == 双等号比较的是变量中保存的内存地址
System.out.println(s1 == s2); // true

String x = new String("xyz");
String y = new String("xyz");

// == 双等号比较的是变量中保存的内存地址
System.out.println(x == y); //false
}
}

image-20230622104128081

数组

Java当中的数组有一维、二维、三维(三维几乎用不到),数组的优点就是检索的过程会非常快,就比如说让你查100个大小数组和查10000个大小数组,你觉得哪个更快? 其实是一样快的,数组的存储位置的元素都是连续存储的,并且每个元素都有下标,而且每个元素的存储大小都是一样的,这样就知道了偏移量,所以只需要算一遍就能计算出最终结果。二维数组就是每个元素都是一个一维数组。

数组需要注意的点:
  1. 数组要求元素的类型一样
  2. 数组的每个元素的大小一样
  3. 数组元素的存储位置是连续的,方便查找

接口

  1. 接口和接口之间不需要有任何的继承关系就可以完成互转,可能会爆出ClassCastExceptino问题
  2. 类转化成没有任何关系的接口是可以的
  3. 类和类之间转化必须得有继承关系
  4. 接口不可以被实例化,接口当中的方法默认是public abstract ,方法是用public final 修饰,也就意味着接口当中所有的元素都是用public修饰,同样接口当中不能写方法体。
  5. 一个接口可以继承多个方法
  6. 一个类可以实现多个接口

接口的出现解决了之前的什么问题? 首先就是之前遗留下的类之间只能单继承的问题,而现实当中往往是多继承关系

Java加载与执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
编译期:(可以在windows上)
第一步:在硬盘的某个位置(随意),新建一个xxx.java文件
第二步:使用记事本或者其它文本编辑器例如EditPlus打开xxx.java文件
第三步:在xxx.java文件中编写“符合java语法规则的”源代码。
第四步:保存(一定要将xxx.java文件保存一下)
第五步:使用编译器(javac【JDK安装后自带】)对xxx.java文件进行编译。

第六步:如果xxx.java文件中编写的源代码是符合语法规则的,编译会通过,
如果xxx.java文件中编写的源代码违背了语法规则,那么编译器会报错,编译器
报错之后class文件是不会生成的,只有编译通过了才会生成class字节码文件。
并且一个java源文件是可以生成多个class文件的。(编译实质上是检查语法)

运行期(JRE在起作用):(可以在windows上,也可以在其他的OS上。)
第七步:如果是在Linux上运行,需要将windows上生成的class文件拷贝过去
不需要拷贝源代码,真正运行的是字节码。(但是源代码也不要删除,有用)

第八步:使用JDK自带的一个命令/工具:java(负责运行的命令/工具)执行字节码

第九步:往下的步骤就全部交给JVM了,就不需要程序员干涉了。
JVM会将字节码文件装载进去,然后JVM对字节码进行解释(解释器负责将字节码
解释为1010101010..等的二进制)

第十步:JVM会将生成的二进制码交给OS操作系统,操作系统会执行二进制码和
硬件进行交互。

JDK\JRE\JVM

1
2
JDK包含JRE,JRE包含JVM,JVM不能独立存在,其他两个可以。
JDK是Java开发的工具箱,JRE是Java程序的运行时环境

并发编程:avocado:

ReentryLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
可重入锁
ReentryLock implements Lock{
void lock() {
sycn.lock();
};
void unlock() {
sycn.release();
};
public class Sycn extends AbstractQueueSychonizer {

}

public Sycn sycn;
}
AQS extends AbstractOwnableSynchonizer {
这个就是线程的对象锁的拥有
volatile int state;// 锁的同步状态
阻塞队列;
条件队列;
}
其中阻塞队列和条件队列都是用Node结点来操作
class Node {
public Thread thread; // 代表当前线程
}
阻塞队列是通过FIFO的顺序进出队列,条件队列是通过满足特定条件之后,线程就能出来了,比较的灵活

同步异步阻塞非阻塞

1
2
3
4
5
客户 -> 服务员 -> 后厨
首先声明: 我们把服务员看作是请求线程,后厨看作是处理线程
1. 同步加阻塞: 客户把点的菜给服务员,服务员把菜单给后厨,同时客户在店里面等着,服务员也是一样,且不能服务于其他客户
2. 同步加非阻塞: 客户把点的菜给服务员,服务员把菜单给后厨,给完之后服务员再去接待其他客户,但是得过一会就得再去问问后厨做好了没有,但是客户还是在店里面等着
3. 异步就是非阻塞的,不存在说什么异步阻塞的,当服务员把菜单给后厨的时候,后厨的台面上面会有一个要做的菜单列表,就相当于一个消息队列一样,你完全没有必要等待后厨,服务员也不用去问后厨做好了没有,做好了后厨会告诉服务员的。

image-20230706154730273

CAS

1
2
CompareAndSwap(offset,变量值,期望值,要改变的成的值)
这个就是自旋锁,具体实现就是自旋锁while(CAS),AtomicInteger(i++的时候,JVM有用到)
优点
1
这个不需要加锁,也就减少了消耗,也避免了死锁问题,提高了性能
缺点
1
2
一直自旋: 当多个线程访问相同的资源,那么就会出现一直自旋的情况
ABA问题: 这个问题其实就是其他线程改了当前变量,从A改成B又改成A,这样其实你去看的时候,感觉没有变化其实是变化的,我们通过加时间轴的方式解决

对象头

image-20230706175315485

1
2
这个classpoint指向的是类加载的时候的元数据(包括类的一些方法和属性之类的)
sychronized之前一直都是重量级锁,Java6之后,通过锁升级的方式变成重量级锁

锁升级

image-20230706180137377

1
从无锁到锁偏向,偏向锁其实就是只给同一个线程去访问,如果有其他线程访问,就升级为轻量级锁,如果超过20次访问,那么就重新变成偏向锁,轻量级锁它也是通过CAS的方式来实现锁重入,也就是同一个线程多次访问同一个对象,对象的state上面就加1,如果遇到了其他的线程访问该对象,那么就会出现锁膨胀,升级为重量级锁,如果线程访问不到重量级锁,那么就通过自旋10次(自旋锁)的方式看看能不能获取到,如果获取不到那么线程就进入阻塞状态。当然也有自适应自旋这个较为智能。

Moniteor对象

image-20230706180436872

1
这个就是重量级锁,sychronized的MarkWord对应着monitor对象,monitor对象有三个属性,waitset,owner,EntryList,waitset就是实现notify和wait的,体现了同步,然后EntryList就是获取该对象的一些阻塞线程队列

对象的强软引用

强引用
1
强引用是对象的引用如果没更换引用对象,那么引用的对象就不能被GC给清楚掉,这样可能会导致内存泄漏
软引用
1
这个引用其实就没有之前那么的强盗了,主打的就是一个谦让,当内存不足的时候就被GC给回收掉,把内存给其他对象让出来
弱引用
1
在发生GC的时候就会回收

JUC常见类

Semaphore(信号量)
1
Semaphore就好比类似一个加减计时器,当资源释放的时候增加资源数,当获取资源的时候减少资源数,会预先设置一个资源的数量,当资源数量为0的时候,还去获取资源,当前线程就会阻塞,需要等到一个线程释放该资源的时候才能获取资源
CountDownLatch
1
CountDownLatch会预先设置一个线程调用次数,也就是countdown()方法,当执行到值为0的时候,await()方法才会返回,如果countDownLatch的值大于0,那么等待线程继续阻塞,CountDownLatch的主要作用就是当多个子线程完成某个任务之后,再继续执行主线程的任务。

JUC集合

CopyOnWriteList
1
这个容器主打的就是读写分离,适合多读少写,写的时候原理是写时复制,也就是说,当写的时候会把容器复制一个,得到一个副本容器,读的时候读旧的,当一个线程在写的时候,读的线程是不会读到正在写的数据,写完之后,把之前指向旧的引用指向新的,然后再把旧的容器给删除掉。

2023-7-23

1
看过CopyOnWriteList源码后发现,CopyOnWriteList每次写的时候扩容只是容量加1,get方法访问如果还没有写完那还得访问原来的。写的时候是加了ReentrantLock这个锁。
缺点:
1
它的这个读的时效性没有那么的好,读总是读久的,要等扩容了新的完成了才行。
对比:
1
ArrayList扩容也是在满了的时候去扩容,扩容的代码是:  int newCapacity = oldCapacity + (oldCapacity >> 1);,如果说扩容后还是满足不了实际需要存储的元素数量,那newCapacity = 需要的元素,底层是把原来的数组元素拷贝到新的数组里面,System.ArrayCopy(),是个Native修饰的方法,意思就是C++底层需要调用的方法。
CopyOnWriteList扩容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
ArrayList扩容
1
2
3
4
5
6
7
8
9
10
11
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
ConCurrentHashMap
1
2
JDK1.8之前是HashTable,这个不支持多线程并发写,因为当有一个线程在写的时候,是对整个容器进行加锁,效率极低,并且在扩容的时候是直接全部拷贝过去,执行的时候会很慢很慢。
JDK1.8之后做了很大改进,出现了ConCurrentHashMap,这个设计是数组+链表的方式,每个槽叫哈希槽,存储的是一个链表,每次写操作的时候只锁住这个链表的头节点,而不用锁住整个表,这样效率就好很多了。

排它锁和共享锁

1
开始我还很难理解,后来我猜测了有没有一种可能的好奇心;当一个事务获取到某行数据的排它锁,那么其他事务可以读该还数据,但是不能获取到读锁和写锁,也就意味着读的时候可能是旧的,也就是不能保证数据的一致性;当一个事务获取到某行数据的共享锁,那么其他事务可以读该行数据,能保证数据读的一致性;

Kafka

架构

1
2
主要是由生成者、消费者、broker以及Zookeeper组成
一个集群里面有多个broker,一个broker可以有多个topic,一个topic有多个partition,每个partition下面有一个log文件

同步机制

1
2
3
每个partition有一个leader,同时有多个follow,每次producer发送消息的时候,有两种选择,让一个半的follow同步,然后发送ack或者让全部的follow和leader同步,然后再发送ack;
如果一个follow宕机了,那么ISR会暂时把Follow给踢出去,等到Follow后来恢复了,并且恢复到了HW
LEO: 每个副本的最后一个Offset

Kafka的优点?

1
2
可靠性: 能够实现数据的持久化存储,也就是将数据写入到磁盘当中
高吞吐量: 能够实现零拷贝,就是数据复制的时候不需要先把数据写入到用户存储区,接受端再通过去用户存储区把数据放入到内核存储区,而是直接在内核存储区去拷贝,这样就能使持久化更快,不需要进行用户存储区和内核存储区的交互

如何保证数据一致性?

1
首先保证数据一致性,那么读写分离就会大打折扣,kafka之前一直主张leader副本来复制读取和发送消息,follow副本只负责同步leader,并且有一个HW(High Water)的概念,也就是说leader副本partition里面有4个message,follow副本分别只有2,3,那么Consumer消费的时候只能读取到2,其他不能读取,为什么这样做呢? 如果leader副本宕机了,选举出来的follow作为新的leader,此时consumer再次读取的时候就有问题了,发现了不一致。

Python:star_and_crescent:

如何创建一个虚拟环境?

1
2
3
首先输入命令: python -m venv env  # Windows
输入以上命令之后我们可以生成一个env文件此时我们激活环境: env\Scripts\activate
退出虚拟环境: deactivate

如何定位参数位置?

1
2
3
4
1.Ctrl + F 进行全局搜索
2.增加snippets(代码片段)
3.使用插件来定位(Hook)
4.通过XHR来定位(比如说请求包含的字段)

加密方式有哪些?

维吉尼亚加密
1
这会有一张矩形表,一个密钥,密钥的长度必须和明文一样长,如果不一样长的话,密钥就得自我复制,注意这种加密只对字母有效无论大小字母

image-20230522095623282

示例:
1
2
3
4
5
明文:I've got it.

密钥:ok

密文:W'fs qcd wd.
凯撒密码
1
这没什么好讲的,主打的就是一个偏移量,ABCDEFGHIJKLMNOPQRSTUVWXYZ,DEFGHIJKLMNOPQRSTUVWXYZABC,偏移量为3
栅栏密码
1
通过交替的组合得到一个新的字符串,比如ABSDASEAF,ASBDAESAF。
摩斯密码
1
这种密码其实就是一一对应的映射表

image-20230522100242906

逆向

猎聘
思路:
1
首先我遇到的问题就是用JS发条去调试代码的时候发现报错, o = new Uint8Array(16);就是这个代码,因为Js发条工具没有这个创建数组的环境所以得通过在pycharm创建并打开js文件,使用本地的Node.js环境,还有一个问题就是这个随机加密函数getRandomValues,其实这种Native method,我是没有在浏览器开发者工具有看到,但是做这种的告诉我得从外面去找这种东西。还有就是可以把代码边的简洁,有些地方是明显不需要的就直接扔掉,哈哈哈,总结出来的新方法,比如说if(c[n]) new Error 知道里面肯定为false就不要了

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
 for (var c = false, s = [], l = 0; l < 256; ++l)
s.push((l + 256).toString(16).substr(1));

function a() {
var r = aa();
return u(r)
}

function u(e) {
var t = 0
, n = (s[e[t + 0]] + s[e[t + 1]] + s[e[t + 2]] + s[e[t + 3]] + "-" + s[e[t + 4]] + s[e[t + 5]] + "-" + s[e[t + 6]] + s[e[t + 7]] + "-" + s[e[t + 8]] + s[e[t + 9]] + "-" + s[e[t + 10]] + s[e[t + 11]] + s[e[t + 12]] + s[e[t + 13]] + s[e[t + 14]] + s[e[t + 15]]).toLowerCase();
return n
}

function aa() {
var r, o = new Uint8Array(16);
return getRandomValues(o)
}

function randoms(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
function getRandomValues(buf) {
var min = 0,
max = 255;
if (buf.length > 65536) {
var e = new Error();
e.code = 22;
e.message = 'Failed to execute \'getRandomValues\' : The ' + 'ArrayBufferView\'s byte length (' + buf.length + ') exceeds the ' + 'number of bytes of entropy available via this API (65536).';
e.name = 'QuotaExceededError';
throw e;
}
if (buf instanceof Uint16Array) {
max = 65535;
} else if (buf instanceof Uint32Array) {
max = 4294967295;
}
for (var element in buf) {
buf[element] = randoms(min, max);
}
return buf;
}

console.log(a())

机器学习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
解释一下“过拟合”是什么?
答:过拟合是指模型在训练数据上表现良好,但在测试数据上表现较差的现象,模型过于复杂,无法泛化到新的数据集上。

什么是梯度下降?
答:梯度下降是求解损失函数最小化的一种常用方法,在训练过程中,通过不断的沿着损失函数梯度方向调整模型参数,以达到最小化损失函数的目的。

什么是深度学习?
答:深度学习是一种以人工神经网络模型为基础的机器学习技术,通过多层次的结构来学习数据的特征表达,可用于图像识别、语音识别、自然语言处理等诸多领域。

解释一下“卷积神经网络”是什么?
答:卷积神经网络是一种特殊的神经网络,主要用于图像、音频、视频等数据的处理,能够自动学习数据的特征并进行分类或回归等任务,其主要特点是进行局部权重共享和参数共享,减少了需要学习的参数数量,降低了网络的复杂度。

什么是“监督学习”?
答:监督学习是一种常见的机器学习方法,需要使用标注好的数据进行模型训练,通过训练得到的模型可以对新的未标注数据进行预测或分类。监督学习适用于分类、回归等场景。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
什么是无监督学习?
答:无监督学习是一种机器学习技术,用于在没有标注数据的情况下,从数据本身中发现隐藏的结构或模式。无监督学习适用于聚类、降维等场景。

什么是深度神经网络?
答:深度神经网络(Deep Neural Network,DNN)是一种特殊的神经网络,由多个隐含层(Hidden Layer)组成,可用于处理复杂的非线性问题,例如图像识别、自然语言处理等。

什么是梯度消失问题?
答:梯度消失问题是指在深度神经网络中,随着误差反向传播(Backpropagation)向前传递,梯度会逐渐变小,甚至趋近于0,导致前面的层参数更新缓慢或不更新的现象。

什么是LSTM?
答:LSTM(Long Short-Term Memory)是一种常用的循环神经网络(Recurrent Neural Network,RNN)结构,专门用于处理序列数据,其主要特点是通过带有门控(Gate)机制的结构来控制记忆单元(Memory Cell)的读写,提高了长期依赖关系的建模能力。

什么是强化学习?
答:强化学习(Reinforcement Learning)是一种基于试错的学习方式,通过智能体(Agent)与环境的交互学习,从环境反馈中逐步调整策略,使得智能体在完成某种任务时能够最大化奖励(Reward)或最小化惩罚(Punishment)。强化学习适用于游戏AI、机器人控制等领域。
1
2
策略迭代算法主要是策略评估、策略改进,通过不断的迭代,收敛到最优策略值
值迭代算法主要是通过不断更新状态值函数迭代来求最优策略值,直至收敛到最优状态值函数,通过状态值函数求得最优策略
1
2
3
符号主义基于物理符号系统假设和有限合理性原理
联接主义基于神经网络和网络间的连接机制与学习算法
行为主义基于控制论和"动作-感知"控制系统

Travel

南京

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
南京南到玄武湖(25分钟)
请扮演一位导游,目前我是8:42到南京站,我想去的目的地是玄武湖、南京博物馆、钟山风景区、秦淮河、先锋书店,请给我生成一份旅游攻略,什么景点早上去,什么景点下午去,什么景点晚上去。以及中午和晚上在哪里吃,吃什么。Let's think step by step。

苏州北 44
石湖北 -> 苏州 21
苏州 -> 南京站: 7:23 ~ 8:42 (94r)

南京站 -> 玄武湖(8分钟) 一号线 9:00 一个小时
玄武湖 -> 上海路(7分钟,朝天宫) (12.5r) 需要预约
玄武湖 -> 新街口(5分钟) -> 明故宫(5分钟) 此地有南京博物馆 ×

明故宫 -> 宿园(2分钟) 钟山风景区(50r) 需要提前购票

宿园 -> 大行宫(5分钟) -> 夫子庙(4分钟)
夫子庙 -> 先锋书店(2分钟)


南京南 -> 苏州 21:41 ~ 22:53(104r)
早上:

1.从南京站出发前往玄武湖,玄武湖公园位于南京市中心,是一个美丽宜人的公园,适合早晨散步和呼吸新鲜空气。
2.玄武湖参观完毕后,前往南京博物馆,这里收藏了大量的中国历史文化遗产和南京特色文物。
中午:
3. 中午您可以前往秦淮河畔的美食街,这里有许多传统的南京小吃,如狮子头、板鸭、金陵饭等。您可以在此品尝地道的南京美食。

下午:
4. 下午您可以前往钟山风景区,这里是南京最高的山峰,有着优美的自然环境和众多的文化景点,如明孝陵、灵谷寺等。
5. 参观完钟山风景区后,您可以前往先锋书店,这是一个非常有特色的书店,可以购买到一些艺术类书籍和文具用品。

晚上:
6. 晚上可以前往秦淮河畔游船,您可以欣赏到南京灯火辉煌的夜景,还可以听到一些戏曲表演和民乐演奏。
7. 晚上也可以选择在秦淮河畔的餐厅里品尝一些特色菜肴,如清蒸活鱼、炒鳝片等。

img

面试题

常熟银行

1
2
3
4
5
6
Java的三大特性。多态,封装,继承。
重载,重写。
MySQL的数据库优化。
乐观锁和悲观锁。
线程创建的方式:1.Thread 2.线程池 3.Runnable。
StringBuilder 和 StringBuffer.

秋招投递

1
2
3
4
5
6
7
8
理想汽车
腾讯校招
特斯拉
快手 --简历挂
美团 --简历过 目前笔试阶段
掌趣
多益网络 - 不知道什么挂
米哈游

Base

BeiJing

image-20230807115445674image-20230807115504150image-20230807115516889

每天记录

2023-7-04

1
这天主要是早上刷了一上午的算法,下午看了Java线程(快乐的野指针),然后看了SpringCloud的东西(Eurake,Feign,熔断与降级,zuul,Config,Bus全局消息),看了王汉远讲的SpringCloud视频内容。SpringCloud服务之间的调用失败了,看的庆哥的,说实话有点low,服务注册我确实是弄好了,就是不能调用

2023-7-05

1
今天上午主要是把Java线程全部给看完了,下午主要是去了解这个RabbitMQ和RocketMQ以及kafka等消息中间件,最后学了一点oo的如何学习一门新的技术的理论。最后刷了一下四数之和这个哈希表的题目。

2023-7-06

1
今天主要是弄懂异步同步阻塞非阻塞概念,刷了一下字符串算法,再就是弄懂CAS和锁升级的问题。

2023-7-07

1
今天主要是学了一下java基础里面的修饰符的使用,然后下午就是刷了一下午算法(字符串之类的)。

2023-7-09

1
今天主要就是学了Redis缓存那一块,也就是那个CSDN上面的,讲的一些Redis如何保持数据一致性,主动更新的概念,还有对缓存击穿和缓存雪崩和缓存穿透的一些概念的理解,以及一些解决这些问题的办法。

CSDN_Redis

2023-7-10

1
今天主要就是把弱引用强引用虚引用都重温了一遍,以及ThreadLocal了解一下

2023-7-11

Threadlocal讲解地址

1
ThreadLocal源码分析, 

2023-7-14

1
彻底把ThreadLocal的具体应用常见,包括它的弱引用和强引用在内存泄漏方面都是一样的,要避免内存泄漏那么我们就需要手动的去remove,并且在访问Thread.ThreadMap中entry对象的key为null的时候,把value同样也设置为null

CSDN的ThreadLocal的讲解

2023-7-16

1
主要是看了看mysql下面的存储引擎有哪些,通过登录mysql,输入show engines,就能看到mysql所有的存储引擎。其中用的最多的就是MyISAM和Innodb,其中MyISAM主要适用于读的操作,而Innodb支持事务和高并发以及外键索引和联级索引。数据库的三大范式。Innodb里面的B+数,看了看之前的视频

2023-7-17

1
今天主要就是早上用scp -r root@远程地址 本地位置 来拷贝文件。下午主要是看了看野指针的JAVASE的内容。

2023-7-18

CSDN_ziplist实现

1
今天主要是了解了Redis的五种数据类型的底层结构,其中最主要是了解散列表(类似于Java当中的HashMap)和跳表以及ziplist, 特别要知道ziplist的底层实现,【整个ziplist的占用字节数】【最后一个节点的首地址到ziplist最前面的字节大小】【元素的个数】【entry----entry】【尾部标记元素】,早上主要是深一步了解什么是幻读,不可重复度,脏读。

2023-7-19

CSDN_JUC

1
今天主要是学习了一下常见的JUC类(Semaphore,CountDownLatch,ReentryLock)和一些线程安全的容器(CopyOnWriteArrayList,ConcurrentHashMap),还要就是初步的学了一会git的进一步使用

2023-7-20

1
感觉干了整整一天的项目,Debug还没有完成 麻了

2023-7-23

1
今天主要是看了看八股的视频,主要有ArrayList源码分析,

2023-7-27

1
今天主要是看了看fafka的一些架构和消息传递原理以及持久化机制,华科佬的Mysql加锁机制

Kafka面试题

Kafka原理

2023-08-01

1
今天早上填个多益网络招聘,下午看咕咪的文档,下午主要是看

Company

思路

1
2
1.databaseList当中的BaseDatabase实体类是哪一个? DataBaseListService是哪一个? 是要自己创建一个吗?
2.之前数据库名有对应id,operate用的是Equal,但是现在要模糊查询,也就是说要Exits

核心代码块

1.拼装SearchField的in的SQL语句

1
List<SearchField> rebuildField = getRebuildField();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public List<SearchField> getRebuildField() {
BaseQueryContext context = getContext();
List<SearchField> searchFields = new ArrayList<>(this.searchFields.size());
for (SearchField searchField : this.searchFields) {
IndexField indexField = context.fieldMapper(searchField.getFieldName());
if (indexField == null) {
searchFields.add(searchField);
continue;
}
if (StringUtils.isBlank(indexField.getValuesMapping())) {
searchFields.add(searchField);
continue;
}
Map<String, ValuesMapping> beanMap =
SpringContextUtils.getContext().getBeansOfType(ValuesMapping.class);
SearchField mapping = beanMap.get(indexField.getValuesMapping()).getMapping(searchField.getValue(),
this.searchFields, searchField);
if (mapping == null) {
return null;
}
searchFields.add(mapping);
}
return searchFields;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public SearchField getMapping(
List<String> values, List<SearchField> searchFields, SearchField searchField) {
List<String> policyList = new ArrayList<>();
Operator operator = searchField.getOperator();
boolean isExact =
operator.equals(Operator.EQUAL) || operator.equals(Operator.NOT_EQUAL);
boolean isForwardQuery =
operator.equals(Operator.EQUAL) || operator.equals(Operator.EXISTS);
for (String value : values) {
List<Long> policyIdListByName =
basePolicyService.findPolicyIdListByName(value, isExact, isForwardQuery);
if (CollectionUtils.isEmpty(policyIdListByName)) {
return null;
}

policyList.addAll(
policyIdListByName.stream()
.map(id -> String.valueOf(id))
.collect(Collectors.toList()));
}
return new SearchField(searchField.getFieldName(), Operator.EQUAL, policyList);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public List<Long> findPolicyIdListByName(
String policyName, boolean isExact, boolean isForwardQuery) {

LambdaQueryWrapper<BasePolicy> like;
if (isForwardQuery) {
like = QueryWrapperUtil.lambdaQuery(BasePolicy.class)
.like(BasePolicy::getName, StrUtils.escapeSQLLike(policyName));
} else {
like = QueryWrapperUtil.lambdaQuery(BasePolicy.class)
.notLike(BasePolicy::getName, StrUtils.escapeSQLLike(policyName));
}
List<BasePolicy> all = this.list(like);
if (CollectionUtils.isEmpty(all)) {
return null;
}
return all.stream().map(basePolicy -> basePolicy.getId()).collect(Collectors.toList());
}
1
queryBuilder.append("(").append(rebuildField.get(i).getEsQuery(getContext())).append(")");

image-20230721112532721

完整SQL语句

1
2
mainBuilder.append(" where auditId in (select auditId from ").append(tableName)
.append(" where ").append(builder.toString());

image-20230721115820750

1
SELECT sqlText,riskLevel,storageTime,policyId,clientIp,alarmId,tenantId,databaseId,sqlPredicate,responseCode FROM dbaudit.audit_log where auditId in (select auditId from audit_log where  1=1 AND toDateTime(storageTime) >= '2023-07-21 00:00:00' AND toDateTime(storageTime) <= '2023-07-21 23:59:59' AND 1=1 AND ((databaseId IN (50001,50000) ) AND (policyId IN (107,207) ) AND (tenantId IN (2) )) order by storageTime desc limit ?,?) order by storageTime desc

审计日志系统功能更新

bug01
1
The bean 'org.springframework.context.annotation.internalScheduledAnnotationProcessor', defined in class path resource [org/springframework/scheduling/annotation/SchedulingConfiguration.class], could not be registered. A bean with that name has already been defined in class path resource [com/idss/dbaudit/config/schedule/annotation/ProgressSchedulingConfiguration.class] and overriding is disabled.
解决办法

参考文档

bug02
1
Error running DbauditApplication. Command line is too long. Shorten the command line via JAR manifest or via a classpath file and rerun
解决办法

Edit项目的Configuration,增加VM option ,short Command line选项

bug03
1
postman报Error: write EPROTO 140600974724440:error:100000f7:SSL routines:OPENSSL_internal:
解决办法

把https改成http就可以了

参考文档

遇到的问题

1
2
3
目前模型类(敏感数据审计和业务审计都是这样)里面只有String DataBaseId, 之前是固定一个数据库Id参与查询,需求是我通过数据库名模糊查询出来有多个Id, 然后参与查询。
实现: 那么我需要改之前的SQL, 把 1.database = -> in 2. 模型类增加DatabaseIdS
问题: 是按我的上面所说来改还是说像昨天说的那个资产监控那样目前这个需求不动了。

告警查询

前端之后得用SearchDataBaseName字段来查询

QueryAuditAlarmBo 前

1
2
3
4
/**
* 数据库id列表
*/
private List<Long> databaseIdList;

QueryAuditAlarmBo 后

1
2
3
4
5
6
7
8
/**
* 数据库名称
*/
private String searchDataBaseName;
/**
* 数据库id列表
*/
private List<Long> databaseIdList;

万能代码

1
2
3
4
5
List<Long> databaseIdListByName =
databaseService.findDatabaseIdListByName(queryAuditAlarmBo.getSearchDataBaseName(), true, true);
if (CollectionUtils.isNotEmpty(databaseIdListByName)) {
queryAuditAlarmBo.setDatabaseIdList(databaseIdListByName);
}
1
2
3
4
if (CollectionUtils.isNotEmpty(queryRiskAnalysisAlarmBo.getDatabaseIdList())) {
boolBuilder.append(" AND databaseId IN (")
.append(StringUtils.join(queryRiskAnalysisAlarmBo.getDatabaseIdList(), ",")).append(") ");
}
1
2
3
addb3c3755c21b7a3859b33e577732a6355d7821
dbb23a63b4ac158631cb225546769905efc18bd2
58811e25e124c57dbcee7310fcb24199b190a987
1
.in(CollectionUtils.isNotEmpty(queryPolicyBo.getDatabaseIdList()),BasePolicy::getDatabaseIdList,queryPolicyBo.getDatabaseIdList())
1
2
3
4
5
6
.and(CollectionUtils.isNotEmpty(queryHighPolicyBo.getDatabaseIdList()),qw->{

for (Long o : queryHighPolicyBo.getDatabaseIdList()) {
qw.like(HighFrequencyStatistics::getDatabaseIdListQuery, ","+o+",").or();
}
})

没有数据的模块

1.会话分析-失败登录

2.数据敏感发现

3.业务审计

4.告警查询

———————————————————————— 2023-07-31 问题已解决 ——————————————————————————–

1
数据库审计系统和我们写的Jar包是独立的, 2.0.1是查的ES,类似于北京联通4A,2.3.1是从ck里面去查,然后我们需要做的事情就是把产生的日志文件发送到FTP上面, 我们导出的数据文件也得是CSV文件的, 然后YML文件得给出一个存储list的方式,
1
目前进度: 熟悉北京联通发送日志的代码和了解ES查询代码,知道yml文件配置数据库ID列表
1
2
3
4
5
6
7
8
9
10
11
12
数据库日志文件
1、范围:18套涉敏系统数据库
2、内容:数据库审计日志《包含增删查改)
3、数据库日志文件后缀名:.xml、.csy、.1og
4、数据库日志文件命名:增量/全量/新增数据标志机构编码接口单元编码 数据日期 重传序号序列号数据库类型 数据库版本号.10g。
示例: a 10200_13014 20170401 00 001 mysq1 5.0.0.1og
需要修改的内容如下:
1)数据日期:填当天日期,如: 20230724
2)重传序号:当天第一次重传的某个表结构文件为01,以此类推
3)序列号:当天上传的第一个表结构文件为001,以此类推。
4)数据库类型:表结构文件对应的数据库类型,如mysql、oracle等
5)数据库版本号:表结构文件对应的数据库版本,如5.0.0、12.1.0.2.0等
1
2
3
if (CollectionUtils.isEmpty(databaseIdListByName)) {
return null;
}
1
2
目前进度: 代码写了CSV文件需要的实体类,如何实现将数据写入到CSV文件,完全弄懂北京联通4A代码
不懂的点: 1. ES查询是要查哪个索引,查询的索引里面的字段是不是就是之前数据库审计系统导出的CSV文件,ES的IP地址和Port是北京联通的吗还是别的. 2. ClickHouse的ip和port目前不清楚 3. 文件命名的具体规则有点模糊不清除
1
整体梳理: 1. 通过版本号区别分别查ES和clickhouse 2. 将查出来的数据放入到CSV文件 3. 将csv文件发送到FTP服务器 4. 打成Jar部署到远端。
1
2
目前进度: 项目的大体框架有了;
todolist: 1. ES需要测试环境 2. 查询到的数据进行数据值的转化(数据库ID -> 数据库Name) 3. 将生成的CSV文件发送到FTP服务器上面 4. CK查询数据的处理(如何传递给消费者)