普通视图

发现新文章,点击刷新页面。
昨天以前Cubik的小站

大 O 表示法

作者 Cubik65536
2024年4月25日 04:06

在本文中,我们将简单了解大 $O$ 表示法。

什么是大 $O$ 表示法?

大 $O$ 表示法是一种用于描述算法的效率的标准符号。它描述的是算法的时间复杂度(Time Complexity),即算法执行所需的时间,以及这个时间如何随着输入规模的增加而变化。大 $O$ 表示法可以表示算法的最优、平均和最差情况下的时间复杂度。

大部分情况下,我们关注的是算法时间复杂度函数的上界,即最坏情况下的时间复杂度。这是因为在实际应用中,我们通常更关心算法在最坏情况下的性能表现。

$O$ 这个字母原本的定义为“Order of”(阶数),使用拉丁语中的大写字母 $Omicron$ 表示,但由于字型相同,也可以理解为拉丁字母大写 “O”.

由于运行代码的计算机的性能不同,所以我们通常不关心具体的运行时间(毫秒、秒等),而是关心算法的时间复杂度(同样的时间复杂度在同样的计算机上需要的运行时间一样),大 $O$ 表示法的参数即为时间复杂度,以及其如何随着输入规模 $n$ 的增加而变化。你通常也可以认为其表示一个循环内部的代码执行多少次才可以完成算法的执行。

恒定时间复杂度 $O(1)$

恒定时间复杂度 $O(1)$ 表示操作的执行时间与输入规模 $n$ 无关,即算法的执行时间是一个常数。这种算法的执行时间是固定的,不随着输入规模的增加而变化。这类操作通常包含:

  • 数学运算(加减乘除、取模等)
  • 直接访问数组元素
  • 存取变量
  • 逻辑判断(if-else、switch-case 等)
  • 函数返回参数

下述是一个恒定时间复杂度 $O(1)$ 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ConstantTimeComplexity {
public static void main(String[] args) {
int a = 10;
int b = 20;
int sum = a + b;
System.out.println("Sum: " + sum);

int[] arr1 = new int[]{1, 2, 3, 4, 5};
int[] arr2 = new int[]{384, 28, 238, 83, 37, 1273, 27, 382, 383, 283};
System.out.println("Element at index 2 in arr1: " + arr1[2]);
System.out.println("Element at index 5 in arr2: " + arr2[5]);
}
}

上述代码中,我们执行了一系列的操作,包括数学运算、数组元素访问、变量存取等,这些操作的执行时间是固定的,不随着输入规模的增加而变化,因此其时间复杂度是 $O(1)$。

注意!就算数组有不同的长度,访问数组元素的时间复杂度仍然是 $O(1)$,因为数组的访问是通过索引来实现的,不需要遍历整个数组。

线性时间复杂度 $O(n)$

线性时间复杂度 $O(n)$ 表示操作的执行时间与输入规模 $n$ 成正比,即算法的执行时间随着输入规模的增加而线性增长。这类操作通常包含:

  • 遍历数组、链表、树等数据结构
  • 递归操作
  • 线性搜索、线性查找等

在现实生活中,我们可以使用以下场景理解:如果你需要读一本书,而你每读一页都需要花费一分钟,那么读完整本书所需的时间就是书的页数(单位分钟)。这就是一个线性时间复杂度 $O(n)$ 的例子。

下述是一个线性时间复杂度 $O(n)$ 的示例:

1
2
3
4
5
6
7
8
public class LinearTimeComplexity {
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (int i = 0; i < arr.length; i++) {
System.out.println("Element at index " + i + ": " + arr[i]);
}
}
}

上述代码中,我们遍历了一个长度为 $10$ 的数组,如果将数组的长度定义为 $n$,因此遍历数组的时间复杂度是 $O(n)$。同时,你应该注意到,System.out.println 方法同样运行了一共 $n$ 次。

要注意的是,线性时间复杂度 $O(n)$ 并不意味着算法的执行时间是固定的,而是随着输入规模的增加而线性增长。

另外,由于 System.out.println 方法以及其中的,获取数组元素的操作都是 $O(1)$ 的,因此整个代码的时间复杂度是 $O(n)$。

如果考虑以下线性搜索的例子:

1
2
3
4
5
6
7
8
9
10
11
12
public class LinearSearch {
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int target = 5;
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
System.out.println("Element " + target + " found at index " + i);
break;
}
}
}
}

其时间复杂度仍然是 $O(n)$,因为就算有可能要寻找的元素在数组中间就被找到了,但是在最坏情况下,要寻找的元素在数组的最后一个位置,这时候就需要遍历整个数组。

二次时间复杂度 $O(n^2)$

二次时间复杂度 $O(n^2)$ 表示操作的执行时间与输入规模 $n$ 的平方成正比,即算法的执行时间随着输入规模的增加而平方增长。这类操作通常包含:

  • 嵌套循环
  • 一些排序算法(如冒泡排序、选择排序等)

下述是一个二次时间复杂度 $O(n^2)$ 的示例:

1
2
3
4
5
6
7
8
9
10
public class QuadraticTimeComplexity {
public static void main(String[] args) {
int[] arr = new int[]{10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length; j++) {
System.out.println("Element at index " + i + " and " + j + ": " + arr[i] + " and " + arr[j]);
}
}
}
}

上述代码中,我们使用了两个嵌套循环,其中外层循环的执行次数是 $n$,内层循环的执行次数也是 $n$,因此整个代码的时间复杂度是 $O(n^2)$。

如果考虑以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
int[] arr = new int[]{10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length; j++) {
for (int k = 0; k < arr.length; k++) {
System.out.println("Element at index " + i + ", " + j + " and " + k + ": " + arr[i] + ", " + arr[j] + " and " + arr[k]);
}
}
}
}
}

其时间复杂度则成为了 $O(n^3)$,因为每层循环的执行次数都是 $n$。接下来不再赘述,你可以自行推导出 $O(n^4)$、$O(n^5)$ 等的时间复杂度。

对数时间复杂度 $O(\log n)$

对数时间复杂度 $O(\log n)$ 表示操作的执行时间与输入规模 $n$ 的对数成正比(此处的对数函数以 $2$ 为底)。二分查找(Binary Search)是一个典型的对数时间复杂度的例子。

下述是一个对数时间复杂度 $O(\log n)$ 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
public class BinarySearch {
public static void main(String[] args) {
int n = 2000;
int i = 1;
int count = 0;
while (i < n) {
i *= 2;
count++;
}
System.out.println("Count: " + count);
}
}

上述代码中,我们使用了一个循环,每次将 $i$ 乘以 $2$,直到 $i$ 大于 $n$,这个循环的执行次数是 $O(\log n)$,因为每次循环 $i$ 的值都会翻倍并以指数级增长来逼近 $n$。你可以通过运行代码来验证。如果 $n = 2000$,那么循环的执行次数是 $11$,符合计算 $log_2(2000) \approx 11.2877$。

同样的,二分查找的时间复杂度是 $O(\log n)$,因为每次查找都会将查找范围缩小一半。

线性对数时间复杂度 $O(n \log n)$

线性对数时间复杂度 $O(n \log n)$ 表示操作的执行时间与输入规模 $n$ 与 $n$ 的对数的乘积成正比。一些排序算法(如快速排序、归并排序等)的时间复杂度就是 $O(n \log n)$。

整体来讲 $O(n \log n)$ 的时间复杂度比 $O(n^2)$ 的时间复杂度要好,但是比 $O(n)$ 的时间复杂度要差。

计算斐波那契数列的时间复杂度也是 $O(n \log n)$。复习一下,一个斐波那契数列的开头是:$0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, …$,其整体定义如下:

$$
f(n) = \begin{cases}
0, & \text{if } n = 0 \newline
1, & \text{if } n = 1 \newline
f(n-1) + f(n-2), & \text{if } n > 1
\end{cases}
\text{where } n \in \mathbb{N}_0
$$

也就是说,在这个数列中,第一项是 0,第二项是 1,之后的每一项都是前两项的和。

下面是 Java 中的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 计算斐波那契数列的第 n 项
* @param n 非负整数
* @return 斐波那契数列的第 n 项
*/
private static int fibonacci(int n) {
if (n == 0) { // 如果 n 是 0,直接返回 0(递归的终止条件)
return 0;
} else if (n == 1) { // 如果 n 是 1,直接返回 1(递归的终止条件)
return 1;
} else {
return fibonacci(n - 1) + fibonacci(n - 2); // 否则,返回 f(n-1) + f(n-2),这样就可以递归地计算斐波那契数列的第 n 项
}
}

你将会发现,如果输入参数 $n$ 增加,由于递归的使用,其时间复杂度将以超过 $O(n)$ 的速度增长,但是不会超过 $O(n^2)$。

