Gallon's Note


  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

raft算法

发表于 2020-01-20

CAP理论

对于一个分布式系统,不可能同时满足以下三点:

  • 一致性(Consistence):在同一时刻所有节点的数据都是一样的。

    • 最终一致性。所有结点的数据并不是立刻一样的,而是在将来的某一个时刻会满足一致性。
    • 强一致性。
  • 可用性(Availability):每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据。

  • 分区容错性(Partition Tolerance):分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障.比如,某个服务器宕机了,集群仍能够对外提供服务。

一个分布式系统不可能同时满足三个特性,最多只可以满足两个,而分区容错性又是我们必须要满足的,因此,一致性和可用性我们只能满足两个。这里的一致性是强一致性,即在同一时刻,所有节点的数据都是一致的。假如我们选择了可用性,放弃了强一致性,可以使用最终一致性来解决。

在满足了分区容错性P后,想要满足一致性C,一个服务的多个节点之间就必须进行数据复制达到数据一致之后再返回给调用者响应,然而在多个节点数据复制的过程中,可能节点之间会出现网络等问题使得数据复制阻塞或失败导致响应超时,服务调用失败,这就失去了系统的可用性A。

​ 如果不强制满足强一致性,那在服务被调用的时候不用管数据复制的问题,直接返回响应,这就满足了可用性,但是由于此服务的多个节点数据可能没有完成复制,节点数据可能不一致,这就失去了系统的一致性。

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。CA之间的取舍要根据实际业务来决定,比如银行转账业务,我们必须要保证他的一致性。比如电商业务,一致性就没那么重要了,可以尽可能保证他的高可用性。

强一致性

主从同步

主从同步复制。
1.Master接受写请求。
2.Master复制日志到slave
3.Master等待,直到所有slve返回成功的消息。
缺点:一个slave失败,会导致Mater阻塞,从而导致整个集群阻塞,可用性低。

多数派

有N/2个slave返回写入成功即成功。

RocketMQ入门

发表于 2020-01-19 | 分类于 消息队列

高可用性

RocketMQ与Kafka集群有很大的不同。

title

Kafka的一台机器可能是partition1的leader,又是partition2的follower.Kafka的一个机器既可能是leader有可能是follower。

RocketMQ也将一个tpic分为多个partition,在RocketMQ叫Queue,也是将Queue均匀分布在各个broker上来实现负载均衡。但是他的leader,follower身份是固定死的,一个leader和他的follower叫做一个broker群组。当一个master出现宕机,消费者可以从他的slave读取消息,生产者将消息写到别的master上面来实现可用性。但是slave上的消息并不一定是完全同步过来的,因此会有少量的消息丢失。但是消息最终不会丢的,一旦master恢复,未同步过去的消息会被消费掉。

文件存储

Kafka入门

发表于 2020-01-19 | 分类于 消息队列

Kafka是基于发布订阅模式的,采用消费者拉消息的模式,消息可以被重复消费。

title

一些概念

批量发送

生产者在生产完一条消息后不是直接发送的,而是分批次写入(类似于缓冲),这样可以提高吞吐量。

主题(topic)和分区(partition)

kafka通过主题进行分类。一个主题可以包含多个分区,消息以追加的方式写入分区,然后以先入先出的方式顺序读取.但是由于一个主题包含多个分区,因此,不能保证整个主题的消息顺序,只可以保证分区内的消息顺序.kafka是没有索引的。kafka是通过offset来标识消息,即该消息在分区中的偏移量.因此,kafka是几乎不支持随机读写的。
当consumer正常消费消息时,offset将会”线性”的向前驱动,即消息将依次顺序被消费.事实上consumer可以使用任意顺序消费消息,它只需要将offset重置为任意值。

为什么需要分区呢?
主要是为了负载均衡,将消息分散到多个server上.

  • 最根本原因是kafka基于文件存储.通过分区,可以,来避免文件尺寸达到单机磁盘的上限,每个partiton都会被当前server(kafka实例)保存;可以将一个topic切分多任意多个partitions,来消息保存/消费的效率.此外越多的partitions意味着可以容纳更多的consumer,有效提升并发消费的能力.
    假设我们只有一个分区,那么我们所有的消息都堆积到这一个服务器上.但是,在我们将topic划分为多个分区上,我们可以将分区均匀的分散到多个服务器上,这样,每台服务器压力就会小很多.
  • 还有一个就是消费者可以并发的读取数据.

生产者

生产者生产消息后,采用顺序写的方式将消息追加写入分区。

流程

  • 创建消息:创建一个ProducerRecord对象,主要包括topic,partition,key,value四项,其中patirion和key是可选的.
  • 序列化,将ProducerRecord对象序列化.
  • 分区选择.若指定分区,直接发送到该分区;若没指定分区,有key,按key哈希计算;若没有key,且没指定分区,采用轮询的方式选择分区。
  • 添加进批次里,等到批次满了统一发送.同一批次里的消息topic和partition都应该是相同的.
  • 服务器返回响应,消息是否写入成功.

发送消息方式

  • 发送即忘记:发送完就不管了。
  • 同步发送。
  • 异步发送:指定回调函数来对发送异常进行处理。

acks

制定了有多少分区副本收到消息,才算是写入成功。

  • acks=0:发送完就不管了,吞吐量高。
  • acks=1:首领节点的副本收到消息即可。
  • acks=all:所有副本都受到消息。

