diff --git "a/docs/\344\270\215\345\220\214\351\242\206\345\237\237\347\232\204\345\255\246\344\271\240\350\267\257\347\272\277/Java/roadmap.md" "b/docs/\344\270\215\345\220\214\351\242\206\345\237\237\347\232\204\345\255\246\344\271\240\350\267\257\347\272\277/Java/java_learning_path.md" similarity index 100% rename from "docs/\344\270\215\345\220\214\351\242\206\345\237\237\347\232\204\345\255\246\344\271\240\350\267\257\347\272\277/Java/roadmap.md" rename to "docs/\344\270\215\345\220\214\351\242\206\345\237\237\347\232\204\345\255\246\344\271\240\350\267\257\347\272\277/Java/java_learning_path.md" diff --git "a/docs/\350\256\241\347\256\227\346\234\272\347\247\221\345\255\246\345\255\246\344\271\240\350\267\257\347\272\277/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\274\226\347\250\213\345\237\272\347\241\200/Java/javaSE.md" "b/docs/\350\256\241\347\256\227\346\234\272\347\247\221\345\255\246\345\255\246\344\271\240\350\267\257\347\272\277/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\274\226\347\250\213\345\237\272\347\241\200/Java/javaSE.md" new file mode 100644 index 0000000..98c82d8 --- /dev/null +++ "b/docs/\350\256\241\347\256\227\346\234\272\347\247\221\345\255\246\345\255\246\344\271\240\350\267\257\347\272\277/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\274\226\347\250\213\345\237\272\347\241\200/Java/javaSE.md" @@ -0,0 +1,1947 @@ +以下内容选自[OI Wiki](https://oi-wiki.org)(感谢) + +## 关于 Java + +Java 是一种广泛使用的计算机编程语言,拥有 **跨平台**、**面向对象**、**泛型编程** 的特性,广泛应用于企业级 Web 应用开发和移动应用开发。 + +## 环境安装 + +参见 [JDK](../tools/compiler.md#jdk)。 + +## 基本语法 + +### 主函数 + +Java 类似 C/C++ 语言,需要一个函数(在面向对象中,这被称为方法)作为程序执行的入口点。 + +Java 的主函数的格式是固定的,形如: + +```java +class Test { + public static void main(String[] args) { + // 程序的代码 + } +} +``` + +一个打包的 Java 程序(名称一般是 `*.jar`)中可以有很多个类似的函数,但是当运行这个程序的时候,只有其中一个函数会被运行,这是定义在 `Jar` 的 `Manifest` 文件中的,在 OI 比赛中一般用不到关于它的知识。 + +### 注释 + +和 C/C++ 一样,Java 使用 `//` 和 `/* */` 分别注释单行和多行。 + +### 基本数据类型 + +| 类型名 | 意义 | +| :-----: | :---: | +| boolean | 布尔类型 | +| byte | 字节类型 | +| char | 字符型 | +| double | 双精度浮点 | +| float | 单精度浮点 | +| int | 整型 | +| long | 长整型 | +| short | 短整型 | +| null | 空 | + +### 声明变量 + +```java +int a = 12; // 设置 a 为整数类型,并给 a 赋值为 12 +String str = "Hello, OI-wiki"; // 声明字符串变量 str +char ch = 'W'; +double PI = 3.1415926; +``` + +### final 关键字 + +`final` 含义是这是最终的、不可更改的结果,被 `final` 修饰的变量只能被赋值一次,赋值后不再改变。 + +```java +final double PI = 3.1415926; +``` + +### 数组 + +```java +// 有十个元素的整数类型数组 +// 其语法格式为 数据类型[] 变量名 = new 数据类型[数组大小] +int[] ary = new int[10]; +``` + +### 字符串 + +- 字符串是 Java 一个内置的类。 + +```java +// 最为简单的构造一个字符串变量的方法如下 +String a = "Hello"; + +// 还可以使用字符数组构造一个字符串变量 +char[] stringArray = { 'H', 'e', 'l', 'l', 'o' }; +String s = new String(stringArray); +``` + +### 包和导入包 + +Java 中的类(`Class`)都被放在一个个包(`package`)里面。在一个包里面不允许有同名的类。在类的第一行通常要说明这个类是属于哪个包的。例如: + +```java +package org.oi-wiki.tutorial; +``` + +包的命名规范一般是:`项目所有者的顶级域.项目所有者的二级域.项目名称`。 + +通过 `import` 关键字来导入不在本类所属的包下面的类。例如下面要用到的 `Scanner`: + +```java +import java.util.Scanner; +``` + +如果想要导入某包下面所有的类,只需要把这个语句最后的分号前的类名换成 `*`。 + +### 输入 + +可以通过 `Scanner` 类来处理命令行输入。 + +```java +package org.oiwiki.tutorial; + +import java.util.Scanner; + +class Test { + public static void main(String[] args) { + Scanner scan = new Scanner(System.in); // System.in 是输入流 + int a = scan.nextInt(); + double b = scan.nextDouble(); + String c = scan.nextLine(); + } +} +``` + +### 输出 + +可以对变量进行格式化输出。 + +| 符号 | 意义 | +| :--: | :---: | +| `%f` | 浮点类型 | +| `%s` | 字符串类型 | +| `%d` | 整数类型 | +| `%c` | 字符类型 | + +```java +class Test { + public static void main(String[] args) { + int a = 12; + char b = 'A'; + double s = 3.14; + String str = "Hello world"; + System.out.printf("%f\n", s); + System.out.printf("%d\n", a); + System.out.printf("%c\n", b); + System.out.printf("%s\n", str); + } +} +``` + +### 控制语句 + +Java 的流程控制语句与 C++ 是基本相同的。 + +#### 选择 + +- if + +```java +class Test { + public static void main(String[] args) { + if ( /* 判断条件 */ ){ + // 条件成立时执行这里面的代码 + } + } +} +``` + +- if...else + +```java +class Test { + public static void main(String[] args) { + if ( /* 判断条件 */ ) { + // 条件成立时执行这里面的代码 + } else { + // 条件不成立时执行这里面的代码 + } + } +} +``` + +- if...else if...else + +```java +class Test { + public static void main(String[] args) { + if ( /* 判断条件 */ ) { + //判断条件成立执行这里面的代码 + } else if ( /* 判断条件2 */ ) { + // 判断条件2成立执行这里面的代码 + } else { + // 上述条件都不成立执行这里面的代码 + } + } +} +``` + +- switch...case + +```java +class Test { + public static void main(String[] args) { + switch ( /* 表达式 */ ){ + case /* 值 1 */: + // 当表达式取得的值符合值 1 执行此段代码 + break; // 如果不加上 break 语句,会让程序按顺序往下执行直到 break + case /* 值 2 */: + // 当表达式取得的值符合值 2 执行此段代码 + break; + default: + // 当表达式不符合上面列举的值的时候执行这里面的代码 + } + } +} +``` + +#### 循环 + +- for + +`for` 关键字有两种使用方法,其中第一种是普通的 `for` 循环,形式如下: + +```java +class Test { + public static void main(String[] args) { + for ( /* 初始化 */; /* 循环的判断条件 */; /* 每次循环后执行的步骤 */ ) { + // 当循环的条件成立执行循环体内代码 + } + } +} +``` + +第二种是类似 C++ 的 `foreach` 使用方法,用于循环数组或者集合中的数据,相当于把上一种方式中的循环变量隐藏起来了,形式如下: + +```java +class Test { + public static void main(String[] args) { + for ( /* 元素类型X */ /* 元素名Y */ : /* 集合Z */ ) { + // 这个语句块的每一次循环时,元素Y分别是集合Z中的一个元素。 + } + } +} +``` + +- while + +```java +class Test { + public static void main(String[] args) { + while ( /* 判定条件 */ ) { + // 条件成立时执行循环体内代码 + } + } +} +``` + +- do...while + +```java +class Test { + public static void main(String[] args) { + do { + // 需要执行的代码 + } while ( /* 循环判断条件 */ ); + } +} +``` + +## 注意事项 + +### 类名与文件名一致 + +创建 Java 源程序需要类名和文件名一致才能编译通过,否则编译器会提示找不到类。通常该文件名会在具体 OJ 中指定。 + +例: + +`Add.java` + +```java +class Add { + public static void main(String[] args) { + // ... + } +} +``` + +在该文件中需使用 `Add` 为类名方可编译通过。 + +## JavaSE高级部分 + +???+ warning "注意" + 以下内容均基于 Java JDK 8 版本编写,不排除在更高版本中有部分改动的可能性。 + +## 更高速的输入输出 + +`Scanner` 和 `System.out.print` 在最开始会工作得很好,但是在处理更大的输入的时候会降低效率,因此我们会需要使用一些方法来提高 IO 速度。 + +### 使用 Kattio + StringTokenizer 作为输入 + +最常用的方法之一是使用来自 Kattis 的 [Kattio.java](https://github.com/Kattis/kattio/blob/master/Kattio.java) 来提高 IO 效率。[^ref1]这个方法会将 `StringTokenizer` 与 `PrintWriter` 包装在一个类中方便使用。而在具体进行解题的时候(假如赛会/组织方允许)可以直接使用这个模板。 + +下方即为应包含在代码中的 IO 模板,由于 Kattis 的原 Kattio 包含一些并不常用的功能,下方的模板经过了一些调整(原 Kattio 使用 MIT 作为协议)。 + +```java +class Kattio extends PrintWriter { + private BufferedReader r; + private StringTokenizer st; + // 标准 IO + public Kattio() { this(System.in, System.out); } + public Kattio(InputStream i, OutputStream o) { + super(o); + r = new BufferedReader(new InputStreamReader(i)); + } + // 文件 IO + public Kattio(String intput, String output) throws IOException { + super(output); + r = new BufferedReader(new FileReader(intput)); + } + // 在没有其他输入时返回 null + 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()); } +} +``` + +而下方代码简单展示了 Kattio 的使用: + +```java +class Test { + public static void main(String[] args) { + Kattio io = new Kattio(); + // 字符串输入 + String str = io.next(); + // int 输入 + int num = io.nextInt(); + // 输出 + io.println("Result"); + // 请确保关闭 IO 流以确保输出被正确写入 + io.close(); + } +} +``` + +### 使用 StreamTokenizer 作为输入 + +在某些情况使用 `StringTokenizer` 会导致 MLE(Memory Limit Exceeded,超过内存上限),此时我们需要使用 `StreamTokenizer` 作为输入。 + +```java +import java.io.*; +public class Main { + // IO 代码 + public static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in), 32768)); + public static PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); + public static double nextDouble() throws IOException { in.nextToken(); return in.nval; } + public static float nextFloat() throws IOException { in.nextToken(); return (float)in.nval; } + public static int nextInt() throws IOException { in.nextToken(); return (int)in.nval; } + public static String next() throws IOException { in.nextToken(); return in.sval; } + public static long nextLong() throws Exception { in.nextToken(); return (long)in.nval;} + + // 使用示例 + public static void main(String[] args) throws Exception { + int n = nextInt(); + out.println(n); + out.close(); + } +} +``` + +### Kattio + StringTokenizer 的方法与 StreamTokenizer 的方法之间的分析与对比 + +1. `StreamTokenizer` 相较于 `StringTokenizer` 使用的内存较少,当 Java 标程 MLE 时可以尝试使用 `StreamTokenizer`,但是 `StreamTokenizer` 会丢失精度,读入部分数据时会出现问题; + - `StreamTokenizer` 源码存在 `Type`,该 `Type` 根据你输入内容来决定类型,倘若你输入类似于 `123oi` 以 **数字开头** 的字符串,他会强制认为你的类型是 `double` 类型,因此在读入中以 `double` 类型去读 `String` 类型便会抛出异常; + - `StreamTokenizer` 在读入 `1e14` 以上大小的数字会丢失精度; +2. 在使用 `PrintWriter` 情况下,需注意在程序结束最后 `close()` 关闭输出流或在需要输出的时候使用 `flush()` 清除缓冲区,否则内容将不会被写入到控制台/文件中。 +3. `Kattio` 是继承自 `PrintWriter` 类,自身对象具有了 `PrintWriter` 的功能,因此可以直接调用 `PrintWriter` 类的函数输出,同时将 `StringTokenizer` 作为了自身的成员变量来修改。而第二种 `Main` 是同时将 `StreamTokenizer` 与 `PrintWriter` 作为了自身的成员变量,因此在使用上有些许差距。 + +综上所述,在大部分情况下,`StringTokenizer` 的使用处境要优越于 `StreamTokenizer`,在极端 MLE 的情况下可以尝试 `StreamTokenizer`,同时 `int` 范围以上的数据 `StreamTokenizer` 处理是无能为力的。 + + +## BigInteger 与数论 + +`BigInteger` 是 Java 提供的高精度计算类,可以很方便地解决高精度问题。 + +### 初始化 + +`BigInteger` 常用创建方式有如下二种: + +```java +import java.io.PrintWriter; +import java.math.BigInteger; + +class Main { + static PrintWriter out = new PrintWriter(System.out); + public static void main(String[] args) { + BigInteger a = new BigInteger("12345678910"); // 将字符串以十进制的形式创建 BigInteger 对象 + out.println(a); // a 的值为 12345678910 + BigInteger b = new BigInteger("1E", 16); // 将字符串以指定进制的形式创建 BigInteger 对象 + out.println(b); // b 的值为 30 + out.close(); + } +} + +``` + +### 基本运算 + +以下均用 `this` 代替当前 `BigIntger` : + +| 函数名 | 功能 | +| :-------------------------: | :-----------------------------------------: | +| `abs()` | 返回 this 的绝对值 | +| `negate()` | 返回 - this | +| `add(BigInteger val)` | 返回 this `+` val | +| `subtract(BigInteger val)` | 返回 this `-` val | +| `multiply(BigInteger val)` | 返回 this `*` val | +| `divide(BigInteger val)` | 返回 this `/` val | +| `remainder(BigInteger val)` | 返回 this `%` val | +| `mod(BigInteger val)` | 返回 this `mod` val | +| `pow(int e)` | 返回 $this^e$ | +| `and(BigInteger val)` | 返回 this `&` val | +| `or(BigInteger val)` | 返回 this `|` val | +| `not()` | 返回 `~` this | +| `xor(BigInteger val)` | 返回 this `^` val | +| `shiftLeft(int n)` | 返回 this `<<` n | +| `shiftRight(int n)` | 返回 this `>>` n | +| `max(BigInteger val)` | 返回 this 与 val 的较大值 | +| `min(BigInteger val)` | 返回 this 与 val 的较小值 | +| `bitCount()` | 返回 this 的二进制中不包括符号位的 1 的个数 | +| `bitLength()` | 返回 this 的二进制中不包括符号位的长度 | +| `getLowestSetBit()` | 返回 this 的二进制中最右边的位置 | +| `compareTo(BigInteger val)` | 比较 this 和 val 值大小 | +| `toString()` | 返回 this 的 10 进制字符串表示形式 | +| `toString(int radix)`。 | 返回 this 的 raidx 进制字符串表示形式 | + +使用案例如下: + +```java +import java.io.PrintWriter; +import java.math.BigInteger; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + static BigInteger a, b; + + static void abs() { + out.println("abs:"); + a = new BigInteger("-123"); + out.println(a.abs()); // 输出 123 + a = new BigInteger("123"); + out.println(a.abs()); // 输出 123 + } + + static void negate() { + out.println("negate:"); + a = new BigInteger("-123"); + out.println(a.negate()); // 输出 123 + a = new BigInteger("123"); + out.println(a.negate()); // 输出 -123 + } + + static void add() { + out.println("add:"); + a = new BigInteger("123"); + b = new BigInteger("123"); + out.println(a.add(b)); // 输出 246 + } + + static void subtract() { + out.println("subtract:"); + a = new BigInteger("123"); + b = new BigInteger("123"); + out.println(a.subtract(b)); // 输出 0 + } + + static void multiply() { + out.println("multiply:"); + a = new BigInteger("12"); + b = new BigInteger("12"); + out.println(a.multiply(b)); // 输出 144 + } + + static void divide() { + out.println("divide:"); + a = new BigInteger("12"); + b = new BigInteger("11"); + out.println(a.divide(b)); // 输出 1 + } + + static void remainder() { + out.println("remainder:"); + a = new BigInteger("12"); + b = new BigInteger("10"); + out.println(a.remainder(b)); // 输出 2 + a = new BigInteger("-12"); + b = new BigInteger("10"); + out.println(a.remainder(b)); // 输出 -2 + } + + static void mod() { + out.println("mod:"); + a = new BigInteger("12"); + b = new BigInteger("10"); + out.println(a.mod(b)); // 输出 2 + a = new BigInteger("-12"); + b = new BigInteger("10"); + out.println(a.mod(b)); // 输出 8 + } + + static void pow() { + out.println("pow:"); + a = new BigInteger("2"); + out.println(a.pow(10)); // 输出 1024 + } + + static void and() { + out.println("and:"); + a = new BigInteger("3"); // 11 + b = new BigInteger("5"); // 101 + out.println(a.and(b)); // 输出 1 + } + + static void or() { + out.println("or:"); + a = new BigInteger("2"); // 10 + b = new BigInteger("5"); // 101 + out.println(a.or(b)); // 输出 7 + } + + static void not() { + out.println("not:"); + a = new BigInteger("2147483647"); // 01111111 11111111 11111111 11111111 + out.println(a.not()); // 输出 -2147483648 二进制为:10000000 00000000 00000000 00000000 + } + + static void xor() { + out.println("xor:"); + a = new BigInteger("6"); // 110 + b = new BigInteger("5"); // 101 + out.println(a.xor(b)); // 011 输出 3 + } + + static void shiftLeft() { + out.println("shiftLeft:"); + a = new BigInteger("1"); + out.println(a.shiftLeft(10)); // 输出 1024 + } + + static void shiftRight() { + out.println("shiftRight:"); + a = new BigInteger("1024"); + out.println(a.shiftRight(8)); // 输出 4 + } + + static void max() { + out.println("max:"); + a = new BigInteger("6"); + b = new BigInteger("5"); + out.println(a.max(b)); // 输出 6 + } + + static void min() { + out.println("min:"); + a = new BigInteger("6"); + b = new BigInteger("5"); + out.println(a.min(b)); // 输出 5 + } + + static void bitCount() { + out.println("bitCount:"); + a = new BigInteger("6"); // 110 + out.println(a.bitCount()); // 输出 2 + } + + static void bitLength() { + out.println("bitLength:"); + a = new BigInteger("6"); // 110 + out.println(a.bitLength()); // 输出 3 + } + + static void getLowestSetBit() { + out.println("getLowestSetBit:"); + a = new BigInteger("8"); // 1000 + out.println(a.getLowestSetBit()); // 输出 3 + } + + static void compareTo() { + out.println("compareTo:"); + a = new BigInteger("8"); + b = new BigInteger("9"); + out.println(a.compareTo(b)); // 输出 -1 + a = new BigInteger("8"); + b = new BigInteger("8"); + out.println(a.compareTo(b)); // 输出 0 + a = new BigInteger("8"); + b = new BigInteger("7"); + out.println(a.compareTo(b)); // 输出 1 + } + + static void toStringTest() { + out.println("toString:"); + a = new BigInteger("15"); + out.println(a.toString()); // 输出 15 + out.println(a.toString(16)); // 输出 f + } + + public static void main(String[] args) { + abs(); + negate(); + add(); + subtract(); + multiply(); + divide(); + remainder(); + mod(); + pow(); + and(); + or(); + not(); + xor(); + shiftLeft(); + shiftRight(); + max(); + min(); + bitCount(); + bitLength(); + getLowestSetBit(); + compareTo(); + toStringTest(); + out.close(); + } +} +``` + +### 数学运算 + +以下均用 `this` 代替当前 `BigIntger` : + +| 函数名 | 功能 | +| :----------------------------------: | :-------------------------------------------: | +| `gcd(BigInteger val)` | 返回 this 的绝对值与 val 的绝对值的最大公约数 | +| `isProbablePrime(int val)` | 返回一个表示 this 是否是素数的布尔值 | +| `nextProbablePrime()` | 返回第一个大于 this 的素数 | +| `modPow(BigInteger b, BigInteger p)` | 返回 this `^` b `mod` p | +| `modInverse(BigInteger p)` | 返回 a `mod` p 的乘法逆元 | + +使用案例如下: + +```java +import java.io.PrintWriter; +import java.math.BigInteger; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + static BigInteger a, b, p; + + static void gcd() { // 最大公约数 + a = new BigInteger("120032414321432144212100"); + b = new BigInteger("240231431243123412432140"); + out.println(String.format("gcd(%s,%s)=%s", a.toString(), b.toString(), a.gcd(b).toString())); // gcd(120032414321432144212100,240231431243123412432140)=20 + } + + static void isPrime() { // 基于米勒罗宾判定该数是否是素数,参数越大准确性越高,复杂度越高。准确性为 (1-1/(val*2)) + a = new BigInteger("1200324143214321442127"); + out.println("a:" + a.toString()); + out.println(a.isProbablePrime(10) ? "a is prime" : "a is not prime"); // a is not prime + } + + static void nextPrime() { // 找出该数的下一个素数 + a = new BigInteger("1200324143214321442127"); + out.println("a:" + a.toString()); + out.println(String.format("a nextPrime is %s", a.nextProbablePrime().toString())); // a nextPrime is 1200324143214321442199 + } + + static void modPow() { // 快速幂,比正常版本要快,内部有数学优化 + a = new BigInteger("2"); + b = new BigInteger("10"); + p = new BigInteger("1000"); + out.println(String.format("a:%s b:%s p:%s", a, b, p)); + out.println(String.format("a^b mod p:%s", a.modPow(b, p).toString()));// 24 + } + + static void modInverse() { // 逆元 + a = new BigInteger("10"); + b = new BigInteger("3"); + out.println(a.modInverse(b)); // a ^ (p-2) mod p = 1 + } + + public static void main(String[] args) { + gcd(); + isPrime(); + nextPrime(); + modPow(); + modInverse(); + out.close(); + } +} +``` + +关于米勒罗宾相关知识可以查阅[Miller–Rabin 素性测试](../math/number-theory/prime.md#millerrabin-素性测试)。 + +## 基本数据类型与包装数据类型 + +### 简介 + +由于基本类型没有面向对象的特征,为了他们参加到面向对象的开发中,Java 为八个基本类型提供了对应的包装类,分别是 `Byte`、`Double`、`Float`、`Integer`、`Long`、`Short`、`Character` 和 `Boolean`。两者之间的对应关系如下: + +| 基本数据类型 | 包装数据类型 | +| :----------: | :----------: | +| `byte` | `Byte` | +| `short` | `Short` | +| `boolean` | `Boolean` | +| `char` | `Character` | +| `int` | `Integer` | +| `long` | `Long` | +| `float` | `Float` | +| `double` | `Double` | + +### 区别 + +此处以 `int` 与 `Integer` 举例: + +1. `Integer` 是 `int` 的包装类,`int` 则是 Java 的一种基本类型数据。 +2. `Integer` 类型实例后才能使用,而 `int` 类型不需要。 +3. `Integer` 实际对应的引用,当 `new` 一个 `Integer` 时,实际上生成了一个对象,而 `int` 则是直接存储数据。 +4. `Integer` 的默认值是 `null`,可接受 `null` 和 `int` 类型的数据, `int` 默认值是 0,不能接受 `null` 类型的数据。 +5. `Integer` 判定二个变量是否相同使用 `==` 可能会导致不正确的结果,只能使用 `equals()`,而 `int` 可以直接使用 `==`。 + +### 装箱与拆箱 + +此处以 `int` 与 `Integer` 举例: + +`Integer` 的本质是对象,`int` 是基本类型,两个类型之间是不能直接赋值的。需要转换时,应将基础类型转换为包装类型,这种做法称为装箱,反过来则称为拆箱。 + +```java +// 基本类型 +int value1 = 1; +// 装箱转换为包装类型 +Integer integer = Integer.valueOf(value1); +// 拆箱转换为基本类型 +int value2 = integer.intValue(); +``` + +Java 5 引入了自动装箱拆箱机制: + +```java +Integer integer = 1; +int value = integer; +``` + +???+ warning "注意" + 虽然 JDK 增加了自动装箱拆箱的机制,但在声明变量时请选择合适的类型,因为包装类型 `Integer` 可以接受 `null`,而基本类型 `int` 不能接受 `null`。因此,对使用 `null` 值的包装类型进行拆箱操作时,会抛出异常。 + +```java +Integer integer = Integer.valueOf(null); +integer.intValue(); // 抛出 java.lang.NumberFormatException 异常 + +Integer integer = null; +integer.intValue(); // 抛出 java.lang.NullPointerException 异常 +``` + +## 继承 + +基于已有的设计创造新的设计,就是面向对象程序设计中的继承。在继承中,新的类不是凭空产生的,而是基于一个已经存在的类而定义出来的。通过继承,新的类自动获得了基础类中所有的成员,包括成员变量和方法,包括各种访问属性的成员,无论是 `public` 还是 `private` 。显然,通过继承来定义新的类,远比从头开始写一个新的类要简单快捷和方便。继承是支持代码重用的重要手段之一。 + +在 Java 中,继承的关键字为 `extends`,且 Java 只支持单继承,但可以实现多接口。 + +在 Java 中,所有类都是 `Object` 类的子类。 + +子类继承父类,所有的父类的成员,包括变量和方法,都成为了子类的成员,除了构造方法。构造方法是父类所独有的,因为它们的名字就是类的名字,所以父类的构造方法在子类中不存在。除此之外,子类继承得到了父类所有的成员。 + +每个成员有不同的访问属性,子类继承得到了父类所有的成员,但是不同的访问属性使得子类在使用这些成员时有所不同:有些父类的成员直接成为子类的对外的界面,有些则被深深地隐藏起来,即使子类自己也不能直接访问。 + +下表列出了不同访问属性的父类成员在子类中的访问属性: + +| 父类成员访问属性 | 在父类中的含义 | 在子类中的含义 | +| :---------------: | :--------------------------------: | :----------------------------------------------------------: | +| `public` | 对所有类开放 | 对所有类开放 | +| `protected` | 只有包内其它类、自己和子类可以访问 | 只有包内其它类、自己和子类可以访问 | +| 缺省(`default`) | 只有包内其它类可以访问 | 如果子类与父类在同一个包内,只有包内其它类可以访问;否则相当于 `private`,不能访问 | +| `private` | 只有自己可以访问 | 不能访问 | + + +## 多态 + +在 Java 中当把一个对象赋值给一个变量时,对象的类型必须与变量的类型相匹配。但由于 Java 有继承的概念,便可重新定义为 **一个变量可以保存其所声明的类型或该类型的任何子类型**。 + +如果一个类型实现了接口,也可以称之为该接口的子类型。 + +Java 中保存对象类型的变量是多态变量。「多态」这个术语(字面意思是许多形态)是指一个变量可以保存不同类型(即其声明的类型或任何子类型)的对象。 + +多态变量: + +1. Java 的对象变量是多态的,它们能保存不止一种类型的对象。 +2. 它们可以保存的是声明类型的对象,或声明类型子类的对象。 +3. 当把子类的对象赋给父类的变量的时候,就发生了向上转型。 + + +## 泛型 + +泛型指在类定义时不设置类中的属性或方法参数的具体类型,而是在使用(或创建对象)时再进行类型的定义。泛型本质是参数化类型,即所操作的数据类型被指定为一个参数。 + +泛型提供了编译时类型安全检测的机制,该机制允许编译时检测非法类型。 + + +## 接口 + +### 简介 + +接口(英文:Interface)在 Java 中是一个抽象类型,是抽象方法的集合,通常以 `interface` 来声明。一个类通过实现接口的方式,从而来继承接口的抽象方法。 + +接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。 + +除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。 + +接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。 + +### 与类的区别 + +1. 接口不能用于实例化对象。 +2. 接口没有构造方法。 +3. 接口中所有的方法必须是抽象方法,Java 8 之后接口中可以使用 `default` 关键字修饰的非抽象方法。 +4. 接口不能包含成员变量,除了 static 和 final 变量。 +5. 接口不是被类继承了,而是要被类实现。 +6. 接口支持多继承,类不支持多继承。 + +### 声明 + +```java +[可见度] interface 接口名称 [extends 其他的接口名] { + // 声明变量 + // 抽象方法 +} +``` + +### 实现 + +```java +...implements 接口名称[, 其他接口名称, 其他接口名称..., ...] ... +``` + +## Lambda 表达式 + +### 简介 + +lambda 表达式也可称为闭包,是 Java 8 的最重要的新特性。 + +lambda 表达式允许把函数作为一个方法的参数(函数作为参数传递进方法中)。 + +使用 lambda 表达式可以使代码变的更加简洁紧凑。 + +### 语法 + +**可选类型声明**:不需要声明参数类型,编译器可以统一识别参数值。 + +**可选的参数圆括号**:一个参数无需定义圆括号,但多个参数需要定义圆括号。 + +**可选的大括号**:如果主体包含了一个语句,就不需要使用大括号。 + +**可选的返回关键字**:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。 + +lambda 表达式声明方式如下: + +以字符串数组按长度排序的自定义比较器为例: + +1. 参数,箭头,一个表达式。 +```java +import java.util.Arrays; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + + public static void main(String[] args) { + String[] plants = {"Mercury", "venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"}; + Arrays.sort(plants, (String first, String second) -> (first.length() - second.length())); + for (String word : plants) { + out.print(word + " "); + } + out.close(); + } +} +``` + +2. 参数,箭头,多条语句。 +```java +import java.io.PrintWriter; +import java.util.Arrays; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + + public static void main(String[] args) { + String[] plants = {"Mercury", "venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"}; + Arrays.sort(plants, (first, second) -> + { + // 形参不写类型,可以从上下文判断出 + int result = first.length() - second.length(); + return result; + }); + for (String word : plants) { + out.print(word + " "); + } + out.close(); + } +} +``` + +`->` 是一个推导符号,表示前面的括号接收到参数,推导后面的返回值(其实就是传递了方法)。 + +3. 常用形式: + +```java +// 1. 不需要参数,返回值为 5 +() -> 5 + +// 2. 接收一个参数(数字类型),返回其 2 倍的值 +x -> 2 * x + +// 3. 接受 2 个参数(数字)并返回他们的差值 +(x, y) -> x – y + +// 4. 接收 2 个 int 类型整数并返回他们的和 +(int x, int y) -> x + y + +// 5. 接受一个 String 对象并在控制台打印,不返回任何值(看起来像是返回 void) +(String s) -> System.out.print(s) +``` + +### 函数式接口 + +1. 是一个接口,符合 Java 接口定义。 +2. 只包含一个抽象方法的接口。 +3. 因为只有一个未实现的方法,所以 lambda 表达式可以自动填上去。 + +函数式接口使用方式如下: +1. 输出长度为 2 的倍数的字符串。 + +```java +import java.io.PrintWriter; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + + public static void main(String[] args) { + String[] plants = {"Mercury", "venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"}; + Test test = s -> { // lambda 表达式作为函数式接口的实例 + if (s.length() % 2 == 0) { + return true; + } + return false; + }; + for (String word : plants) { + if (test.check(word)) { + out.print(word + " "); + } + } + out.close(); + } +} + +interface Test { + public boolean check(String s); +} +``` +2. 实现加减乘除四则运算。 +```java +import java.io.PrintWriter; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + + public static double calc(double a, double b, Calculator util) { + return util.operation(a, b); + } + + public static void main(String[] args) { + Calculator util[] = new Calculator[4]; // 定义函数式接口数组 + util[0] = (a, b) -> a + b; + util[1] = (a, b) -> a - b; + util[2] = (a, b) -> a * b; + util[3] = (a, b) -> a / b; + double a = 20, b = 15; + for (Calculator c : util) { + System.out.println(calc(a, b, c)); + } + out.close(); + } +} + +interface Calculator { + public double operation(double a, double b); +} +``` + + +## Collection + +`Collection` 是 Java 中的接口,被多个泛型容器接口所实现。在这里,`Collection` 是指代存放对象类型的数据结构。 + +Java 中的 `Collection` 元素类型定义时必须为对象,不能为基本数据类型。 + +以下内容用法均基于 Java 里多态的性质,均是以实现接口的形式出现。 + +常用的接口包括 `List`、`Queue`、`Set` 和 `Map`。 + +### 容器定义 + +1. 当定义泛型容器类时,需要在定义时指定数据类型。 + +例如: + +```java +List list1 = new LinkedList<>(); +``` + +2. 倘若不指定数据类型,而当成 `Object` 类型随意添加数据,在 Java 8 中虽能编译通过,但会有很多警告风险。 + +例如: + +```java +List list = new ArrayList<>(); +list.add(1); +list.add(true); +list.add(1.01); +list.add(1L); +list.add("I am String"); +``` + +因此,如果没有特殊需求的话不推荐第 2 种行为,编译器无法帮忙检查存入的数据是否安全。`list.get(index)` 取值时无法明确数据的类型(取到的数据类型都为 `Object`),需要手动转回原来的类型,稍有不慎可能出现误转型异常。 + +如果是明确了类型如 `List`,此时编译器会检查放入的数据类型,只能放入整数的数据。声明集合变量时只能使用包装类型 `List` 或者自定义的 `Class`,而不能是基本类型如 `List`。 + +### List + +#### ArrayList + +`ArrayList` 是支持可以根据需求动态生长的数组,初始长度默认为 10。如果超出当前长度便扩容 $\dfrac{3}{2}$。 + +##### 初始化 + +```java +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + + public static void main(String[] args) { + List list1 = new ArrayList<>(); // 创建一个名字为 list1 的可自增数组,初始长度为默认值(10) + List list2 = new ArrayList<>(30); // 创建一个名字为list2的可自增数组,初始长度为 30 + List list3 = new ArrayList<>(list2); // 创建一个名字为 list3 的可自增数组,使用 list2 里的元素和 size 作为自己的初始值 + } +} +``` + +#### LinkedList + +`LinkedList` 是双链表。 + +##### 初始化 + +```java +import java.io.PrintWriter; +import java.util.LinkedList; +import java.util.List; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + + public static void main(String[] args) { + List list1 = new LinkedList<>(); // 创建一个名字为 list1 的双链表 + List list2 = new LinkedList<>(list1); // 创建一个名字为 list2 的双链表,将 list1 内所有元素加入进来 + } +} +``` + +#### 常用方法 + +以下均用 `this` 代替当前 `List`: + +| 函数名 | 功能 | +| :-----------------------: | :-------------------------------------------: | +| `size()` | 返回 this 的长度 | +| `add(Integer val)` | 在 this 尾部插入一个元素 | +| `add(int idx, Integer e)` | 在 this 指定位置插入一个元素 | +| `get(int idx)` | 返回 this 中第 idx 位置的值,若越界则抛出异常 | +| `set(int idx, Integer e)` | 修改 this 中第 idx 位置的值 | + +使用案例及区别对比: + +```java +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + static List array = new ArrayList<>(); + static List linked = new LinkedList<>(); + + static void add() { + array.add(1); // 时间复杂度为 O(1) + linked.add(1); // 时间复杂度为 O(1) + } + + static void get() { + array.get(10); // 时间复杂度为 O(1) + linked.get(10); // 时间复杂度为 O(11) + } + + static void addIdx() { + array.add(0, 2); // 最坏情况下时间复杂度为 O(n) + linked.add(0, 2); // 最坏情况下时间复杂度为 O(n) + } + + static void size() { + array.size(); // 时间复杂度为 O(1) + linked.size(); // 时间复杂度为 O(1) + } + + static void set() { // 该方法返回值为原本该位置元素的值 + array.set(0, 1); // 时间复杂度为 O(1) + linked.set(0, 1); // 最坏时间复杂度为 O(n) + } + +} +``` + +#### 遍历 + +```java +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + static List array = new ArrayList<>(); + static List linked = new LinkedList<>(); + + static void function1() { // 朴素遍历 + for (int i = 0; i < array.size(); i++) { + out.println(array.get(i)); // 遍历自增数组,复杂度为 O(n) + } + for (int i = 0; i < linked.size(); i++) { + out.println(linked.get(i)); // 遍历双链表,复杂度为 O(n^2),因为 LinkedList 的 get(i) 复杂度是 O(i) + } + } + + static void function2() { // 增强 for 循环遍历 + for (int e : array) { + out.println(e); + } + for (int e : linked) { + out.println(e); // 复杂度均为 O(n) + } + } + + static void function3() { // 迭代器遍历 + Iterator iterator1 = array.iterator(); + Iterator iterator2 = linked.iterator(); + while (iterator1.hasNext()) { + out.println(iterator1.next()); + } + while (iterator2.hasNext()) { + out.println(iterator2.next()); + } // 复杂度均为 O(n) + } + +} +``` + +???+ warning "注意" + 不要在 `for/foreach` 遍历 `List` 的过程中删除其中的元素,否则会抛出异常。 + + 原因也很简单,`list.size()` 改变了,但在循环中已循环的次数却是没有随之变化。原来预计在下一个 `index` 的数据因为删除的操作变成了当前 `index` 的数据,运行下一个循环时操作的会变为原来预计在下下个 `index` 的数据,最终会导致操作的数据不符合预期。 + +### Queue + +#### LinkedList + +可以使用 `LinkedList` 实现普通队列,底层是链表模拟队列。 + +##### 初始化 + +```java +Queue q = new LinkedList<>(); +``` + +`LinkedList` 底层实现了 `List` 接口与 `Deque` 接口,而 `Deque` 接口继承自 `Queue` 接口,所以 `LinkedList` 可以同时实现 `List` 与 `Queue` 。 + +#### ArrayDeque + +可以使用 `ArrayDeque` 实现普通队列,底层是数组模拟队列。 + +##### 初始化 + +```java +Queue q = new ArrayDeque<>(); +``` + +`ArrayDeque` 底层实现了 `Deque` 接口,而 `Deque` 接口继承自 `Queue` 接口,所以 `ArrayDeque` 可以实现 `Queue`。 + +#### LinkedList 与 ArrayDeque 在实现 Queue 接口上的区别 + +1. **数据结构**:在数据结构上,`ArrayDeque` 和 `LinkedList` 都实现了 Java Deque 双端队列接口。但 `ArrayDeque` 没有实现了 Java List 列表接口,所以不具备根据索引位置操作的行为。 +2. **线程安全**:`ArrayDeque` 和 `LinkedList` 都不考虑线程同步,不保证线程安全。 +3. **底层实现**:在底层实现上,`ArrayDeque` 是基于动态数组的,而 `LinkedList` 是基于双向链表的。 +4. **在遍历速度上**:`ArrayDeque` 是一块连续内存空间,基于局部性原理能够更好地命中 CPU 缓存行,而 `LinkedList` 是离散的内存空间对缓存行不友好。 +5. **在操作速度上**:`ArrayDeque` 和 `LinkedList` 的栈和队列行为都是 $O(1)$ 时间复杂度,`ArrayDeque` 的入栈和入队有可能会触发扩容,但从均摊分析上看依然是 $O(1)$ 时间复杂度。 +6. **额外内存消耗上**:`ArrayDeque` 在数组的头指针和尾指针外部有闲置空间,而 `LinkedList` 在节点上增加了前驱和后继指针。 + +#### PriorityQueue + +`PriorityQueue` 是优先队列,默认是小根堆。 + +##### 初始化 + +```java +Queue q1 = new PriorityQueue<>(); // 小根堆 +Queue q2 = new PriorityQueue<>((x, y) -> {return y - x;}); // 大根堆 +``` + +#### 常用方法 + +以下均用 `this` 代替当前 `Queue` : + +| 函数名 | 功能 | +| :------------------: | :-------------------------------: | +| `size()` | 返回 this 的长度 | +| `add(Integer val)` | 入队 | +| `offer(Integer val)` | 入队 | +| `isEmpty()` | 判断队列是否为空,为空则返回 true | +| `peek()` | 返回队头元素 | +| `poll()` | 返回队头元素并删除 | + +使用案例及区别对比: + +```java +import java.io.PrintWriter; +import java.util.LinkedList; +import java.util.PriorityQueue; +import java.util.Queue; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + static Queue q1 = new LinkedList<>(); + static Queue q2 = new PriorityQueue<>(); + + static void add() { // add 和 offer 功能上没有差距,区别是是否会抛出异常 + q1.add(1); // 时间复杂度为 O(1) + q2.add(1); // 时间复杂度为 O(logn) + } + + static void isEmpty() { + q1.isEmpty(); // 时间复杂度为 O(1) + q2.isEmpty(); // 空间复杂度为 O(1) + } + + static void size() { + q1.size(); // 时间复杂度为 O(1) + q2.size(); // 返回 q2 的长度 + } + + static void peek() { + q1.peek(); // 时间复杂度为 O(1) + q2.peek(); // 时间复杂度为 O(logn) + } + + static void poll() { + q1.poll(); // 时间复杂度为 O(1) + q2.poll(); // 时间复杂度为 O(logn) + } +} +``` + +#### 遍历 + +```java +import java.io.PrintWriter; +import java.util.LinkedList; +import java.util.PriorityQueue; +import java.util.Queue; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + static Queue q1 = new LinkedList<>(); + static Queue q2 = new PriorityQueue<>(); + + static void test() { + while (!q1.isEmpty()) { // 复杂度为 O(n) + out.println(q1.poll()); + } + while (!q2.isEmpty()) { // 复杂度为 O(nlogn) + out.println(q2.poll()); + } + } + +} +``` +### Deque + +`Deque` 是 `Java` 中的双端队列,我们通常用其进行队列的操作以及栈的操作。 + +#### 主要函数 +以下均用 `this` 代替当前 `Deque` : + +| 函数名 | 功能 | +| :-----------------------: | :--------------------------------------: | +| `push(Integer val)` | 将一个元素从队头加入this,等效于addFirst | +| `pop()` | 将队头元素删除,等效于removeFirst | +| `addFirst(Integer val)` | 将一个元素从队头加入this | +| `removeFirst()` | 将队头元素删除,并返回该元素 | +| `addLast(Integer val)` | 将一个元素从队尾加入this | +| `removeLast()` | 将队尾元素删除,并返回该元素 | +| `offerFirst(Integer val)` | 将一个元素从队头加入this | +| `pollFirst()` | 将队头元素删除,并返回该元素 | +| `offerLast(Integer val)` | 将一个元素从队尾加入this | +| `pollLast()` | 将队尾元素删除,并返回该元素 | +| `add(Integer val)` | 将一个元素从队尾加入this | +| `offer(Integer val)` | 将一个元素从队尾加入this | +| `poll()` | 将队头元素删除,并返回该元素 | +| `remove()` | 将队头元素删除,并返回该元素 | +| `peekFirst()` | 返回队头元素 | +| `peekLast()` | 返回队尾元素 | + +`add`、`remove` 操作在遇到异常时会抛出异常,而`offer`、 `poll` 不会抛出异常。 + +#### 栈的操作 +```java +import java.util.ArrayDeque; +import java.util.Deque; + +public class Main { + static Deque stack = new ArrayDeque<>(); + static int[] a = {1, 2, 3, 4, 5}; + + public static void main(String[] args) { + for (int v : a) { + stack.push(v); + } + while (!stack.isEmpty()) { //输出 5 4 3 2 1 + System.out.println(stack.pop()); + } + } +} + +``` +#### 双端队列的操作 +```java +import java.util.ArrayDeque; +import java.util.Deque; + +public class Main { + static Deque deque = new ArrayDeque<>(); + + static void insert() { + deque.addFirst(1); + deque.addFirst(2); + deque.addLast(3); + deque.addLast(4); + } + + public static void main(String[] args) { + insert(); + while (!deque.isEmpty()) { //输出 2 1 3 4 + System.out.println(deque.poll()); + } + insert(); + while (!deque.isEmpty()) { //输出 4 3 1 2 + System.out.println(deque.pollLast()); + } + } +} +``` + + +### Set + +`Set` 是保持容器中的元素不重复的一种数据结构。 + +#### HashSet + +随机位置插入的 `Set`。 + +##### 初始化 + +```java +Set s1 = new HashSet<>(); +``` + +#### LinkedHashSet + +保持插入顺序的 `Set`。 + +##### 初始化 + +```java +Set s2 = new LinkedHashSet<>(); +``` + +#### TreeSet + +保持容器中元素有序的 `Set`,默认为升序。 + +##### 初始化 + +```java +Set s3 = new TreeSet<>(); +Set s4 = new TreeSet<>((x, y) -> {return y - x;}); // 降序 +``` + +##### TreeSet的更多使用 + +这些方法是`TreeSet`新创建并实现的,我们无法使用`Set`接口调用以下方法,因此我们创建方式如下: + +```java +TreeSet s3 = new TreeSet<>(); +TreeSet s4 = new TreeSet<>((x, y) -> {return y - x;}); // 降序 +``` + +以下均用 `this` 代替当前 `TreeSet` : + +| 函数名 | 功能 | +| :--------------------: | :-------------------------------------------: | +| `first()` | 返回 this 中第一个元素,无则返回 null | +| `last()` | 返回 this 中最后一个元素,无则返回 null | +| `floor(Integer val)` | 返回集合中 <=val 的第一个元素,无则返回 null | +| `ceiling(Integer val)` | 返回集合中 >=val 的第一个元素,无则返回 null | +| `higher(Integer val)` | 返回集合中 >val 的第一个元素,无则返回 null | +| `lower(Integer val)` | 返回集合中 set = new TreeSet<>(); + for(int v:a) { + set.add(v); + } + Integer a2 = set.first(); + System.out.println(a2); //返回 1 + Integer a3 = set.last(); + System.out.println(a3); //返回 7 + Integer a4 = set.floor(5); + System.out.println(a4); //返回 4 + Integer a5 = set.ceiling(6); + System.out.println(a5); //返回 6 + Integer a6 = set.higher(7); + System.out.println(a6); //返回 null + Integer a7 = set.lower(2); + System.out.println(a7); //返回 1 + Integer a8 = set.pollFirst(); + System.out.println(a8); //返回 1 + Integer a9 = set.pollLast(); + System.out.println(a9); //返回 7 + } +} +``` + + + +#### Set常用方法 + +以下均用 `this` 代替当前 `Set` : + +| 函数名 | 功能 | +| :-----------------------: | :-------------------------------: | +| `size()` | 返回 this 的长度 | +| `add(Integer val)` | 插入一个元素进 this | +| `contains(Integer val)` | 判断 this 中是否有元素 val | +| `addAll(Collection e)` | 将一个容器里的所有元素添加进 this | +| `retainAll(Collection e)` | 将 this 改为两个容器内相同的元素 | +| `removeAll(Collection e)` | 将 this 中与 e 相同的元素删除 | + +使用案例:求并集、交集、差集。 + +```java +import java.io.PrintWriter; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + static Set s1 = new HashSet<>(); + static Set s2 = new LinkedHashSet<>(); + + static void add() { + s1.add(1); + } + + static void contains() { // 判断 set 中是否有元素值为 2,有则返回 true,否则返回 false + s1.contains(2); + } + + static void test1() { // s1 与 s2 的并集 + Set res = new HashSet<>(); + res.addAll(s1); + res.addAll(s2); + } + + static void test2() { // s1 与 s2 的交集 + Set res = new HashSet<>(); + res.addAll(s1); + res.retainAll(s2); + } + + static void test3() { // 差集:s1 - s2 + Set res = new HashSet<>(); + res.addAll(s1); + res.removeAll(s2); + } +} +``` + +#### 遍历 + +```java +import java.io.PrintWriter; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + static Set s1 = new HashSet<>(); + static Set s2 = new LinkedHashSet<>(); + + static void test() { + for (int key : s1) { + out.println(key); + } + out.close(); + } +} +``` + +### Map + +`Map` 是维护键值对 `` 的一种数据结构,其中 `Key` 唯一。 + +#### HashMap + +随机位置插入的 `Map`。 + +##### 初始化 + +```java +Map map1 = new HashMap<>(); +``` + +#### LinkedHashMap + +保持插入顺序的 `Map`。 + +##### 初始化 + +```java +Map map2 = new LinkedHashMap<>(); +``` + +#### TreeMap + +保持 `key` 有序的 `Map`,默认升序。 + +##### 初始化 + +```java +Map map3 = new TreeMap<>(); +Map map4 = new TreeMap<>((x, y) -> {return y - x;}); // 降序 +``` + +#### 常用方法 + +以下均用 `this` 代替当前 `Map`: + +| 函数名 | 功能 | +| :-------------------------------: | :-----------------------------------: | +| `put(Integer key, Integer value)` | 插入一个元素进 this | +| `size()` | 返回 this 的长度 | +| `containsKey(Integer val)` | 判断 this 中是否有元素 key 为 val | +| `get(Integer key)` | 将 this 中对应的 key 的 value 返回 | +| `keySet` | 将 this 中所有元素的 key 作为集合返回 | + +使用案例: + +```java +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + + static Map map1 = new HashMap<>(); + static Map map2 = new LinkedHashMap<>(); + static Map map3 = new TreeMap<>(); + static Map map4 = new TreeMap<>((x,y)->{return y-x;}); + + static void put(){ // 将 key 为 1、value 为 1 的元素返回 + map1.put(1, 1); + } + static void get(){ // 将 key 为 1 的 value 返回 + map1.get(1); + } + static void containsKey(){ // 判断是否有 key 为 1 的键值对 + map1.containsKey(1); + } + static void KeySet(){ + map1.keySet(); + } +} +``` + +#### 遍历 + +```java +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + + static Map map1 = new HashMap<>(); + + static void print() { + for (int key : map1.keySet()) { + out.println(key + " " + map1.get(key)); + } + } +} +``` + +当然,在面向对象的世界里,你的参数是什么都可以,包括 `Collection` 与自定义类型。 + +例如 `Map` 也可以定义为: + +```java +Map> map = new HashMap<>(); +``` + +## Arrays + +`Arrays` 是 `java.util` 中对数组操作的一个工具类。方法均为静态方法,可使用类名直接调用。 + +### Arrays.sort() + +`Arrays.sort()` 是对数组进行的排序的方法,主要重载方法如下: + +```java +import java.util.Arrays; +import java.util.Comparator; + +public class Main { + static int[] a = new int[10]; + static Integer[] b = new Integer[10]; + static int firstIdx, lastIdx; + + public static void main(String[] args) { + Arrays.sort(a); // 1 + Arrays.sort(a, firstIdx, lastIdx); // 2 + Arrays.sort(b, new Comparator() { // 3 + @Override + public int compare(Integer o1, Integer o2) { + return o2 - o1; + } + }); + Arrays.sort(b, firstIdx, lastIdx, new Comparator() { // 4 + @Override + public int compare(Integer o1, Integer o2) { + return o2 - o1; + } + }); + // 由于 Java 8 后有 Lambda 表达式,第三个重载及第四个重载亦可写为 + Arrays.sort(b, (x, y) -> { // 5 + return y - x; + }); + Arrays.sort(b, (x, y) -> { // 6 + return y - x; + }); + } +} +``` + +**序号所对应的重载方法含义:** + +1. 对数组 a 进行排序,默认升序。 +2. 对数组 a 的指定位置进行排序,默认升序,排序区间为左闭右开 `[firstIdx, lastIdx)`。 +3. 对数组 a 以自定义的形式排序,第二个参数 `-` 第一个参数为降序,第一个参数 `-` 第二个参数为升序,当自定义排序比较器时,数组元素类型必须为对象类型。 +4. 对数组 a 的指定位置进行自定义排序,排序区间为左闭右开 `[firstIdx, lastIdx)`,当自定义排序比较器时,数组元素类型必须为对象类型。 +5. 和 3 同理,用 Lambda 表达式优化了代码长度。 +6. 和 4 同理,用 Lambda 表达式优化了代码长度。 + +**`Arrays.sort()` 底层函数:** + +1. 当你 `Arrays.sort` 的参数数组元素类型为基本数据类型(`byte`、`short`、`char`、`int`、`long`、`double`、`float`)时,默认为 `DualPivotQuicksort`(双轴快排),复杂度最坏可以达到 $O(n^2)$。 +2. 当你 `Arrays.sort` 的参数数组元素类型为非基本数据类型时,则默认为 `legacyMergeSort` 和 `TimSort`(归并排序),复杂度为$O(n\log n)$。 + +可以通过如下代码验证: + +???+note "[Codeforces 1646B - Quality vs Quantity](https://codeforces.com/problemset/problem/1646/B)" + 题意概要:有 $n$ 个数,你需要将其分为 2 组,是否能存在 1 组的长度小于另 1 组的同时和大于它。 + +??? note "例题代码" + ```java + import java.io.BufferedReader; + import java.io.IOException; + import java.io.InputStreamReader; + import java.io.PrintWriter; + import java.util.Arrays; + import java.util.StringTokenizer; + + public class Main { + static class FastReader { + StringTokenizer st; + BufferedReader br; + + public FastReader() { + br = new BufferedReader(new InputStreamReader(System.in)); + } + + String next() { + while (st == null || !st.hasMoreElements()) { + try { + st = new StringTokenizer(br.readLine()); + } catch (IOException e) { + e.printStackTrace(); + } + } + return st.nextToken(); + } + + int nextInt() { + return Integer.parseInt(next()); + } + + long nextLong() { + return Long.parseLong(next()); + } + + double nextDouble() { + return Double.parseDouble(next()); + } + + String nextLine() { + String str = ""; + try { + str = br.readLine(); + } catch (IOException e) { + e.printStackTrace(); + } + return str; + } + } + + static PrintWriter out = new PrintWriter(System.out); + static FastReader in = new FastReader(); + + static void solve() { + int n = in.nextInt(); + Integer[] a = new Integer[n + 10]; + for (int i = 1; i <= n; i++) { + a[i] = in.nextInt(); + } + Arrays.sort(a, 1, n + 1); + long left = a[1]; + long right = 0; + int x = n; + for (int i = 2; i < x; i++, x--) { + left = left + a[i]; + right = right + a[x]; + if (right > left) { + out.println("YES"); + return; + } + } + out.println("NO"); + } + + public static void main(String[] args) { + int t = in.nextInt(); + while (t-- > 0) { + solve(); + } + out.close(); + } + } + ``` + +如果你将以上代码的 a 数组类型由 `Integer` 修改为 `int`,会导致 TLE。 + +### Arrays.binarySearch() + +`Arrays.binarySearch()` 是对数组连续区间进行二分搜索的方法,前提是数组必须有序,时间复杂度为 $O(\log_n)$,主要重载方法如下: + +```java +import java.util.Arrays; + +public class Main { + static int[] a = new int[10]; + static Integer[] b = new Integer[10]; + static int firstIdx, lastIdx; + static int key; + + public static void main(String[] args) { + Arrays.binarySearch(a, key); // 1 + Arrays.binarySearch(a, firstIdx, lastIdx, key); // 2 + } +} +``` + +源码如下: + +```java + private static int binarySearch0(int[] a, int fromIndex, int toIndex, int key) { + int low = fromIndex; + int high = toIndex - 1; + + while (low <= high) { + int mid = (low + high) >>> 1; + int midVal = a[mid]; + + if (midVal < key) + low = mid + 1; + else if (midVal > key) + high = mid - 1; + else + return mid; // key found + } + return -(low + 1); // key not found. + } +``` + +**序号所对应的重载方法含义:** + +1. 从数组 a 中二分查找是否存在 `key`,如果存在,便返回其下标。若不存在,则返回一个负数。 +2. 从数组 a 中二分查找是否存在 `key`,如果存在,便返回其下标,搜索区间为左闭右开 `[firstIdx,lastIdx)`。若不存在,则返回一个负数。 + +### Arrays.fill() + +`Arrays.fill()` 方法将数组中连续位置的元素赋值为统一元素。其接受的参数为数组、`fromIndex`、`toIndex` 和需要填充的数。方法执行后,数组左闭右开区间 `[firstIdx,lastIdx)` 内的所有元素的值均为需要填充的数。 + +## Collections + +`Collections` 是 `java.util` 中对集合操作的一个工具类。方法均为静态方法,可使用类名直接调用。 + +### Collections.sort() + +`Collections.sort()` 底层原理为将其中所有元素转化为数组调用 `Arrays.sort()`,完成排序后再赋值给原本的集合。又因为 Java 中 `Collection` 的元素类型均为对象类型,所以始终是归并排序去处理。 + +该方法无法对集合指定区间排序。 + +底层源码: + +```java + default void sort(Comparator c) { + Object[] a = this.toArray(); + Arrays.sort(a, (Comparator) c); + ListIterator i = this.listIterator(); + for (Object e : a) { + i.next(); + i.set((E) e); + } + } +``` + +### Collections.binarySearch() + +`Collections.binarySearch()` 是对集合中指定区间进行二分搜索,功能与 `Arrays.binarySearch()` 相同。 + +```java +Collections.binarySearch(list, key); +``` + +该方法无法对指定区间进行搜索。 + +### Collections.swap() + +`Collections.swap()` 的功能是交换集合中指定二个位置的元素。 + +```java + Collections.swap(list, i, j); +``` + +## 其他 + +### 1. -0.0 != 0.0 + +在 Java 中,如果单纯是数值类型,-0.0 = 0.0 。若是对象类型,则 -0.0 != 0.0 。倘若你尝试用 `Set` 统计斜率数量时,这个问题就会带来麻烦。 +提供的解决方式是在所有的斜率加入 `Set` 前将值增加 0.0。 + +```java +import java.io.PrintWriter; + +public class Main { + static PrintWriter out = new PrintWriter(System.out); + + static void A() { + Double a = 0.0; + Double b = -0.0; + out.println(a.equals(b)); // false + } + + static void B() { + Double a = 0.0; + Double b = -0.0 + 0.0; + out.println(a.equals(b)); // true + } + + static void C() { + double a = 0.0; + double b = -0.0; + out.println(a == b); // true + } + + + public static void main(String[] args) { + A(); + B(); + C(); + out.close(); + } +} +``` +## 参考资料 + +[^ref1]: [Input & Output - USACO Guide](https://usaco.guide/general/input-output?lang=java#method-3---io-template) diff --git "a/docs/\350\256\241\347\256\227\346\234\272\347\247\221\345\255\246\345\255\246\344\271\240\350\267\257\347\272\277/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\274\226\347\250\213\345\237\272\347\241\200/Python/introduction_python.md" "b/docs/\350\256\241\347\256\227\346\234\272\347\247\221\345\255\246\345\255\246\344\271\240\350\267\257\347\272\277/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\274\226\347\250\213\345\237\272\347\241\200/Python/introduction_python.md" new file mode 100644 index 0000000..c740ee8 --- /dev/null +++ "b/docs/\350\256\241\347\256\227\346\234\272\347\247\221\345\255\246\345\255\246\344\271\240\350\267\257\347\272\277/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\274\226\347\250\213\345\237\272\347\241\200/Python/introduction_python.md" @@ -0,0 +1,1043 @@ +## 关于 Python + +Python 是一门已在世界上广泛使用的解释型语言。它提供了高效的高级数据结构,还能简单有效地面向对象编程,也可以在算法竞赛。 + +### Python 的优点 + +- Python 是一门 **解释型** 语言:Python 不需要编译和链接,可以在一定程度上减少操作步骤。 +- Python 是一门 **交互式** 语言:Python 解释器实现了交互式操作,可以直接在终端输入并执行指令。 +- Python **易学易用**:Python 提供了大量的数据结构,也支持开发大型程序。 +- Python **兼容性强**:Python 同时支持 Windows、macOS 和 Unix 操作系统。 +- Python **实用性强**:从简单的输入输出到科学计算甚至于大型 WEB 应用,都可以写出适合的 Python 程序。 +- Python **程序简洁、易读**:Python 代码通常比实现同种功能的其他语言的代码短。 +- Python **支持拓展**:Python 会开发 C 语言程序(即 CPython),支持把 Python 解释器和用 C 语言开发的应用链接,用 Python 扩展和控制该应用。 + +### 学习 Python 的注意事项 + +- 目前主要使用的 Python 版本是 Python 3.7 及以上的版本,Python 2 和 Python 3.6 及以前的 Python 3 已经 [不被支持](https://devguide.python.org/versions/#unsupported-versions),但仍被一些老旧系统与代码所使用。本文将 **介绍较新版本的 Python**。如果遇到 Python 2 代码,可以尝试 [`2to3`](https://docs.python.org/zh-cn/3/library/2to3.html) 程序将 Python 2 代码转换为 Python 3 代码。 +- Python 的设计理念和语法结构 **与一些其他语言的差异较大**,隐藏了许多底层细节,所以呈现出实用而优雅的风格。 +- Python 是高度动态的解释型语言,因此其 **程序运行速度相对较慢**,尤其在使用其内置的 `for` 循环语句时。在使用 Python 时,应尽量使用 `filter`、`map` 等内置函数,或使用 [列表生成](https://www.pythonforbeginners.com/basics/list-comprehensions-in-python) 语法的手段来提高程序性能。 + +## 环境搭建 + +参见 [Python 3](../tools/compiler.md#python-3)。或者: + +- Windows:也可以在 Microsoft Store 中免费而快捷地获取 Python。 +- macOS/Linux:通常情况下,大部分的 Linux 发行版中已经自带了 Python。如果只打算学习 Python 语法,并无其它开发需求,不必另外安装 Python。 + + ???+ warning "注意" + 在一些默认安装(指使用软件包管理器安装)Python 的系统(如 Unix 系统)中,应在终端中运行 `python3` 打开 Python 3 解释器。[^ref1] + +此外,也可以通过 venv、conda、Nix 等工具管理 Python 工具链和 Python 软件包,创建隔离的虚拟环境,避免出现依赖问题。 + +作为一种解释型语言,Python 的执行方式和 C++ 有所不同,这种差异在使用 IDE 编程时往往得不到体现,因此这里需要强调一下运行程序的不同方式。 + +当在命令行中键入 `python3` 或刚刚打开 IDLE 时,你实际进入了一种交互式的编程环境,也称「REPL」(「读取 - 求值 - 输出」循环),初学者可以在这里输入语句并立即看到结果,这让验证一些语法变得极为容易,我们也将在后文中大量使用这种形式。 + +但若要编写完整的程序,你最好还是新建一个文本文件(通常后缀为 `.py`),然后在命令行中执行 `python3 filename.py`,就能够运行代码看到结果了。 + +### 通过镜像下载安装文件 + +目前国内关于 **源码** 的镜像缓存主要是 [北京交通大学自由与开源软件镜像站](https://mirror.bjtu.edu.cn/python/) 和 [华为开源镜像站](https://repo.huaweicloud.com/python/),可以到那里尝试下载 Python 安装文件。 + +## 使用 `pip` 安装第三方库 + +Python 的生命力很大程度上来自于丰富的第三方库,编写一些实用程序时「调库」是常规操作,`pip` 是首选的安装第三方库的程序。自 Python 3.4 版本起,它被默认包含在 Python 二进制安装程序中。 + +`pip` 中的第三方库主要存储在 [Python 包索引(PyPI)](https://pypi.org/) 上,用户也可以指定其它第三方库的托管平台。使用方法可参照 [pypi 镜像使用帮助 - 清华大学开源软件镜像站](https://mirrors.tuna.tsinghua.edu.cn/help/pypi/) 等使用帮助。你可以在 [MirrorZ](https://mirrorz.org/list/pypi) 上获取更多 PyPI 镜像源。 + +## 基本语法 + +Python 的语法简洁而易懂,也有许多官方和第三方文档与教程。这里仅介绍一些对 OIer 比较实用的语言特性,你可以在 [Python 文档](https://docs.python.org/zh-cn/3/) 和 [Python Wiki](https://wiki.python.org/moin/) 等网页上了解更多关于 Python 的教程。 + +### 注释 + +加入注释并不会对代码的运行产生影响,但加入注释可以使代码更加易懂易用。 + +```python +# 用 # 字符开头的是单行注释 + +""" +跨多行字符串会用三引号 +(即三个单引号或三个双引号) +包裹,但也通常被用于注释 +""" +``` + +加入注释代码并不会对代码产生影响。我们鼓励加入注释来使代码更加易懂易用。 + +### 基本数据类型 + +#### 一切皆对象 + +在 Python 中,你无需事先声明变量名及其类型,直接赋值即可创建各种类型的变量: + +```pycon +>>> x = -3 # 语句结尾不用加分号 +>>> x +-3 +>>> f = 3.1415926535897932384626; f # 实在想加分号也可以,这里节省了一行 +3.141592653589793 +>>> s1 = "O" +>>> s1 # 怎么显示成单引号了?有区别吗? +'O' +>>> b = 'A' == 65 # 明明在 C/C++ 中是成立的 +>>> b # 与众不同的是 True, False 首字母均大写,可能与内置常量的命名约定有关 +False +>>> True + 1 == 2 and not False != 0 # Python 可能喜欢单词胜过符号 +True +``` + +但这不代表 Python 没有类型的概念,实际上解释器会根据赋值或运算自动推断变量类型,你可以使用内置函数 `type()` 查看这些变量的类型: + +```pycon +>>> type(x) + +>>> type(f) + +>>> type(s1) # 请注意,不要给字符串起名为 str,不信试试看是否真的可以这么做 + +>>> type(b) + +``` + +???+ note "[**内置函数**](https://docs.python.org/zh-cn/3/library/functions.html) 是什么?" + 在 C/C++ 中,很多常用函数都分散在不同的头文件中,但 Python 的解释器内置了许多实用且通用的函数,你可以直接使用而无需注意它们的存在,但这也带来了小问题,这些内置函数的名称多为常见单词,你需要注意避免给自己的变量起相同的名字,否则可能会产生奇怪的结果。 + +正如我们所看到的,Python 内置有整数、浮点数、字符串和布尔类型,可以类比为 C++ 中的 `int`,`float`,`string` 和 `bool`。但有一些明显的不同之处,比如没有 `char` 字符类型,也没有 `double` 类型(但 `float` 其实对应 C 中的双精度),如果需要更精确的浮点运算,可以使用标准库中的 [decimal](https://docs.python.org/zh-cn/3/library/decimal.html) 模块,如果需要用到复数,Python 还内置了 `complex` 类型(而这也意味着最好不要给变量起名为 `complex`)。 +可以看到这些类型都以 `class` 开头,而这正是 Python 不同于 C++ 的关键之处,Python 程序中的所有数据都是由对象或对象间关系来表示的,函数是对象,类型本身也是对象: + +```pycon +>>> type(int) + +>>> type(pow) # 求幂次的内置函数,后文会介绍 + +>>> type(type) # type() 也是内置函数,但有些特殊,感兴趣可自行查阅 + +``` + +你或许会觉得这些概念一时难以理解且没有用处,所以我们暂时不再深入,在后文的示例中你或许能慢慢体会到,Python 的对象提供了强大的方法,我们在编程时应当优先考虑围绕对象而不是过程进行操作,这会让我们的代码显得更加紧凑明晰。 + +#### 数字运算 + +有人说,你可以把你系统里装的 Python 当作一个多用计算器,这是事实。 +在交互模式下,你可以在提示符 `>>>` 后面输入一个表达式,就像其他大部分语言(如 C++)一样使用运算符 `+`、`-`、`*`、`/`、`%` 来对数字进行运算,也可以使用 `()` 来进行符合结合律的分组,读者可以自行试验,在这里我们仅展示与 C++ 差异较大的部分: + +```pycon +>>> 5.0 * 6 # 浮点数的运算结果是浮点数 +30.0 +>>> 15 / 3 # 与 C/C++ 不同,除法永远返回浮点 float 类型 +5.0 +>>> 5 / 100000 # 位数太多,结果显示成科学计数法形式 +5e-05 +>>> 5 // 3 # 使用整数除法(地板除)则会向下取整,输出整数类型 +1 +>>> -5 // 3 # 符合向下取整原则,注意这与 C/C++ 不同 +-2 +>>> 5 % 3 # 取模 +2 +>>> -5 % 3 # 负数取模结果一定是非负数,这点也与 C/C++ 不同,不过都满足 (a//b)*b+(a%b)==a +1 +>>> x = abs(-1e4) # 求绝对值的内置函数 +>>> x += 1 # 没有自增/自减运算符 +>>> x # 科学计数法默认为 float +10001.0 +``` + +在上面的实践中可以发现,除法运算(`/`)永远返回浮点类型(在 Python 2 中返回整数)。如果你想要整数或向下取整的结果的话,可以使用整数除法(`//`)。同样的,你也可以像 C++ 中一样,使用模(`%`)来计算余数,科学计数法的形式也相同。 + +特别地,Python 用 `**` 即可进行幂运算,还通过内置的 `pow(a, b, mod)` 提供了 [快速幂](../math/binary-exponentiation.md) 的高效实现。 + +Python 的字符串类型包含 Unicode 字符,这意味着任何字符串都会存储为 Unicode。[^ref2]在 Python 中,可以对一个 Unicode 字符使用内置函数 `ord()` 将其转换为对应的 Unicode 编码,逆向的转换使用内置函数 `chr()`。 + +如果想把数转换为对应的字符串,可使用 Python 内置函数 `str()`,也可以使用 f-string 实现;反之,可以使用 `int()` 和 `float()` 两个函数。 + +Python 的字符串类型还有 [许多方便的功能](https://docs.python.org/zh-cn/3/library/stdtypes.html#text-sequence-type-str)。由于本文篇幅有限,这里不一一介绍。 + +#### 数据类型判断 + +对于一个变量,可以使用 `type(object)` 返回变量的类型,例如 `type(8)` 和 `type('a')` 的值分别为 `` 和 ``。 + +### 输出和输入 + +#### 输出 + +对于一个变量,可以使用 `type(object)` 返回变量的类型,例如 `type(8)` 和 `type('a')` 的值分别为 `` 和 ``。 + +Python 中,还可以使用 `**` 运算符和内置的 `pow(base, exp, mod=None)` 函数进行幂运算,使用 `abs(x)` 求数的绝对值。 + +```pycon +>>> 3 ** 4 # 幂运算 +81 +>>> 2 ** 512 +13407807929942597099574024998205846127479365820592393377723561443721764030073546976801874298166903427690031858186486050853753882811946569946433649006084096 +>>> pow(2, 512, int(1e4)) # 即 2**512 % 10000 的快速实现, 1e4 是 float 所以要转 int +4096 +>>> 2048 ** 2048 # 在IDLE里试试大整数? +>>> 0.1 + 0.1 + 0.1 - 0.3 == 0. # 和 C/C++ 一样需要注意浮点数不能直接判相等 +False +``` + +#### 字符串 + +Python 3 提供了强大的基于 [Unicode](https://docs.python.org/zh-cn/3/howto/unicode.html#unicode-howto) 的字符串类型,使用起来和 C++ 中的 string 类似,一些概念如转义字符也都相通,除了加号拼接和索引访问,还额外支持数乘 `*` 重复字符串,和 `in` 操作符。 + +```pycon +>>> s1 = "O" # 单引号和双引号都能包起字符串,有时可节省转义字符 +>>> s1 += 'I-Wiki' # 为和 C++ 同步建议使用双引号 +>>> 'OI' in s1 # 检测子串很方便 +True +>>> len(s1) # 类似 C++ 的 s.length(),但更通用 +7 +>>> s2 = """ 感谢你的阅读 +... 欢迎参与贡献! +""" # 使用三重引号的字符串可以跨越多行 +>>> s1 + s2 +'OI-Wiki 感谢你的阅读\n欢迎参与贡献!' +>>> print(s1 + s2) # 这里使用了 print() 函数打印字符串 +OI-Wiki 感谢你的阅读 +欢迎参与贡献! +>>> s2[2] * 2 + s2[3] + s2[-1] # 负数索引从右开始计数,加上len(s),相当于模n的剩余类环 +'谢谢你!' +>>> s1[0] = 'o' # str 是不可变类型,不能原地修改,其实 += 也是创建了新的对象 +Traceback (most recent call last): + File "", line 1, in +TypeError: 'str' object does not support item assignment +``` + +Python 支持多种复合数据类型,可将不同值组合在一起。最常用的 `list`,类型是用方括号标注、逗号分隔的一组值。例如,`[1, 2, 3]` 和 `['a','b','c']` 都是列表。 + +除了索引,字符串还支持*切片*,它的设计非常精妙又符合直觉,格式为 `s[左闭索引:右开索引:步长]`: + +```pycon +>>> s = 'OI-Wiki 感谢你的阅读\n欢迎参与贡献!' +>>> s[:8] # 省略左闭索引则从头开始 +'OI-Wiki ' +>>> s[8:14] # 左闭右开设计的妙处,长度恰好为14-8=6,还和上一个字符串无缝衔接 +'感谢你的阅读' +>>> s[-4:] # 省略右开索引则直到结尾 +'与贡献!' +>>> s[8:14:2] # 步长为2 +'感你阅' +>>> s[::-1] # 步长为 -1 时,获得了反转的字符串 +'!献贡与参迎欢\n读阅的你谢感 ikiW-IO' +>>> s # 但原来的字符串并未改变 +'OI-Wiki 感谢你的阅读\n欢迎参与贡献!' +``` + +C/C++ 中 `char` 类型可以和 对应的 ASCII 码互转,而在 Python 中你可以对一个 Unicode 字符使用内置函数 `ord()` 将其转换为对应的 Unicode 编码,逆向的转换使用内置函数 `chr()`。 + +如果想把数字转换成对应的字符串,可以使用内置函数 `str()`,反之可以使用 `int()` 和 `float()`,你可以类比为 C/C++ 中的强制类型转换,但括号不是加在类型上而是作为函数的一部分括住参数。 + +Python 的字符串类型提供了许多强大的方法,包括计算某字符的索引与出现次数,转换大小写等等,这里就不一一列举,强烈建议查看 [官方文档](https://docs.python.org/zh-cn/3/library/stdtypes.html#text-sequence-type-str) 熟悉常用方法,遇到字符串操作应当首先考虑使用这些方法而非自力更生。 + +### 开数组 + +从 C++ 转过来的同学可能很迷惑怎么在 Python 中开数组,这里就介绍在 Python 开「数组」的语法,需要强调我们介绍的其实是几种 [序列类型](https://docs.python.org/zh-cn/3/library/stdtypes.html#iterator-types),和 C 的数组有着本质区别,而更接近 C++ 中的 `vector`。 + +#### 使用 `list` + +列表(`list`)大概是 Python 中最常用也最强大的序列类型,列表中可以存放任意类型的元素,包括嵌套的列表,这符合数据结构中「广义表」的定义。请注意不要将其与 C++ STL 中的双向链表 [`list`](./csl/sequence-container.md#list) 混淆,故本文将使用「列表」而非 `list` 以免造成误解。 + +```pycon +>>> [] # 创建空列表,注意列表使用方括号 +[] +>>> nums = [0, 1, 2, 3, 5, 8, 13]; nums # 初始化列表,注意整个列表可以直接打印 +[0, 1, 2, 3, 5, 8, 13] +>>> nums[0] = 1; nums # 支持索引访问,还支持修改元素 +[1, 1, 2, 3, 5, 8, 13] +>>> nums.append(nums[-2]+nums[-1]); nums # append() 同 vector 的 push_back(),也都没有返回值 +[1, 1, 2, 3, 5, 8, 13, 21] +>>> nums.pop() # 弹出并返回末尾元素,可以当栈使用;其实还可指定位置,默认是末尾 +21 +>>> nums.insert(0, 1); nums # 同 vector 的 insert(position, val) +[1, 1, 1, 2, 3, 5, 8, 13] +>>> nums.remove(1); nums # 按值移除元素(只删第一个出现的),若不存在则抛出错误 +[1, 1, 2, 3, 5, 8, 13] +>>> len(nums) # 求列表长度,类似 vector 的 size(),但 len() 是内置函数 +7 +>>> nums.reverse(); nums # 原地逆置 +[13, 8, 5, 3, 2, 1, 1] +>>> sorted(nums) # 获得排序后的列表 +[1, 1, 2, 3, 5, 8, 13] +>>> nums # 但原来的列表并未排序 +[13, 8, 5, 3, 2, 1, 1] +>>> nums.sort(); nums # 原地排序,可以指定参数 key 作为排序标准 +[1, 1, 2, 3, 5, 8, 13] +>>> nums.count(1) # 类似 std::count() +2 +>>> nums.index(1) # 返回值首次出现项的索引号,若不存在则抛出错误 +0 +>>> nums.clear(); nums # 同 vector 的 clear() +``` + +以上示例展现了列表与 `vector` 的相似之处,`vector` 中常用的操作一般也都能在列表中找到对应方法,不过某些方法如 `len()`,`sorted()` 会以内置函数的面目出现,而 STL 算法中的函数如 `find()`,`count()`,`max_element()`,`sort()`,`reverse()` 在 Python 中又成了对象的方法,使用时需要注意区分,更多方法请参见官方文档的 [列表详解](https://docs.python.org/zh-cn/3/tutorial/datastructures.html#more-on-lists)。下面将展示列表作为 Python 的基本序列类型的一些强大功能: + +Python 支持多种复合数据类型,可将不同值组合在一起。最常用的 `list`,类型是用方括号标注、逗号分隔的一组值。例如,`[1, 2, 3]` 和 `['a','b','c']` 都是列表。 + +```pycon +>>> lst = [1, '1'] + ["2", 3.0] # 列表直接相加生成一个新列表 +>>> lst # 这里存放不同的类型只是想说明可以这么做,但这不是好的做法 +[1, '1', '2', 3.0] +>>> 3 in lst # 实用的成员检测操作,字符串也有该操作且还支持子串检测 +True +>>> [1, '1'] in lst # 仅支持单个成员检测,不会发现「子序列」 +False +>>> lst[1:3] = [2, 3]; lst # 切片并赋值,原列表被修改 +[1, 2, 3, 3.0] +>>> lst[::-1] # 获得反转后的新列表 +[3.0, 3, 2, 1] +>>> lst *= 2; lst # 数乘拼接 +[1, 2, 3, 3.0, 1, 2, 3, 3.0] +>>> del lst[4:]; lst # 也可写 lst[4:] = [],del 语句不止可以用于删除序列中元素 +[1, 2, 3, 3.0] +``` + +以上示例展现了列表作为序列的一些常用操作,可以看出许多操作如切片是与字符串相通的,但字符串是「不可变序列」而列表是「可变序列」,故可以通过切片灵活地修改列表。在 C/C++ 中我们往往会通过循环处理字符数组,下面将展示如何使用 [「列表推导式」](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions) 在字符串和列表之间转换: + +```pycon +>>> # 建立一个 [65, 70) 区间上的整数数组,range 也是一种类型,可看作左闭右开区间,第三个参数为步长可省略 +>>> nums = list(range(65,70)) # 记得 range 外面还要套一层 list() +[65, 66, 67, 68, 69] +>>> lst = [chr(x) for x in nums] # 列表推导式的典型结构,[exp for var in iterable if cond] +>>> lst # 上两句可以合并成 [chr(x) for x in range(65,70)] +['A', 'B', 'C', 'D', 'E'] +>>> s = ''.join(lst); s # 用空字符串 '' 拼接列表中的元素生成新字符串 +'ABCDE' +>> list(s) # 字符串生成字符列表 +['A', 'B', 'C', 'D', 'E'] +>>> # 如果你不知道有 s.lower() 方法就可能写出下面这样新瓶装旧酒的表达式 +>>> ''.join([chr(ord(ch) - 65 + 97) for ch in s if ch >= 'A' and ch <= 'Z']) +'abcde' +``` + +下面演示一些在 OI 中更常见的场景,比如二维「数组」: + +```pycon +>>> vis = [[0] * 3] * 3 # 开一个3*3的全0数组 +>>> vis +[[0, 0, 0], [0, 0, 0], [0, 0, 0]] +>>> vis[0][0] = 1; vis # 怎么会把其他行也修改了? +[[1, 0, 0], [1, 0, 0], [1, 0, 0]] +>>> # 先来看下一维列表的赋值 +>>> a1 = [0, 0, 0]; a2 = a1; a3 = a1[:] # 列表也可以直接被赋给新的变量 +>>> a1[0] = 1; a1 # 修改列表 a1,似乎正常 +[1, 0, 0] +>>> a2 # 怎么 a2 也被改变了 +[1, 0, 0] +>>> a3 # a3 没有变化 +[0, 0, 0] +>>> id(a1) == id(a2) and id(a1) != id(a3) # 内置函数 id() 给出对象的「标识值」,可类比为地址,地址相同说明是一个对象 +True +>>> vis2 = vis[:]; # 拷贝一份二维列表看看 +>>> vis[0][1] = 2; vis # vis 肯定还是被批量修改 +>>> [[1, 2, 0], [1, 2, 0], [1, 2, 0]] +>>> vis2 # 但 vis2 是切片拷贝的怎么还是被改了 +>>> [[1, 2, 0], [1, 2, 0], [1, 2, 0]] +>>> id(vis) != id(vis2) # vis 和 vis2 确实不是一个对象啊 +True +>>> # 谜底揭晓,vis2 虽然不是 vis 的引用,但其中对应行都指向相同的对象 +>>> [[id(vis[i]) == id(vis2[i]) for i in range(3)] +[True, True, True] +>>> # 回看二维列表自身 +>>> [id(x) for x in vis] # 具体数字和这里不一样但三个值一定相同,说明是三个相同对象 +[139760373248192, 139760373248192, 139760373248192] +``` + +其实我们一直隐瞒了一个重要事实,Python 中赋值只传递了引用而非创建新值,你可以创建不同类型的变量并赋给新变量,验证发现二者的标识值是相同的,只不过直到现在我们才介绍了列表这一种可变类型,而给数字、字符串这样的不可变类型赋新值时实际上创建了新的对象,故而前后两个变量互不干扰。但列表是可变类型,所以我们修改一个列表的元素时,另一个列表由于指向同一个对象所以也被修改了。创建二维数组也是类似的情况,示例中用乘法创建二维列表相当于把 `[0]*3` 这个一维列表重复了 3 遍,所以涉及其中一个列表的操作会同时影响其他两个列表。更不幸的是,在将二维列表赋给其他变量的时候,就算用切片来拷贝,也只是「浅拷贝」,其中的元素仍然指向相同的对象,解决这个问题需要使用标准库中的 [`deepcopy`](https://docs.python.org/3/library/copy.html),或者尽量避免整个赋值二维列表。不过还好,创建二维列表时避免创建重复的列表还是比较简单,只需使用「列表推导式」: + +```pycon +>>> vis1 = [[0] * 3 for _ in range(3)] # 把用不到的循环计数变量设为下划线 _ 是一种惯例 +>>> # 但在 REPL 中 _ 默认指代上一个表达式输出的结果,故也可使用双下划线 +>>> vis1 +[[0, 0, 0], [0, 0, 0], [0, 0, 0]] +>>> [id(x) for x in vis1] # 具体数字和这里不一样但三个值一定不同,说明是三个不同对象 +[139685508981248, 139685508981568, 139685508981184] +>>> vis1[0][0] = 1 +[[1, 0, 0], [0, 0, 0], [0, 0, 0]] +>>> a2[0][0] = 10 # 访问和赋值二维数组 +``` + +我们未讲循环的用法就先介绍了列表推导式,这是由于 Python 是高度动态的解释型语言,因此其程序运行有大量的额外开销。尤其是 **for 循环在 Python 中运行的奇慢无比**。因此在使用 Python 时若想获得高性能,尽量使用使用列表推导式,或者 `filter`,`map` 等内置函数直接操作整个序列来避免循环,当然这还是要根据具体问题而定。 + +#### 使用 NumPy + +??? note "什么是 NumPy" + [NumPy](https://numpy.org/) 是著名的 Python 科学计算库,提供高性能的数值及矩阵运算。在测试算法原型时可以利用 NumPy 避免手写排序、求最值等算法。NumPy 的核心数据结构是 `ndarray`,即 n 维数组,它在内存中连续存储,是定长的。此外 NumPy 核心是用 C 编写的,运算效率很高。不过需要注意,它不是标准库的一部分,可以使用 `pip install numpy` 安装,但不保证 OI 考场环境中可用。 + +下面的代码将介绍如何利用 NumPy 建立多维数组并进行访问。 + +```pycon +>>> import numpy as np # 请自行搜索 import 的意义和用法 +>>> np.empty(3) # 开容量为3的空数组,注意没有初始化为0 +array([0.00000000e+000, 0.00000000e+000, 2.01191014e+180]) +>>> np.zeros((3, 3)) # 开 3*3 的数组,并初始化为0 +array([[0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.]]) +>>> a1 = np.zeros((3, 3), dtype=int) # 开3×3的整数数组 +>>> a1[0][0] = 1 # 访问和赋值 +>>> a1[0, 0] = 1 # 更友好的语法 +>>> a1.shape # 数组的形状 +(3, 3) + +>>> a1[:2, :2] # 取前两行、前两列构成的子阵,无拷贝 +array([[1, 0], + [0, 0]]) + +>>> a1[0, 2] # 获取第 1、3 列,无拷贝 +array([[1, 0], + [0, 0], + [0, 0]]) +>>> np.max(a1) # 获取数组最大值 +1 +>>> a1.flatten() # 将数组展平 +array([1, 0, 0, 0, 0, 0, 0, 0, 0]) + +>>> np.sort(a1, axis = 1) # 沿行方向对数组进行排序,返回排序结果 +array([[0, 0, 1], + [0, 0, 0], + [0, 0, 0]]) +>>> a1.sort(axis = 1) # 沿行方向对数组进行原地排序 +``` + +#### 使用 `array` + +[`array`](https://docs.python.org/zh-cn/3/library/array.html) 是 Python 标准库提供的一种高效数值数组,可以紧凑地表示基本类型值的数组,但不支持数组嵌套,也很少见到有人使用它,这里只是顺便提一下。 + +若无特殊说明,后文出现「数组」一般指「列表」。 + +### [输入输出](https://docs.python.org/3/tutorial/inputoutput.html) + +Python 中的输入输出主要通过内置函数 `input()` 和 `print()` 完成,`print()` 的用法十分符合直觉: + +```pycon +>>> a = [1,2,3]; print(a[-1]) # 打印时默认末尾换行 +3 +>>> print(ans[0], ans[1]) # 可以输出任意多个变量,默认以空格间隔 +1 2 +>>> print(a[0], a[1], end='') # 令 end='', 使末尾不换行 +1 2>>> +>>> print(a[0], a[1], sep=', ') # 令 sep=', ',改变间隔样式 +1, 2 +>>> print(str(a[0]) + ', ' + str(a[1])) # 输出同上,但是手动拼接成一整个字符串 +``` + +算法竞赛中通常只涉及到基本的数值和字符串输出,以上用法基本足够,只有当涉及到浮点数位数时需要用到格式化字符串输出。格式化有三种方法,第一种也是最老旧的方法是使用 `printf()` 风格的 `%` 操作符;另一种是利用 [`format` 函数](https://docs.python.org/3/library/string.html#formatstrings),写起来比较长;第三种是 Python 3.6 新增的 [f-string](https://docs.python.org/zh-cn/3/tutorial/inputoutput.html#formatted-string-literals),最为简洁,但不保证考场中的 Python 版本足够新。详细丰富的说明可以参考 [这个网页](https://www.python-course.eu/python3_formatted_output.php),尽管更推荐使用 `format()` 方法,但为了获得与 C 接近的体验,下面仅演示与 `printf()` 类似的老式方法: + +```pycon +>>> pi = 3.1415926; print('%.4f' % pi) # 格式为 %[flags][width][.precision]type +3.1416 +>>> '%.4f - %8f = %d' % (pi, 0.1416, 3) # 右边多个参数用 () 括住,后面会看到其实是「元组」 +'3.1416 - 0.141600 = 3' +``` + +`input()` 函数的行为接近 C++ 中的 `getline()`,即将一整行作为字符串读入,且末尾没有换行符,但在算法竞赛中,常见的输入形式是一行输入多个数值,因此就需要使用字符串的 `split()` 方法并搭配列表推导式得到存放数值类型的列表,下面以输入 n 个数求平均值为例演示输入 n 个数得到「数组」的方法: + +```pycon +>>> s = input('请输入一串数字: '); s # 自己调试时可以向 input() 传入字符串作为提示 +请输入一串数字: 1 2 3 4 5 6 +'1 2 3 4 5 6' +>>> a = s.split(); a +['1', '2', '3', '4', '5', '6'] +>>> a = [int(x) for x in a]; a +[1, 2, 3, 4, 5, 6] +>>> # 以上输入过程可写成一行 a = [int(x) for x in input().split()] +>>> sum(a) / len(a) # sum() 是内置函数 +3.5 +``` + +有时题目会在每行输入固定几个数,比如边的起点、终点、权重,如果只用上面提到的方法就只能每次读入数组然后根据下标赋值,这时可以使用 Python 的「拆包」特性一次赋值多个变量: + +```pycon +>>> u, v, w = [int(x) for x in input().split()] +1 2 4 +>>> print(u,v,w) +1 2 4 +``` + +题目中经常遇到输入 N 行的情况,可我们还没有讲最基本的循环语句,但 Python 强大的序列操作能在不使用循环的情况下应对多行输入,下面假设将各条边的起点、终点、权值分别读入三个数组: + +```pycon +>>> N = 4; mat = [[int(x) for x in input().split()] for i in range(N)] +1 3 3 +1 4 1 +2 3 4 +3 4 1 +>>> mat # 先按行读入二维数组 +[[1, 3, 3], [1, 4, 1], [2, 3, 4], [3, 4, 1]] +>>> u, v, w = map(list, zip(*mat)) +# *将 mat 解包得到里层的多个列表 +# zip() 将多个列表中对应元素聚合成元组,得到一个迭代器 +# map(list, iterable) 将序列中的元素(这里为元组)转成列表 +>>> print(u, v, w) # 直接将 map() 得到的迭代器拆包,分别赋值给 u, v, w +[1, 1, 2, 3] [3, 4, 3, 4] [3, 1, 4, 1] +``` + +上述程序实际上相当于先读入一个 N 行 3 列的矩阵,然后将其转置成 3 行 N 列的矩阵,也就是外层列表中嵌套了 3 个列表,最后将代表这起点、终点、权值的 3 个列表分别赋值给 u, v, w。内置函数 [`zip()`](https://docs.python.org/zh-cn/3/library/functions.html#zip) 可以将多个等长序列中的对应元素拼接在「元组」内,得到新序列。而 `map()` 其实是函数式编程的一种操作,它将一个给定函数作用于 `zip()` 所产生序列的元素,这里就是用 `list()` 将元组变成列表。你可以自行练习使用 `*` 和 [`zip()`](https://docs.python.org/zh-cn/3/library/functions.html#zip),[`map()`](https://docs.python.org/zh-cn/3/library/functions.html#map) 以理解其含义。需要注意的是 Python 3 中 `zip()` 和 `map()` 创建的不再返回列表而是返回迭代器,这里暂不解释它们之间的异同,你可以认为迭代器可以产生列表中的各个元素,用 `list()` 套住迭代器就能生成列表。 + +### [控制流程](https://docs.python.org/zh-cn/3/tutorial/controlflow.html) + +尽管我们已经学习了 Python 的许多特性,但到目前为止我们展示的 Python 代码都是单行语句,这掩盖了 Python 和 C 在代码风格上的重大差异:首先,Python 中不用 `{}` 而是用缩进表示块结构,如果缩进没有对齐会直接报错,如果 tab 和 空格混用也会报错;其次,块结构开始的地方比如 `if` 和 `for` 语句的行末要有冒号 `:`。这有助于代码的可读性,但你也可能怀念 C 那种自由的体验,毕竟如果复制粘贴时因为丢失缩进而不得不手动对齐是很恼人的。 + +#### 循环结构 + +列表推导式能在一行内高效地完成批量操作,但有时为了压行我们已经显得过分刻意,许多场景下还是只能使用循环结构,所以我们再以读入多行数据为例展示 Python 中的循环是如何编写的: + +```python +# 请注意从现在开始我们不再使用 REPL,请自行复制多行数据 +u, v, w = ([] for i in range(3)) # 多变量赋值 +for i in range(4): # 这里假设输入 4 行数据 + _u, _v, _w = [int(x) for x in input().split()] + u.append(_u), v.append(_v), w.append(_w) + # 不可进行类似 cin >> u[i] >> v[i] >> w[i] 的操作,因为必定超出列表当前的长度 + # 当然你可以选择初始化长度为 MAXN 的全 0 列表,不过需要记住真实长度并删掉多余元素 +print(u, v, w) +``` + +需要注意,Python 中的 for 循环和 C/C++ 有较大的差别,其作用类似 C++ 11 引入的 [「基于范围的循环」](./new.md#基于范围的-for-循环),实质是迭代序列中的元素,比如编写循环遍历数组下标需要迭代 `range(len(lst))`,而非真正定义起始和终止条件,所以使用起来并没有 C/C++ 灵活。 + +下面再用 while 循环展示行数不定的情况下如何输入: + +```python +u, v, w = [], [], [] # 多变量赋值,其实同上 +s = input() # 注意 Python 中赋值语句不能放在条件表达式中 +while s: # 不能像 C 那样 while(!scanf()) + # 用切片拼接避免了 append(),注意列表推导式中又嵌套了列表 + u[len(u):], v[len(v):], w[len(w):] = [[int(x)] for x in s.split()] + s = input() +# Python 3.8 引入了 walrus operator 海象运算符后,你可以节省两行,但考场环境很可能不支持 +while s := input(): + u[len(u):], v[len(v):], w[len(w):] = [[int(x)] for x in s.split()] +print(u, v, w) +``` + +#### 选择结构 + +和 C/C++ 大同小异,一些形式上的差别都在下面的示例中有所展示,此外还需注意条件表达式中不允许使用赋值运算符(Python 3.8 以上可用 [`:=`](https://www.python.org/dev/peps/pep-0572/)),以及 [没有 switch 语句](https://docs.python.org/zh-cn/3/faq/design.html#why-isn-t-there-a-switch-or-case-statement-in-python)。 + +```python +# 条件表达式两侧无括号 +if 4 >= 3 > 2 and 3 != 5 == 5 != 7: + print("关系运算符可以连续使用") + x = None or [] or -2 + print("&& || !", "与 或 非", "and or not", sep='\n') + print("善用 and/or 可节省行数") + if not x: + print("负数也是 True,不执行本句") + elif x & 1: + print("用 elif 而不是 else if\n" + "位运算符与 C 相近,偶数&1 得 0,不执行本句") + else: + print("也有三目运算符") if x else print("注意结构") +``` + +#### 异常处理 + +尽管 C++ 中有 [try 块](https://zh.cppreference.com/w/cpp/language/try_catch) 用于异常处理,但竞赛中一般从不使用,而 Python 中常见的是 [EAFP](https://docs.python.org/zh-cn/3/glossary.html#term-eafp) 风格,故而代码中可能大量使用 [`try-except`](https://docs.python.org/zh-cn/3/reference/compound_stmts.html#the-try-statement) 语句,在后文介绍 `dict` 这一结构时还会用到,这里展示: + +```python +s = "OI-wiki" +pat = "NOIP" +x = s.find(pat) # find() 找不到返回 -1 +try: + y = s.index(pat) # index() 找不到则抛出错误 + print(y) # 这句被跳过 +except ValueError: + print("没找到") + try: + print(y) # 此时 y 并没有定义,故又会抛出错误 + except NameError as e: + print("无法输出 y") + print("原因:", e) +``` + +#### [文件读写](https://docs.python.org/3/reference/compound_stmts.html#the-with-statement) + +Python 内置函数 [`open()`](https://docs.python.org/3/library/functions.html#open) 用于文件读写,为了防止读写过程中出错导致文件未被正常关闭,这里只介绍使用 [`with`](https://docs.python.org/3/reference/compound_stmts.html#the-with-statement) 语句的安全读写方法: + +```python +a = [] +with open('in.txt') as f: + N = int(f.readline()) # 读入第一行的 N + a[len(a):] = [[int(x) for x in f.readline().split()] for i in range(N)] + +with open('out.txt', 'w') as f: + f.write('1\n') +``` + +关于文件读写的函数有很多,分别适用于不同的场景,由于 OI 赛事尚不支持使用 Python,这里从略。 + +### 内置容器 + +Python 内置了许多强大的容器类型,只有熟练使用并了解其特点才能真正让 Python 在算法竞赛中有用武之地,除了上面详细介绍的 `list`(列表),还有 `tuple`(元组)、[`dict`](https://docs.python.org/zh-cn/3/library/stdtypes.html#mapping-types-dict)(字典)和 `set`(集合)这几种类型。 + +元组可以简单理解成不可变的列表,不过还需注意「不可变」的内涵,如果元组中的某元素是可变类型比如列表,那么仍可以修改该列表的值,元组中存放的是对列表的引用所以元组本身并没有改变。元组的优点是开销较小且「[可哈希](https://docs.python.org/zh-cn/3/glossary.html)」,后者在创建字典和集合时非常有用。 + +```python +tup = tuple([[1,2], 4]) # 由列表得到元组 +# 等同于 tup = ([1,2], 4) +tup[0].append(3) +print(tup) +a, b = 0, "I-Wiki" # 多变量赋值其实是元组拆包 +print(id(a), id(b)) +b, a = a, b +print(id(a), id(b)) # 你应该会看到 a, b 的 id 值现在互换了 +# 这更说明 Python 中,变量更像是名字,赋值只是让其指代对象 +``` + +字典就像 C++ STL 中的 [`map`](./csl/associative-container.md#map)(请注意和 Python 中内置函数 [`map()`](https://docs.python.org/zh-cn/3/library/functions.html#map) 区分)用于存储键值对,形式类似 [JSON](https://docs.python.org/3/library/json.html),但 JSON 中键必须是字符串且以双引号括住,字典则更加灵活强大,可哈希的对象都可作为字典的键。需要注意 Python 几次版本更新后字典的特性有了较多变化,包括其中元素的顺序等,请自行探索。 + +```python +dic = {'key': "value"} # 基本形式 +dic = {chr(i): i for i in range(65, 91)} # 大写字母到对应 ASCII 码的映射,注意断句 +dic = dict(zip([chr(i) for i in range(65, 91)], range(65,91))) # 效果同上 +dic = {dic[k]: k for k in dic} # 将键值对逆转,for k in dic 迭代其键 +dic = {v: k for k, v in dic.items()} # 和上行作用相同,dic.items() 以元组存放单个键值对 +dic = {k: v for k, v in sorted(dic.items(), key=lambda x:-x[1])} # 字典按值逆排序,用到了 lambda 表达式 + +print(dic['A']) # 返回 dic 中 以 'A' 为键的项,这里值为65 +dic['a'] = 97 # 将 d[key] 设为 value,字典中原无 key 就是直接插入 +if 'b' in dic: # LBYL(Look Before You Leap) 风格 + print(dic['b']) # 若字典中无该键则会出错,故先检查 +else: + dic['b'] = 98 + +# 经典场景 统计出现次数 +# 新键不存在于原字典,需要额外处理 +try: # EAFP (Easier to Ask for Forgiveness than Permission) 风格 + cnter[key] += 1 +except KeyError: + cnter[key] = 1 +``` + +集合就像 C++ STL 中的 [`set`](./csl/associative-container.md#set),不会保存重复的元素,可以看成只保存键的字典。需要注意集合和字典都用 `{}` 括住,不过单用 `{}` 会创建空字典而不是空集合,这里就不再给出示例。 + +### 编写函数 + +Python 中定义函数无需指定参数类型和返回值类型,无形中为 OI 选手减少了代码量 + +```python +def add(a, b): + return a + b # 动态类型的优势,a和b也可以是字符串 + + +def add_no_swap(a, b): + print('in func #1:', id(a), id(b)) + a += b + b, a = a, b + print('in func #2:', id(a), id(b)) # a, b 已交换 + return a, b # 返回多个值,其实就是返回元组,可以拆包接收 + + +lst1 = [1, 2]; lst2 = [3, 4] +print('outside func #1:', id(lst1), id(lst2)) +add_no_swap(lst1, lst2) +# 函数外 lst1, lst2 并未交换 +print('outside func #2:', id(lst1), id(lst2)) +# 不过值确实已经改变 +print(lst1, lst2) +``` + +#### 默认参数 + +Python 中函数的参数非常灵活,有关键字参数、可变参数等,但在算法竞赛中这些特性的用处并不是很大,这里只介绍一下默认参数,因为 C++ 中也有默认参数,且在 Python 中使用默认参数很有可能遇到坑。 + +```python +def append_to(element, to=[]): + to.append(element) + return to + +lst1 = append_to(12) +lst2 = append_to(42) +print(lst1, lst2) + +# 你可能以为输出是 [12] [42] +# 但运行结果其实是 [12] [12, 42] + +# 这是因为默认参数的值仅仅在函数定义的时候赋值一次 +# 默认参数的值应该是不可变对象,使用 None 占位是一种最佳实践 +def append_to(element, to=None): + if to is None: + to = [] + to.append(element) + return to +``` + +#### 类型标注 + +Python 是一个动态类型检查的语言,以灵活但隐式的方式处理类型,Python 解释器仅仅在运行时检查类型是否正确,并且允许在运行时改变变量类型,俗话说「动态类型一时爽,代码重构火葬场」,程序中的一些错误可能在运行时才会暴露: + +```pycon +>>> if False: +... 1 + "two" # This line never runs, so no TypeError is raised +... else: +... 1 + 2 +... +3 + +>>> 1 + "two" # Now this is type checked, and a TypeError is raised +TypeError: unsupported operand type(s) for +: 'int' and 'str' +``` + +Python 3.5 后引入了类型标注,允许设置函数参数和返回值的类型,但只是作为提示,并没有实际的限制作用,需要静态检查工具才能排除这类错误(例如 [PyCharm](https://www.jetbrains.com/pycharm/) 和 [Mypy](http://mypy-lang.org/)),所以显得有些鸡肋,对于 OIer 来说更是只需了解,可按如下方式对函数的参数和返回值设置类型标注: + +```python +def headline( + text, # type: str + width = 80, # type: int + fill_char = "-", # type: str +): # type: (...) -> str + return f"{text.title()}".center(width, fill_char) + +print(headline("type comments work", width = 40)) +``` + +除了函数参数,变量也是可以类型标注的,你可以通过调用 `__annotations__` 来查看函数中所有的类型标注。变量类型标注赋予了 Python 静态语言的性质,即声明与赋值分离: + +```pycon +>>> nothing: str +>>> nothing +NameError: name 'nothing' is not defined + +>>> __annotations__ +{'nothing': } +``` + +## 装饰器 + +装饰器是一个函数,接受一个函数或方法作为其唯一的参数,并返回一个新函数或方法,其中整合了修饰后的函数或方法,并附带了一些额外的功能。简而言之,可以在不修改函数代码的情况下,增加函数的功能。相关知识可以参考 [官方文档](https://docs.python.org/3/glossary.html#term-decorator)。 + +部分装饰器在竞赛中非常实用,比如 [`lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache),可以为函数自动增加记忆化的能力,在递归算法中非常实用: + +`@lru_cache(maxsize=128,typed=False)` + +- 传入的参数有 2 个:`maxsize` 和 `typed`,如果不传则 `maxsize` 的默认值为 128,`typed` 的默认值为 `False`。 +- 其中 `maxsize` 参数表示的是 LRU 缓存的容量,即被装饰的方法的最大可缓存结果的数量。如果该参数值为 128,则表示被装饰方法最多可缓存 128 个返回结果;如果 `maxsize` 传入为 `None` 则表示可以缓存无限个结果。 +- 如果 `typed` 设置为 `True`,不同类型的函数参数将被分别缓存,例如,`f(3)` 和 `f(3.0)` 会缓存两次。 + +以下是使用 `lru_cache` 优化计算斐波那契数列的例子: + +```python +@lru_cache(maxsize = None) +def fib(n): + if n < 2: + return n + return fib(n - 1) + fib(n - 2) +``` + +## 常用内置库 + +在这里介绍一些写算法可能用得到的内置库,具体用法可以自行搜索或者阅读 [官方文档](https://docs.python.org/3/library/index.html)。 + +| 库名 | 用途 | +| ------------------------------------------------------------------- | -------------- | +| [`array`](https://docs.python.org/3/library/array.html) | 定长数组 | +| [`argparse`](https://docs.python.org/3/library/argparse.html) | 命令行参数处理 | +| [`bisect`](https://docs.python.org/3/library/bisect.html) | 二分查找 | +| [`collections`](https://docs.python.org/3/library/collections.html) | 有序字典、双端队列等数据结构 | +| [`fractions`](https://docs.python.org/3/library/fractions.html) | 有理数 | +| [`heapq`](https://docs.python.org/3/library/heapq.html) | 基于堆的优先级队列 | +| [`io`](https://docs.python.org/3/library/io.html) | 文件流、内存流 | +| [`itertools`](https://docs.python.org/3/library/itertools.html) | 迭代器 | +| [`math`](https://docs.python.org/3/library/math.html) | 数学函数 | +| [`os.path`](https://docs.python.org/3/library/os.html) | 系统路径等 | +| [`random`](https://docs.python.org/3/library/random.html) | 随机数 | +| [`re`](https://docs.python.org/3/library/re.html) | 正则表达式 | +| [`struct`](https://docs.python.org/3/library/struct.html) | 转换结构体和二进制数据 | +| [`sys`](https://docs.python.org/3/library/sys.html) | 系统信息 | + +## 从例题对比 C++ 与 Python + +??? note "[例题 洛谷 P4779【模板】单源最短路径(标准版)](https://www.luogu.com.cn/problem/P4779)" + 给定一个 $n(1 \leq n \leq 10^5)$ 个点、$m(1 \leq m \leq 2\times 10^5)$ 条有向边的带非负权图,请你计算从 $s$ 出发,到每个点的距离。数据保证能从 $s$ 出发到任意点。 + +### 声明常量 + +=== "C++" + ```cpp + #include + using namespace std; + const int N = 1e5 + 5, M = 2e5 + 5; + ``` + +=== "Python" + ```python + try: # 引入优先队列模块 + import Queue as pq #python version < 3.0 + except ImportError: + import queue as pq #python3.* + + N = int(1e5 + 5) + M = int(2e5 + 5) + INF = 0x3f3f3f3f + ``` + +### 声明前向星结构体和其它变量 + +=== "C++" + ```cpp + struct qxx { + int nex, t, v; + }; + + qxx e[M]; + int h[N], cnt; + + void add_path(int f, int t, int v) { e[++cnt] = (qxx){h[f], t, v}, h[f] = cnt; } + + typedef pair pii; + priority_queue, greater> q; + int dist[N]; + ``` + +=== "Python" + ```python + class qxx: # 前向星类(结构体) + def __init__(self): + self.nex = 0 + self.t = 0 + self.v = 0 + + e = [qxx() for i in range(M)] # 链表 + h = [0 for i in range(N)] + cnt = 0 + + dist = [INF for i in range(N)] + q = pq.PriorityQueue() # 定义优先队列,默认第一元小根堆 + + def add_path(f, t, v): # 在前向星中加边 + # 如果要修改全局变量,要使用 global 来声明 + global cnt, e, h + # 调试时的输出语句,多个变量使用元组 + # print("add_path(%d,%d,%d)" % (f,t,v)) + cnt += 1 + e[cnt].nex = h[f] + e[cnt].t = t + e[cnt].v = v + h[f] = cnt + ``` + +### Dijkstra 算法 + +=== "C++" + ```cpp + void dijkstra(int s) { + memset(dist, 0x3f, sizeof(dist)); + dist[s] = 0, q.push(make_pair(0, s)); + while (q.size()) { + pii u = q.top(); + q.pop(); + if (dist[u.second] < u.first) continue; + for (int i = h[u.second]; i; i = e[i].nex) { + const int &v = e[i].t, &w = e[i].v; + if (dist[v] <= dist[u.second] + w) continue; + dist[v] = dist[u.second] + w; + q.push(make_pair(dist[v], v)); + } + } + } + ``` + +=== "Python" + ```python + def nextedgeid(u): # 生成器,可以用在 for 循环里 + i = h[u] + while i: + yield i + i = e[i].nex + + + def dijkstra(s): + dist[s] = 0 + q.put((0, s)) + while not q.empty(): + u = q.get() # get 函数会顺便删除堆中对应的元素 + if dist[u[1]] < u[0]: + continue + for i in nextedgeid(u[1]): + v = e[i].t + w = e[i].v + if dist[v] <= dist[u[1]]+w: + continue + dist[v] = dist[u[1]]+w + q.put((dist[v], v)) + ``` + +### 主函数 + +=== "C++" + ```cpp + int n, m, s; + + int main() { + scanf("%d%d%d", &n, &m, &s); + for (int i = 1; i <= m; i++) { + int u, v, w; + scanf("%d%d%d", &u, &v, &w); + add_path(u, v, w); + } + dijkstra(s); + for (int i = 1; i <= n; i++) printf("%d ", dist[i]); + return 0; + } + ``` + +=== "Python" + ```python + if __name__ == '__main__': + # 一行读入多个整数。注意它会把整行都读进来 + n, m, s = map(int, input().split()) + for i in range(m): + u, v, w = map(int, input().split()) + add_path(u, v, w) + + dijkstra(s) + + for i in range(1, n + 1): + print(dist[i], end = ' ') + + print() + ``` + +### 完整代码 + +=== "C++" + ```cpp + #include + using namespace std; + const int N = 1e5 + 5, M = 2e5 + 5; + + struct qxx { + int nex, t, v; + }; + + qxx e[M]; + int h[N], cnt; + + void add_path(int f, int t, int v) { e[++cnt] = (qxx){h[f], t, v}, h[f] = cnt; } + + typedef pair pii; + priority_queue, greater> q; + int dist[N]; + + void dijkstra(int s) { + memset(dist, 0x3f, sizeof(dist)); + dist[s] = 0, q.push(make_pair(0, s)); + while (q.size()) { + pii u = q.top(); + q.pop(); + if (dist[u.second] < u.first) continue; + for (int i = h[u.second]; i; i = e[i].nex) { + const int &v = e[i].t, &w = e[i].v; + if (dist[v] <= dist[u.second] + w) continue; + dist[v] = dist[u.second] + w; + q.push(make_pair(dist[v], v)); + } + } + } + + int n, m, s; + + int main() { + scanf("%d%d%d", &n, &m, &s); + for (int i = 1; i <= m; i++) { + int u, v, w; + scanf("%d%d%d", &u, &v, &w); + add_path(u, v, w); + } + dijkstra(s); + for (int i = 1; i <= n; i++) printf("%d ", dist[i]); + return 0; + } + ``` + +=== "Python" + ```python + try: # 引入优先队列模块 + import Queue as pq # python version < 3.0 + except ImportError: + import queue as pq # python3.* + + N = int(1e5+5) + M = int(2e5+5) + INF = 0x3f3f3f3f + + class qxx: # 前向星类(结构体) + def __init__(self): + self.nex = 0 + self.t = 0 + self.v = 0 + + e = [qxx() for i in range(M)] # 链表 + h = [0 for i in range(N)] + cnt = 0 + + dist = [INF for i in range(N)] + q = pq.PriorityQueue() # 定义优先队列,默认第一元小根堆 + + def add_path(f, t, v): # 在前向星中加边 + # 如果要修改全局变量,要使用 global 来声名 + global cnt, e, h + # 调试时的输出语句,多个变量使用元组 + # print("add_path(%d,%d,%d)" % (f,t,v)) + cnt += 1 + e[cnt].nex = h[f] + e[cnt].t = t + e[cnt].v = v + h[f] = cnt + + def nextedgeid(u): # 生成器,可以用在 for 循环里 + i = h[u] + while i: + yield i + i = e[i].nex + + def dijkstra(s): + dist[s] = 0 + q.put((0, s)) + while not q.empty(): + u = q.get() + if dist[u[1]] < u[0]: + continue + for i in nextedgeid(u[1]): + v = e[i].t + w = e[i].v + if dist[v] <= dist[u[1]]+w: + continue + dist[v] = dist[u[1]]+w + q.put((dist[v], v)) + + + # 如果你直接运行这个python代码(不是模块调用什么的)就执行命令 + if __name__ == '__main__': + # 一行读入多个整数。注意它会把整行都读进来 + n, m, s = map(int, input().split()) + for i in range(m): + u, v, w = map(int, input().split()) + add_path(u, v, w) + + dijkstra(s) + + for i in range(1, n + 1): + # 两种输出语法都是可以用的 + print("{}".format(dist[i]), end=' ') + # print("%d" % dist[i],end=' ') + + print() # 结尾换行 + ``` + +## 参考文档 + +1. Python Documentation, +2. Python 官方中文教程, +3. Learn Python3 In Y Minutes, +4. Real Python Tutorials, +5. 廖雪峰的 Python 教程, +6. GeeksforGeeks: Python Tutorials, + +## 参考资料和注释 + +[^ref1]: [2. Python 解释器—Python 3 文档](https://docs.python.org/zh-cn/3/tutorial/interpreter.html#id1) + +[^ref2]: [Unicode 指南—Python 3 文档](https://docs.python.org/zh-cn/3/howto/unicode.html#the-string-type) diff --git a/mkdocs.yml b/mkdocs.yml index dfefa91..56c1582 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,8 +56,15 @@ theme: nav: - 前言&使用指南: "index.md" - 关于我们: "aboutus.md" + - 计算机科学学习路线: + - CS基础: + - 编程基础: + - Python: + - Python快速上手: "计算机科学学习路线/计算机基础/编程基础/Java/javaSE.md" + -Java: + - JavaSE基础: "计算机科学学习路线/计算机基础/编程基础/Java/JavaSE.md" - Python: "python/introduction_python.md" - 不同领域的学习路线: - Java: - - 路线总览: "不同领域的学习路线/Java/roadmap.md" + - 路线总览: "不同领域的学习路线/Java/java_learning_path.md" - JavaSE基础: "不同领域的学习路线/Java/javaSE.md"