总结

大 $O$ 表示法是一种用于描述算法的效率的标准符号,它描述的是算法的时间复杂度,即算法执行所需的时间,以及这个时间如何随着输入规模的增加而变化。大部分情况下,我们关注的是算法时间复杂度函数的上界,即最坏情况下的时间复杂度。

要确定一个算法的时间复杂度,你基本只需要计算循环内部的代码执行了多少次,然后根据执行次数来确定时间复杂度。

接下来是一个常见的时间复杂度的可视化图表:

GL & HF!

二维数组、类类型数组与 ArrayList

作者 Cubik65536
2024年2月22日 11:20

本文将简单介绍 Java 中的二维数组、以类为元素类型的数组以及 ArrayList。

二维数组

在深入讨论二维数组之前,我们先来复习一下一维数组。在 Java 中,一维数组由相同类型的元素组成,一个一维数组的常用语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建一个长度为 5 的 int 类型数组
int[] arr = new int[5];
// 获取数组的长度
int len = arr.length;
// 获取数组的第一个元素
int first = arr[0];
// 修改数组的第一个元素
arr[0] = 1;

// 数组的元素类型可以是原始数据类型 (primitive data type) ,也可以是一个类 (例如 String 类)
String[] strArray = new String[5];
// 或者任何一个用户自定义的类
MyClass[] myClassArray = new MyClass[5];

// 注意,与原始数据类型数组不同,以类为元素的数组在创建时并不会自动初始化(不会给元素授予默认值,每个元素都是 null),需要手动初始化
// 该示例也同样展示了一维数组的遍历
for (int i = 0; i < myClassArray.length; i++) {
myClassArray[i] = new MyClass();
}

现在发挥一下想象力,将一维数组想象成一排柜子,每个单元里都只能放同类的一样物品。但是,对一维数组来说,每排柜子都只有一个,二维数组则为这每排柜子增加了列的概念。

对 Java 来说,一个二维数组其实就是每个元素均为一个一维数组的数组。一个二维数组的常用语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建一个 3x3 的 int 类型二维数组
int[][] arr = new int[3][3];
// 获取数组中的第二个数组中的第一个元素
int num = arr[1][0];
// 修改数组中的第二个数组中的第一个元素
arr[1][0] = 1;
// 获取整体数组的长度
int len = arr.length;
// 获取每个元素数组的长度
int len1 = arr[0].length;

// 同理,二维数组的元素类型也可以是一个类
MyClass[][] myClassArray = new MyClass[3][3];
// 一个二维数组的简单遍历
for (int i = 0; i < myClassArray.length; i++) {
for (int j = 0; j < myClassArray[i].length; j++) {
myClassArray[i][j] = new MyClass();
}
}

例题

  1. 输入一个 4x4 的 int 类型二维数组,寻找其中的最大值并输出其坐标。

    解题方法
    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 class Main {
    public static void main(String[] args) {
    // 创建一个 4x4 的 int 类型二维数组
    int[][] arr = new int[][]{
    {182, 1723, 28, 12},
    {18, 238, 229, 128},
    {283, 2891, 38, 1},
    {0, -29, 382, 29}
    };

    // 创建一个变量用于存储最大值,其初始值为 int 类型的最小值,以确保数组中的任意值都会比其大
    int max = Integer.MIN_VALUE;
    // 创建两个变量用于存储最大值的坐标
    int x = 0, y = 0;

    // 遍历二维数组
    for (int i = 0; i < arr.length; i++) {
    for (int j = 0; j < arr[i].length; j++) {
    // 如果当前元素大于 max,则将 max 更新为当前元素,并记录其坐标
    if (arr[i][j] > max) {
    max = arr[i][j];
    x = i;
    y = j;
    }
    }
    }

    // 输出最大值及其坐标
    System.out.println("max: " + max + ", x: " + x + ", y: " + y);
    }
    }
  2. 输入一个 4x4 的 int 类型二维数组,使用冒泡排序 (bubble sort) 对其进行从大到小的排序。

    解题方法(较为简单)
    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
    public class Main {
    public static void main(String[] args) {
    // 创建一个 4x4 的 int 类型二维数组
    int[][] arr = new int[][]{
    {182, 1723, 28, 12},
    {18, 238, 229, 128},
    {283, 2891, 38, 1},
    {0, -29, 382, 29}
    };

    // 使用冒泡排序对二维数组进行排序
    // 冒泡排序的算法本质决定了最多只需要 n 次遍历即可完成排序,其中 n 为数组的长度
    for (int c = 0; c < arr.length * arr[0].length; c++) { // 由于二维数组的长度为 4x4,所以这里的 n 为 16,是大数组的长度乘以小数组的长度
    // 冒泡排序核心算法:将每个元素与其后一个元素进行比较,如果前者小于后者,则交换两者的位置(我们需要从大到小排序)
    for (int i = 0; i < arr.length; i++) {
    int j;
    for (j = 0; j < arr[i].length - 1; j++) {
    // 将每个元素与其后一个元素进行比较,如果前者小于后者,则交换两者的位置
    // 注意:此处 j 只到每行的倒数第二个元素,因为如果 j 到最后一个元素,那么 j + 1 就会越界
    if (arr[i][j] < arr[i][j + 1]) {
    int temp = arr[i][j];
    arr[i][j] = arr[i][j + 1];
    arr[i][j + 1] = temp;
    }
    }
    // 但是,上面只检查到每行的倒数第二个元素肯定是不够的,我们还需要将每行的最后一个元素和下一行的第一个元素进行比较并按需交换
    if (i < 3) { // 此处的 if 确保我们不去检查最后一行的最后一个元素,它后面没有元素了
    if (arr[i][j] < arr[i + 1][0]) {
    int temp = arr[i][j];
    arr[i][j] = arr[i + 1][0];
    arr[i + 1][0] = temp;
    }
    }
    }
    }
    }
    }
    解题方法(优化版)
    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
    public class Main {
    public static void main(String[] args) {
    // 创建一个 4x4 的 int 类型二维数组
    int[][] arr = new int[][]{
    {182, 1723, 28, 12},
    {18, 238, 229, 128},
    {283, 2891, 38, 1},
    {0, -29, 382, 29}
    };

    // 使用冒泡排序对二维数组进行排序
    // 冒泡排序的算法本质决定了最多只需要 n 次遍历即可完成排序,其中 n 为数组的长度
    bool swapped = true;
    while (swapped) { // 实际上,在某些情况下,我们不需要遍历 n 次就可以完成排序,所以我们可以通过判断上一次“排序”是否有交换来提前结束排序,如果上一次没有交换,那么说明数组已经排序完成
    swapped = false; // 每次遍历开始前,我们都假设没有交换
    // 其余算法与上面的解题方法相同
    // 冒泡排序核心算法:将每个元素与其后一个元素进行比较,如果前者小于后者,则交换两者的位置(我们需要从大到小排序)
    for (int i = 0; i < arr.length; i++) {
    int j;
    for (j = 0; j < arr[i].length - 1; j++) {
    // 将每个元素与其后一个元素进行比较,如果前者小于后者,则交换两者的位置
    // 注意:此处 j 只到每行的倒数第二个元素,因为如果 j 到最后一个元素,那么 j + 1 就会越界
    if (arr[i][j] < arr[i][j + 1]) {
    int temp = arr[i][j];
    arr[i][j] = arr[i][j + 1];
    arr[i][j + 1] = temp;
    swapped = true;
    }
    }
    // 但是,上面只检查到每行的倒数第二个元素肯定是不够的,我们还需要将每行的最后一个元素和下一行的第一个元素进行比较并按需交换
    if (i < 3) { // 此处的 if 确保我们不去检查最后一行的最后一个元素,它后面没有元素了
    if (arr[i][j] < arr[i + 1][0]) {
    int temp = arr[i][j];
    arr[i][j] = arr[i + 1][0];
    arr[i + 1][0] = temp;
    swapped = true;
    }
    }
    }
    }
    }
    }

类数组

正如上面提到过的,数组的元素可以不是 int, double, char 等原始数据类型,也可以是一个类。我们可以使用这个特性来完成一些比较复杂的操作。

例题

创建一个类 Student,包含两个属性 namescore,分别为学生的姓名和分数。然后,输入一个 Student 类型的数组,对其按照分数从大到小进行排序。最后以此输出学生的姓名和分数。

解题方法
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
class Student {
// 学生类,包含两个属性 name 和 score
private String name;
private int score;

// 构造函数
public Student(String name, int score) {
this.name = name;
this.score = score;
}

// 获取学生的成绩
public int getScore() {
return score;
}
}