消费者

高可用性

假如说服务器宕机了怎么办,通过集群来解决。Kafka使用zookeeper来管理集群。多台机器存储数据,当leader宕机了,重新选取一台新的就行了。

多副本冗余

因为kafka是以partition为单位的。其每一个分区都有多个副本,其中,一个leader,其他都是follower,只有leader进行读写。而且多个副本分布在多台机器上,这样,一台服务器宕机,可以通过选举的方式重新选举leader,依然保持是可用的。而且,应该每一个分区的leader分布在不同的机器上,达到负载均衡的目的。例如,机器1存放:leader1,follower2,follower3.机器2存放:follower1,leader2,follower3.机器1存放:follower1,follower2,leader3.这样的话,每一个分区的leader分布在不同的机器上

写数据

写数据的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦 follower 同步好数据了,就会发送 ack 给 leader。具有同步复制以及异步复制两种模式。

读数据

只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。

leader follower同步问题

写数据的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦 follower 同步好数据了,就会发送 ack 给 leader。

ISR机制

ISR保存的是与leader数据一致的副本列表,根据副本与leader的交互时间差(每隔XXs会向leader发送一个请求)与信息条数差(follower差leader几条信息)判断是否同步,当超过某个阈值就将其移除ISR。

只有ISR里面的成员才可能被选为leader。当follower又重新追赶上leader之后,将其重新放入ISR。

幂等性

消息不重复消费。

原因

由于提交偏移量可能会有延迟性,会造成Kafka消息重复的问题。

假如采用定时提交,消费者消费完了一部分消息还未提交,突然宕机了。再均衡之后,消费该分区新的消费者会读取原来的偏移量,这样,未提交偏移量的这部分消息会重复消费。

解决

对于每条消息,MQ内部生成一个全局唯一、与业务无关的消息ID:inner-msg-id。当MQ-server接收到消息时,先根据inner-msg-id判断消息是否重复发送,再决定是否将消息落地到DB中。这样,有了这个inner-msg-id作为去重的依据就能保证一条消息只能一次落地到DB。

可靠性

不丢失消息。

生产者

ack=0时,生产者向broker发送消息,发送途中消息丢失。因为ack=0,发送完就不管了,就不会重试发送消息,消息丢失。

ack=all可以解决。

broker

ack==1,假如消息到达broker,而且写入了leader,但此时leader宕机了,而且并没有写入follower,那么选举的新的leader肯定是follower中的,消息会丢失。

ack=all可以解决。

消费者

自动提交的问题。如果在消息处理完成前就提交了offset,那么就有可能造成数据的丢失。

关闭自动提交可以解决。

顺序性

什么导致无序

  • 生产者重试。假如发送1,2,2成功了,1由于网络原因丢失,重新发送1,顺序就变味了2,1.
  • 分区的问题。假如1,2被发送到了两个不同分区,1所在分区消费的慢,2所在分区消费的快,顺序变为2,1.
  • 并行消费的问题。即使1,2在一个分区,且未乱序,可能该分区有两个消费者A,B,A先读取1处理,B后读取2处理,B处理比较快。乱序。

保证局部有序

分区内有序。

例如订单场景,创建,付款,发货需要严格有序。

解决生产者重试问题:每一次确认消息完全到达分区了在发送下一条消息。

解决分区问题:将同一个订单消息发送到同一个分区。可以根据订单号哈希。

解决并行消费问题。分区只有一个消费者。或者对分区加锁。

保证全局有序

不仅仅是分区内有序,而是所有分区都有序。

那就只能之创建一个分区了。

文件存储

同一个topic下有多个不同的 partition,每个 partiton 为一个目录,partition 的名称规则为:topic 名称 + 有序序号。所以,partition是物理概念,topic是逻辑概念。

partition又可以拆分为多个大小相等的segment。拆分为多个segment,一个是因为这样查找效率高;另一个segment为了删除过期的消息而设置的。kafka中消息的写入是直接追加到文件尾部,而消息被消费后也不会被删除,但是我们可以设置一个过期时间进行消息的删除。我们删除消息直接删除segment文件即可。

segment包括一个索引文件,一个数据文件。segment文件的命名:segment文件名为上一个segment文件最后一条消息的offset。

索引文件存储的message在数据文件中的偏移,快速定位message。

文件存储高效原因

分段

采用分段存储的话,因为数据文件以该段中最小的offset命名。这样在查找指定offset的Message的时候,用二分查找就可以定位到该Message在哪个段中。

索引文件

每一个数据文件都有一个索引文件,便于快速寻找消息在段中的位置。

稀疏索引

index file并没有为数据文件中的每条message建立索引,而是采取稀疏索引存储方式,每隔一定字节的数据建立一条索引,它减少了索引文件大小,通过map可以直接内存操作,稀疏索引为数据文件的每个对应message设置一个元数据指针,它比稠密索引节省了更多的存储空间,但查找起来需要消耗更多的时间。

文件读写高效原因

写入优化:顺序写入+Mmap。

读取优化:sendFile零拷贝+读写空中接力

顺序写入

kafka是顺序追加写入的,避免了磁道寻址的时间,顺序写入比随机写入,尤其是机械硬盘效率要大得多。。

MMap

采用MMap来写入数据到磁盘。

传统的文件写入方式是将数据从用户空间转移到内核空间,再由内核空间拷贝到PageCache,最后由操作系统异步刷盘将pageCache中数据落盘。

