宝塔部署Docker—Mysql镜像

InterviewCoder

转自 https://blog.csdn.net/u011630259/article/details/124497343

# 宝塔部署 Docker—Mysql 镜像

如何在面板上使用 Docker 项目管理器快速创建多个版本的 MySQL?手把手的教你如何创建多个版本的 MySQL 服务
环境介绍:
宿主机:CentOS7.9
配置:2 核 4G(测试机器,生产环境建议配置高点)
面板版本:7.9.26(测试版)
Docker 项目管理器:3.9

img

Docker 版本:Docker version 20.10.14, build a224086
测试 MySQL 版本:
MySQL5.7.37
MySQL8.0.28
1、获取 MySQL 版本

img

默认情况下,直接输入 mysql 名,会拉取 mysql:latest 镜像,就是最新版本的镜像,指定版本后拉取的是指定版本的 MySQL 镜像,如 mysql:5.7.37

img

2、创建容器:

指定容器中数据库的密码:

img

MYSQL_ROOT_PASSWORD=dapaotest1

img

3、容器创建成功后,进入终端命令行查看数据库

img

4、创建数据库表

1
2
3
4
5
6
7
8
9
10
11
12
查看数据库的命令
show databases;
创建数据库的命令
create database dapaodocker;
创建用户的命令
create user 'dapaodocker'@'%' identified by 'dapao666!';
授权
grant all on dapaodocker.* to dapaodocker@'%';

查看用户权限
select host,user from user;
测试数据库是否可以连接

img

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Flutter】flutter 在真机上调试卸载安装包后卡住没反应问题

InterviewCoder

# 【Flutter】flutter 在真机上调试卸载安装包后卡住没反应问题

# 问题描述

把自己手机插上数据线后,用 flutter 进行真机调试,本来配合 vscode 跑得好好的,不知道是写了 bug 还是咋地,目标效果出不来,以为是热更新失败了,最后只好把安装在手机上的 app 卸载了,想当然地以为运行 flutter run 后在手机上会贴心的提示我重装一遍,哪知给我突然卡住了:

1
2
3
4
Running Gradle task 'assembleDebug'...
✓ Built build/app/outputs/flutter-apk/app-debug.apk.
Installing build/app/outputs/flutter-apk/app.apk...

一直卡在这个安装环节上,等半天是一点反应都没有,试了各种方法重启应用、改用 Andriod Studio 什么的,毛用没有,最后还得是在 StackOveFlow 上找到了法子:

# 解决方法:

Change the applicationId in android\app\build.gradle file like this:

from :

1
2
3
4
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.xxxxxxxxxxx.yyyyyy1"
}

to :

1
2
3
4
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.xxxxxxxxxxx.yyyyyy2"
}

改一下 flutter 工程目录下 andriod\appbuild.gradle 文件中的 applicationId 重新 build 就能重新在手机上安装调试 app 了,我丢!

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Java】volatile关键字

InterviewCoder

# 【Java】volatile 关键字

# volatile

# 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
public class ApiTest {

public static void main(String[] args) {
final VT vt = new VT();

Thread Thread01 = new Thread(vt);
Thread Thread02 = new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException ignore) {
}
vt.sign = true;
System.out.println("vt.sign = true 通知 while (!sign) 结束!");
}
});

Thread01.start();
Thread02.start();
}

}

class VT implements Runnable {

public boolean sign = false;

public void run() {
while (!sign) {
}
System.out.println("你坏");
}
}

这段代码,是两个线程操作一个变量,程序期望当 sign 在线程 Thread01 被操作 vt.sign = true 时,Thread02 输出 你坏

但实际上这段代码永远不会输出 你坏,而是一直处于死循环。这是为什么呢?接下来我们就一步步讲解和验证。

# 2. 加上 volatile 关键字

我们把 sign 关键字加上 volatitle 描述,如下:

1
2
3
4
5
6
7
8
9
10
class VT implements Runnable {

public volatile boolean sign = false;

public void run() {
while (!sign) {
}
System.out.println("你坏");
}
}

测试结果

1
2
3
4
vt.sign = true 通知 while (!sign) 结束!
你坏

Process finished with exit code 0

volatile 关键字是 Java 虚拟机提供的的最轻量级的同步机制,它作为一个修饰符出现,用来修饰变量,但是这里不包括局部变量哦

在添加 volatile 关键字后,程序就符合预期的输出了 你坏。从我们对 volatile 的学习认知可以知道。volatile 关键字是 JVM 提供的最轻量级的同步机制,用来修饰变量,用来保证变量对所有线程可见性。

正在修饰后可以让字段在线程见可见,那么这个属性被修改值后,可以及时的在另外的线程中做出相应的反应。

# 3. volatile 怎么保证的可见性

# 3.1 无 volatile 时,内存变化

无volatile时,内存变化

首先是当 sign 没有 volatitle 修饰时 public boolean sign = false; ,线程 01 对变量进行操作,线程 02 并不会拿到变化的值。所以程序也就不会输出结果 “你坏”

# 3.2 有 volatile 时,内存变化

有volatile时,内存变化

当我们把变量使用 volatile 修饰时 public volatile boolean sign = false; ,线程 01 对变量进行操作时,会把变量变化的值强制刷新的到主内存。当线程 02 获取值时,会把自己的内存里的 sign 值过期掉,之后从主内存中读取。所以添加关键字后程序如预期输出结果。

# 4. 反编译解毒可见性

类似这样有深度的技术知识,最佳的方式就是深入理解原理,看看它到底做了什么才保证的内存可见性操作。

# 4.1 查看 JVM 指令

指令javap -v -p VT

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 volatile boolean sign;
descriptor: Z
flags: ACC_PUBLIC, ACC_VOLATILE

org.itstack.interview.test.VT();
descriptor: ()V
flags:
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field sign:Z
9: return
LineNumberTable:
line 35: 0
line 37: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lorg/itstack/interview/test/VT;

public void run();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field sign:Z
4: ifne 10
7: goto 0
10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
13: ldc #4 // String 你坏
15: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
18: return
LineNumberTable:
line 40: 0
line 42: 10
line 43: 18
LocalVariableTable:
Start Length Slot Name Signature
0 19 0 this Lorg/itstack/interview/test/VT;
StackMapTable: number_of_entries = 2
frame_type = 0 /* same */
frame_type = 9 /* same */
}

从 JVM 指令码中只会发现多了, ACC_VOLATILE ,并没有什么其他的点。所以,也不能看出是怎么实现的可见性。

# 4.2 查看汇编指令

通过 Class 文件查看汇编,需要下载 hsdis-amd64.dll 文件,复制到 JAVA_HOME\jre\bin\server目录下 。下载资源如下:

另外是执行命令,包括:

  1. 基础指令: java -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
  2. 指定打印: -XX:CompileCommand=dontinline,类名.方法名
  3. 指定打印: -XX:CompileCommand=compileonly,类名.方法名
  4. 输出位置: > xxx

最终使用: java -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=dontinline,ApiTest.main -XX:CompileCommand=compileonly,ApiTest.mian

指令可以在 IDEA 中的 Terminal 里使用,也可以到 DOS 黑窗口中使用

另外,为了更简单的使用,我们把指令可以配置到 idea 的 VM options 里,如下图:

Idea VM options 配置编译指令

配置完成后,不出意外的运行结果如下:

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
Loaded disassembler from C:\Program Files\Java\jdk1.8.0_161\jre\bin\server\hsdis-amd64.dll
Decoding compiled method 0x0000000003744990:
Code:
Argument 0 is unknown.RIP: 0x3744ae0 Code size: 0x00000110
[Disassembling for mach='amd64']
[Entry Point]
[Constants]
# {method} {0x000000001c853d18} 'getSnapshotTransformerList' '()[Lsun/instrument/TransformerManager$TransformerInfo;' in 'sun/instrument/TransformerManager'
# [sp+0x40] (sp of caller)
0x0000000003744ae0: mov r10d,dword ptr [rdx+8h]
0x0000000003744ae4: shl r10,3h
0x0000000003744ae8: cmp r10,rax
0x0000000003744aeb: jne 3685f60h ; {runtime_call}
0x0000000003744af1: nop word ptr [rax+rax+0h]
0x0000000003744afc: nop
[Verified Entry Point]
0x0000000003744b00: mov dword ptr [rsp+0ffffffffffffa000h],eax
0x0000000003744b07: push rbp
0x0000000003744b08: sub rsp,30h ;*aload_0
; - sun.instrument.TransformerManager::getSnapshotTransformerList@0 (line 166)

0x0000000003744b0c: mov eax,dword ptr [rdx+10h]
0x0000000003744b0f: shl rax,3h ;*getfield mTransformerList
; - sun.instrument.TransformerManager::getSnapshotTransformerList@1 (line 166)

0x0000000003744b13: add rsp,30h
...

运行结果就是汇编指令,比较多这里就不都放了。我们只观察🕵重点部分:

1
2
3
4
5
6
7
0x0000000003324cda: mov    0x74(%r8),%edx     ;*getstatic state
; - VT::run@28 (line 27)

0x0000000003324cde: inc %edx
0x0000000003324ce0: mov %edx,0x74(%r8)
0x0000000003324ce4: lock addl $0x0,(%rsp) ;*putstatic state
; - VT::run@33 (line 27)

编译后的汇编指令中,有 volatile 关键字和没有 volatile 关键字,主要差别在于多了一个 lock addl $0x0,(%rsp) ,也就是 lock 的前缀指令。

lock 指令相当于一个内存屏障,它保证如下三点:

  1. 将本处理器的缓存写入内存。
  2. 重排序时不能把后面的指令重排序到内存屏障之前的位置。
  3. 如果是写入动作会导致其他处理器中对应的内存无效。