public class Main {
public static void main(String[] args) {
// 创建一个长度为 3 的 Student 类型数组
Student[] students = new Student[3];

for (int i = 0; i < students.length; i++) {
// 初始化学生数组的每个元素
// 此处我使用了 Student 加元素的索引来命名学生的姓名,以及一个随机数来模拟学生的分数
// 实际上,这里的初始化过程可以是任何你想要的,可以是用户输入
students[i] = new Student("Student" + i, (int)(Math.random() * 100));
}

// 使用冒泡排序对学生数组按成绩进行排序
boolean swapped = true;
while (swapped) {
swapped = false;
for (int i = 0; i < students.length - 1; i++) {
if (students[i].getScore() < students[i + 1].getScore()) {
Student temp = students[i];
students[i] = students[i + 1];
students[i + 1] = temp;
swapped = true;
}
}
}

// 输出学生的姓名和分数
for (Student student : students) {
System.out.println(student.name + ": " + student.getScore());
}
}
}

总而言之,使用类作为数组的元素类型可以让我们在处理一些复杂的数据时更加方便。以上述例题为例,如果我们分开使用两个数组来存储学生的姓名和分数,由于我们只在排序成绩,但同时需要保证姓名和分数的对应关系,相关操作会变的相当麻烦。但是,如果我们使用一个 Student 类型的数组,那么我们就可以保证姓名和分数的对应关系,从而更加方便地进行排序。

ArrayList

ArrayList 是 Java 中的一个类,它仍然是一个 Array (数组)。但是,与数组不同的是,ArrayList 的长度是可以动态变化的。在 Java 中,ArrayList 是一个泛型类(目前不要花过多时间了解泛型类是个啥),我们可以使用它来存储任何类型的数据。

示例代码:

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
import java.util.ArrayList; // 导入 ArrayList 类,你的 IDE 或许可以帮你把这件事做了。

public class Main {
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<>(); // 创建一个 String 类型的 ArrayList
strings.add("Hello"); // 使用 add 方法向 ArrayList 中添加元素,你可以直接写入元素的值,该元素会被添加到 ArrayList 的末尾
strings.add("Java");
strings.add("Programming");
strings.add("Language");
strings.add(1, "World"); // 你也可以使用 add 方法的重载版本,指定元素的索引,该元素会被添加到指定索引的位置
System.out.println(strings); // [Hello, World, Java, Programming, Language]
strings.remove(2); // 使用 remove 方法移除指定索引的元素
System.out.println(strings); // [Hello, World, Programming, Language]
strings.remove("World"); // 使用 remove 方法移除指定元素
System.out.println(strings); // [Hello, Programming, Language]

ArrayList<Integer> integers = new ArrayList<>(); // 创建一个 Integer 类型的 ArrayList
// 值得注意的是,ArrayList 的元素类型不可以是原始数据类型,所以我们需要使用包装类 (wrapper class) 来代替
// 你可以将包装类理解为只有一个对应的原始数据类型的类,例如 Integer 对应 int
integers.add(1); // 你仍然可以使用 add 方法向 ArrayList 中添加元素
integers.add(2);
integers.add(3);
integers.add(4);
integers.add(5);
integers.add(1, 6); // 你也可以使用 add 方法的重载版本,指定元素的索引,该元素会被添加到指定索引的位置
System.out.println(integers); // [1, 6, 2, 3, 4, 5]
integers.remove(2); // 使用 remove 方法移除指定索引的元素
System.out.println(integers); // [1, 6, 3, 4, 5]
integers.remove(Integer.valueOf(6)); // 使用 remove 方法移除指定元素,此处值得注意的是,你不能直接写入 “6”,因为这样会被认为是索引,而不是元素,所以你需要使用包装类的 valueOf 方法
System.out.println(integers); // [1, 3, 4, 5]
// 你可以使用 size 方法获取 ArrayList 的长度
System.out.println(integers.size()); // 4
// 虽然 ArrayList 的长度是可以动态变化的,但是如果频繁更改 ArrayList 的长度,会对性能产生一定的影响,所以你可以使用 ensureCapacity 方法来提前设置 ArrayList 的容量
// 你也可以在初始化 ArrayList 时指定其容量而不再需要单独使用 ensureCapacity 方法,例如:ArrayList<Integer> integers = new ArrayList<>(10);
integers.ensureCapacity(10);
// 但是 ensureCapacity 方法只是设置了 ArrayList 的容量,size 方法返回的长度仍然是实际元素的个数
System.out.println(integers.size()); // 4
integers.add(6);
integers.add(7);
// 当你确认 ArrayList 的长度不会再发生变化时,你可以使用 trimToSize 方法来释放多余的空间,此时 ArrayList 的容量会被设置为实际元素的个数
integers.trimToSize();
// 同样的,size 方法返回的长度仍然是实际元素的个数
System.out.println(integers.size()); // 6

integers.set(0, 10); // 你可以使用 set 方法来修改指定索引的元素,此处将索引为 0 的元素修改为 10
System.out.println(integers); // [10, 3, 4, 5, 6, 7]
// 你也可以使用 get 方法来获取指定索引的元素
System.out.println(integers.get(0)); // 10
}
}

将 ArrayList 转换为数组

有时候,我们需要将 ArrayList 转换为数组。我们有两种方法可以做到这一点:

第一种方法是创建一个相同长度的,新的数组,然后遍历 ArrayList,将其中的元素逐个添加到新数组中。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.*;

public class Main {
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<>();
strings.add("String1");
strings.add("String2");
strings.add("String3");
strings.add("String4");

// 创建一个相同长度的新数组
String[] array = new String[strings.size()];

// 遍历 ArrayList,将其中的元素逐个添加到新数组中
for (int i = 0; i < strings.size(); i++) {
array[i] = strings.get(i);
}
}
}

或者,我们可以使用 toArray 方法来完成这个任务。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.*;

public class Main {
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<>();
strings.add("String1");
strings.add("String2");
strings.add("String3");
strings.add("String4");

// 使用 toArray 方法将 ArrayList 转换为数组
// 注意,传入给 toArray 方法的参数是一个新的数组,该数组的长度为 ArrayList 的长度
String[] array = strings.toArray(new String[strings.size()]);
}
}

例题

  1. 写一个程序,创建一个长度为 10 的 String 类型的 ArrayList,然后向其中添加 10 个字符串。接着创建一个长度为 10 的 Integer 类型的 ArrayList,然后向其中添加 10 个整数。最后,将 String 类型的 ArrayList 中元素索引为偶数的元素删除掉,将 Integer 类型的 ArrayList 中元素索引为奇数的元素删除掉。

    解题方法
    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
    import java.util.*;

    public class Main {
    public static void main(String[] args) {
    ArrayList<String> strings = new ArrayList<>();
    strings.add("String0");
    strings.add("String1");
    strings.add("String2");
    strings.add("String3");
    strings.add("String4");
    strings.add("String5");
    strings.add("String6");
    strings.add("String7");
    strings.add("String8");
    strings.add("String9");

    ArrayList<Integer> integers = new ArrayList<>();
    integers.add(0);
    integers.add(1);
    integers.add(2);
    integers.add(3);
    integers.add(4);
    integers.add(5);
    integers.add(6);
    integers.add(7);
    integers.add(8);
    integers.add(9);

    // 创建新的 ArrayList 并不难,但是删除元素的时候需要注意,因为每次删除元素后,ArrayList 的长度会发生变化,需要删除的元素的索引也会发生变化,所以我们需要点小技巧
    // 接下来两个循环都使用了一些神奇的算法,如果你不太理解,可以尝试在草稿纸上画出数组,然后一步一步地模拟删除的过程,你就会发现其中的规律
    // 规律:为了删除索引为偶数的元素,我们其实正好只需要删除从 0 开始,每次 +1 的位置的元素,直至数组的长度的一半;对于奇数的情况,我们其实只需要删除从 1 开始,其他同理

    // 删除 String 类型的 ArrayList 中索引为偶数的元素
    int stringsSize = strings.size();
    for (int i = 0; i < stringsSize / 2; i++) {
    strings.remove(i);
    }

    // 删除 Integer 类型的 ArrayList 中索引为奇数的元素
    int integersSize = integers.size();
    for (int i = 1; i < integersSize / 2 + 1; i++) {
    integers.remove(i);
    }
    }
    }
  2. 写一个程序,创建一个长度为 10 的 String 类型的 ArrayList,然后向其中添加 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
    25
    26
    27
    28
    import java.util.*;

    public class Main {
    public static void main(String[] args) {
    ArrayList<String> strings = new ArrayList<>();
    strings.add("String");
    strings.add("String1");
    strings.add("String");
    strings.add("String3");
    strings.add("String");
    strings.add("String5");
    strings.add("String");
    strings.add("String7");
    strings.add("String");
    strings.add("String9");

    // 实际上,我们只需要将每个元素和它后面的所有元素进行一下比较就可以了
    // 如果后面的元素和当前元素相同,那么我们就可以删除后面的元素
    for (int i = 0; i < strings.size(); i++) {
    for (int j = i + 1; j < strings.size(); j++) {
    if (strings.get(i).equals(strings.get(j))) {
    strings.remove(j);
    j--; // 此处要注意,删除元素后,后面的元素会向前移动,所以我们需要将 j 减一,以确保被向前移动的元素也会被检查
    }
    }
    }
    }
    }