而MMap省去了用户空间到内核空间的过程,直接将数据写入到pageCache,再由操作系统将pageCache中的数据落盘。

也有一个很明显的缺陷——不可靠,写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。如果在pageCache还未落盘的时候宕机了,数据会丢失。

Kafka提供了一个参数——producer.type来控制是不是主动flush,如果Kafka写入到mmap之后就立即flush然后再返回Producer叫同步(sync);写入mmap之后立即返回Producer不调用flush叫异步(async)。

sendFile零拷贝

消费者在读取消息的时候,使用sendFile。

传统I/O与socket传输:磁盘数据->内核缓冲区->用户空间->内核socket缓冲区->网卡缓存,四次拷贝。

而sendFile直接从内核缓冲区发送到网卡缓存,省掉了两次拷贝。

读写空中接力

  • 当写操作发生时,它只是将数据写入Page Cache中,并将该页置上dirty标志。
  • 当读操作发生时,它会首先在Page Cache中查找内容,如果有就直接返回了,没有的话就会从磁盘读取文件再写回Page Cache。
  • 可见,只要生产者与消费者的速度相差不大,消费者会直接读取之前生产者写入Page Cache的数据,大家在内存里完成接力,根本没有磁盘访问。而比起在内存中维护一份消息数据的传统做法,这既不会重复浪费一倍的内存,Page Cache又不需要GC(可以放心使用大把内存了),而且即使Kafka重启了,Page Cache还依然在。

高可用

kafka文件存储

kafka文件读写

kafka读写问题

pageCache问题

消息队列简介

发表于 2020-01-19 | 分类于 消息队列

啊啊消息队列就是用来存储消息的容器,假如A应用需要将消息传送给B,那么他并不是直接发送给B,而是将消息发送到消息队列中,由B直接从消息队列中获取。

阅读全文 »

反射机制

发表于 2020-01-16 | 分类于 java

何为反射

反射是框架设计的灵魂。在运行时才通过类名动态的将class对象加载进内存,通过类的class对象就可以获取类的全部信息,包括方法,字段等。一般常用于框架设计中。

创建一个对象的过程

  • 将字节码文件加载进内存,形成运行时内存结构,形成class对象。
  • 分配堆内存空间。
  • 调用构造器,创造一个空白对象。
  • 子类调用父类构造器。
  • 执行子类构造器。

img

所以,实例化一个对象,是离不开class对象的。

class对象

class对象主要包含Field,Method,Constructor。

img

因此,class对象包含了这个类的所有信息,通过Field,Method就可以直接访问类的成员。

反射使用

获取class对象

1
2
3
Class c2 = Class.forName("[D");
Class c1 = Boolean.class;
Class c4 = instance.getClass();

获取成员

getField

通过反射甚至可以访问私有变量。

获取并调用方法

1
2
3
4
5
6
7
8
Class<?> klass = methodClass.class;
//创建methodClass的实例
Object obj = klass.newInstance();
//获取methodClass类的add方法,传入int.class是为了确定唯一的方法,以防重载
Method method = klass.getMethod("add",int.class,int.class);
//调用method对应的方法 => add(1,4)
Object result = method.invoke(obj,1,4);
System.out.println(result);

应用场景

JDBC数据库连接

Spring框架配置

leetcode字符串

发表于 2020-01-02

32. 最长有效括号

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
/**
* 可以直接使用栈来做,栈中存放的是下标。
* 当元素是'(',直接入栈下标,当元素是')'且栈顶是'(',将栈顶出栈,并且减去栈顶下标,然后就得到当前合法序列的长度
* */
public int longestValidParentheses(String s) {
if(s==null||s.length()==0){
return 0;
}
Stack<Integer> stack = new Stack<>();
stack.push(-1);
int res = 0;
for(int i=0;i<s.length();i++){
if(s.charAt(i)=='('){
stack.push(i);
}else{
stack.pop();
if(stack.isEmpty()){
stack.push(i);
}else{
res = Math.max(res,i-stack.peek());
}
}
}
return res;
}


public int longestValidParentheses(String s) {
int[] dp = new int[s.length()];
int res = 0;
int max = 0;
//假如直接是'(',那么dp[i]直接等于0,如果是')',那么需要看i-dp[i-1]-1个位置是否是'(',假如是的话+2,还需要再看i-dp[i-1]-1前一个位置的dp
for(int i=1;i<s.length();i++){
if(s.charAt(i)==')'){
int pre = i-dp[i-1]-1;
if(pre>=0&&s.charAt(pre)=='('){
dp[i] = dp[i-1]+2+(pre-1>=0?dp[pre-1]:0);
max= Math.max(max,dp[i]);
}
}
}
return max;
}

20.有效的括号

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<>();
for(int i=0;i<s.length();i++){
if(s.charAt(i)==']'||s.charAt(i)==')'||s.charAt(i)=='}'){
if(!stack.isEmpty()){
char c1 = stack.pop();
if(!isSym(c1,s.charAt(i))){
return false;
}
}else{
return false;
}
}else{
stack.push(s.charAt(i));
}
}
return stack.isEmpty();
}
public boolean isSym(char c1,char c2){
return (c1=='('&&c2==')')||(c1=='['&&c2==']')||(c1=='{'&&c2=='}');
}

leetcode牛客进阶班