那么,这里的 1、3 就是用来保证被修饰的变量,保证内存可见性。

# 5. 不加 volatile 也可见吗

1
有质疑就要有验证

我们现在再把例子修改下,在 while (!sign) 循环体中添加一段执行代码,如下;

1
2
3
4
5
6
7
8
9
10
11
12
class VT implements Runnable {

public boolean sign = false;

public void run() {
while (!sign) {
System.out.println("你好");
}
System.out.println("你坏");
}

}

修改后去掉了 volatile 关键字,并在 while 循环中添加一段代码。现在的运行结果是:

1
2
3
4
5
6
7
8
...
你好
你好
你好
vt.sign = true 通知 while (!sign) 结束!
你坏

Process finished with exit code 0

咋样,又可见了吧!

这是因为在没 volatile 修饰时,jvm 也会尽量保证可见性。有 volatile 修饰的时候,一定保证可见性。

# 总结

  • volatile 会控制被修饰的变量在内存操作上主动把值刷新到主内存,JMM 会把该线程对应的 CPU 内存设置过期,从主内存中读取最新值。
  • 那么,volatile 如何防止指令重排也是内存屏障,volatile 的内存屏故障是在读写操作的前后各添加一个 StoreStore 屏障,也就是四个位置,来保证重排序时不能把内存屏障后面的指令重排序到内存屏障之前的位置。
  • 另外 volatile 并不能解决原子性问题,即对于复合操作(比如 i++ 等)无法保证线程安全,如果需要解决原子性问题,需要使用 synchronzied 或者 lock。
  • 使用 volatile 虽然可以提供比较高的可见性和顺序性的保障,但并不是绝对可靠的,还需要根据具体的业务场景和代码需求来进行综合考虑和评估。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【数据结构】手写实现Hash表,并解决哈希冲突

InterviewCoder

# 【数据结构】手写实现 Hash 表,并解决哈希冲突

# 一、前言

# 哈希表的历史

哈希散列的想法在不同的地方独立出现。1953 年 1 月,汉斯・彼得・卢恩 (Hans Peter Luhn) 编写了一份 IBM 内部备忘录,其中使用了散列和链接。开放寻址后来由 AD Linh 在 Luhn 的论文上提出。大约在同一时间,IBM Research 的 Gene Amdahl、Elaine M. McGraw、Nathaniel Rochester 和 Arthur Samuel 为 IBM 701 汇编器实现了散列。 线性探测的开放寻址归功于 Amdahl,尽管 Ershov 独立地有相同的想法。“开放寻址” 一词是由 W. Wesley Peterson 在他的文章中创造的,该文章讨论了大文件中的搜索问题。

# 二、哈希数据结构

哈希表的存在是为了解决能通过 O (1) 时间复杂度直接索引到指定元素。

这是什么意思呢?通过我们使用数组存放元素,都是按照顺序存放的,当需要获取某个元素的时候,则需要对数组进行遍历,获取到指定的值。如图所示;

img

而这样通过循环遍历比对获取指定元素的操作,时间复杂度是 O (n),也就是说如果你的业务逻辑实现中存在这样的代码是非常拉胯的。那怎么办呢?这就引入了哈希散列表的设计。


在计算机科学中,一个哈希表(hash table、hash map)是一种实现关联数组的抽象数据结构,该结构将键通过哈希计算映射到值。

也就是说我们通过对一个 Key 值计算它的哈希并与长度为 2 的 n 次幂的数组减一做与运算,计算出槽位对应的索引,将数据存放到索引下。那么这样就解决了当获取指定数据时,只需要根据存放时计算索引 ID 的方式再计算一次,就可以把槽位上对应的数据获取处理,以此达到时间复杂度为 O (1) 的情况。如图所示;

img

哈希散列虽然解决了获取元素的时间复杂度问题,但大多数时候这只是理想情况。因为随着元素的增多,很可能发生哈希冲突,或者哈希值波动不大导致索引计算相同,也就是一个索引位置出现多个元素情况。如图所示;

img

李二狗拎瓢冲 都有槽位的下标索引 03 的 叮裆猫 发生冲突时,情况就变得糟糕了,因为这样就不能满足 O (1) 时间复杂度获取元素的诉求了。

# 三、实现哈希散列

​ 哈希散列是一个非常常见的数据结构,无论是我们使用的 HashMap、ThreaLocal 还是你在刷题中位了提升索引效率,都会用到哈希散列。

​ 只要哈希桶的长度由负载因子控制的合理,每次查找元素的平均时间复杂度与桶中存储的元素数量无关。另外许多哈希表设计还允许对键值对的任意插入和删除,每次操作的摊销固定平均成本。

# 1. 哈希碰撞

说明:通过模拟简单 HashMap 实现,去掉拉链寻址等设计,验证元素哈新索引位置碰撞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HashMap01<K, V> implements Map<K, V> {

private final Object[] tab = new Object[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
tab[idx] = value;
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
return (V) tab[idx];
}
}

img

  • HashMap01 的实现只是通过哈希计算出的下标,散列存放到固定的数组内。那么这样当发生元素下标碰撞时,原有的元素就会被新的元素替换掉。

测试

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test_hashMap01() {
Map<String, String> map = new HashMap01<>();
map.put("01", "花花");
map.put("02", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));

// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
}

img

1
2
3
4
06:58:41.691 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
06:58:41.696 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:苗苗

Process finished with exit code 0
  • 通过测试结果可以看到,碰撞前 map.get (“01”) 的值是 花花 ,两次下标索引碰撞后存放的值则是 苗苗
  • 这也就是使用哈希散列必须解决的一个问题,无论是在已知元素数量的情况下,通过扩容数组长度解决,还是把碰撞的元素通过链表存放,都是可以的。

# 2. 拉链寻址

说明:既然我们没法控制元素不碰撞,但我们可以对碰撞后的元素进行管理。比如像 HashMap 中拉链法一样,把碰撞的元素存放到链表上。这里我们就来简化实现一下。

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
public class HashMap02BySeparateChaining<K, V> implements Map<K, V> {

private final LinkedList<Node<K, V>>[] tab = new LinkedList[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
if (tab[idx] == null) {
tab[idx] = new LinkedList<>();
tab[idx].add(new Node<>(key, value));
} else {
tab[idx].add(new Node<>(key, value));
}
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
for (Node<K, V> kvNode : tab[idx]) {
if (key.equals(kvNode.getKey())) {
return kvNode.value;
}
}
return null;
}

static class Node<K, V> {
final K key;
V value;

public Node(K key, V value) {
this.key = key;
this.value = value;
}

public K getKey() {
return key;
}

public V getValue() {
return value;
}

}
}

img

  • 因为元素在存放到哈希桶上时,可能发生下标索引膨胀,所以这里我们把每一个元素都设定成一个 Node 节点,这些节点通过 LinkedList 链表关联,当然你也可以通过 Node 节点构建出链表 next 元素即可。
  • 那么这时候在发生元素碰撞,相同位置的元素就都被存放到链表上了,获取的时候需要对存放多个元素的链表进行遍历获取。

测试

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test_hashMap02() {
Map<String, String> map = new HashMap02BySeparateChaining<>();
map.put("01", "花花");
map.put("05", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));

// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
}

img

1
2
3
4
07:21:16.654 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:22:44.651 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花

Process finished with exit code 0
  • 此时第一次和第二次获取 01 位置的元素就都是 花花 了,元素没有被替代。因为此时的元素是被存放到链表上了。

# 3. 开放寻址

说明:除了对哈希桶上碰撞的索引元素进行拉链存放,还有不引入新的额外的数据结构,只是在哈希桶上存放碰撞元素的方式。它叫开放寻址,也就是 ThreaLocal 中运用斐波那契散列 + 开放寻址的处理方式。

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
public class HashMap03ByOpenAddressing<K, V> implements Map<K, V> {

private final Node<K, V>[] tab = new Node[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
if (tab[idx] == null) {
tab[idx] = new Node<>(key, value);
} else {
for (int i = idx; i < tab.length; i++) {
if (tab[i] == null) {
tab[i] = new Node<>(key, value);
break;
}
}
}
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
for (int i = idx; i < tab.length; i ++){
if (tab[idx] != null && tab[idx].key == key) {
return tab[idx].value;
}
}
return null;
}

static class Node<K, V> {
final K key;
V value;

public Node(K key, V value) {
this.key = key;
this.value = value;
}

}
}

img

  • 开放寻址的设计会对碰撞的元素,寻找哈希桶上新的位置,这个位置从当前碰撞位置开始向后寻找,直到找到空的位置存放。
  • 在 ThreadLocal 的实现中会使用斐波那契散列、索引计算累加、启发式清理、探测式清理等操作,以保证尽可能少的碰撞。