[LCTT 翻译转载] 软件开发|Git 提交是差异、快照还是历史记录?

作者 Cubik65536
2024年1月31日 03:00

原文:Do we think of git commits as diffs, snapshots, and/or histories?
首发:Git 提交是差异、快照还是历史记录? @Linux 中国
首发时间:2024-01-21 18:47 (UTC+8)
作者:Pratham Patel
译者:LCTT Cubik

Git 提交是差异、快照还是历史记录?

alt

大家好!我一直在慢慢摸索如何解释 Git 中的各个核心理念(提交、分支、远程、暂存区),而提交这个概念却出奇地棘手。

要明白 Git 提交是如何实现的对我来说相当简单(这些都是确定的!我可以直接查看!),但是要弄清楚别人是怎么看待提交的却相当困难。所以,就像我最近一直在做的那样,我在 Mastodon 上问了一些问题。

大家是怎么看待 Git 提交的?

我进行了一个 非常不科学的调查,询问大家是怎么看待 Git 提交的:是快照、差异,还是所有之前提交的列表?(当然,把它看作这三者都是合理的,但我很好奇人们的 主要 观点)。这是调查结果:

alt

结果是:

  • 51% 差异
  • 42% 快照
  • 4% 所有之前的提交的历史记录
  • 3% “其他”

我很惊讶差异和快照两个选项的比例如此接近。人们还提出了一些有趣但相互矛盾的观点,比如 “在我看来,提交是一个差异,但我认为它实际上是以快照的形式实现的” 和 “在我看来,提交是一个快照,但我认为它实际上是以差异的形式实现的”。关于提交的实际实现方式,我们稍后再详谈。

在我们进一步讨论之前:我们的说 “一个差异” 或 “一个快照” 都是什么意思?

什么是差异?

我说的“差异”可能相当明显:差异就是你在运行 git show COMMIT_ID 时得到的东西。例如,这是一个 rbspy 项目中的拼写错误修复:

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/src/ui/summary.rs b/src/ui/summary.rs
index 5c4ff9c..3ce9b3b 100644
--- a/src/ui/summary.rs
+++ b/src/ui/summary.rs
@@ -160,7 +160,7 @@ mod tests {
";

let mut buf: Vec<u8> = Vec::new();
- stats.write(&mut buf).expect("Callgrind write failed");
+ stats.write(&mut buf).expect("summary write failed");
let actual = String::from_utf8(buf).expect("summary output not utf8");
assert_eq!(actual, expected, "Unexpected summary output");
}

你可以在 GitHub 上看到它: https://github.com/rbspy/rbspy/commit/24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b

什么是快照?

我说的 “快照” 是指 “当你运行 git checkout COMMIT_ID 时得到的所有文件”。

Git 通常将提交的文件列表称为 “树”(如“目录树”),你可以在 GitHub 上看到上述提交的所有文件:

https://github.com/rbspy/rbspy/tree/24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b(它是 /tree/ 而不是 /commit/

“Git 是如何实现的”真的是正确的解释方式吗?

我最常听到的关于学习 Git 的建议大概是 “只要学会 Git 在内部是如何表示事物的,一切都会变得清晰明了”。我显然非常喜欢这种观点(如果你花了一些时间阅读这个博客,你就会知道我 喜欢 思考事物在内部是如何实现的)。

但是作为一个学习 Git 的方法,它并没有我希望的那么成功!通常我会兴奋地开始解释 “好的,所以 Git 提交是一个快照,它有一个指向它的父提交的指针,然后一个分支是一个指向提交的指针,然后……”,但是我试图帮助的人会告诉我,他们并没有真正发现这个解释有多有用,他们仍然不明白。所以我一直在考虑其他方案。

但是让我们还是先谈谈内部实现吧。

Git 是如何在内部表示提交的 —— 快照

在内部,Git 将提交表示为快照(它存储每个文件当前版本的 “树”)。我在 在一个 Git 仓库中,你的文件在哪里? 中写过这个,但下面是一个非常快速的内部格式概述。

这是一个提交的表示方式:

1
2
3
4
5
6
7
$ git cat-file -p 24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b
tree e197a79bef523842c91ee06fa19a51446975ec35
parent 26707359cdf0c2db66eb1216bf7ff00eac782f65
author Adam Jensen <adam@acj.sh> 1672104452 -0500
committer Adam Jensen <adam@acj.sh> 1672104890 -0500

Fix typo in expectation message

以及,当我们查看这个树对象时,我们会看到这个提交中仓库根目录下每个文件/子目录的列表:

1
2
3
4
5
6
7
8
9
$ git cat-file -p e197a79bef523842c91ee06fa19a51446975ec35
040000 tree 2fcc102acd27df8f24ddc3867b6756ac554b33ef .cargo
040000 tree 7714769e97c483edb052ea14e7500735c04713eb .github
100644 blob ebb410eb8266a8d6fbde8a9ffaf5db54a5fc979a .gitignore
100644 blob fa1edfb73ce93054fe32d4eb35a5c4bee68c5bf5 ARCHITECTURE.md
100644 blob 9c1883ee31f4fa8b6546a7226754cfc84ada5726 CODE_OF_CONDUCT.md
100644 blob 9fac1017cb65883554f821914fac3fb713008a34 CONTRIBUTORS.md
100644 blob b009175dbcbc186fb8066344c0e899c3104f43e5 Cargo.lock
100644 blob 94b87cd2940697288e4f18530c5933f3110b405b Cargo.toml

这意味着检出一个 Git 提交总是很快的:对 Git 来说,检出昨天的提交和检出 100 万个提交之前的提交一样容易。Git 永远不需要重新应用 10000 个差异来确定当前状态,因为提交根本就不是以差异的形式存储的。

快照使用 packfile 进行压缩

我刚刚提到了 Git 提交是一个快照,但是,当有人说 “在我看来,提交是一个快照,但我认为它在实现上是一个差异” 时,这其实也是对的!Git 提交并不是以你可能习惯的差异的形式表示的(它们不是以与上一个提交的差异的形式存储在磁盘上的),但基本的直觉是,如果你要对一个 10,000 行的文件编辑 500 次,那么存储 500 份文件的效率会很低。

Git 有一个将文件以差异的形式存储的方法。这被称为 “packfile”,Git 会定期进行垃圾回收,将你的数据压缩成 packfile 以节省磁盘空间。当你 git clone 一个仓库时,Git 也会压缩数据。

这里,我没有足够的篇幅来完整地解释 packfile 是如何工作的(Aditya Mukerjee 的 《解压 Git packfile》是我最喜欢的解释它们是如何工作的文章)。不过,我可以在这里简单总结一下我对 deltas 工作原理的理解,以及它们与 diff 的区别:

  • 对象存储为 “原始文件” 和一个 “变化量delta” 的引用
  • 变化量是一系列例如 “读取第 0 到 100 字节,然后插入字节 ‘hello there’,然后读取第 120 到 200 字节” 的指令。它从原始文件中拼凑出新的文本。所以没有 “删除” 的概念,只有复制和添加。
  • 我认为变化量的层次较少:我不知道如何检查 Git 究竟要经过多少层变化量才能得到一个给定的对象,但我的印象是通常不会很多。可能少于 10 层?不过,我很想知道如何才能真正查出来。
  • 原始文件不一定来自上一个提交,它可以是任何东西。也许它甚至可以来自一个更晚的提交?我不确定。
  • 没有一个 “正确的” 算法来计算变化量,Git 只是有一些近似的启发式算法

当你查看差异时,实际上发生了一些奇怪的事情

当我们运行 git show SOME_COMMIT 来查看某个提交的差异时,实际上发生的事情有点反直觉。我的理解是:

  1. Git 会在 packfile 中查找并应用变化量来重建该提交和其父提交的树。
  2. Git 会对两个目录树(当前提交的目录树和父提交的目录树)进行差异比较。通常这很快,因为几乎所有的文件都是完全一样的,所以 git 只需比较相同文件的哈希值就可以了,几乎所有时候都不用做什么。
  3. 最后 Git 会展示差异