发表于 2019-12-21 | 分类于 leetcode

9. 回文数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public boolean isPalindrome(int x) {
if(x<0){
return false;
}
//计算x的最高位数,12321 计算出10000
int help = 1;
while(x/help>=10){
help*=10;
}
while(x!=0) {
//x/help计算第一个位置 x%10计算最后一个位置
if (x / help != x % 10) {
return false;
}
//x%help/10 12321->232,因为少了两位,所以help/100
x = x%help/10;
help/=100;
}
return true;
}

未排序正数数组中累加和为定值的最长子数组长度

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
/**
* 未排序正数数组中累加和为定值的最长子数组长度
* 准备两个指针,left以及right,以sum表示left以及right之间的数值和。
* 假如sum<k,则right后移;
* sum==k,记录下长度,并将left左移;
* sum>k,left直接左移
* */

public int getMaxLength(int[] nums,int k) {
if (nums == null || nums.length == 0 || k <= 0) {
return 0;
}
int left = 0;
int right = 0;
int sum = 0;
int maxLength = 0;
while (right < nums.length) {
//假如sum<k,说明不够长,还需要加元素,right往右移
if (sum < k) {
right++;
//注意下标越界判断
if(right==nums.length){
break;
}
sum += nums[right];
//sum==k,记录长度,太满了,缩小长度,将left右移
} else if (sum == k) {
maxLength = Math.max(maxLength, right - left + 1);
sum -= nums[left];
left++;
//sum>k,太满了,left右移
} else {
sum -= nums[left];
left++;
}
}
return maxLength;
}

未排序整数数组中累加和为定值的最长子数组长度

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
/**
* 未排序整数数组中累加和为定值的最长子数组长度,可为正,负,0
* sum用来记录从0到i的子数组之和,使用map存储所有出现的sum值以及位置
* 在map中查找sum-k,假如存在,那么i与该位置之间就是满足的子数组,与最值比较
* 假如sum-k不存在,那么以nums[i]结尾的情况下没有累加和为k的子数组
* */
public int subarraySum(int[] nums, int k) {
if(nums==null||nums.length==0){
return 0;
}
int sum = 0;
int max = 0;
int count = 0;
//存储sum值以及他第一次出现的位置
Map<Integer,Integer> map = new HashMap<>();
map.put(0,-1);
for(int index=0;index<nums.length;index++){
sum+=nums[index];
if(map.containsKey(sum-k)){
max = Math.max(max,index-map.get(sum-k));
}
if(!map.containsKey(sum)){
map.put(sum,index);
}
}
return max;
}

525. 连续数组

给定一个二进制数组, 找到含有相同数量的 0 和 1 的最长连续子数组(的长度)。

该题与上题类似,可以将0转化为-1,这样求解累加和是0的最长子数组长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int findMaxLength(int[] nums) {
if(nums==null||nums.length==0){
return 0;
}
//存储某个sum值以及第一次出现的位置
Map<Integer,Integer> map = new HashMap<>();
int sum = 0;
int max = 0;
map.put(0,-1);
for(int i=0;i<nums.length;i++){
sum+=nums[i]==0?-1:nums[i];
if(map.containsKey(sum-0)){
max = Math.max(max,i-map.get(sum-0));
}
if(!map.containsKey(sum)){
map.put(sum,i);
}
}
return max;
}

560. 和为K的子数组

给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 整数数组,依旧使用sum-k的方式
* */
public int subarraySum2(int[] nums, int k) {
if(nums==null||nums.length==0){
return 0;
}
int sum = 0;
int count = 0;
//map中存储的是某个sum值出现的次数
Map<Integer,Integer> map = new HashMap<>();
//初始0值出现了一次
map.put(0,1);
for(int index=0;index<nums.length;index++){
sum+=nums[index];
if(map.containsKey(sum-k)){
count+=map.get(sum-k);
}
//存储sum出现的次数,假如map中已经存在,直接++,未存在,直接存储1
map.put(sum,map.getOrDefault(sum,0)+1);
}
return count;
}

未排序数组中累加和小于或等于给定值的最长子数组长度

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
public int maxLength(int[] arr,int k){
//计算以i结尾的最小子数组长度
int[] minSums = new int[arr.length];
int[] minEnds = new int[arr.length];
minSums[arr.length-1] = minSums[arr.length-1];
minEnds[arr.length-1] = arr.length-1;
for(int i=arr.length-2;i>=0;i--){
if(minSums[i+1]<0){
minSums[i] = minSums[i+1]+arr[i];
minEnds[i] = minEnds[i+1];
}else{
minSums[i] = arr[i];
minEnds[i] = i;
}
}
//扩展窗口,end是窗口最右位置的下一个位置,i是窗口的左侧
int sum = 0;
int end = 0;
int max = 0;
for(int i=0;i<arr.length;i++){
//将窗口向右扩,直到>k
while(end<arr.length&&sum+minSums[end]<=k){
sum+=minSums[end];
end = minEnds[end]+1;
}
//扩充到不能再扩了,形成了一个结果
max = Math.max(max,end-i);
//移动左侧位置
if(end>i){
sum-=arr[i];
}else{
end = i+1;
}
}
return max;
}

约瑟夫环问题

leetcode递归与动态规划

发表于 2019-12-21

0-1背包

有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。每件物品仅使用一次。

主要特点:每个物品只有一件,选择放或者不放。