测试

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test_hashMap03() {
Map<String, String> map = new HashMap03ByOpenAddressing<>();
map.put("01", "花花");
map.put("05", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
}

img

1
2
3
4
5
07:20:22.382 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:20:22.387 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:20:22.387 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:HashMap{tab=[null,{"key":"01","value":"花花"},{"key":"09","value":"蛋蛋"},{"key":"12","value":"苗苗"},null,{"key":"05","value":"豆豆"},null,null]}

Process finished with exit code 0
  • 通过测试结果可以看到,开放寻址对碰撞元素的寻址存放,也是可用解决哈希索引冲突问题的。

# 四、常见面试问题

  • 介绍一下散列表

    1
    答:散列表(Hash Table)是一种基于哈希函数进行快速访问的数据结构,具有极高的查询和插入效率。它的基本思想是将要存储的数据元素通过哈希函数映射到一个固定的位置上,然后在该位置上进行查找或插入操作
  • 为什么使用散列表

    1
    答:散列表之所以被广泛使用,是因为它可以支持常数级别的查询、插入和删除操作,这使得它非常适合于处理大规模数据和实时数据的场景。例如,在搜索引擎中,需要对数十亿条数据进行快速检索,散列表就是一种很好的选择。
  • 拉链寻址和开放寻址的区别

    1
    2
    3
    4
    5
    答:散列表的主要问题是哈希冲突,在不同的键被映射到同一个位置上时,就会产生哈希冲突。为了解决哈希冲突,常见的方法有拉链法和开放寻址。

    拉链法是指在哈希表中的每个位置上维护一个链表,当多个键映射到同一个位置时,它们会被保存在该位置对应的链表上。查找某个键时,先根据哈希函数计算出该键在哈希表中的位置,然后在该位置对应的链表上进行查找。

    开放寻址是指当发生哈希冲突时,通过探测(probing)寻找哈希表中下一个空的位置,直到找到可以存储该键的位置或者遍历了整个哈希表。具体的探测方法包括线性探测、二次探测、双重哈希等。查找某个键时,计算该键在哈希表中的位置,并在该位置及其后续位置上进行查找。
  • 还有其他什么方式可以解决散列哈希索引冲突

    1
    答:除了拉链法和开放寻址,还有一些其他方式可以解决哈希冲突,如再哈希法、建立公共溢出区等。
  • 对应的 Java 源码中,对于哈希索引冲突提供了什么样的解决方案

    1
    答:在 Java 中,对于哈希索引冲突,一般使用拉链法来处理。Java 的 HashMap 和 Hashtable 都采用了拉链法,并支持动态扩容和缩容,以适应不同的数据规模。同时,为了提高查询效率,在 Java 8 中,HashMap 还引入了红黑树,用于优化链表长度过长的情形。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【数据结构】手写实现ArrayList

InterviewCoder

# 【数据结构】手写实现 ArrayList

# 一、前言

# 数组是数据结构还是数据类型?

数组只是个名称,它可以描述一组操作,也可以命名这组操作。数组的数据操作,是通过 idx->val 的方式来处理。它不是具体要求内存上要存储着连续的数据才叫数组,而是说,通过连续的索引 idx,也可以线性访问相邻的数据。

那么当你定义了数据的存储方式,也就定义了数据结构。所以它也是被归类为数据结构。

# 二、数组数据结构

数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型数据的集合。

img

数组的特点:

  1. 数组是相同数据类型的元素集合(int 不能存放 double)
  2. 数组中各元素的存储是有先后顺序的,它们在内存中按照这个顺序连续存放到一起。内存地址连续。
  3. 数组获取元素的时间复杂度为 O (1)

# 1. 一维数组

一维数组是最常用的数组,其他很多数据结构的变种也都是从一维数组来的。例如 HashMap 的拉链寻址结构,ThreadLocal 的开放寻址结构,都是从一维数组上实现的。

# 2. 二维数组

img

二维以及多维数组,在开发场景中使用到的到是不多,不过在一些算法逻辑,数学计算中到是可以使用。

# 三、实现数组列表

在 Java 的源码中,数组是一个非常常用的数据结构,很多其他数据结构也都有数组的影子。

# 1. 基本设计

数组是一个固定的、连续的、线性的数据结构,那么想把它作为一个自动扩展容量的数组列表,则需要做一些扩展。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 默认初始化空间
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空元素
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* ArrayList 元素数组缓存区
*/
transient Object[] elementData;
  1. 初始化 ArrayList 阶段,如果不指定大小,默认会初始化一个空的元素。这个时候是没有默认长度的。
  2. 那么什么时候给初始化的长度呢?是在首次添加元素的时候,因为所有的添加元素操作,也都是需要判断容量,以及是否扩容的。那么在 add 添加元素时统一完成这个事情,还是比较好处理的。
  3. 之后就是随着元素的添加,容量是会不足的。当容量不足的是,需要进行扩容操作。同时还得需要把旧数据迁移到新的数组上。所以数据的迁移算是一个比较耗时的操作

# 2. 添加元素

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public boolean add(E e) {
// 确保内部容量
int minCapacity = size + 1;
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 判断扩容操作
if (minCapacity - elementData.length > 0) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 添加元素
elementData[size++] = e;
return true;
}

这是一份简化后的 ArrayList#add 操作

  1. 判断当前容量与初始化容量,使用 Math.max 函数取最大值最为最小初始化空间。
  2. 接下来是判断 minCapacity 和元素的数量,是否达到了扩容。首次创建 ArrayList 是一定会扩容的,也就是初始化 DEFAULT_CAPACITY = 10 的容量。
  3. Arrays.copyOf 实际上是创建一个新的空间数组,之后调用的 System.arraycopy 迁移到新创建的数组上。这样后续所有的扩容操作,也就都保持统一了。
  4. ArrayList 扩容完成后,就是使用 elementData [size++] = e; 添加元素操作了。

# 3. 移除元素

ArrayList 的重点离不开对 System.arraycopy 的使用,它是一个本地方法,可以让你从原数组的特定位置,迁移到新数组的指定位置和迁移数量。如图 2-5 所示,数据迁移 测试代码在 java-algorithms

img

删除元素

1
2
3
4
5
6
7
8
9
10
public E remove(int index) {
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0) {
// 从原始数组的某个位置,拷贝到目标对象的某个位置开始后n个元素
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
  • ArrayList 的元素删除,就是在确定出元素位置后,使用 System.arraycopy 拷贝数据方式移动数据,把需要删除的元素位置覆盖掉。
  • 此外它还会把已经删除的元素设置为 null 一方面让我们不会在读取到这个元素,另外一方面也是为了 GC

# 4. 获取元素

1
2
3
4
5
6
7
8
9
10
public E get(int index) {
return (E) elementData[index];
}
@Override
public String toString() {
return "ArrayList{" +
"elementData=" + Arrays.toString(elementData) +
", size=" + size +
'}';
}
  • 获取元素就比较简单了,直接从 elementData 使用索引直接获取即可。这个是一个 O (1) 操作。也正因为搜索元素的便捷性,才让 ArrayList 使用的那么广泛。同时为了兼容可以通过元素来获取数据,而不是直接通过下标,引出了 HashMap 使用哈希值计算下标的计算方式,也引出了斐波那契散列。它们的设计都是在尽可能减少元素碰撞的情况下,尽可能使用贴近 O (1) 的时间复杂度获取数据。这些内容的学习可以阅读小傅哥的《Java 面经手册》也可以随着本系列章节内容的铺设逐步覆盖到算法后进行学习

# 四、数组列表测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void test_array_list() {
cn.bugstack.algorithms.data.array.List<String> list = new ArrayList<>();
list.add("01");
list.add("02");
list.add("03");
list.add("04");
list.add("05");
list.add("06");
list.add("07");
list.add("08");
list.add("09");
list.add("10");
list.add("11");
list.add("12");

System.out.println(list);

list.remove(9);

System.out.println(list);
}

测试结果

img

1
2
3
4
ArrayList{elementData=[01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, null, null, null], size=12}
ArrayList{elementData=[01, 02, 03, 04, 05, 06, 07, 08, 09, 11, 12, null, null, null, null], size=11}

Process finished with exit code 0
  • 测试案例中包括了在我们自己实现的 ArrayList 中顺序添加元素,逐步测试扩容迁移元素,以及删除元素后数据的迁移。
  • 最终的测试结果可以看到,一共有 12 个元素,其中 idx=9 的元素被删除前后,元素的迁移变化。

# 六、常见面试问题

数据结构中有哪些是线性表数据结构?

1
答:线性表数据结构是指一种特殊的数据结构,其中数据元素之间存在线性关系。常见的线性表数据结构包括:数组、链表、栈和队列等。

数组的元素删除和获取,时间复杂度是多少?

1
答:数组的元素删除和获取的时间复杂度均为O(1),因为数组中的元素是连续存储的,并且可以通过下标直接访问元素。删除操作通常是将待删除元素后面的所有元素向前移动一位,然后将数组长度减一;获取操作则是直接返回对应下标的元素值。

ArrayList 中默认的初始化长度是多少?

1
答:ArrayList 中默认的初始化长度是10。在创建一个新的 ArrayList 对象时,如果没有指定初始容量,那么就会使用默认容量 10 创建一个空数组。

ArrayList 中扩容的范围是多大一次?

1
答:ArrayList 中扩容的范围是每次增加原来数组大小的一半。当 ArrayList 容量不足以容纳更多元素时,就会自动进行扩容操作。具体来说,ArrayList 扩容时会创建一个新的数组,其大小为原来数组大小的 1.5 倍,然后将原来数组中的元素复制到新数组中。该扩容策略可以保证 ArrayList 在大多数情况下都只需要进行少数几次扩容操作。

ArrayList 是如何完成扩容的,System.arraycopy 各个入参的作用是什么?

1
2
3
4
5
6
7
8
9
答:
ArrayList 是通过 System.arraycopy() 方法完成扩容操作的。具体实现过程如下:

1.当添加新元素时,如果当前内部数组已经满了,就需要进行扩容操作。
2.首先计算出新数组的大小,其大小为原有数组大小的 1.5 倍。
3.创建一个新的数组,并将原有数组中的元素复制到新数组中。这里使用 System.arraycopy() 方法来完成元素的复制操作,第一个参数是原数组,第二个参数是原数组中需要复制的起始位置,第三个参数是目标数组,第四个参数是目标数组中存放复制数据的起始位置,第五个参数是需要复制的元素个数。
4.最后,将 ArrayList 的内部数组设置为新数组。

System.arraycopy() 方法具有较高的效率,因为它可以直接利用底层系统级别的内存复制机制进行数据复制,避免了通过循环逐个复制元素的低效率问题。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【数据结构】手写实现LinkedList

# 【数据结构】手写实现 LinkedList

# 一、前言

# 链表的历史

于 1955-1956 年,由兰德公司的 Allen Newell、Cliff Shaw 和 Herbert A. Simon 开发了链表,作为他们的信息处理语言的主要数据结构。链表的另一个早期出现是由 Hans Peter Luhn 在 1953 年 1 月编写的 IBM 内部备忘录建议在链式哈希表中使用链表。

到 1960 年代初,链表和使用这些结构作为主要数据表示的语言的实用性已经很好地建立起来。MIT 林肯实验室的 Bert Green 于 1961 年 3 月在 IRE Transactions on Human Factors in Electronics 上发表了一篇题为 “用于符号操作的计算机语言” 的评论文章,总结了链表方法的优点。1964 年 4 月,Bobrow 和 Raphael 的一篇评论文章 “列表处理计算机语言的比较” 发表在 ACM 通讯中。

# 二、链表数据结构

在计算机科学中,链表是数据元素的线性集合,元素的线性顺序不是由它们在内存中的物理地址给出的。它是由一组节点组成的数据结构,每个元素指向下一个元素,这些节点一起,表示线性序列。

img

在最简单的链表结构下,每个节点由数据和指针(存放指向下一个节点的指针)两部分组成,这种数据结构允许在迭代时有效地从序列中的任何位置插入或删除元素。

链表的数据结构通过链的连接方式,提供了可以不需要扩容空间就更高效的插入和删除元素的操作,在适合的场景下它是一种非常方便的数据结构。但在一些需要遍历、指定位置操作、或者访问任意元素下,是需要循环遍历的,这将导致时间复杂度的提升。

# 三、链表分类类型

链表的主要表现形式分为;单向链表、双向链表、循环链表,接下来我们分别介绍下。

# 1. 单向链表

单链表包含具有数据字段的节点以及指向节点行中的下一个节点的 “下一个” 字段。可以对单链表执行的操作包括插入、删除和遍历。

img

# 2. 双向链表

在 “双向链表” 中,除了下一个节点链接之外,每个节点还包含指向序列中 “前一个” 节点的第二个链接字段。这两个链接可以称为’forward(‘s’)和’backwards’,或’next’和’prev’(‘previous’)。

img

# 3. 循环链表

在列表的最后一个节点中,链接字段通常包含一个空引用,一个特殊的值用于指示缺少进一步的节点。一个不太常见的约定是让它指向列表的第一个节点。在这种情况下,列表被称为 “循环” 或 “循环链接”;否则,它被称为 “开放” 或 “线性”。它是一个列表,其中最后一个指针指向第一个节点。

img

# 四、实现一个链表

# 1. 链表节点

1
2
3
4
5
6
7
8
9
10
11
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;

public Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
  • 链表的数据结构核心根基就在于节点对象的使用,并在节点对象中关联当前节点的上一个和下一个节点。通过这样的方式构建出链表结构。
  • 但也因为在链表上添加每个元素的时候,都需要创建新的 Node 节点,所以这也是一部分耗时的操作。

# 2. 头插节点

1
2
3
4
5
6
7
8
9
10
void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
}