所以,Git 会将变化量转换为快照,然后计算差异。它感觉有点奇怪,因为它从一个类似差异的东西开始,最终得到另一个类似差异的东西,但是变化量和差异实际上是完全不同的,所以这是说得通的。

也就是说,我认为 Git 将提交存储为快照,而 packfile 只是一个实现细节,目的是节省磁盘空间并加快克隆速度。我其实从来没必要知道 packfile 是如何工作的,但它确实能帮助我理解 Git 是如何在不占用太多磁盘空间的情况下将提交快照化的。

一个 “错误的” Git 理解:提交是差异

我认为一个相当常见的,对 Git 的 “错误” 的理解是:

  • 提交是以基于上一个提交的差异的形式存储的(加上指向父提交的指针和作者和消息)。
  • 要获取提交的当前状态,Git 需要从头开始重新应用所有之前的提交。

这个理解当然是错误的(在现实中,提交是以快照的形式存储的,差异是从这些快照计算出来的),但是对我来说它似乎非常有用而且有意义!在考虑合并提交时会有一点奇怪,但是或许我们可以说这只是基于合并提交的第一个父提交的差异。

我认为这个错误的理解有的时候非常有用,而且对于日常 Git 使用来说它似乎并没有什么问题。我真的很喜欢它将我们最常使用的东西(差异)作为最基本的元素——它对我来说非常直观。

我也一直在思考一些其他有用但 “错误” 的 Git 理解,比如:

  • 提交信息可以被编辑(实际上不能,你只是复制了一个相同的提交然后给了它一个新的信息,旧的提交仍然存在)
  • 提交可以被移动到一个不同的基础上(类似地,它们是被复制了)

我认为有一系列非常有意义的、 “错误” 的对 Git 的理解,它们在很大程度上都受到 Git 用户界面的支持,并且在大多数情况下都不会产生什么问题。但是当你想要撤销一个更改或者出现问题时,它可能会变得混乱。

将提交视为差异的一些优势

就算我知道在 Git 中提交是快照,我可能大部分时间也都将它们视为差异,因为:

  • 大多时候我都在关注我正在做的 更改 —— 如果我只是改变了一行代码,显然我主要是在考虑那一行代码而不是整个代码库的当前状态
  • 点击 GitHub 上的 Git 提交或者使用 git show 时,你会看到差异,所以这只是我习惯看到的东西
  • 我经常使用变基,它就是关于重新应用差异的

将提交视为快照的一些优势

但是我有时也会将提交视为快照,因为:

  • Git 经常对文件的移动感到困惑:有时我移动了一个文件并编辑了它,Git 无法识别它是否被移动过,而是显示为 “删除了 old.py,添加了 new.py”。这是因为 Git 只存储快照,所以当它显示 “移动 old.py -> new.py” 时,只是猜测,因为 old.pynew.py 的内容相似。
  • 这种方式更容易理解 git checkout COMMIT_ID 在做什么(重新应用 10000 个提交的想法让我感到很有压力)
  • 合并提交在我看来更像是快照,因为合并的提交实际上可以是任何东西(它只是一个新的快照!)。它帮助我理解为什么在解决合并冲突时可以进行任意更改,以及为什么在解决冲突时要小心。

其他一些关于提交的理解

Mastodon 的一些回复中还提到了:

  • 有关提交的 “额外的” 带外信息,比如电子邮件、GitHub 拉取请求或者你和同事的对话
  • 将“差异”视为一个“之前的状态 + 之后的状态”
  • 以及,当然,很多人根据情况的不同以不同的方式看待提交

人们在谈论提交时使用的其他一些词可能不那么含糊:

  • “修订”(似乎更像是快照)
  • “补丁”(看起来更像是差异)

就到这里吧!

我很难了解人们对 Git 有哪些不同的理解。尤其棘手的是,尽管 “错误” 的理解往往非常有用,但人们却非常热衷于警惕 “错误” 的心智模式,所以人们不愿意分享他们 “错误” 的想法,生怕有什么 Git 解释者会站出来向他们解释为什么他们是错的。(这些 Git 解释者通常是出于善意的,但是无论如何它都会产生一种负面影响)

但是我学到了很多!我仍然不完全清楚该如何谈论提交,但是我们最终会弄清楚的。

感谢 Marco Rogers、Marie Flanagan 以及 Mastodon 上的所有人和我讨论 Git 提交。

(题图:DA/cc0cada9-4945-4248-8635-3f89dcebd6ef)


via: https://jvns.ca/blog/2024/01/05/do-we-think-of-git-commits-as-diffs--snapshots--or-histories/

作者:Julia Evans
选题:lujun9972
译者:Cubik65536
校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

2023 年 6 至 8 月总结

作者 Cubik65536
2023年8月27日 00:09

好久没有写月报了,这几个月发生了不少事情,来跟我一起回顾一下吧!

6 月

毕业

在中学的 5 年生活结束了!考试什么的都还算顺利,然后 6 月 21 日…

我的学校的脸书帖子

我所在的第 256 届正式毕业了,可惜我没等到毕业典礼结束扔毕业帽,因为…

回国

距离毕业典礼结束还有一段时间我就和母亲往机场赶,这是我到加拿大将近 8 年后第一次回国…

西行之旅

回国之后见了一下长辈们,其中,我的舅爷和舅奶每年都会去包头住一段时间,于是叔叔提议开车送他们回包头,而且他要去银川办事,所以可以顺便让我、他和我父亲去那边玩一圈,于是我们三个在几乎没有什么详细计划的情况下就出发了…

这一程吃到了不少美食也看到了很多风景,但是最让我印象深刻的应该就是金昌的火星一号基地了。我可能会再单独写篇文来介绍一下这个地方,不过进场火星基地作为以航天为主题的景点,拥有一个火星基地模拟场馆,一个有各种展品的展示区,以及一片居住区。放几个图:

火星一号基地大门

火星一号基地中心的牌子

展示区-火箭发射台

展示区-返回舱

展示区-返回舱内部

展示区-空间站

模拟基地外部

模拟基地-控制中心

模拟基地-健身房

我们甚至在火星基地景区内部吃了个烧烤并住了一晚,挺奇特的体验。另外一个令我印象深刻的是赛里木湖,我们还在湖边住了一晚,来看看航拍吧(已经过景区要求的报备程序,飞手是我,一个无人机新手):

赛里木湖

(如果无法播放,可以点击这里查看)

之后回北京我和父亲坐了普快和高铁,算是第一次坐火车了。那这趟旅行先讲这么多。

7 月

7 月大部分时间都待在家里,但是也出了趟"远“门。我和 @mmdjiji 一起去了趟 IDO 42nd 动漫游戏嘉年华,这也算是我第一次去漫展了,放几张图吧:

合影~

VIP 票

人超多

车展-1

车展-2

车展-3

车展-4

社恐不敢拍照!呜呜呜

不过还是感谢吉吉送我的票~

8 月

又要回加拿大了… 进入了新的学校,不过还是成功考上了想要的计算机科学专业(据部门主任说,我们系是今年最难进的之一)。

另外 8 月 30 日是我的生日,还有 4 天(写稿时是 26 号),那提前祝我 18 岁生日快乐吧!

那么,先这样了,下个月见!

Markdown 示例

作者 Cubik65536
2024年2月27日 04:04

本文件的源文件是一个 Markdown 文件,用于测试部分 Markdown 特性的兼容性。

目录 (Table of Contents)

[TOCM]

[TOC]

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6

标题(用底线的形式)Heading (underline)

This is an H1

This is an H2

字符效果和横线等


删除线 删除线(开启识别 HTML 标签时)
斜体字 斜体字
粗体 粗体
粗斜体 粗斜体

上标:X2,下标:O2

缩写 (同 HTML 的 abbr 标签)

即更长的单词或短语的缩写形式,前提是开启识别 HTML 标签时,已默认开启

The HTML specification is maintained by the W3C.

引用 Blockquotes

引用文本 Blockquotes

引用的行内混合 Blockquotes

引用:如果想要插入空白换行(即 <br/> 标签),在插入处先键入两个以上的空格然后回车即可,普通链接

锚点与链接 Links

普通链接

普通链接带标题

直接链接:https://github.com

锚点链接

mailto:test.test@gmail.com

GFM a-tail link @pandao 邮箱地址自动链接 test.test@gmail.com www@vip.qq.com

@pandao

多语言代码高亮 Codes

行内代码 Inline code

执行命令:npm install marked

缩进风格

即缩进四个空格,也做为实现类似 <pre> 预格式化文本 ( Preformatted Text ) 的功能。

<?php    echo "Hello world!";?>

预格式化文本:

| First Header  | Second Header || ------------- | ------------- || Content Cell  | Content Cell  || Content Cell  | Content Cell  |

多行代码块

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
#include <bits/stdc++.h>