f(i,v)表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:f(i,v)=max{f(i-1,v),f(i-1,v-c[i])+w[i]}。

放第i件物品:f(i,v) = 第i件物品的价值+i-1件物品放入v-第i件物品的花费

不放第i件物品:就是前i-1件物品放入容量为v的背包中。

416. 分割等和子集

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

完全背包

每件物品使用次数不限制。

多重背包

每个物品可选的次数不同。

汉诺塔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 汉诺塔问题
* 递归方法来解决。
* from to help
* 一开始from有n个元素,需要将这n个元素移动到to中。可以分为三个步骤:
* 1.将n-1个元素从from移动到help
* 2.将第n个元素从from直接移到to
* 3.将n-1个元素从help移动到to
* */
public void hanuo(int N,String from,String to,String help){
if(N==1){
System.out.println("move 1 "+" form "+from+" to "+to);
}else{
hanuo(N-1,from,help,to);
System.out.println("move "+N+" from "+from+" to "+to);
hanuo(N-1,help,to,from);
}
}

198.打家劫舍

1
2
3
4
5
6
7
8
9
10
11
12
13
//dp[i]到第i家可以偷多少钱dp[i]=max(dp[i-1],dp[i-2]+nums[i-1])
public int rob(int[] nums) {
if(nums.length<1){
return 0;
}
int[] dp = new int[nums.length+1];
dp[0] = 0;
dp[1] = nums[0];
for(int i=2;i<=nums.length;i++){
dp[i] = Math.max(dp[i-2]+nums[i-1],dp[i-1]);
}
return dp[nums.length];
}

打印字符串子序列

例如abc,打印a , ab , abc ,ac, bc,以及空字符串

1
2
3
4
5
6
7
8
9
public void printAllSub(char[] arr,int i,String res){
if(i==arr.length){
System.out.println(res);
return ;
}
//存在两种情况,打印该元素以及不打印该元素
printAllSub(arr,i+1,res);
printAllSub(arr,i+1,res+String.valueOf(arr[i]));
}

生牛

1576899784912

64. 最小路径和

暴力递归解法

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
public int minPathSum(int[][] grid) {
if(grid.length==0||grid[0].length==0){
return 0;
}
return helperMinPath(grid,0,0);
}

/**
* 递归来解决
* 该方法表示从(row,col)走到右下角的距离
* 1.到达最右下角节点,直接返回结果
* 2.res = min(向右走,向下走)
* */
public int helperMinPath(int[][] grid,int row,int col){
int val = grid[row][col];
//走到最右下角点
if(row==(grid.length-1)&&col == grid[0].length-1){
return grid[row][col];
}
//走到最后一行或者是最后一列
if(row==grid.length-1){
return val+helperMinPath(grid,row,col+1);
}
if(col == grid[0].length-1){
return val+helperMinPath(grid,row+1,col);
}

//取向下走以及往右走的最小值
int down = helperMinPath(grid,row+1,col);
int right = helperMinPath(grid,row,col+1);
return Math.min(down,right)+val;
}

实际上,暴力递归有许多计算是重复的,这样造成时间复杂度过高,暴力嘀咕可以转化为动态规划。

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
public int minPathSum(int[][] grid) {
if(grid.length==0||grid[0].length==0){
return 0;
}
return DPMinPath(grid);
}

public int minPathSum(int[][] grid) {
return DPMinpath(grid);
}
public int DPMinpath(int[][] matrix){
if(matrix.length==0||matrix[0].length==0){
return 0;
}
int m = matrix.length;
int n = matrix[0].length;
int[][] dp = new int[m][n];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i==0&&j==0){
dp[i][j] = matrix[i][j];
}else if(i==0){
dp[i][j] = dp[i][j-1]+matrix[i][j];
}else if(j==0){
dp[i][j] = dp[i-1][j]+matrix[i][j];
}else{
dp[i][j] = Math.min(dp[i-1][j]+matrix[i][j],dp[i][j-1]+matrix[i][j]);
}
}
}
return dp[m-1][n-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
public int minPathSum(int[][] grid) {
return DPMinpath(grid);
}
public int DPMinpath(int[][] matrix){
if(matrix.length==0||matrix[0].length==0){
return 0;
}
int m = matrix.length;
int n = matrix[0].length;
int[] dp = new int[n];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i==0&&j==0){
dp[0] = matrix[i][j];
}else if(i==0){
dp[j] = dp[j-1]+matrix[i][j];
}else if(j==0){
dp[j] = dp[j]+matrix[i][j];
}else{
dp[j] = Math.min(dp[j]+matrix[i][j],dp[j-1]+matrix[i][j]);
}
}
}
return dp[n-1];
}
}

任意个数字相加等于aim?

1576925330206

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
/**第一种是递归方法来解决。每个元素有选择以及不选择两种可能
* 假如i==arr.length,说明已经走完了最后一个元素,判断sum是否等于aim
* 对于每一个元素,我们有选择加他或者不加他两种可能
* @param arr1:输入的数组
* @param i:数组中第几个数据
* @param sum
* @param aim:目标和
* @return
*/
public boolean process1(int[] arr1,int i,int sum,int aim){
if(i==arr1.length){
return sum==aim;
}
//分为选择该元素以及不选择该元素两种可能
return process1(arr1,i+1,sum,aim)||process1(arr1,i+1,sum+arr1[i],aim);
}