img

  • 头插的操作流程,先把头节点记录下来。之后创建一个新的节点,新的节点构造函数的头节点入参为 null,通过这样的方式构建出一个新的头节点。
  • 原来的头结点,设置 f.prev 连接到新的头节点,这样的就可以完成头插的操作了。另外如果原来就没有头节点,头节点设置为新的节点即可。最后记录当前链表中节点的数量,也就是你使用 LinkedList 获取 size 时候就是从这个值获取的。

# 3. 尾插节点

1
2
3
4
5
6
7
8
9
10
11
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null) {
first = newNode;
} else {
l.next = newNode;
}
size++;
}

img

  • 尾差节点与头插节点正好相反,通过记录当前的结尾节点,创建新的节点,并把当前的结尾节点,通过 l.next 关联到新创建的节点上。同时记录 size 节点数量值。

# 4. 拆链操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
E unlink(Node<E> x) {
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
return element;
}

img

  • unlink 是一种拆链操作,只要你给定一个元素,它就可以把当前这个元素的上一个节点和一个节点进行相连,之后把自己拆除。
  • 这个方法常用于 remove 移除元素操作,因为整个操作过程不需要遍历,拆除元素后也不需要复制新的空间,所以时间复杂读为 O (1)

# 5. 删除节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}

img

  • 删除元素的过程需要 for 循环判断比删除元素的值,找到对应的元素,进行删除。
  • 循环比对的过程是一个 O (n) 的操作,删除的过程是一个 O (1) 的操作。所以如果这个链表较大,删除的元素又都是贴近结尾,那么这个循环比对的过程也是比较耗时的。

# 五、链表使用测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
List<String> list = new LinkedList<>();
// 添加元素
list.add("a");
list.addFirst("b");
list.addLast("c");
// 打印列表
list.printLinkList();
// 头插元素
list.addFirst("d");
// 删除元素
list.remove("b");
// 打印列表
list.printLinkList();
}

测试结果

1
2
3
4
目前的列表,头节点:b 尾节点:c 整体:b,a,c,
目前的列表,头节点:d 尾节点:c 整体:d,a,c,

Process finished with exit code 0
  • 按照我们的测试链表对数据的操作过程,从测试结果可以看到,已经满足了链表数据结构的使用。

# 六、常见面试问题

  • 描述一下链表的数据结构?

    1
    2
    答:
    链表是一种常见的数据结构,用于按顺序存储和访问元素集合。它由多个节点组成,每个节点包含两个部分:数据和指向下一个节点的指针。
  • Java 中 LinkedList 使用的是单向链表、双向链表还是循环链表?

    1
    2
    答:
    Java 中的 LinkedList 使用的是双向链表,每个节点都有一个指向前置节点和后继节点的指针。这使得在 LinkedList 中进行插入和删除操作的效率比 ArrayList 更高。
  • 链表中数据的插入、删除、获取元素,时间复杂度是多少?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    答:
    插入元素(O(n)):
    在链表中插入一个元素通常需要执行以下操作:
    在待插入位置之前查找元素O(n)
    将新元素插入到该位置O(1)
    因此,插入元素的时间复杂度是O(n)。

    删除元素(O(n)):
    在链表中删除一个元素通常需要执行以下操作:
    在待删除位置之前查找元素O(n)
    删除该位置上的元素O(1)
    因此,删除元素的时间复杂度也是O(n)。

    获取元素(O(n)):
    在链表中获取一个元素通常需要遍历整个链表,直到找到目标元素。
    因此,获取元素的时间复杂度也是O(n)。
  • 什么场景下使用链表更合适?

    1
    2
    3
    4
    答:
    1.频繁的进行插入和删除操作
    2.不需要随机访问元素
    3.单纯的遍历操作

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【算法】算法的时间与空间复杂度

InterviewCoder

# 【算法】算法的时间与空间复杂度

# 算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。

# 那么我们应该如何去衡量不同算法之间的优劣呢?

主要还是从算法所占用的「时间」和「空间」两个维度去考量。

  • 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
  • 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。

因此,评价一个算法的效率主要是看它的时间复杂度和空间复杂度情况。然而,有的时候时间和空间却又是「鱼和熊掌」,不可兼得的,那么我们就需要从中去取一个平衡点。

下面我来分别介绍一下「时间复杂度」和「空间复杂度」的计算方式。

# 一、时间复杂度

我们想要知道一个算法的「时间复杂度」,很多人首先想到的的方法就是把这个算法程序运行一遍,那么它所消耗的时间就自然而然知道了。

这种方式可以吗?当然可以,不过它也有很多弊端。
这种方式非常容易受运行环境的影响,在性能高的机器上跑出来的结果与在性能低的机器上跑的结果相差会很大。而且对测试时使用的数据规模也有很大关系。再者,并我们在写算法的时候,还没有办法完整的去运行呢。

因此,另一种更为通用的方法就出来了:「 大 O 符号表示法 」,即 T (n) = O (f (n))

我们先来看个例子:

1
2
3
4
5
for(i=1; i<=n; ++i)
{
j = i;
j++;
}

通过「 大 O 符号表示法 」,这段代码的时间复杂度为:O (n) ,为什么呢?

在 大 O 符号表示法中,时间复杂度的公式是: T (n) = O ( f (n) ),其中 f (n) 表示每行代码执行次数之和,而 O 表示正比例关系,这个公式的全称是:算法的渐进时间复杂度

我们继续看上面的例子,假设每行代码的执行时间都是一样的,我们用 1 颗粒时间 来表示,那么这个例子的第一行耗时是 1 个颗粒时间,第三行的执行时间是 n 个颗粒时间,第四行的执行时间也是 n 个颗粒时间(第二行和第五行是符号,暂时忽略),那么总时间就是 1 颗粒时间 + n 颗粒时间 + n 颗粒时间 ,即 (1+2n) 个颗粒时间,即: T (n) = (1+2n)* 颗粒时间,从这个结果可以看出,这个算法的耗时是随着 n 的变化而变化,因此,我们可以简化的将这个算法的时间复杂度表示为:T (n) = O (n)

为什么可以这么去简化呢,因为大 O 符号表示法并不是用于来真实代表算法的执行时间的,它是用来表示代码执行时间的增长变化趋势的。