using namespace std;
const int MAX_N = 100;
const int MAX_SUM = 1e5;
bool dp[MAX_N + 1][MAX_SUM + 1];
int main() {
int n;
cin >> n;
vector<int> coins_values(n);
for (int i = 0; i < n; i++) {
cin >> coins_values[i];
}
dp[0][0] = true;
for (int i = 1; i <= n; i++) {
for (int current_sum = 0; current_sum <= MAX_SUM; current_sum++) {
dp[i][current_sum] = dp[i - 1][current_sum];
int prev_sum = current_sum - coins_values[i - 1];
if (prev_sum >= 0 && dp[i - 1][prev_sum]) {
dp[i][current_sum] = true;
}
}
}
vector<int> possible;
for (int sum = 1; sum <= MAX_SUM; sum++) {
if (dp[n][sum]) {
possible.push_back(sum);
}
}
cout << (int)(possible.size()) << endl;
for (int sum : possible) {
cout << sum << " ";
}
cout << endl;
}
Kattio Class in 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
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
import java.io.*;
import java.util.*;

class Kattio extends PrintWriter {
private BufferedReader r;
private StringTokenizer st;

// standard input
public Kattio() {
this(System.in, System.out);
}

public Kattio(InputStream i, OutputStream o) {
super(o);
r = new BufferedReader(new InputStreamReader(i));
}

// USACO-style file input
public Kattio(String problemName) throws IOException {
super(new FileWriter(problemName + ".out"));
r = new BufferedReader(new FileReader(problemName + ".in"));
}

// read next line
public String readLine() {
try {
return r.readLine();
} catch (IOException e) {
}
return null;
}

// returns null if no more input
public String next() {
try {
while (st == null || !st.hasMoreTokens())
st = new StringTokenizer(r.readLine());
return st.nextToken();
} catch (Exception e) {
}
return null;
}

public int nextInt() {
return Integer.parseInt(next());
}

public double nextDouble() {
return Double.parseDouble(next());
}

public long nextLong() {
return Long.parseLong(next());
}
}

图片 Images

Image:

Image

Follow your heart.

Follow your heart

图为:厦门白城沙滩

图片加链接 (Image + Link):

李健首张专辑《似水流年》封面

图为:李健首张专辑《似水流年》封面


列表 Lists

无序列表(减号)Unordered Lists (-)

  • 列表一
  • 列表二
  • 列表三

无序列表(星号)Unordered Lists (*)

  • 列表一
  • 列表二
  • 列表三

无序列表(加号和嵌套)Unordered Lists (+)

  • 列表一
  • 列表二
    • 列表二 -1
    • 列表二 -2
    • 列表二 -3
  • 列表三
    • 列表一
    • 列表二
    • 列表三

有序列表 Ordered Lists (-)

  1. 第一行
  2. 第二行
  3. 第三行

GFM task list

  • [x] GFM task list 1
  • [x] GFM task list 2
  • [ ] GFM task list 3
    • [ ] GFM task list 3-1
    • [ ] GFM task list 3-2
    • [ ] GFM task list 3-3
  • [ ] GFM task list 4
    • [ ] GFM task list 4-1
    • [ ] GFM task list 4-2

绘制表格 Tables

项目价格数量
计算机$16005
手机$1212
管线$1234
First HeaderSecond Header
Content CellContent Cell
Content CellContent Cell
First HeaderSecond Header
Content CellContent Cell
Content CellContent Cell
Function nameDescription
help()Display the help window.
destroy()Destroy your computer!
Left-AlignedCenter AlignedRight Aligned
col 3 issome wordy text$1600
col 2 iscentered$12
zebra stripesare neat$1
ItemValue
Computer$1600
Phone$12
Pipe$1

特殊符号 HTML Entities Codes

© & ¨ ™ ¡ £
& < > ¥ € ® ± ¶ § ¦ ¯ « ·

X² Y³ ¾ ¼ × ÷ »

18ºC " '

Emoji 表情 😃

Blockquotes

GFM task lists & Emoji & fontAwesome icon emoji & editormd logo emoji :editormd-logo-5x

  • [x] 😃 @mentions, 😃 #refs, links, formatting, and tags supported :editormd-logo:;
  • [x] list syntax required (any unordered or ordered list supported) :editormd-logo-3x:;
  • [x] [ ] 😃 this is a complete item 😃;
  • [ ] []this is an incomplete item test link :fa-star: @pandao;
  • [ ] [ ]this is an incomplete item :fa-star: :fa-gear:;
    • [ ] 😃 this is an incomplete item test link :fa-star: :fa-gear:;
    • [ ] 😃 this is :fa-star: :fa-gear: an incomplete item test link;

反斜杠 Escape

*literal asterisks*

科学公式 TeX (KaTeX)

$E=mc^2$

行内的公式 $E=mc^2$

行内的 $E=mc^2$ 公式。

$$x > y$$

$$(\sqrt{3x-1}+(1+x)^2)$$

$$\sin(\alpha)^{\theta}=\sum_{i=0}^{n}(x^i + \cos(f))$$

多行公式:

$$
\displaystyle
\frac{1}{
\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{
\frac25 \pi}} = 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {
1+\frac{e^{-6\pi}}
{1+\frac{e^{-8\pi}}
{1+\cdots} }
}
}
$$

绘制流程图 Flowchart

绘制序列图 Sequence Diagram

End

Warp 终端上手第一印象

作者 Cubik65536
2022年4月17日 12:13

前段时间在逛 GitHub 的时候偶然发现了 Warp 这款终端,而它带来的一些例如输出块,辅助等功能也立即吸引了我,所有我立即将其下载并上手了一下。

登录

截至目前(2022 年 04 月 17 日),初次使用 Warp 仍然需要登录 GitHub 才可以使用,根据其隐私页面的解释:

When Warp comes out of beta, login via Github will not be required.

Right now, in our beta phase, we require a user login in order to access Warp. This just gives us access to the email associated with your Github account.

目前需要登录才可以使用是因为 Warp 目前在 Beta 阶段,而且开发者需要收集测试人员的联系方式。基于大部分软件测试都需要收集测试者的联系方式的情况,Warp 软件本身就开源的情况,以及 GitHub 在登录时仅提供邮箱的提示,我认为目前可以信任这款软件,我也相信在 Beta 结束后这款软件就不再需要登录。如果你真的非常在乎登录,那么建议等待正式版发布。

第一眼

在启动之后,你会看到这个页面(这并非我第一次启动的页面,而且我更改了主题):

它包含一个我们都很熟悉的命令输入行(底部),以及两个快捷键提示。如果你只需要一个普通的终端,那么你已经可以直接使用了。

命令面板

在使用 + P 之后,你将能够看到这个框

这就是你的 Command Palette(命令面板)。在这个提示框里你能看到目前所有的功能和与其绑定的快捷键。

命令提示:

在使用 ^ + + R 之后,你将能够看到如下提示:

这就是 ”工作流” 页面,本质上,这就是将各种工具的常用命令集合在一起,你可以通过选中它们来选择你需要进行的操作。Warp 将会负责自动帮你补全命令的大部分内容和提示你需要填写的参数,而你只需要填写一部分参数。例如增加一个 git 远程分支:

而且用户可以自行增加其他的 “工作流”,来让 Warp 自动帮你生成所需的命令,而你每次只需要填写部分内容。

同时,使用 ^ + R 将会看到一个可以浏览的命令历史界面(直接点击上方向键也会显示一个略微简易一点的命令历史界面)。

命令块

我们使用的许多指令都会有很多输出,在 Warp 中,点击一段输出将会高亮整个命令和其输出,按上/下方向键可以前往前/后一个命令的开头。

辅助的使用

其实某些人是不喜欢辅助的,他们认为这会降低在无辅助工具的情况下使用各种工具的能力。其实这无不道理,目前各种 IDE 层出不穷,每个 IDE 都有自己的辅助和补全功能,而例如 GitHub Copilot 的 AI 代码生成更是发展快速。我相信不少人一下子从装满插件的 IDE 换成记事本写代码都会极其不适应。但同时,这些辅助又确实提升了我们的工作效率,见仁见智吧。

总结

Warp 的一些想法确实很不错,我目前仍为大量的使用 Warp,所以无法给出完整的点评,但是看得出来 Warp 的开发者是有独特的想法的。而且 Warp 以后还会主打团队协作功能,例如多人分享命令行或者直接在命令行里运行文档里的指令,这是值得期待的。


如果你也对这个命令行有兴趣的话,可以前往他们的官网下载(目前仅支持 macOS,但官方表示 Linux, Windows, 和 Web (WASM) 都在开发中)。GL&HF!

将Linux作为第二主力系统(一)