/**
* DP版本
* 两个可变参数:sum以及i(元素个数)
* */

public boolean process1(int[] arr1,int aim){
//计算sum范围
int sum = 0;
for(int ele:arr1){
sum+=ele;
}
boolean[][] dp = new boolean[arr1.length+1][sum+1];
/**
* if(i==arr1.length){
* return sum==aim;
* }
* 计算特殊情况
* */
for(int i=0;i<=sum;i++){
dp[arr1.length][i] = i==aim?true:false;
}
for(int i=1;i<arr1.length;i++){
for(int j=0;j<=sum;j++){
dp[i][j] = dp[i+1][j]||dp[i+1][j+arr1[j]];
}
}
return dp[0][0];
}

53. 最大子序和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

/**
* 集合组成是以i个元素结尾的字段,比如[2,1,-1,4]有四个,分别是以2,1,-1,4结尾的子段
* f(i) = max(f(i-1),0)+nums[i],f(i)表示以i作为结尾的子段的所有元素之和
* */
public int maxSubArray(int[] nums) {
if(nums.length==1){
return nums[0];
}
int[] dp = new int[nums.length];
dp[0] = nums[0];
int max = dp[0];
for(int i=1;i<nums.length;i++){
dp[i] = Math.max(dp[i-1],0)+nums[i];
max = Math.max(max,dp[i]);
}
return max;
}

120. 三角形最小路径和

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
/**
* 特殊情况:col = 0 dp[i][0] = dp[i-1][0]+triangle.get(i).get(0);
* 每个list中的最后一个元素
* dp[i][j] = min(dp[i-1][j],dp[i-1][j-1])+当前值
* */
public int minimumTotal(List<List<Integer>> triangle) {
if(triangle==null||triangle.size()==0){
return 0;
}
int[][] dp = new int[triangle.size()][triangle.size()];
dp[0][0] = triangle.get(0).get(0);
for(int i=1;i<triangle.size();i++){
dp[i][0] = dp[i-1][0]+triangle.get(i).get(0);
}
for(int i=1;i<triangle.size();i++){
List<Integer> data = triangle.get(i);
dp[i][data.size()-1] = dp[i-1][data.size()-2]+data.get(data.size()-1);
}
for(int i=1;i<triangle.size();i++){
List<Integer> data = triangle.get(i);
for(int j=1;j<data.size()-1;j++){
dp[i][j] = Math.min(dp[i-1][j-1],dp[i-1][j])+data.get(j);
}
}
int min = Integer.MAX_VALUE;
for(int i=0;i<triangle.get(triangle.size()-1).size();i++){
min = Math.min(min,dp[triangle.size()-1][i]);
}
return min;
}

换钱方法数

1578623250591

递归版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 假设[5,10,25,1],targrt = 15
* 使用0张5元的,剩下的15元在[10,25,1]中凑得。使用1张5元的,剩下的10元在[10,25,1]中凑得。使用两张5元的,剩下的5元在[10,25,1]中凑得。使用3张5元的,剩下0元,不用凑了。
* 对[10,25,1]继续递归这个过程,直到遍历完这个数组。
* */
public int coinChange(int[] coins, int target) {
return helper(coins,0,target);
}

public int helper(int[] coins,int index,int amount){
int res = 0;
//遍历完数组,假如还剩下0元,那么这是一种选择方法
if(index==coins.length){
return amount==0?1:0;
}
for(int i=0;coins[index]*i<=amount;i++){
res+=helper(coins,index+1,amount-coins[index]*i);
}
return res;
}

动态规划版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 使用动态规划来完成,两个变量,index,amount,我们需要求的值是index=0,amount=target
* 特殊情况:index==coins.length,amount==0?1:0
* 普通情况:dp[i][j] = dp[index+1][amount-coins[index]]+dp[index+1][amount-coins[index]*2]....
* */

public int coinChange2(int[] coins, int target) {
//定义动态规划数组,index以及amount两个变量
int[][] dp = new int[coins.length+1][target+1];
dp[coins.length][0] = 1;
for(int i=1;i<=target;i++) {
dp[coins.length][i] = 0;
}
for(int i=coins.length-1;i>=0;i--){
for(int j=0;j<=target;j++){
// for(int k=0;j-coins[i]*k>=0;k++) {
// dp[i][j] += dp[i + 1][j - coins[i]*k];
// }
dp[i][j] = dp[i+1][j]+(j-coins[i]>=0?dp[i][j-coins[i]]:0);
}
}
return dp[0][target];
}

486.纸牌博弈问题

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
66
/**
* 暴力递归
* f(i,j):绝顶聪明的人在arr[i...j]上先拿纸牌,可以获得什么分数
* s(i,j):绝顶聪明的人在arr[i...j]上后拿纸牌,可以获得什么分数
* 返回玩家1先拿纸牌的分数以及玩家2后拿纸牌的分数
* */
public boolean PredictTheWinner(int[] nums) {
return first(nums,0,nums.length-1)>second(nums,0,nums.length-1);
}

/**
* 先拿纸牌获取的分数
* start==end:return nums[start]
* strat!=end:可以取nums[start]或者是nums[end],
* 取nums[start],那么,在[start+1,end]中后取
* 取nums[end],那么,在[start,end-1]中后取
* 因为他绝顶聪明,所以会选取两个最大值取
* */
public int first(int[] nums,int start ,int end){
if(start==end){
return nums[start];
}
return Math.max(nums[start]+second(nums,start+1,end),nums[end]+second(nums,start,end-1));
}