所以上面的例子中,如果 n 无限大的时候,T (n) = time (1+2n) 中的常量 1 就没有意义了,倍数 2 也意义不大。因此直接简化为 T (n) = O (n) 就可以了。

常见的时间复杂度量级有:

  • 常数阶 O (1)
  • 对数阶 O (logN)
  • 线性阶 O (n)
  • 线性对数阶 O (nlogN)
  • 平方阶 O (n²)
  • 立方阶 O (n³)
  • K 次方阶 O (n^k)
  • 指数阶 (2^n)

上面从上至下依次的时间复杂度越来越大,执行的效率越来越低。

下面选取一些较为常用的来讲解一下(没有严格按照顺序):

  1. 常数阶 O (1)

无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是 O (1),如:

1
2
3
4
5
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;

上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用 O (1) 来表示它的时间复杂度。

  1. 线性阶 O (n)

这个在最开始的代码示例中就讲解过了,如:

1
2
3
4
5
for(i=1; i<=n; ++i)
{
j = i;
j++;
}

这段代码,for 循环里面的代码会执行 n 遍,因此它消耗的时间是随着 n 的变化而变化的,因此这类代码都可以用 O (n) 来表示它的时间复杂度。

  1. 对数阶 O (logN)

还是先来看代码:

1
2
3
4
5
int i = 1;
while(i<n)
{
i = i * 2;
}

从上面代码可以看到,在 while 循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。我们试着求解一下,假设循环 x 次之后,i 就大于 n 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2^n
也就是说当循环 log2^n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(logn)

  1. 线性对数阶 O (nlogN)

线性对数阶 O (nlogN) 其实非常容易理解,将时间复杂度为 O (logn) 的代码循环 N 遍的话,那么它的时间复杂度就是 n * O (logN),也就是了 O (nlogN)。

就拿上面的代码加一点修改来举例:

1
2
3
4
5
6
7
8
for(m=1; m<n; m++)
{
i = 1;
while(i<n)
{
i = i * 2;
}
}
  1. 平方阶 O (n²)

平方阶 O (n²) 就更容易理解了,如果把 O (n) 的代码再嵌套循环一遍,它的时间复杂度就是 O (n²) 了。
举例:

1
2
3
4
5
6
7
8
for(x=1; i<=n; x++)
{
for(i=1; i<=n; i++)
{
j = i;
j++;
}
}

这段代码其实就是嵌套了 2 层 n 循环,它的时间复杂度就是 O (n*n),即 O (n²)
如果将其中一层循环的 n 改成 m,即:

1
2
3
4
5
6
7
8
for(x=1; i<=m; x++)
{
for(i=1; i<=n; i++)
{
j = i;
j++;
}
}

那它的时间复杂度就变成了 O (m*n)

  1. 立方阶 O (n³)K 次方阶 O (n^k)

参考上面的 O (n²) 去理解就好了,O (n³) 相当于三层 n 循环,其它的类似。

除此之外,其实还有 平均时间复杂度、均摊时间复杂度、最坏时间复杂度、最好时间复杂度 的分析方法,有点复杂,这里就不展开了。

# 二、空间复杂度

既然时间复杂度不是用来计算程序具体耗时的,那么我也应该明白,空间复杂度也不是用来计算程序实际占用的空间的。

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S (n) 来定义。

空间复杂度比较常用的有:O (1)、O (n)、O (n²),我们下面来看看:

  1. 空间复杂度 O (1)

如果算法执行所需要的临时空间不随着某个变量 n 的大小而变化,即此算法空间复杂度为一个常量,可表示为 O (1)
举例:

1
2
3
4
5
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;

代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S (n) = O (1)

  1. 空间复杂度 O (n)

我们先看一个代码:

1
2
3
4
5
6
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
j = i;
j++;
}

这段代码中,第一行 new 了一个数组出来,这个数据占用的大小为 n,这段代码的 2-6 行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S (n) = O (n)

以上,就是对算法的时间复杂度与空间复杂度基础的分析,欢迎大家一起交流。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【MySQL】MySQL主从开关机顺序

InterviewCoder

MySQL 主从开关机顺序
停应用 -> 停数据库(先备后主) -> 改配置 -> 启数据库(先主后备)-> 启应用

关闭 MySQL 从库
a. 先查看当前的主从同步状态 show slave statusG; 看是否双 yes
b. 执行 stop slave
c. 停止从库服务 mysqladmin shutdown -u 用户名 -p 密码
d. 查看是否还有 mysql 的进程 ps -ef | grep mysql
d. 如果部署了多个实例,那每个实例都要按照以上步骤来操作

关闭 MySQL 主库
a. 停止主库服务 mysqladmin shutdown -u 用户名 -p 密码
b. 查看是否还有 mysql 的进程 ps -ef | grep mysql

启动 MySQL 主库
a. 启动主库服务 mysqladmin start -u 用户名 -p 密码
b. 查看 mysql 的进程 ps -ef | grep mysql

启动 MySQL 从库
a. 启动从库服务 mysqladmin start -u 用户名 -p 密码
b. 启动复制 start slave;
c. 检查同步状态 show slave statusG; 是否双 yes
d. 查看 mysql 的进程 ps -ef | grep mysql

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【算法】使用Java实现斐波那契数列的三种方法,太酷啦

InterviewCoder

# 【算法】使用 Java 实现斐波那契数列的三种方法,太酷啦

# Java 实现斐波那契数列的三种方法

什么是斐波那契数列

  • 这里借用一下度娘的一段话:斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多・斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为 “兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……
    其规律很明显,从第 3 个数开始,每个数都等于它前两个数的和。
    那么通过 java 可以如何实现斐波那契数列呢?这里介绍三种方法。
  • 通过代码实现以下效果:当你输入 n 时,会获取斐波那契数列的第 n 个数的值。

# 1.for 循环实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static int fibonacci(int n) {
if (n <= 1) {
return n;
}

int[] fib = new int[n+1];
fib[0] = 0;
fib[1] = 1;

for (int i = 2; i <= n; i++) {
fib[i] = fib[i-1] + fib[i-2];
}

return fib[n];
}

# 2. 平方根实现

1
2
3
4
public static int fibonacci(int n) {
double goldenRatio = (1 + Math.sqrt(5)) / 2;
return (int) Math.round(Math.pow(goldenRatio, n) / Math.sqrt(5));
}

# 3. 矩阵快速幂实现

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
public static int fibonacci(int n) {
if (n <= 1) {
return n;
}

int[][] base = {{1, 1}, {1, 0}};
int[][] result = pow(base, n - 1);

return result[0][0];
}

private static int[][] pow(int[][] base, int power) {
int[][] result = new int[base.length][base.length];
for (int i = 0; i < base.length; i++) {
result[i][i] = 1;
}

while (power > 0) {
if (power % 2 == 1) {
result = multiply(result, base);
}
base = multiply(base, base);
power /= 2;
}

return result;
}

private static int[][] multiply(int[][] a, int[][] b) {
int[][] c = new int[a.length][b[0].length];
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < b[0].length; j++) {
for (int k = 0; k < b.length; k++) {
c[i][j] += a[i][k] * b[k][j];
}
}
}
return c;
}

OK,到这里 java 实现斐波那契数列的三种写法就全部写完了,如果大家还有其他方法,欢迎交流~

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【算法】斐波那契数列的概念

InterviewCoder

# 【算法】斐波那契数列的概念

# 斐波那契数列的概念

斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多・斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为 “兔子数列”。

斐波那契数列指的是这样一个数列:

​ 0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711……

​ 它的规律是:这个数列从第 3 项开始,每一项都等于前两项之和。

​ 在数学上,斐波那契数列以如下被以递推的方法定义:F*(0)=0,*F*(1)=1, *F*(n)=*F*(n - 1)+*F*(n - 2)(*n* ≥ 2,*n* ∈ N*),显然,斐波那契数列是一个线性递推数列 *。


# 斐波那契数列的实现

​ 常用的实现斐波那契数列的方法分为两大类:递归和循环。

# 1. 递归实现

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int F(int n) //斐波那契数列函数 递归形式
{
if(n == 0) //初始化
return 0;
if(n == 1 || n == 2)
return 1;
return F(n-1) + F(n-2); //如果n != 1 && n != 2 进行递归运算
}

int main()
{
int t,n;
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
printf("%d\n", F(n));
}
return 0;
}

# 2. 迭代实现(优先使用

# 6091: 斐波那契数列

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

#include <stdio.h>
int fibonacci(int n) //定义斐波那契函数
{
if(n == 0) //定义初始值
return 0;
if(n == 1 || n == 2)
return 1;
int a=1,b=1,c=0; //定义初始值
//用一个for循环,a、b分别为前两项,c为前两项之和,得到c后进行交换更新a、b的值,进行n次交换即可。
for(int i=3;i<=n;i++) //更新操作
{
c = a+b;
a = b;
b = c;
}
return c; //c即为结果输出
}

int main()
{
int t,n;
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
printf("%d\n", fibonacci(n));
}
return 0;
}

​ 递归和迭代的改进算法可以参考 ** 微 光的斐波那契数列 **。

​ 因为递归算法存在着大量的重复计算,在 N 趋近于较大值时,可能会造成内存溢出或超时的情况,又因为使用迭代算法的情况下同样可以实现计算斐波那契数列第 N 项的功能,所以在 N 值很大时我们优先使用迭代算法。


# 斐波那契数列的应用

  • 10 个连续的斐波那契数的和 = 第 7 个数的 11 倍
  • 前 n 项和 = 第 n + 2 项 - 第 2 项
  • 从第 2 项开始,第 2n - 1 项的平方比 2n * (2n - 2) 多 1;第 2n 项的平方比 2n * (2n - 2) 少 1。