作者 Cubik65536
2021年12月6日 06:40

这段时间刚好搬家,于是获得了一个额外可以放置第二台主力机和一个额外显示器的机会。趁着这个机会,再加上喜欢折腾,于是我决定为这第二台机器装上Linux。在本篇中,我将会讲到为什么要选择Linux、谁更适合Linux以及我会选择的Linux发行。

为什么选择 Liunx?

其实选择 Linux 的理由很简单。Linux 是一个开源、开放、可定制性强的操作系统。而且相对偏向开发者群体。所以这对我一个喜欢折腾的业余开发来说,在合适不过了。当然 Linux 相对于 Windows还有两个优点,就是系统占用的资源小,而且不强制使用某些硬件(例如 TPM 2.0)。对我来说,这两个优点是个非常重要的,具体的我会在以后安装系统时再提。

谁会适合使用 Linux?

Linux 并非适合所有人,其实大部分人都不太适合。先不说部分 Linux 的配置繁琐,大部分的 Linux 桌面环境(Desktop Environment)的体验都不如 Winodws 和 macOS。而且部分,甚至是大部分常用软件都与 Linux(至少是一部分发行版)有兼容问题。所以,到底谁不适合使用 Linux 呢?如果你希望所有的都交给操作系统,基本上不需要考虑驱动的问题,设置全靠自动,安装软件只想下一步,换句话说只想开箱即用完全不折腾的话,Linux 并不适合你。如果你一看需要命令行就头疼,Linux 不适合你。如果你遇到问题完全不想查资料,Linux 还是不适合你。如果你经常使用的一些软件完全不支持 Linux 并且没有很好的解决/替代方案时,Linux 也不适合你。那到底谁适合使用 Linux?如果你喜欢定制你的系统,愿意折腾的话,Linux 是个不错的选择。如果你喜欢折腾,在遇到问题时有时间而且愿意查询各种解决方案和各种文档,Linux 也适合你。同时,鉴于 Linux 在开发环境中的优势,使用 Linux 写代码总体来说会是个挺不错的选择。

我会考虑使用的 Linux 发行

本列表中列出的是我在确定要更换系统是立即想到的几个我可能会经常使用的操作系统,并非操作系统排名,也并不会列出所有操作系统,仅为根据个人习惯进行的选择。

CentOS/Rocky Linux/其他RHEL下游

由于都是RHEL的下游,所以我把它们放在了一起。RHEL以及下游是个非常优秀的操作系统。稳定而且安全。但是由于是服务器操作系统,并不谁非常适合日常使用,故暂不考虑。(如果仅做开发的话,其实这几个系统也非常优秀。)我基本上所有的服务器都在使用这些操作系统。

提醒:CentOS Stream并不包含在其中,而CentOS也将只会继续更新维护Stream。

Ubuntu

Ubuntu 可能是新手们认识到的第一个 Linux 了,开箱即用且易于上手,但是由于长期使用RHEL操作系统,我早已习惯RHEL系列的包管理器等工具,因此暂不考虑。

Fedora

RHEL上游操作系统。与RHEL以及下游操作系统本质上并无太多差别。包管理器等工具也都是相同的。最大的不同是,Fedora的操作系统是滚动更新的,这意味着Fedora会比其他RHEL体系中的系统更早收到新功能,但这也意味着安全性和稳定性相对的降低。不过对于日常使用来说,这些缺失带来的问题并不会像在服务器环境下那么严重。因此,Fedora 还是目前最常用的 Linux 工作站操作系统之一。而且 Fedora 也有一个第三方组织 Fedora Spins 提供着预装着个大桌面环境的 Fedora ISO。同时,由于我对RHEL系列较为熟悉,所以Fedora将会是我优先选择的操作系统之一。

在此提一句,RHEL体系的更新顺序(从上游排名到下游)为:

Fedora -> CentOS Stream -> RHEL -> CentOS、Rocky等下游

Arch Linux

Arch Linux 一直以极高的定制度闻名。但是,我目前更偏向适用一个相对可以开箱即用的操作系统,所以暂不考虑。

Artix Liunx

Artix Linux 是个基于 Arch Linux 的操作系统,相对原本的 Arch,Artix 已经为用户进行了一些配置,但是仍然保留着很大的定制度。而且不使用 systemd。但是由于并不算很火,网络上缺乏各种教程等材料,所以暂时也不考虑。

Manjaro

基于 Arch 的 Linux 中最新手友好的操作系统。Manjaro 官方为其提供了不少的软件,而且官方也提供了大部分常用桌面环境的ISO。在对新手如此友好的同时,Arch Linux 高定制度等原本的优势也被 Manjaro 保留了。同时,由于 Manjraro 在社区的知名度,网络上也存在不少 Manjaro 相关的教程等内容。所以 Manjaro 也会是我备选系统之一。

下一步

在真正的向实体机安装操作系统之前,我将会在虚拟机内先安装我目前唯二真正的备选操作系统:Fedora 和 Manjaro。我将会使用我最常用的 KDE Plasma 桌面,虚拟机统一分配2核心CPU,4GB内存。我会在接下来一段时间内定期更新这两个操作系统的使用体验,并最终决定使用哪个系统。

对了

我也准备将 Arch 安装在虚拟机中,这样想折腾系统的时候可以玩玩。相对于实体机,虚拟机对完炸了的情况下的备份和回滚更加方便。所以,喜欢折腾的各位不妨试试。

结语

对于喜欢折腾的人来说,在一个相对性能已经不够 Windows 使用的电脑上安装 Linux 并偶尔使用不失为一个好方法。当然,鉴于目前的情况,Linux 仍然不适合普通用户 100% 日常使用。但是,如果你想找点事做,折腾一把,不妨试试 Linux,GL&HF!

我对 Notability 转向订阅制的看法、事件后续以及 Notability 软件的替代品

作者 Cubik65536
2021年11月2日 05:33

(本文撰写于2021年11月1日)

就在今天(2021年11月1日),Notability 官方宣布他们转变为一个免费订阅制软件。而这一决定的公开立刻引起了轩然大波。我将详细讲一下事情的经过,我的看法以及一些解决方案。

事件起因

北美洲东部时间2021年11月1日上午11点55分,Notability 官方在 Twitter 上发推:《We’re officially a FREE app and have some big updates to share!(我们正式成为了一个免费应用并且有一些大更新要分享!)》。在此推文中包含的文章里,Notability宣布软件将提供免费下载,同时对编辑(没错,编辑,而且实测增删内容都算编辑)、手写识别、备份等功能做出了限制。而以前以买断制购买的用户仅能获得一年的订阅补偿(部分用户似乎并没有收到此补偿),在此之后这些用户也将转变为免费用户。在此信息宣布之后,大量已有用户都表示了不满。

随后,大量的用户根据苹果审核条款「3.1.2(a) - 在转变经营模式为订阅制的时候,开发者不得移除已有用户可以享受的功能」为理由向苹果投诉应用。

在北美洲东部时间2021年11月2日下午7点50分,Notability宣布将会为以买断制购买的用户一直提供他们已经购买的服务和一些维护成本较低的服务,至此这件事情才算结束。

2021年11月9日,我收到了来自 Notability 的更新,我的订阅模式变成了 Classic,这代表我可以以买断制用户的身份继续使用我购买过的功能。

我的看法

其实我并不反对订阅制软件。作为一个开发者,我也部分的认为订阅制软件将会是以后应用开发的大局势。但是作为一个用户,我也很讨厌软件开发商在转变商业模式的时候将我已经购买过的应用拿掉,然后要求我再次付费来获得此功能。所以我对目前 Notability 提出的解决方案是相对满意的。而且我作为一个用了4年Notability的用户,我个人认为其实大量的新功能我是用不到的,所以新功能需要订阅也不对我产生什么太大的影响。不过,此次事件仍然拉低了我对 Ginger Lab,也就是 Notability 开发公司的印象,也直接导致我决定开始寻找其他替代方案。

Notability 的替代方案

GoodNotes

在 iPad 上一直占据榜首的笔记软件,除了 Notability 自然就是 GoodNotes。GoodNotes 采用买断,但是大版本更新需要重新付费的商业模式。但也同时给予用户不更新的权利。GoodNotes 相对于 Notability 缺少了录音和数学公式功能,但是带来了更好的文件分类系统、更多的免费笔记本模版、更多笔的类型和可自定义参数等功能。同时 GoodNotes 也提供 macOS 版本,一旦购买 iOS 版本,macOS 版本会随之被添加到你的账户中。如果你有充足的预算,GoodNotes 仍然是最好的替代方案。

Microsoft OneNote