/**
* 后拿纸牌获取的分数
* start==end:直接返回0
* strat!=end:对手可以取nums[start]或者是nums[end],
* 对手取nums[start],那么,玩家就可以在[start+1,end]中先取
* 对手取nums[end],那么,玩家就可以在[start,end-1]中先取
* 因为对手也是绝顶聪明,所以会留取最差的情况给玩家,返回最小值
* */
public int second(int[] nums,int start ,int end){
if(start==end){
return 0;
}
return Math.min(first(nums,start+1,end),first(nums,start,end-1));
}


/**
* 递归改动态规划
* 需要准备两个动态规划数组,f以及s
* 两个变量:start,end
* */

public boolean PredictTheWinner(int[] nums) {
if(nums.length<=1){
return true;
}
int[][] f = new int[nums.length][nums.length];
int[][] s = new int[nums.length][nums.length];
for(int i=0;i<nums.length;i++){
f[i][i] = nums[i];
}
for(int i=nums.length-2;i>=0;i--){
for(int j=i+1;j<nums.length;j++){
if(i!=j){
f[i][j] = Math.max(nums[i]+s[i+1][j],nums[j]+s[i][j-1]);
s[i][j] = Math.min(f[i+1][j],f[i][j-1]);
}
}
}
return f[0][nums.length-1]>=s[0][nums.length-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
/**
* 机器人到达指定位置方法数
* N:N个位置
* M:机器人开始的位置
* K:需要走的步数
* P:最终来到的位置
* */
public int walkWay(int N,int M,int K,int P){
if(N<2||M<1||K<1||P<1||M>N||P>N){
return 0;
}
return walk(N,M,K,P);
}

/**
* @param cur:当前所处的位置
* @param rest:剩余的步数
*
*/
public int walk(int N,int cur,int rest,int P){
//步数走完了,看是否停在了P
if(rest==0){
return cur==P?1:0;
}
//假如走到了1位置,那么只能向右走
if(cur==1){
return walk(N,2,rest-1,P);
}
//假如走到了N位置,那么只能向左走
if(cur==N){
return walk(N,N-1,rest-1,P);
}
//其他位置,可以向左走向右走
return walk(N,cur+1,rest-1,P)+walk(N,cur-1,rest-1,P);
}
/**
* 递归转动态规划
* 可变参数:cur,rest
* 返回:dp[M][K]
* */
public int walkWay2(int N,int M,int K,int P){
if(N<2||M<1||K<1||P<1||M>N||P>N){
return 0;
}
//K:rest N:cur
int[][] dp = new int[K+1][N+1];
dp[0][P] = 1;
//因为rest==0时是初始的特殊情况,所以,所有值都应该根据rest==0时求得,所以应该rest值先进性遍历,即<=k
for(int i =1;i<=K;i++){
for(int j=1;j<=N;j++){
if(j==1){
dp[i][j] = dp[i-1][2];
}else if(j==N){
dp[i][j] = dp[i-1][N-1];
}else{
dp[i][j] = dp[i-1][j-1]+dp[i-1][j+1];
}
}
}
return dp[K][M];
}

leetcode贪心策略

发表于 2019-12-20

分金条

1576848273437

这是一种求最小花费的问题,而且总共的代价是由子代价累加得到的,可以使用哈夫曼树来解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int min_money(int[] arr){
//定义一个小顶堆,每次取出两个最小的元素作为哈夫曼树的构建
PriorityQueue<Integer> data = new PriorityQueue<>();
for(int i=0;i<arr.length;i++){
data.add(arr[i]);
}
//构建哈夫曼树,每次取出两个最小元素作为合并的两个节点,并将合并的值重新放入堆中
int cur = 0;
int res = 0;
while(data.size()>1){
cur = data.poll()+data.poll();
data.add(cur);
res+= cur;
}
return res;
}

502. IPO

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
public class Node{
private int cost;
private int profit;
public Node(int cost,int profit){
this.cost = cost;
this.profit = profit;
}
}
public class MinHeapComparator implements Comparator<Node> {
@Override
public int compare(Node o1, Node o2) {
return o1.cost-o2.cost;
}
}
public class MaxHeapComparator implements Comparator<Node>{
@Override
public int compare(Node o1, Node o2) {
return o2.profit-o1.profit;
}
}
public int findMaximizedCapital(int k, int W, int[] Profits, int[] Capital) {
//小顶堆,按照花费排序
PriorityQueue<Node> minCostHeap = new PriorityQueue<>(new MinHeapComparator());
//大顶堆,按照收益排序
PriorityQueue<Node> maxProfitHeap = new PriorityQueue<>(new MaxHeapComparator());
for(int i=0;i<Profits.length;i++){
Node node = new Node(Capital[i],Profits[i]);
minCostHeap.add(node);
}

/***将minCostHeap中所有<=W的元素全部移到MaxProfit中,这样,MaxProfit中存储的就是所有小于总资产的可挑选的项目,
* 并且按照收益排序,因此,直接获取MaxProfit堆顶元素即可,直到k次项目选择完毕,或者是大顶堆没有元素(没有项目<=手中的资产)*/
for(int i =0;i<k;i++){
while(!minCostHeap.isEmpty()&&minCostHeap.peek().cost<=W){
maxProfitHeap.add(minCostHeap.poll());
}
if(maxProfitHeap.size()<1){
return W;
}
W+=maxProfitHeap.poll().profit;
}
return W;
}

会议室项目宣讲

一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。 给你每一个项目开始的时间和结束的时间(给你一个数组,里面 是一个个具体的项目),你来安排宣讲的日程,要求会议室进行 的宣讲的场次最多。返回这个最多的宣讲场次。

贪心策略:

  • 开始时间最早的项目先安排。反例:开始时间最早,但持续时间占了一整天,其他项目无法安排。
  • 持续时间最短的先安排。反例:这样安排会导致结束时间在此期间和开始时间在此期间的所有项目不能安排。
  • 最优策略:最先结束的项目先安排。
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 class Program{
private int start;
private int end;
public Program(int start,int end){
this.start = start;
this.end = end;
}

}
public class ProgramComparator implements Comparator<Program>{
@Override
public int compare(Program o1, Program o2) {
return o1.end-o2.end;
}
}
/**
* 以结束时间最早作为贪心策略
* 因此,每次需要取出大于等于上一次结束时间的且结束时间最小的项目
* */
public int bestArrange(Program[] programs, int start) {
Arrays.sort(programs,new ProgramComparator());
int result = 0;
for(int i=0;i<programs.length;i++){
if(start<=programs[i].start){
result++;
start = programs[i].end;
}
}
return result;
}

leetcode哈希表

发表于 2019-12-19 | 分类于 leetcode

1576758278802

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
/**
* 需要建立两个Map,mapA={value,插入顺序} MapB={插入顺序,value}
* */
public class RandomPool<K> {
HashMap<K,Integer> mapA = new HashMap<>();
HashMap<Integer,K> mapB = new HashMap<>();
private int index = 0;
//插入元素,需要插入两个map
public void insert(K key){
if(!mapA.containsKey(key)) {
mapA.put(key, index);
mapB.put(index, key);
index++;
}
}

//删除元素的时候,假如mapB key在0-25之间,这样,删除某一个元素的话,key就不是均匀分布的,因此,需要从最后一个位置补元素
public void delete(K key){
if(mapA.containsKey(key)) {
int del_index = mapA.get(key);
int lastIndex = --index;
//将最后一个元素移到删除位置
K lastEle = mapB.get(lastIndex);
mapA.put(lastEle,del_index);
mapB.put(del_index,lastEle);
mapB.remove(lastIndex);
mapA.remove(key);
}
}

//随机等概率获取元素,mapB key在0-index之间,所以Math.random()*index即可返回随机的元素下标
public K getRandom(){
if(index==0){
return null;
}
return mapB.get((int)(Math.random()*index));
}

}

海量数据判重问题

假如使用哈希表的话,需要存储上亿条级别的数据,十分浪费空间,尽管查询时间复杂度是O(1).

一般来说,对于这种情况,可以使用布隆过滤器来实现。

比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。

布隆过滤器是一个 bit 向量或者说 bit 数组,长这样:

img

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为:

img

如何判断某一个对象是之前的某一个输入对象呢?

依旧是将该值进行K个哈希函数的计算,求出映射之后的位置是0还是1,假如有0的话,那么该值一定不是之前的某一个输入对象。假如全是1的话,也不一定能保证一定就是之前的某一个输入对象,只是有一定的概率是。

过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,如何确定过滤器大小?

img

img

其中,n为元素个数,p为容错率。

一致性哈希

在处理多台服务器负载均衡的时候,传统的hash方式。

img

虽然可以达到负载均衡的目的,但是,假如我们需要增加服务器,或者需要移除服务器,就需要重新hash计算之前缓存的所有数据,十分不便。

一致性哈希算法可以解决这个问题,达到高效数据迁移的目的。

一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,它的取模法不是对服务器数量进行取模,而是对整个哈希值空间。

img

将各个服务器使用Hash进行一个哈希,这样每台机器就能确定其在哈希环上的位置。

img

接下来使用如下算法定位数据访问到相应服务器:将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。

img

一致性Hash算法的容错性和可扩展性

假如某一个节点宕机,例如C,则只需要将BC之间的数据重新哈希到D上,不需要重哈希全部数据。

增加节点也是,假如BC间添加一个节点,只需要将B与该节点之间的数据重哈希。

一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。

数据倾斜问题

假如机器比较少,可能造成机器在整个环上分布不均匀。例如:

img

解决该问题可以使用虚拟节点机制。

例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点:

img

同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。这样就解决了服务节点少时数据倾斜的问题。

200. 岛屿数量

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 numIslands(char[][] grid) {
if(grid.length==0||grid[0].length==0){
return 0;
}
int res = 0;
int row = grid.length;
int col = grid[0].length;
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
if(grid[i][j]=='1'){
res++;
infect(grid,i,j,row,col);
}
}
}
return res;
}

public void infect(char[][]grid,int i,int j,int M,int N){
if(i<0||j<0||i>=M||i>=N||grid[i][j]!='1'){
return ;
}
grid[i][j]='2';
infect(grid,i-1,j,M,N);
infect(grid,i+1,j,M,N);
infect(grid,i,j-1,M,N);
infect(grid,i,j+1,M,N);
}
12…5
SKJin

SKJin

49 日志
12 分类
16 标签
RSS
GitHub E-Mail
© 2020 SKJin
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4