# 1**. 黄金分割 **

** 黄金分割:** 把任一线段分割成两段,使 大段 / 全段 = 小段 / 大段, 比值经过计算之后,就是黄金分割比。

img

​ 斐波那契数列:随着数列项数的增加,前一项与后一项之比越来越逼近 ** 黄金分割 * 的数值 0.6180339887……***

​ 卢卡斯数列:斐波那契数列的推广形式,卢卡斯数列的形式为:1, 3, 4, 7, 11, 18,29,47…… 卢卡斯数列的相邻两项比值的极限恰好也是二分之根号五减一,即黄金分割比。所以说,卢卡斯抓住了斐波那契数列的本质。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
int main()
{
double f[100];
f[1]=1, f[2]=1;
for(int i=3; i<=50; i++)
{
f[i]=f[i-1]+f[i-2];
}
int n;
scanf("%d",&n);
printf("%.10lf\n",f[n]/f[n+1]);
}

# 2. 杨辉三角

img

​ 如图所示作 45 度斜线,之后做直线的平行线,将每条直线所经过的数,即得之和即为斐波那契数列


# 3. 兔子数列

* 斐波那契数列又因数学家 * 列昂纳多・斐波那契 * 以兔子繁殖为例子而引入,故又称为 “* 兔子数列 *”。***

​ **** 兔子数列是指:**** 一般而言,兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子来。如果所有兔子都不死,那么一年以后可以繁殖多少对兔子?

​ 很容易发现 “兔子数列” 问题和斐波那契数列问题相同,都是一样的解法。

# 1376: 母牛的故事

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

#include<stdio.h>
void DataInit(int *a)
{
for(int i = 4; i < 55; i++)
a[i] = a[i-1] + a[i-3];
}
int main()
{
int a[55] = {0, 1, 2, 3};
int n;
DataInit(a);
while(scanf("%d", &n)!=EOF)
if(n)
printf("%ld\n", a[n]);
else break;
return 0;
}

# 4. 排列组合

​ 有一段楼梯,有 10 级台阶,规定每一步只能跨一级或两级,要登上第 10 级台阶有几种不同的走法?

​ 这也是一个斐波那契数列:

​ 登第一级台阶,有一种走法,0->1;

​ 登第二级台阶,有两种走法,0->1->2,0->2;

​ 登第三级台阶,有三种走法,0->1->2->3,0->1->3,0->2->3;

​ 登第四级台阶,有五种走法,0->1->2->3->4,0->1->2->4,0->1->3->4,0->2->3->4,0->2->4;

​ …

​ 即 1, 2, 3, 5, 8, 13,… 到 10 级,就是 89 种走法,与斐波那契数列的规律契合

​ 类似的斐波那契数列的规律运用还有很多。

​ (1, 1, 2, 3, 5, 8, 13, 21, 33, 54, 89…)

# 5. 矩形面积

右下图可知,斐波那契数列与矩形面积的生成相关。

img

​ 不难发现一个规律,即 ** 生成的矩形中,所有小正方形的面积之和等于大矩形的面积。** 即: img

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Flutter】flutter发展前景如何?

InterviewCoder

img

Flutter 刚刚从 Google 刚刚推向 Android 市场的时候,我就开始对 Flutter 开始了学习之路;但由于当时 Flutter 许多东西尚未完善而没有推出稳定的版本,所以也就没有对其进行深入的学习,直到如今 Flutter重出江湖,在市场上也得到了蓬勃发展及许多业内大佬力推,我便又再次入坑 Flutter

实现 UI 和交互高级开发者必备技能,也是掌握 Flutter 开发重点;同样 Flutter 跨平台特性原生不能比拟的,更何况还有不弱的性能表现;而性能往往是由生命周期来决定的

# 何为 Flutter 的生命周期?

如果你是一名开发人员,那么你一定不会对生命周期感到陌生;当你在学习 Flutter 的时候,Flutter 也有自己的生命周期,只有通过了解 Flutter生命周期,才能知道应该在哪里来写业务逻辑

# Flutter 生命周期

img

如上图所示,Flutter 生命周期大体上可以分为三个阶段: 初始化、状态变化、销毁;下面依次说明各个阶段的工作

初始化阶段(插入渲染树

  • 对应执行构造方法和 initState

状态变化阶段(在渲染树中存在)

  • 开新的 widget 或者调用 setState 方法

销毁阶段(从渲染树种移除)

如果之前你对 Flutter 有一点点了解的话,你会发现 Flutter 中有两个主要的 Widget: StatelessWidget(无状态)StatefulWidget(有状态)

# StatelessWidget

  • 无状态组件] 是不可变的,这意味着它们的属性不能变化,所有的值都是最终的;可以理解为将外部传入的数据转化为界面展示的内容,只会渲染一次
  • 对于无状态组件生命周期只有 build 这个过程;无状态组件的构建方法通常只在三种情况下会被调用:小组件第一次被插入树中,小组件的父组件改变其配置,以及它所依赖的 InheritedWidget 发生变化时

# StatefulWidget

  • 有状态组件持有的状态可能在 Widget 生命周期中发生变化,是定义交互逻辑和业务逻辑;可以理解为具有动态可交互的内容界面,会根据数据的变化进行多次渲染

# 实现一个 StatefulWidget 至少需要两个类:

一个是 StatefulWidget 类 另一个是 Sate 类

  • StatefulWidget 类本身是不可变的,但是 State 类Widget 生命周期中始终存在
  • StatefulWidget 将其可变状态存储在由 createState 方法创建的 State 对象中,或者存储在该 State 订阅的对象中

# Fultter 的优势在哪里?

# 快速开发和迭代