作为微软 Office 系列的一员,OneNote 没有什么可以挑剔的。OneNote 默认向 Onedrive 备份并且同步的体验也非常不错。而且数学识别功能一直在线。但是缺点就是… 一贯的 Office 系列设计,有的时候看着真的有点难受。如果你已经有了 Office,而且需要跨平台写作,OneNote 将会是个不错的选择。

备忘录

没错,说的就是 iOS/iPadOS/macOS 自带的备忘录。鉴于最新的 iOS/iPadOS 15 和 macOS Monterey 都有了快速备忘录的功能,搭配 iCloud 的同步,苹果自家对 Apple Pencil 的适配,如果你对笔记软件的额外功能没有太多要求,备忘录其实是个不错的选择。


就这样吧!GL & HF!

git 的基础使用与 FastGit 加速

作者 Cubik65536
2021年8月12日 21:46

在本文中,我将简单的提到如何使用 git,以及如何使用 FastGit 加速 GitHub

如何打开命令行

Windows

使用 Win(键盘上的Windows标志) + R 按键,输入 cmd,或者右键开始,选择 命令提示符Powershell

macOS

Command + 空格,搜索 终端 或者 Terminal

Linux

有桌面环境的去应用列表找找 终端 或者 Terminal

没有的… 你没开玩笑吧?大佬请离开这个页面 这就是你的命令行!

Git 使用基础

安装 Git

首先,你需要在你的计算机上安装 git,你可以先通过在终端执行以下命令来检查 git 是否已安装:

1
git --version

而如果输出为 git version x.xx.x (x.xx.x 为版本号,每人的输出可能不一样),则代表你已经安装。

提示

一般来说,如果你购买的是 Apple Mac 系列计算机,则你的系统中应该已经预装了 git

如果你没有安装,请按照以下方式安装:

Windows

前往下载页面,点击Windows。打开安装包并一直下一步即可。

macOS

1
$ brew install git

Linux

基于RPM的Linux发行版本(SUSE,RHEL等)
1
$ sudo dnf install git-all
基于Debian的Linux发行版吧(例如Ubuntu)
1
$ sudo apt install git-all

Git 的基础操作

注意,本章所有内容都将以 https://github.com/author/repo 作为演示仓库链接,在使用时记得替换为你自己的仓库。

提示

本篇针对的读者是没有任何基础但是需要临时使用 git 与其他人合作的用户。故不提起 init 等其他操作。更详细的使用可以参考 xaoxuu 大佬的Git实用教程 和 xugaoyi 大佬的Git学习笔记

“登录”

准确的来说是将你的用户名和邮箱配置好,让 git 知道这是你做的。

1
2
git config --global user.name "用户名"
git config --global user.email "邮箱"

使用以下命令可以检查配置是否正常:

1
git config --global --list

由于这个身份极易被伪造,所以某些人会使用PGP来验证签名。如果你在用,你就需要进行额外操作。大佬请离开这个页面x2

clone

简单来说,这个操作是为了将你的文件下载到你的机器上,打开命令行,输入以下命令

1
git clone https://github.com/author/repo

在你完成之后,所有云端文件都应该存储到你的计算机上了。如何找到文件夹?

Windows

查看下方截图红框内的内容,这就是你的路径。

CMD路径

macOS & Linux

一般都在你的 User 家目录下,使用了 cd 命令的各位请自行寻找 大佬请离开这个页面x3

编辑

ummm。打开你喜欢的编辑器,写吧!

推荐编辑器:Visual Studio Code(样样行),Typora(Markdown,md文件)

commit

这一步简单来说就是告诉软件,你要提交一个更改。先进入你的 git 路径(一般是你 clone 的路径再 cd <文件夹名>

提示

文件夹名一般就是 repo 名称,即为 https://github.com/author/repo 中的 repo

输入以下命令

1
git commit -m "信息"

信息 一般填写你更改的内容

push

这一步简单来说就是把你commit过的内容提交给服务器。输入:

1
git push origin

pull

这一步简单来说就是把服务器有但你本机没有的更改(例如别人做的改动)弄到你机器上。输入:

1
git pull origin

使用 FastGit 加速

由于某些地区 GitHub 访问受限,所以我们需要使用 FastGit 来进行加速。使用方法很简单:将链接内的 github.com 替换为 hub.fastgit.org 即可。

即为将 https://github.com/author/repo 替换为 https://hub.fastgit.org/author/repo

其他操作可参考 FastGit用户手册

注册 MS 365 Developer Program 并确保它可以长期使用

作者 Cubik65536
2021年7月23日 09:03
提示

本文有部分参考、引用 Restent Ou《如何续订 MS 365 E5 开发者计划》一文。 本文有使用该《如何续订 MS 365 E5 开发者计划》一文的图片。才不是懒得自己截图了呢 《如何续订 MS 365 E5 开发者计划》一文基于CC BY-NC 4.0协议

相信不少人都听说过 Microsoft 365 Developer Program,或者说 Microsoft E5。这是微软为开发人员推出的计划,目的是可以为开发者提供全套的 Microsoft 365 套件。当然不少人(包括我的朋友 Restent)都依靠着这个计划为自己团队提供带有域名的邮箱访问。

在本文中,我将提到如何注册这个计划,以及如何一直延续这个项目的可用性。

一、注册 Microsoft 365 Developer Program

进入官网

首先,前往 Microsoft 365 Developer Program 官网 并点击立即加入

登录你的 Microsoft 账户

在这里,你需要选择一个微软账户(你的 hotmail 或 outlook 账户)并登录

设置你的 E5 订阅

现在,你应该会进到Microsoft 365 开发人员计划页面,点击设置 E5 订阅。然后你会看到下方这样的页面:

SetupE5.png

填入你的基本信息

在此处,你应该会看到一个让你填写信息的页面,选择你的国家,填写你的用户名和域。注意,这里的域会生成一个 你输入的东西+.onmicrosoft.com 组成的域名,如果你希望使用你自己的域名,需要等到注册完成之后进入管理页面设置。然后写好你的密码。这里填写的信息就是你的 E5 订阅管理员的账户信息。

然后验证你的手机号码。

然后你会看到下方这样的页面:

EnterYourInfo.png

注意

我的账户已经与我的 GitHub 账户进行了关联,所以上方会有 GitHub 徽标,新注册用户是没有该徽标的!

到这里你就设置成功了,你接下来可以到 Microsoft 365 Admin Center 处登录你刚刚注册的,以onmicrosoft.com结尾的账户。在那个站点上,你可以添加用户,域名以及做出其他设置。这些以后再讲。

二、续期你的 E5 订阅

微软不可能就这么往外送订阅对吧?微软给 Microsoft 365 Developer Program 成员续期是有要求的。目前已知要求是“持续有开发活动”。但我并未找到任何资料直接解释如何衡量开发活动是否足够。

或许你知道以前有各种 GitHub 仓库来帮助你续期该计划,但是这种东西说挂就挂。所以接下来解释一个更靠谱一点的方案:关联 GitHub 活动

关联 GitHub

如果你尚未关联 GitHub,则你应该可以在你的仪表盘界面上方看到这样的提示:

新增内容!将开发人员帐户与 GitHub 帐户链接。GitHub 活动将订阅续订 Microsoft 365 开发人员版。

点击 链接账户 ,或者点击右上方的小齿轮,再点击左侧的 已链接的账户

设置

已链接的账户

点击 同意并链接 Github 账户,并在 Github 认证即可。认证完成后你将会看到:

认证后

这样,只要你经常有 GitHub 活动(commit,issue 和 pull request 等全部被计算在内),你大概率就会获得续期。


就是这样!那我们,下次再见~

使用 Visual Studio Code SSH 远程访问控制 CentOS 7 服务器

作者 Cubik65536
2021年1月11日 03:16

一、配置服务器

服务器配置

操作系统:CentOS 7 x86_64 版

网卡:Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller

配置服务器

直接安装 CentOS 即可,CentOS 7 已经预装了我们所需的所有程序。但是你仍然需要在防火墙策略内放行 SSH 服务所需要的 22 端口。

二、配置 Visual Studio Code

打开 Visual Studio Code 并在扩展商店内搜索 Remote,找到 Remote Development 并安装,它会同时安装我们需要的 Remote - SSH 等插件。

至此,我们就完成了 Visual Studio 的配置。

三、访问服务器

点击右下角的远程访问按钮并选择 Remote SSH:Connect to Host…

选择 Add New SSH Host,输入『用户名@ip 地址』并回车,之后选择一个要更新的 SSH 配置文件,然后在右下角的添加成功弹窗处选择 Connect 连接远程服务器。

在上方弹窗提示 Enter password for『用户名@ip 地址』时,输入该用户的密码并回车。至此你就已经成功连接到你的远程服务器上了!此时你的右下角远程连接提示处应该会显示你的远程主机 ip。

❌
❌