Flutter 自身具有热修复(热重载的功能,尽管有使用的限制,但是它依然能够为开发过程提供更高的效率;另外,Flutter SDK 还允许我们修复崩溃和继续从应用程序停止的地方进行调试

# 页面流畅、样式美观

对于不同的平台(Android 和 iOS)Flutter 提供了风格不同控件,以满足不同平台设计理念

# 提供原生性能

Flutter 提供了一种 ** 响应式视图,无须 JavaScript桥接 **;强大的 API 使得实现复杂的页面效果成为可能;高性能的 ** 渲染机制 ** 使得 120 FPS 的高频率 可以轻而易举的实现;当界面上的图片数量越来越多时,与 React Native 相比,Flutter 的优势会越来越明显

# 灵活的跨平台开发

Flutter 可以单独作为开发框架完成整个 App 的开发,也可以与现有原生代码相结合实现 Hybrid 混合模式的开发

# 那 Flutter 需要学吗?

Flutter 抛弃了原生系统控件Webview,使用自研高性能渲染引擎来绘制 Widget,预先 (AOT) 编译,运行时直接执行 Native (arm) 代码Dart 代码执行 (在 UI TaskRunner),图片下载 (IO TaskRunner),真正的渲染 (GPU TaskRunner) ,同平台的通信等 (Platform TaskRunner 即 Native 概念下的 ** 主线程) 是互相隔离 ** 的

针对布局等的化;布局计算时单次树走动即可完成;Relayout Boundary 机制:如果 Childsize固定的,那么不会因为 ChildRelayout 导致 Parent ReLayout布局优化,都让 Flutter 脱颖而出

如上所述 Flutter 于谷歌而言,这是他们重新整理 跨平台生态环境 决心的体现,Flutter 所展现的内容,也是谷歌想拓展和维护的方向;对于长期苦恼于 跨平台 选择的广大 Android 开发者 而言,Flutter 可谓是谷歌为我们提供的 指路明灯

目前开发速度,只要不出大的纰漏按部就班往前推进,在不久的将来Google 一定可以把 Flutter 平台打造得非常完美,届时又会改变 ** 移动开发技术格局 ** 了

也许,Flutter 系列的部分库还没成熟到成为你工作的第一选择,但是,深入学习 Flutter 组件会为你日常的开发带来一些想法

总的来说,Flutter 对广大开发者而言是 利远远大于弊的

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Flutter 流畅度优化组件 Keframe

InterviewCoder

最近在开发一款 APP,核心场景类似于帖子这类的,所以下拉加载页面的流畅度成为最棘手的问题,在掘金上看见了这篇文章: https://juejin.cn/post/6979781997568884766

Nayuta 的 Keframe 组件正好可以满足帧率优化的问题

# 项目依赖:

pubspec.yaml 中添加 keframe 依赖

1
2
dependencies:
keframe: version

组件仅区分非空安全与空安全版本

非空安全使用: 1.0.2

空安全版本使用: 2.0.2

github 地址:github.com/LianjiaTech…

pub 查看:pub.dev/packages/ke…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#SizeCacheWidget用来包裹最外层的list
#FrameSeparateWidget用来包裹list的子集即可
example:
SizeCacheWidget(
child:ListView || CustomScrollView(
slivers[
SliverList(
delegate: SliverChildBuilderDelegate
(BuildContext context, int index){
return FrameSeparateWidget(
child: ···
)
}
)
]
)
)

# 快速上手:

如下图所示

image.png

假如现在页面由 A、B、C、D 四部分组成,每部分耗时 10ms,在页面时构建为 40ms。使用分帧组件 FrameSeparateWidget 嵌套每一个部分。页面构建时会在第一帧渲染简单的占位,在后续四帧内分别渲染 A、B、C、D。

对于列表,在每一个 item 中嵌套 FrameSeparateWidget ,并将 ListView 嵌套在 SizeCacheWidget 内即可。

image.png


# 构造函数说明

FrameSeparateWidget :分帧组件,将嵌套的 widget 单独一帧渲染

类型 参数名 是否必填 含义
Key key
int index 分帧组件 id,使用 SizeCacheWidget 的场景必传,SizeCacheWidget 中维护了 index 对应的 Size 信息
Widget child 实际需要渲染的 widget
Widget placeHolder 占位 widget,尽量设置简单的占位,不传默认是 Container ()

SizeCacheWidget:缓存子节点中,分帧组件嵌套的实际 widget 的尺寸信息

类型 参数名 是否必填 含义
Key key
Widget child 子节点中如果包含分帧组件,则缓存实际的 widget 尺寸
int estimateCount 预估屏幕上子节点的数量,提高快速滚动时的响应速度

# 方案设计与分析:

卡顿的本质,就是 单帧的绘制时间过长。基于此自然衍生出两种思路解决:

1、减少一帧的绘制耗时,因为导致耗时过长的原因有很多,比如不合理的刷新,或者绘制时间过长,都有可能,需要具体问题具体分析,后面我会分享一些我的优化经验。

2、在不对耗时优化下,将一帧的任务拆分到多帧内,保证每一帧都不超时。这也是本组件的设计思路,分帧渲染。

如下图所示:

image.png

原理并不复杂,问题在于如何在 Flutter 中实践这一机制。

因为涉及到帧与系统的调度,自然联想到看 SchedulerBinding 中有无现成的 API。

发现了 scheduleTask 方法,这是系统提供的一个执行任务的方法,但这个方法存在两个问题:

  • 1、其中的渲染任务是优先级进行堆排序,而堆排序是不稳定排序,这会导致任务的执行顺序并非 FIFO。从效果上来看,就是列表不会按照顺序渲染,而是会出现跳动渲染的情况
  • 2、这个方法本身存在调度问题,我已经提交 issue 与 pr,不过一直卡在单元测试上,如果感兴趣可以以在这里交流谈论。

fix: Tasks scheduled through ‘SchedulerBinding.instance.scheduleTask’… #82781

最终,参考这个设计结合 endOfFrame 方法的使用,完成了分帧队列。整个渲染流程变为下图所示:

image.png

对于列表构建场景来说,假设屏幕上能显示五个 item。首先在第一帧的时候,列表会渲染 5 个占位的 Widget,同时添加 5 个高优先级任务到队列中,这里的任务可以是简单的将占位 Widget 和实际 item 进行替换,也可通过渐变等动画提升体验。在后续的五帧中占位 Widget 依次被替换成实际的列表 item。

ListView 流畅度翻倍!!Flutter 卡顿分析和通用优化方案 这篇文章中有更加详细的分析。

# 一些展示效果(Example 说明请查看 Github

卡顿的页面往往都是由多个复杂 widget 同时渲染导致。通过为复杂的 widget 嵌套分帧组件 FrameSeparateWidget 。渲染时,分帧组件会在第一帧同时渲染多个 palceHolder ,之后连续的多帧内依次渲染复杂子项,以此提升页面流畅度。

例如 example 中的优化前示例:

1
2
3
4
5
6
7
8
ListView.builder(
itemCount: childCount,
itemBuilder: (c, i) => CellWidget(
color: i % 2 == 0 ? Colors.red : Colors.blue,
index: i,
),
)
复制代码

其中 CellWidget 高度为 60,内部嵌套了三个 TextField 的组件(整体构建耗时在 9ms 左右)。

优化仅需为每一个 item 嵌套分帧组件,并为其设置 placeHolder (placeHolder 尽量简单,样式与实际 item 接近即可)。

在列表情况下,给 ListView 嵌套 SizeCacheWidget ,同时建议将预加载范围 cacheExtent 设置大一点,例如 500(该属性默认为 250),提升慢速滑动时候的体验。

Screenrecording_20210611_194905.gif (占位与实际列表项不一致时,首次渲染抖动,二次渲染正常)

此外,也可以给 item 嵌套透明度 / 位移等动画,优化视觉上的效果。

效果如下图:

Screenrecording_20210315_133310.gif Screenrecording_20210315_133848.gif

# 分帧的成本

当然分帧方案也非十全十美,在我看来主要有两点成本:

1、额外的构建开销:整个构建过程的构建消耗由「n * widget 消耗 」变成了「n *( widget + 占位)消耗 + 系统调度 n 帧消耗」。可以看出,额外的开销主要由占位的复杂度决定。如果占位只是简单的 Container,测试后发现整体构建耗时大概提升在 15 % 左右。这种额外开销对于当下的移动设备而言,成本几乎可以不计。

2、视觉上的变化:如同上面的演示,组件会将 item 分帧渲染,页面在视觉上出现占位变成实际 widget 的过程。但其实由于列表存在缓存区域(建议将缓存区调大),在高端机或正常滑动情况下用户并无感知。而在中低端设备上快速滑动能感觉到切换的过程,但比严重顿挫要好。


# 优化前后对比演示

注:gif 帧率只有 20

优化前 优化后
优化前 优化后

# 最后:一点点思考

列表优化篇到此告一段落,在整个开源实践过程中,有两点感触较深:

作者:Nayuta
链接:https://juejin.cn/post/6979781997568884766

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

node&npm&cnpm&webpack&yarn安装教程

InterviewCoder

# Node.js

​ nodejs 是一个让 JavaScript 运行在服务端的开发平台,它让 JavaScript 成为与 PHP、Python、Perl、Ruby 等服务端语言平起平坐的脚本语言

​ nodejs 能做 web 开发,REST 开发,小程序开发等等,它就是使用 JavaScript 进行开发的,也就是说,基本上每个 web 开发的人员都可以比较轻松的转到 nodejs 平台,nodejs 就像是 JavaScript 抛弃 window,document 等这些 dom 对象后的东西的一个封装

nodejs 下载地址:https://nodejs.org/en/

# Node.js 安装:

​ 1 . 首先要查看本机是否已安装 nodeJs,打开命令提示符,输入 node&node -v 查看。

​ 2 . 打开 Node.js Setup 文件执行安装 选择安装路径,全部下一步。

​ 3 . 安装完毕之后,重新打开一个命令提示符窗口,出入 node -v

现在,我们来 Hello World 一下,开启命令行窗口,输入 node,进入 node 的命令行,我们可以输入 console.log (“hello world”)img

1
2
3
或者,我们可以创建一个web服务,进入node命令行后,输入
View Code
回车后,在浏览器输入:http://localhost:8000就输出了hello world

# npm:

# npm 安装:

Nodejs 下的包管理器。

下载 Node 后自带一个旧版本的 npm。


# cnpm:

Nodejs 下的包管理器,国内淘宝镜像版本。

# cnpm 安装:

首先要有 node 环境和 npm 环境–

1、安装 cnpm,输入以下命令:

1
npm install -g cnpm --registry=https://registry.npm.taobao.org

如图所示:

img

2、输入 cnpm -v ,检测是否正常

img


# webpack:

它主要的用途是通过 CommonJS 的语法把所有浏览器端需要发布的静态资源做相应的准备,比如资源的合并和打包。

# webpack 安装:

首先需要安装好 nodeJs&npm 的环境~

1
2
node -v   // 查看node的版本 不能太高
npm -v //查看npm的版本

全局安装:

打开命令行(win+R 输入 cmd)

img

1
2
3
npm install webpack webpack-cli -g --save-dev #输入并执行下载webpack 
-g #全局安装
--save-dev #信息写入package.json的devDependencies中

img

下载好后在命令行执行 webpack -v 查看 webpack 的版本号,正常显示说明安装好了


# yarn:

Yarn 是 facebook 发布的一款取代 npm 的包管理工具。
​ Yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快,在执行代码之前,Yarn 会通过算法校验每个安装包的完整性,使用详细、简洁的锁文件格式和明确的安装算法,Yarn 能够保证在不同系统上无差异的工作。

# yarn 安装:

首先需要 node.js & npm 环境
输入命令: npm install -g yarn
查看版本:yarn -V & --version

1
2
3
Yarn 淘宝源安装,分别复制粘贴以下代码行到黑窗口运行即可
yarn config set registry https://registry.npm.taobao.org -g
yarn config set sass_binary_site http://cdn.npm.taobao.org/dist/node-sass -g

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
yarn的常用命令:
安装yarn:npm install -g yarn
安装成功后,查看版本号:yarn --version
创建文件夹 :md yarn
进入yarn文件夹:cd yarn
初始化项目:yarn init // 同npm init,执行输入信息后,会生成package.json文件

yarn的配置项:
yarn config list // 显示所有配置项
yarn config get <key> //显示某配置项
yarn config delete <key> //删除某配置项
yarn config set <key> <value> [-g|--global] //设置配置项

安装包:
yarn install //安装package.json里所有包,并将包及它的所有依赖项保存进yarn.lock
yarn install --flat //安装一个包的单一版本
yarn install --force //强制重新下载所有包
yarn install --production //只安装dependencies里的包
yarn install --no-lockfile //不读取或生成yarn.lock
yarn install --pure-lockfile //不生成yarn.lock

添加包(会更新package.json和yarn.lock):
yarn add [package] // 在当前的项目中添加一个依赖包,会自动更新到package.json和yarn.lock文件中
yarn add [package]@[version] // 安装指定版本,这里指的是主要版本,如果需要精确到小版本,使用-E参数
yarn add [package]@[tag] // 安装某个tag(比如beta,next或者latest)
//不指定依赖类型默认安装到dependencies里,你也可以指定依赖类型:

yarn add --dev/-D // 加到 devDependencies
yarn add --peer/-P // 加到 peerDependencies
yarn add --optional/-O // 加到 optionalDependencies
//默认安装包的主要版本里的最新版本,下面两个命令可以指定版本:

yarn add --exact/-E // 安装包的精确版本。例如yarn add foo@1.2.3会接受1.9.1版,但是yarn add foo@1.2.3 --exact只会接受1.2.3版
yarn add --tilde/-T // 安装包的次要版本里的最新版。例如yarn add foo@1.2.3 --tilde会接受1.2.9,但不接受1.3.0

发布包
yarn publish

移除一个包
yarn remove <packageName>:移除一个包,会自动更新package.json和yarn.lock

更新一个依赖
yarn upgrade 用于更新包到基于规范范围的最新版本

运行脚本
yarn run 用来执行在 package.json 中 scripts 属性下定义的脚本

显示某个包的信息
yarn info <packageName> 可以用来查看某个模块的最新版本信息

缓存
yarn cache
yarn cache list # 列出已缓存的每个包 yarn cache dir # 返回 全局缓存位置 yarn cache clean # 清除缓存

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

CAS-ABA问题

InterviewCoder

# CAS-ABA 问题

# 一。概述:

​ ABA 问题是在多线程并发的情况下,发生的一种现象。上一次记录了有关 CAS 操作的一些知识,CAS 通过比较内存中的一个数据是否是预期值,如果是就将它修改成新值,如果不是则进行自旋,重复比较的操作,直到某一刻内存值等于预期值再进行修改。而 ABA 问题则是在 CAS 操作中存在的一个经典问题,这个问题某些时候不会带来任何影响,某些时候却是影响很大的。

# 二。什么是 ABA 问题?

# 理解一:

​ 当执行 campare and swap 会出现失败的情况。例如,一个线程先读取共享内存数据值 A,随后因某种原因,线程暂时挂起,同时另一个线程临时将共享内存数据值先改为 B,随后又改回为 A。随后挂起线程恢复,并通过 CAS 比较,最终比较结果将会无变化。这样会通过检查,这就是 ABA 问题。 在 CAS 比较前会读取原始数据,随后进行原子 CAS 操作。这个间隙之间由于并发操作,最终可能会带来问题。

# 理解二:

“ABA” 问题:假设 t1 线程工作时间为 10 秒,t2 线程工作时间为 2 秒,那么可能在 A 的工作期间,主内存中的共享变量 A 已经被 t2 线程修改了多次,只是恰好最后一次修改的值是该变量的初始值,虽然用 CAS 判定出来的结果是期望值,但是却不是原来那个了 =======》“狸猫换太子”
相当于是只关心共享变量的起始值和结束值,而不关心过程中共享变量是否被其他线程动过。
有些业务可能不需要关心中间过程,只要前后值一样就行,但是有些业务需求要求变量在中间过程不能被修改。

只靠 CAS 无法保证 ABA 问题,需要使用 “原子引用” 才能解决!!!!

# 三.ABA 问题的解决:

# 原子引用:(存在 ABA 问题)

案例:

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
 package InterviewTest;

import java.util.concurrent.atomic.AtomicReference;

class User{
String name;
int age;

public User(String name,int age) {
this.name=name;
this.age=age;
}

@Override
public String toString() {
return "User [name=" + name + ", age=" + age + "]";
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User z3 = new User("z3",25);
User li4 = new User("li4",25);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(z3);
System.out.println(atomicReference);
System.out.println(atomicReference.compareAndSet(z3, li4)+
" "+atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(li4, z3)+
" "+atomicReference.get().toString());
}
}

# 带版本号的原子引用(解决 ABA 问题)

AtomicStampedReference 版本号原子引用:
案例:两种原子引用的对比

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105

package InterviewTest;

import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo {


static AtomicReference<Integer> atomicReference
= new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference
= new AtomicStampedReference<>(100,1);


public static void main(String[] args) {

System.out.println("************以下是ABA问题的产生**************");
new Thread(()->{
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
},"t1").start();

new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(atomicReference.compareAndSet(100, 2019)
+" "+atomicReference.get());
},"t2").start();


try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("************以下是ABA问题的解决**************");

new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()
+" "+" 第一次版本号:"+stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

atomicStampedReference.compareAndSet(100,
101,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()
+" "+" 第2次版本号:"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,
100,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()
+" "+" 第3次版本号:"+atomicStampedReference.getStamp());

},"t3").start();

new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()
+" "+" 第一次版本号:"+stamp);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}

boolean result = atomicStampedReference.compareAndSet(
100,
2019,
stamp,
stamp+1);
System.out.println(Thread.currentThread().getName()+
" 修改成功否:"+result+" 当前最新实际版本号:"
+atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName()+
" 当前实际最新值:"
+atomicStampedReference.getReference());

},"t4").start();
}

}

输出:
************以下是ABA问题的产生**************
true 2019
************以下是ABA问题的解决**************
t3 第一次版本号:1
t4 第一次版本号:1
t3 第2次版本号:2
t3 第3次版本号:3
t4 修改成功否:false 当前最新实际版本号:3
t4 当前实际最新值:100

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Vue】vue-json-editor json编辑器.md

InterviewCoder

# 【Vue】vue-json-editor json 编辑器.md

# 一、概述

现有一个 vue 项目,需要一个 json 编辑器,能够格式化 json 数据,同时也支持编辑功能。

vue-json-editor 插件就可以实现这个功能

# 二、vue-json-editor 使用

# 安装插件

1
npm install vue-json-editor --save

# 使用

test.vue

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

<template>
<div style="width: 70%;margin-left: 30px;margin-top: 30px;">
<vue-json-editor
v-model="resultInfo"
:showBtns="false"
:mode="'code'"

@json-change="onJsonChange"
@json-save="onJsonSave"
@has-error="onError"
/>
<br>
<el-button type="primary" @click="checkJson">确定</el-button>
</div>
</template>

<script>
// 导入模块
import vueJsonEditor from 'vue-json-editor'

export default {
// 注册组件
components: { vueJsonEditor },
data() {
return {
hasJsonFlag:true, // json是否验证通过
// json数据
resultInfo: {
'employees': [
{
'firstName': 'Bill',
'lastName': 'Gates'
},
{
'firstName': 'George',
'lastName': 'Bush'
},
{
'firstName': 'Thomas',
'lastName': 'Carter'
}
]
}
}
},
mounted: function() {
},
methods: {
onJsonChange (value) {
// console.log('更改value:', value);
// 实时保存
this.onJsonSave(value)
},
onJsonSave (value) {
// console.log('保存value:', value);
this.resultInfo = value
this.hasJsonFlag = true
},
onError(value) {
// console.log("json错误了value:", value);
this.hasJsonFlag = false
},
// 检查json
checkJson(){
if (this.hasJsonFlag == false){
// console.log("json验证失败")
// this.$message.error("json验证失败")
alert("json验证失败")
return false
} else {
// console.log("json验证成功")
// this.$message.success("json验证成功")
alert("json验证成功")
return true
}
},
}
}
</script>

<style>
</style>

插件参数说明:

1
2
3
4
5
6
7
8
9
<vue-json-editor
v-model="resultInfo" // 绑定数据resultInfo
:showBtns="false" // 是否显示保存按钮
:mode="'code'" // 默认编辑模式
// 显示中文默认英文
@json-change="onJsonChange" // 数据改变事件
@json-save="onJsonSave" // 数据保存事件
@has-error="onError" // 数据错误事件
/>

相关说明:

resultInfo 默认绑定的变量,这个变量可以为空,编辑器会显示为 {}

:showBtns 这里不显示保存按钮,为什么呢?原因有 2 个。1. 默认样式不好看。2. 只能当 json 数据正确,才能点击保存按钮,否则禁止点击。

json-change,json-save,has-error 这 3 个事件,是会实时触发的。

这里我额外加了一个检测方法,用来判断 json 数据是否正确。默认标记为 true,当不正确时,会改变状态为 false。

# 访问

点击确定,提示成功

img

改为错误的,点击确定,会提示失败。

img

注意:这个 json 编辑会带有下来菜单,实际项目中,需要去除,比较用户误操作。

在实际使用中发现几个问题:

\1. 输入中文时,传给后端的值不多

\2. 输入大量 json 时,会有部分数据丢失。

因此,我们使用下面的编辑器 bin-code-editor

# 三、bin-code-editor

# 安装模块

1
npm install bin-code-editor -d

# 引入

在 main.js 中写入 2 行

1
2
import CodeEditor from 'bin-code-editor';
Vue.use(CodeEditor);

test.vue

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
<template>
<div style="width: 70%;margin-left: 30px;margin-top: 30px;">
<b-code-editor v-model="jsonStr" :auto-format="true" :smart-indent="true" theme="dracula" :indent-unit="4" :line-wrap="false" ref="editor"></b-code-editor>
<br>
<el-button type="primary" @click="onSubumit">提交</el-button>
</div>
</template>

<script>
const jsonData =`{
"employees": [{
"firstName": "Bill",
"lastName": "Gates"
}, {
"firstName": "George",
"lastName": "Bush"
}, {
"firstName": "Thomas",
"lastName": "Carter"
}]
}`
export default {
data() {
return {
jsonStr:jsonData
}
},
methods: {
// 检测json格式
isJSON(str) {
if (typeof str == 'string') {
try {
var obj=JSON.parse(str);
if(typeof obj == 'object' && obj ){
return true;
}else{
return false;
}

} catch(e) {
return false;
}
}else if (typeof str == 'object' && str) {
return true;
}
},
onSubumit(){
if (!this.isJSON(this.jsonStr)){
this.$message.error(`json格式错误`)
return false
}
this.$message.success('json格式正确')
}
}
}
</script>

<style>

</style>

访问测试页面,效果如下:

img

输入错误的值,点击执行,会有提示

img

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!