# JavaSE 基础 — Day04 笔记

> **日期**：2026-07-16  
> **项目**：`c260716`  
> **作者**：WanJL

---

## 一、多维数组

### 1. 多维数组概述

Java 中的多维数组本质上是**数组的数组**：

| 维度 | 本质 | 说明 |
|------|------|------|
| **一维数组** | 元素是数据类型的变量或常量 | `int[]` → 元素是 `int` |
| **二维数组** | 元素是一维数组 | `int[][]` → 元素是 `int[]` |
| **三维数组** | 元素是二维数组 | `int[][][]` → 元素是 `int[][]` |

> **核心概念**：二维数组的每个元素其实是一维数组的**引用地址值**。

#### 二维数组的定义格式

```java
数组类型[][] 数组名 = new 数组类型[二维数组长度][一维数组长度];
// 示例：3行2列的二维数组
int[][] arr = new int[3][2];  // [行数][列数]
```

---

### 2. 二维数组的遍历

二维数组需要**两层循环嵌套**来遍历：

```java
// 外层循环控制行数（二维数组的长度）
for (int i = 0; i < arr.length; i++) {
    // 内层循环控制列数（每个一维数组的长度）
    for (int j = 0; j < arr[i].length; j++) {
        System.out.print(arr[i][j] + " ");
    }
    System.out.println();
}
```

- `arr.length` — 二维数组的长度（即行数）
- `arr[i].length` — 第 i 行一维数组的长度（即列数）

---

### 3. 二维数组的静态初始化

```java
int[][] arr1 = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};
```

---

### 4. 不规则的二维数组（每个一维数组长度不同）

二维数组中的一维数组**不必长度相同**：

```java
int[] arr01 = {1, 3};
int[] arr02 = {4, 5, 6};
int[] arr03 = {7, 8, 9, 15};
int[][] arr2 = {arr01, arr02, arr03};  // 存放的是一维数组的引用地址
```

遍历时同样使用两层循环，每个内层循环以对应一维数组的长度为界：

```java
for (int i = 0; i < arr2.length; i++) {
    for (int j = 0; j < arr2[i].length; j++) {
        System.out.print(arr2[i][j] + " ");
    }
    System.out.println();
}
```

---

## 二、增强 for 循环（foreach）

### 1. foreach 概述

**增强 for 循环（foreach）** 是 Java 提供的一种简化数组遍历的语法，作用和普通 `for` 循环一样，主要用于**遍历数组或集合中的所有元素**。

#### 语法格式

```java
for (数据类型 变量 : 数组名) {
    代码块
}
```

#### 基本示例

```java
int[] arr = {1, 2, 3, 4, 5, 6, 8};
for (int a : arr) {
    System.out.print(a + " ");
}
// 输出：1 2 3 4 5 6 8
```

> ⚠️ **注意**：foreach 遍历数组时**无法获取索引值**，所以只适合**遍历全部元素**。如果需要操作某个指定索引位置的元素，仍需使用普通 `for` 循环。

---

### 2. foreach 遍历二维数组

```java
int[] arr01 = {1, 3};
int[] arr02 = {4, 5, 6};
int[] arr03 = {7, 8, 9, 15};
int[][] arr2 = {arr01, arr02, arr03};

for (int[] a : arr2) {    // 外层 foreach：遍历二维数组，得到每个一维数组
    for (int a1 : a) {    // 内层 foreach：遍历一维数组，得到每个元素
        System.out.print(a1 + " ");
    }
    System.out.println();
}
```

> 外层 `int[] a` 表示二维数组中的每个元素是一维数组，内层 `int a1` 表示一维数组中的每个元素是 `int`。

---

### 3. for 循环 vs foreach 对比

| 特性 | 普通 for 循环 | 增强 for 循环（foreach） |
|------|-------------|------------------------|
| **语法** | `for(int i=0; i<len; i++)` | `for(int a : arr)` |
| **索引访问** | 支持（通过 `i`） | 不支持 |
| **适用场景** | 需要索引、修改元素、部分遍历 | 只需要遍历全部元素 |
| **代码简洁性** | 较繁琐 | 更简洁 |
| **可读性** | 一般 | 更好 |

---

## 三、数组工具类 `java.util.Arrays`

### 1. 概述

`java.util.Arrays` 是 Java 提供的**数组工具类**，包含大量操作数组的静态方法。

> **命名特点**：如果一个类的类名是**名词 + `s`**，大多数情况下这个类就是**工具类**。

**使用前提**：需要导包

```java
import java.util.Arrays;
// import 是导包的关键字，后面跟着【类全路径名】—— 包名.类名
```

---

### 2. 常用方法

| 方法 | 作用 | 示例 |
|------|------|------|
| `Arrays.sort(arr)` | 对数组进行**升序排序** | `Arrays.sort(arr);` |
| `Arrays.toString(arr)` | 将数组转换为**字符串**输出 | `System.out.println(Arrays.toString(arr));` |
| `Arrays.copyOf(arr, newLength)` | **拷贝数组**（扩展或截断，多余补默认值） | `int[] copy = Arrays.copyOf(arr, 10);` |
| `Arrays.copyOfRange(arr, from, to)` | 拷贝数组的**指定范围** `[from, to)` | `int[] range = Arrays.copyOfRange(arr, 1, 4);` |
| `Arrays.binarySearch(arr, key)` | **二分查找**（数组必须先排序），返回索引值（未找到返回负数） | `int index = Arrays.binarySearch(arr, 689);` |

#### 详细示例

```java
int[] arr = {7, 6, 78, 1, 689, 87, 165, 7, 4, 5, 8};

// 排序
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
// 输出：[1, 4, 5, 6, 7, 7, 8, 78, 87, 165, 689]

// 拷贝（扩展长度，多余补 0）
int[] copy = Arrays.copyOf(arr, 10);
System.out.println(Arrays.toString(copy));

// 拷贝指定范围 [1, 4) — 包含索引1，不包含索引4
int[] range = Arrays.copyOfRange(arr, 1, 4);
System.out.println(Arrays.toString(range));  // 输出：[4, 5, 6]

// 二分查找（需先排序），返回元素在数组中的索引
int index = Arrays.binarySearch(arr, 689);
System.out.println(index);  // 输出：10（排序后 689 在索引 10）
```

> ⚠️ `Arrays.binarySearch()` 要求数组**必须先排序**，否则结果不确定。

---

## 四、冒泡排序（Bubble Sort）

### 1. 冒泡排序概述

**冒泡排序**是所有排序算法中最直观、最基础的一种。它的名字来源于排序过程就像水里的泡泡一样，大的元素会逐渐"冒"到数组的末端。

#### 核心思想

> 重复遍历数组，依次比较**相邻的两个元素**，如果顺序错误（如从小到大排序时前一个比后一个大）就交换。每轮遍历把当前未排序部分的最大值"冒泡"到正确位置。

#### 算法步骤图解

以数组 `{5, 3, 8, 6, 4}` 为例，从小到大排序：

```
第1轮（i=0）：将最大值 8 冒泡到最后
  [5, 3, 8, 6, 4] → 比较 5>3 → 交换 → [3, 5, 8, 6, 4]
  [3, 5, 8, 6, 4] → 比较 5<8 → 不交换
  [3, 5, 8, 6, 4] → 比较 8>6 → 交换 → [3, 5, 6, 8, 4]
  [3, 5, 6, 8, 4] → 比较 8>4 → 交换 → [3, 5, 6, 4, 8]  ← 8 到位

第2轮（i=1）：将最大值 6 冒泡到倒数第二位
  [3, 5, 6, 4, 8] → 比较 3<5 → 不交换
  [3, 5, 6, 4, 8] → 比较 5<6 → 不交换
  [3, 5, 6, 4, 8] → 比较 6>4 → 交换 → [3, 5, 4, 6, 8]  ← 6 到位

第3轮（i=2）：将最大值 5 冒泡到倒数第三位
  [3, 5, 4, 6, 8] → 比较 3<5 → 不交换
  [3, 5, 4, 6, 8] → 比较 5>4 → 交换 → [3, 4, 5, 6, 8]  ← 5 到位

第4轮（i=3）：将最大值 4 冒泡到倒数第四位
  [3, 4, 5, 6, 8] → 比较 3<4 → 不交换  ← 4 已在正确位置

排序完成 ✅
```

---

### 2. 基础版冒泡排序

```java
int[] arr = {1, 456, 782, 5, 8, 4, 598, 1, 46, 68, 465, 74};

// 冒泡排序需要两层循环
for (int i = 0; i < arr.length - 1; i++) {
    // 外层循环：控制冒泡的轮数（共 arr.length-1 轮）
    // 内层循环：在未排序区间 [0, arr.length-1-i] 中相邻比较
    // 因为 i 后面的元素已经有序，不需要再比较了
    for (int j = 0; j < arr.length - 1 - i; j++) {
        if (arr[j] > arr[j + 1]) {
            // 交换位置（加减法交换）
            arr[j] = arr[j] + arr[j + 1];
            arr[j + 1] = arr[j] - arr[j + 1];
            arr[j] = arr[j] - arr[j + 1];
        }
    }
}
System.out.println(Arrays.toString(arr));
```

**代码要点**：
| 部分 | 说明 |
|------|------|
| **外层循环** `i` | 控制冒泡的**轮数**，`arr.length - 1` 轮 |
| **内层循环** `j` | 在未排序区间 **相邻比较**，范围 `[0, arr.length-1-i)` |
| **`arr.length-1-i`** | 每轮结束后，末尾 `i` 个元素已有序，无需再比较 |
| **相邻比较** | `arr[j] > arr[j+1]` 表示前大后小，需要交换 |

> **为什么外层循环是 `arr.length-1` 轮？**
> 因为 n 个元素的数组，最多需要 n-1 轮冒泡就可以完成排序（最后一轮只剩一个元素，无需再排）。

> **为什么内层循环是 `arr.length-1-i`？**
> 第 i 轮结束后，数组末尾的 i 个元素已经是有序的（每轮冒泡出一个最大值），所以内层只需要比较前 `arr.length-1-i` 个元素。

---

### 3. 优化版冒泡排序

如果某轮冒泡过程中**没有发生任何交换**，说明数组已经有序，可以**提前结束排序**。

```java
int[] arr2 = {1, 456, 782, 5, 8, 4, 598, 1, 46, 68, 465, 74};

for (int i = 0; i < arr2.length - 1; i++) {
    boolean b = false;  // 标记本轮是否有交换
    for (int j = 0; j < arr2.length - 1 - i; j++) {
        if (arr2[j] > arr2[j + 1]) {
            // 交换位置
            arr2[j] = arr2[j] + arr2[j + 1];
            arr2[j + 1] = arr2[j] - arr2[j + 1];
            arr2[j] = arr2[j] - arr2[j + 1];
            b = true;   // 本轮有交换
        }
    }
    // 如果本轮没有发生任何交换，说明数组已经有序，提前结束
    if (b == false) {
        break;
    }
    System.out.println("第" + (i + 1) + "轮冒泡后：" + Arrays.toString(arr2));
}
```

**优化点**：引入 `boolean` 标记变量，如果某一轮没有发生交换，说明数组已有序，直接 `break` 退出循环。

---

### 4. 冒泡排序总结

| 特性 | 说明 |
|------|------|
| **算法类型** | 比较排序、交换排序 |
| **时间复杂度（平均）** | O(n²) |
| **时间复杂度（最好）** | O(n) — 数组已有序（优化版） |
| **时间复杂度（最坏）** | O(n²) — 数组逆序 |
| **空间复杂度** | O(1) — 原地排序，不需要额外空间 |
| **稳定性** | ✅ 稳定（相等元素的相对位置不变） |
| **适用场景** | 小规模数据排序，教学演示 |

> **数据结构与算法初探**：数组是最基础的**线性结构**之一，冒泡排序是最基础的**排序算法**之一。后续还会学习选择排序、插入排序、快速排序等更多算法。

---

## 五、选择排序（Selection Sort）

### 1. 选择排序概述

**选择排序**的思路比冒泡排序更"聪明"——冒泡排序是不断交换相邻元素，而选择排序**每轮只做一次交换**。

#### 核心思想

> 每一轮从**剩余未排序的元素**中找出**最小的元素**，直接把它放到**未排序部分的第 1 位**（即已排序区间的末尾）。

#### 算法步骤图解

以数组 `{29, 10, 14, 37, 13}` 为例，从小到大排序：

```
第1轮（i=0）：在 [0, 4] 找最小值 10，与 arr[0]=29 交换
  [29, 10, 14, 37, 13] → 找到最小值 10 (索引 1)
  → 交换 → [10, 29, 14, 37, 13]  ← 10 到位

第2轮（i=1）：在 [1, 4] 找最小值 13，与 arr[1]=29 交换
  [10, 29, 14, 37, 13] → 找到最小值 13 (索引 4)
  → 交换 → [10, 13, 14, 37, 29]  ← 13 到位

第3轮（i=2）：在 [2, 4] 找最小值 14，已在正确位置，不交换
  [10, 13, 14, 37, 29] → 找到最小值 14 (索引 2)
  → minIndex == i，不交换  ← 14 已在正确位置

第4轮（i=3）：在 [3, 4] 找最小值 29，与 arr[3]=37 交换
  [10, 13, 14, 37, 29] → 找到最小值 29 (索引 4)
  → 交换 → [10, 13, 14, 29, 37]  ← 29、37 到位

排序完成 ✅
```

---

### 2. 选择排序代码实现

```java
import java.util.Arrays;

int[] arr = {1, 456, 782, 5, 8, 4, 598, 1, 46, 68, 465, 74};

// 外层循环：i 既是已排序区间的末尾边界，也是当前要确定的位置
for (int i = 0; i < arr.length - 1; i++) {
    // 假设当前未排序区间的第 1 个元素就是最小的
    int minIndex = i;  // 假设 i 就是最小元素的索引值
    // 内层循环：在未排序区间 [i+1, arr.length-1] 找到真正的最小值
    for (int j = i + 1; j < arr.length; j++) {
        if (arr[j] < arr[minIndex]) {
            minIndex = j;  // 更新最小元素的索引值
        }
    }
    // 如果最小元素索引值 minIndex 和当前 i 不相等，就交换位置
    if (minIndex != i) {
        arr[minIndex] = arr[minIndex] + arr[i];
        arr[i] = arr[minIndex] - arr[i];
        arr[minIndex] = arr[minIndex] - arr[i];
    }
    System.out.println("第" + (i + 1) + "轮选择后：" + Arrays.toString(arr));
}
```

**代码要点**：

| 部分 | 说明 |
|------|------|
| **外层循环** `i` | 控制选择的轮数，`arr.length - 1` 轮；`i` 也是当前要确定的位置 |
| **内层循环** `j` | 在**未排序区间** `[i+1, arr.length-1]` 中寻找最小值 |
| **`minIndex`** | 记录当前找到的最小元素的索引值，初始为 `i` |
| **交换条件** | 当 `minIndex != i` 时，说明最小值不在当前位置，需要交换 |
| **每轮只交换一次** | 找到最小值后，直接与当前位置交换，比冒泡排序更高效 |

---

### 3. 冒泡排序 vs 选择排序对比

| 特性 | 冒泡排序 | 选择排序 |
|------|---------|---------|
| **核心思路** | 相邻比较，大的往后冒 | 每轮选最小值，放到正确位置 |
| **每轮交换次数** | 多次（可能每轮交换 n 次） | **最多 1 次** |
| **比较次数** | 固定 O(n²/2) | 固定 O(n²/2) |
| **时间复杂度（平均）** | O(n²) | O(n²) |
| **时间复杂度（最好）** | O(n) — 优化版 | O(n²) — 仍要遍历全部 |
| **时间复杂度（最坏）** | O(n²) | O(n²) |
| **空间复杂度** | O(1) | O(1) |
| **稳定性** | ✅ 稳定 | ❌ 不稳定 |
| **适用场景** | 小规模数据、教学演示 | 小规模数据、交换成本高的场景 |

> **稳定性说明**：选择排序是不稳定的，因为当最小值与前面的元素交换时，可能改变相等元素的相对顺序。例如 `[5, 5, 2]` → 第一轮将 `2` 与第一个 `5` 交换，两个 `5` 的相对顺序就变了。

---

### 4. 两种排序的过程对比

以数组 `{5, 3, 8, 6, 4}` 为例：

**冒泡排序**（每轮可能多次交换）：
```
第1轮：5>3→交换, 5<8不换, 8>6→交换, 8>4→交换 → [3,5,6,4,8]
第2轮：3<5不换, 5<6不换, 6>4→交换          → [3,5,4,6,8]
第3轮：3<5不换, 5>4→交换                   → [3,4,5,6,8]
第4轮：3<4不换                              → [3,4,5,6,8]
```

**选择排序**（每轮最多一次交换）：
```
第1轮：找最小值 3 (索引1)，与 arr[0]=5 交换  → [3,5,8,6,4]
第2轮：找最小值 4 (索引4)，与 arr[1]=5 交换  → [3,4,8,6,5]
第3轮：找最小值 5 (索引4)，与 arr[2]=8 交换  → [3,4,5,6,8]
第4轮：找最小值 6 (索引3)，已在正确位置不交换 → [3,4,5,6,8]
```

---

## 六、插入排序（Insertion Sort）

### 1. 插入排序概述

**插入排序**是三种基础排序（冒泡、选择、插入）中最实用、使用最广泛的一种 O(n²) 排序算法。它的思想非常符合人类整理东西的习惯——就像**打扑克牌时抓牌插入到手牌的正确位置**。

#### 核心思想

> 把数组分为"**已排序**"和"**未排序**"两部分。每次从**未排序部分**取出第 1 个元素，在**已排序部分**从后往前找到合适的位置并插入。

#### 算法步骤图解

以数组 `{5, 3, 8, 6, 4}` 为例，从小到大排序：

```
初始： [5 | 3, 8, 6, 4]   竖线左边为已排序，右边为未排序

第1轮（i=1）：取未排序第1个元素 current=3，与已排序部分 [5] 比较
  5 > 3 → 5 向后移 → [5, 5 | 8, 6, 4]
  找到插入位置 j+1=0，插入 current=3
  → [3, 5 | 8, 6, 4]  ← 3 和 5 有序

第2轮（i=2）：取 current=8，与已排序部分 [3, 5] 比较
  5 < 8 → 无需移动，直接插入在末尾
  → [3, 5, 8 | 6, 4]  ← 8 已在正确位置

第3轮（i=3）：取 current=6，与已排序部分 [3, 5, 8] 比较
  8 > 6 → 8 向后移 → [3, 5, 8, 8 | 4]
  5 < 6 → 停止，找到插入位置 j+1=2，插入 current=6
  → [3, 5, 6, 8 | 4]  ← 6 插入到 5 和 8 之间

第4轮（i=4）：取 current=4，与已排序部分 [3, 5, 6, 8] 比较
  8 > 4 → 8 向后移 → [3, 5, 6, 8, 8]
  6 > 4 → 6 向后移 → [3, 5, 6, 6, 8]
  5 > 4 → 5 向后移 → [3, 5, 5, 6, 8]
  3 < 4 → 停止，找到插入位置 j+1=1，插入 current=4
  → [3, 4, 5, 6, 8] ✅ 排序完成
```

---

### 2. 插入排序代码实现

```java
import java.util.Arrays;

int[] arr = {1, 456, 782, 5, 8, 4, 598, 1, 46, 68, 465, 74};

// 从第2个元素开始，假设 arr[0] 已经是已排序部分
for (int i = 1; i < arr.length; i++) {
    // 获取当前要插入的元素（未排序的第1个）
    int current = arr[i];
    // 已排序部分的最后一个索引值
    int j = i - 1;
    // 从后往前扫描已排序部分，寻找插入位置
    // 如果已排序的元素大于 current，就向后移动一位
    while (j >= 0 && arr[j] > current) {
        arr[j + 1] = arr[j];
        j--;
    }
    // 此时 j+1 就是 current 应该插入的位置
    arr[j + 1] = current;
    System.out.println("第" + i + "轮插入后：" + Arrays.toString(arr));
}
```

**代码要点**：

| 部分 | 说明 |
|------|------|
| **外层循环** `i` | 从索引 1 开始，`i` 是当前要处理的"未排序的第 1 个元素"位置 |
| **`current`** | 暂存当前要插入的元素值，防止被覆盖 |
| **内层 while 循环** | 从后往前扫描已排序部分，比 `current` 大的元素逐一后移 |
| **`arr[j+1] = current`** | 在找到的正确位置插入当前元素 |
| **循环条件** | `j >= 0 && arr[j] > current` — 未越界且当前比较值比 `current` 大 |

---

### 3. 三种排序算法对比

| 特性 | 冒泡排序 | 选择排序 | 插入排序 |
|------|---------|---------|---------|
| **核心思路** | 相邻比较，大的往后冒 | 每轮选最小值交换 | 将元素插入已排序区的正确位置 |
| **每轮交换次数** | 多次（可能 n 次） | **最多 1 次** | 多次（元素后移） |
| **平均时间复杂度** | O(n²) | O(n²) | O(n²) |
| **最好时间复杂度** | O(n) — 优化版 | O(n²) | **O(n)** — 已有序 |
| **最坏时间复杂度** | O(n²) | O(n²) | O(n²) |
| **空间复杂度** | O(1) | O(1) | O(1) |
| **稳定性** | ✅ 稳定 | ❌ 不稳定 | ✅ 稳定 |
| **实用性** | 低（教学用） | 中（交换成本高时适用） | **高**（小规模数据首选） |

> **三种排序的实用性排序**：插入排序 > 选择排序 > 冒泡排序
>
> **为什么插入排序最实用？**
> - 当数据**基本有序**时，插入排序的时间复杂度可达到 **O(n)**
> - 插入排序是**稳定的**，不会破坏相等元素的相对顺序
> - 插入排序的代码简洁，实际运行效率在 O(n²) 级别中最高

---

## 七、Scanner 键盘输入

### 1. Scanner 概述

`java.util.Scanner` 是 Java 提供的**键盘输入工具类**，可以读取用户在控制台中输入的数据。

#### 使用步骤

| 步骤 | 说明 | 代码 |
|------|------|------|
| **① 导包** | 引入 Scanner 类（IDEA 可自动导包） | `import java.util.Scanner;` |
| **② 创建对象** | 创建 Scanner 对象，绑定到系统输入 | `Scanner sc = new Scanner(System.in);` |
| **③ 读取数据** | 调用对应的方法读取不同类型的数据 | `sc.nextInt()`、`sc.next()` 等 |
| **④ 关闭资源** | 关闭 Scanner 释放资源 | `sc.close();` |

> `System.in` 属于**系统标准输入流**，Scanner 通过它来读取用户在控制台的输入。

---

### 2. 常用方法

| 方法 | 读取类型 | 示例 |
|------|---------|------|
| `sc.nextByte()` | 整数 byte | `byte b = sc.nextByte();` |
| `sc.nextShort()` | 整数 short | `short s = sc.nextShort();` |
| `sc.nextInt()` | 整数 int | `int i = sc.nextInt();` |
| `sc.nextLong()` | 整数 long | `long l = sc.nextLong();` |
| `sc.nextFloat()` | 小数 float | `float f = sc.nextFloat();` |
| `sc.nextDouble()` | 小数 double | `double d = sc.nextDouble();` |
| `sc.next()` | **单个字符串**（以空格/换行符为结束符） | `String s = sc.next();` |
| `sc.nextLine()` | **整行字符串**（以换行符为结束符） | `String line = sc.nextLine();` |
| `sc.nextBoolean()` | 布尔值 boolean | `boolean b = sc.nextBoolean();` |
| `sc.next().charAt(0)` | 字符 char | `char c = sc.next().charAt(0);` |

#### 基本示例

```java
Scanner sc = new Scanner(System.in);

// 读取整数
int a = sc.nextInt();
int b = sc.nextInt();
System.out.println("a + b = " + (a + b));

// 读取字符串
String s = sc.next();
System.out.println(s);

sc.close();
```

#### 综合练习 — 用户信息采集

```java
Scanner sc = new Scanner(System.in);

System.out.println("请输入您的姓名：");
String name = sc.next();

System.out.println("请输入您的年龄：");
int age = sc.nextInt();

System.out.println("请输入您的身高（米）：");
double height = sc.nextDouble();

System.out.println("您是否是在校学生（true/false）：");
boolean isStudent = sc.nextBoolean();

System.out.println("\n========用户信息========");
System.out.println("姓名：" + name);
System.out.println("年龄：" + age);
System.out.println("身高：" + height);
System.out.println("在校学生：" + isStudent);

sc.close();
```

---

### 3. Scanner 的常见陷阱

#### 陷阱：`nextInt()` 后面使用 `nextLine()` 的问题

```java
int a = sc.nextInt();
String s = sc.nextLine();

System.out.println(a);  // 输出数字
System.out.println(s);  // ❌ 直接输出空字符串，没有等待输入！
```

**问题原因**：
- `nextInt()` 读取完数字后，**换行符 `\n` 仍停留在输入缓冲区**中
- `nextLine()` 以换行符为结束符，遇到缓冲区中的换行符就**直接结束了**
- 所以 `s` 读取到的是一个空字符串，程序不会等待用户输入

**解决方案**：在 `nextInt()` 之后调用一次 `nextLine()` **吃掉多余的换行符**。

```java
int a = sc.nextInt();
sc.nextLine();    // ✅ 吃掉缓冲区中的换行符
String s = sc.nextLine();  // ✅ 正常读取整行字符串

System.out.println(a);
System.out.println(s);
```

> ⚠️ **注意事项**：
> - 在控制台中使用键盘输入的值，**必须符合 Scanner 接收的类型**，否则会抛出异常导致程序结束
> - 例如用 `nextInt()` 读取时输入了字母，会抛出 `InputMismatchException`

---

### 4. `next()` vs `nextLine()` 区别

| 特性 | `next()` | `nextLine()` |
|------|----------|-------------|
| **结束符** | 空格、Tab、换行符 | **仅换行符** |
| **能否读取空格** | ❌ 不能（遇到空格停止） | ✅ 能（读取整行） |
| **典型场景** | 读取单个单词 | 读取包含空格的句子 |

```java
// 输入：Hello World
System.out.println(sc.next());      // 输出：Hello（遇到空格停止）
System.out.println(sc.nextLine());  // 输出：World（读取剩余部分）
```

---

## 八、课堂练习详解

### 练习 01：首尾对称交换数组元素

**题目**：定义数组 `{10, 20, 30, 40, 50}`，将第 1 个与第 5 个交换、第 2 个与第 4 个交换（首尾对称交换）。

**核心思路**：使用**加减法交换**（不借助第三个变量）实现两个位置的元素互换。

```java
int[] arr = {10, 20, 30, 40, 50};

// 首尾交换：第1位和第5位
arr[0] = arr[0] + arr[4];
arr[4] = arr[0] - arr[4];
arr[0] = arr[0] - arr[4];

// 首尾交换：第2位和第4位
arr[1] = arr[1] + arr[3];
arr[3] = arr[1] - arr[3];
arr[1] = arr[1] - arr[3];
```

**变量交换的三种方式**：

| 方式 | 代码 | 优点 | 缺点 |
|------|------|------|------|
| **借助临时变量** | `tmp = a; a = b; b = tmp;` | 直观、安全 | 多用一个变量 |
| **加减法交换** | `a = a + b; b = a - b; a = a - b;` | 无需额外变量 | 可能溢出（大数） |
| **异或交换** | `a = a ^ b; b = a ^ b; a = a ^ b;` | 无需额外变量 | 可读性差 |

---

### 练习 02：数组循环右移

**题目**：定义数组 `{1, 2, 3, 4, 5}`，将数组中的所有元素向右移动 2 个位置，超出数组范围的元素循环移到数组开头。

**解题思路**：

先理解**全体向右移动 1 位**的过程：
1. 先把最后一个元素保存起来
2. 倒数第 2 位 → 倒数第 1 位
3. 倒数第 3 位 → 倒数第 2 位
4. ...依此类推...
5. 第 1 位（索引 0）→ 第 2 位（索引 1）
6. 把保存起来的最后一个元素 → 第 1 位（索引 0）

**执行步骤图解**（右移 1 位）：
```
原始： [1, 2, 3, 4, 5]
第1步：保存最后一个元素 last = 5
第2步：从后往前移动 ←
       [1, 2, 3, 4, 4]   // arr[4]=arr[3]
       [1, 2, 3, 3, 4]   // arr[3]=arr[2]
       [1, 2, 2, 3, 4]   // arr[2]=arr[1]
       [1, 1, 2, 3, 4]   // arr[1]=arr[0]
第3步：arr[0] = last
结果： [5, 1, 2, 3, 4]
```

**代码实现**：

```java
int[] arr = {1, 2, 3, 4, 5};
int k = 2;  // 右移位数

// 如果k大于数组长度，取模（移动6次 = 移动1次）
k = k % arr.length;

// 整体右移k位（循环右移）
for (int i = 0; i < k; i++) {
    // 保存最后一个元素
    int last = arr[arr.length - 1];
    // 从后往前，每个元素向后移动1位
    for (int j = arr.length - 1; j > 0; j--) {
        arr[j] = arr[j - 1];
    }
    // 把最后一个元素放到第0位
    arr[0] = last;
}
```

> **关键点**：
> - 内层循环必须**从后往前**遍历（`j = length-1` → `j > 0`），否则前面的值会被覆盖
> - `k = k % arr.length` 解决 k 大于数组长度时的重复移动问题（如移动 6 次 = 移动 1 次）

---

### 练习 03：数组去重

**题目**：定义数组 `{5, 2, 8, 2, 5, 9, 1, 8, 3}`，去除重复元素，只保留第一次出现的元素，输出去重后的新数组。

**核心思路**：
1. **第一步**：统计不重复元素的个数
2. **第二步**：创建新数组（长度为统计结果）
3. **第三步**：将不重复元素放入新数组

**判断元素是否重复的方式**：检查当前元素 `arr[i]` 在它**之前**是否出现过（即 `arr[0]` ~ `arr[i-1]` 中是否有相同的值）。

```java
int[] arr = {5, 2, 8, 2, 5, 9, 1, 8, 3};
int count = 0;  // 不重复元素的个数

// 第一步：统计不重复元素的个数
for (int i = 0; i < arr.length; i++) {
    boolean b = false;  // 标记：先认为当前元素没有重复
    for (int j = 0; j < i; j++) {  // 检查 arr[i] 在它之前是否出现过
        if (arr[i] == arr[j]) {
            b = true;   // 找到重复，标记改为 true
            break;      // 跳出内层循环
        }
    }
    if (b == false) {   // 没有重复，计数
        count++;
    }
}

// 第二步：创建新数组
int[] newArr = new int[count];
int index = 0;  // 新数组的索引

// 第三步：将不重复元素放入新数组
for (int i = 0; i < arr.length; i++) {
    boolean b = false;
    for (int j = 0; j < i; j++) {
        if (arr[i] == arr[j]) {
            b = true;
            break;
        }
    }
    if (b == false) {           // 不重复的元素
        newArr[index] = arr[i]; // 放入新数组
        index++;
    }
}

// 输出新数组
for (int i = 0; i < newArr.length; i++) {
    System.out.print(newArr[i] + " ");
}
// 输出：5 2 8 9 1 3
```

> **核心技巧**：
> - 使用**布尔标记 `boolean b`** 来判断元素是否重复
> - 内层循环 `j < i` 只检查当前元素**之前**的元素，确保只保留第一次出现的元素
> - 两步走：先统计个数 → 再填充数据（因为数组长度在创建后不可改变）

---

## 九、课后作业讲解（7月15日作业）

### 作业 07：数组元素求和

**题目**：定义数组 `{11, 22, 33, 44, 55}`，用 `for` 循环遍历数组，计算所有元素的总和。

**知识点**：数组遍历、累加求和

```java
int[] arr = {11, 22, 33, 44, 55};
int sum = 0;  // 记录累加和
for (int i = 0; i < arr.length; i++) {
    sum += arr[i];  // 通过 arr[i] 获取每个元素，累加到 sum
}
System.out.println(sum);  // 输出：165
```

---

### 作业 08：找数组最大值

**题目**：定义数组 `{3, 8, 1, 9, 4, 7, 2}`，找出数组中的最大值。

**知识点**：数组遍历、打擂法（假设法）

```java
int[] arr = {3, 8, 1, 9, 4, 7, 2};
int max = arr[0];  // 先假设第一个元素是最大值
for (int i = 1; i < arr.length; i++) {
    if (arr[i] > max) {  // 如果有元素比当前最大值还大
        max = arr[i];    // 更新最大值
    }
}
System.out.println(max);  // 输出：9
```

> **打擂法核心思路**：先假设第一个元素是最大值，然后遍历数组，遇到更大的就更新。

---

### 作业 09：数组倒序输出

**题目**：定义数组 `{1, 2, 3, 4, 5}`，从后往前遍历，将元素倒序输出。

**知识点**：for 循环逆序遍历

```java
int[] arr = {1, 2, 3, 4, 5};
// 从最大索引开始，递减到 0
for (int i = arr.length - 1; i >= 0; i--) {
    System.out.println(arr[i]);
}
// 输出：
// 5
// 4
// 3
// 2
// 1
```

> 从 `arr.length - 1`（最后一个元素的索引）开始，到 `0`（第一个元素的索引）结束，每次递减 1。

---

### 作业 10：统计奇偶数个数

**题目**：定义数组 `{12, 7, 9, 24, 15, 8, 31, 46}`，遍历数组，统计奇数和偶数的个数。

**知识点**：数组遍历、取模判断、统计计数

```java
int[] arr = {12, 7, 9, 24, 15, 8, 31, 46};
int count = 0;  // 奇数个数
for (int i = 0; i < arr.length; i++) {
    if (arr[i] % 2 != 0) {  // 余2不等于0，说明是奇数
        count++;
    }
}
System.out.println("奇数个数：" + count + "，偶数个数：" + (arr.length - count));
// 输出：奇数个数：4，偶数个数：4
```

> **偶数的判定**：`arr[i] % 2 == 0`  
> **奇数的判定**：`arr[i] % 2 != 0`  
> 偶数个数 = 数组总长度 - 奇数个数

---

## 十、随堂练习要点

1. **多维数组本质**：数组的数组，二维数组的元素是一维数组的引用地址
2. **二维数组定义**：`int[][] arr = new int[3][2];`（3 行 2 列）
3. **二维数组遍历**：两层循环嵌套，外层行数 `arr.length`，内层列数 `arr[i].length`
4. **不规则二维数组**：每个一维数组长度可以不同
5. **foreach 语法**：`for(数据类型 变量 : 数组名) { }`，适合遍历全部元素
6. **foreach 局限性**：无法获取索引值，无法修改元素，只能读取
7. **Arrays 工具类**：`java.util.Arrays`，操作数组的静态方法集合
8. **`Arrays.sort(arr)`**：对数组进行升序排序
9. **`Arrays.toString(arr)`**：将数组转换为字符串输出
10. **`Arrays.copyOf(arr, n)`**：拷贝数组，指定新长度，多余补默认值
11. **`Arrays.copyOfRange(arr, from, to)`**：拷贝指定范围 [from, to)
12. **`Arrays.binarySearch(arr, key)`**：二分查找（必须先排序）
13. **冒泡排序**：重复遍历数组，相邻元素两两比较，大的往后冒
14. **冒泡排序-外层循环**：控制冒泡轮数，共 `arr.length - 1` 轮
15. **冒泡排序-内层循环**：在未排序区间 `[0, arr.length-1-i]` 相邻比较
16. **冒泡排序-优化**：引入 `boolean` 标记，本轮无交换则提前结束
17. **选择排序**：每轮从未排序区间选出最小值，与未排序区间的第 1 个元素交换
18. **选择排序-外层循环**：`i` 是当前要确定的位置，范围 `[0, arr.length-2]`
19. **选择排序-内层循环**：在未排序区间 `[i+1, arr.length-1]` 找最小值索引
20. **选择排序-minIndex**：记录最小元素的索引值，初始为 `i`
21. **选择排序-交换条件**：`minIndex != i` 时才交换，每轮最多交换 1 次
22. **选择排序 vs 冒泡排序**：选择排序每轮最多交换 1 次，冒泡排序可能交换多次
23. **插入排序**：将未排序的第 1 个元素插入到已排序部分的正确位置
24. **插入排序-分治思想**：把数组分为"已排序"和"未排序"两部分
25. **插入排序-外层循环**：`i` 从 1 开始，标记当前要处理的未排序元素
26. **插入排序-current**：暂存当前要插入的元素值，防止被覆盖
27. **插入排序-内层 while**：从后往前扫描已排序部分，比 current 大的元素逐一后移
28. **插入排序-实用性**：三种基础排序中最实用，数据基本有序时效率可达 O(n)
29. **三种排序实用性排序**：插入排序 > 选择排序 > 冒泡排序
30. **Scanner 键盘输入**：`java.util.Scanner` 类读取控制台输入
31. **Scanner 使用步骤**：导包 → 创建对象 → 调用方法读取 → 关闭资源
32. **Scanner 常用方法**：`nextInt()`、`nextDouble()`、`next()`、`nextLine()`、`nextBoolean()` 等
33. **`next()` vs `nextLine()`**：`next()` 以空格/换行结束，`nextLine()` 仅以换行结束
34. **Scanner 陷阱**：`nextInt()` 后使用 `nextLine()` 会读取到空字符串
35. **Scanner 陷阱解决**：在 `nextInt()` 后调用一次 `nextLine()` 吃掉换行符
36. **Scanner 类型匹配**：输入的值必须符合 Scanner 接收的类型，否则抛出 `InputMismatchException`
37. **变量交换-临时变量法**：`tmp = a; a = b; b = tmp;`
38. **变量交换-加减法**：`a = a + b; b = a - b; a = a - b;`
39. **首尾交换**：利用对称性，第 i 位与第 length-1-i 位交换
40. **循环右移**：从后往前逐个移动，保存最后一个元素放到开头
41. **取模优化**：`k = k % arr.length` 解决 k 大于数组长度的问题
42. **数组去重思路**：统计不重复个数 → 创建新数组 → 填充不重复元素
43. **布尔标记法**：使用 `boolean` 变量标记元素是否重复
44. **打擂法找最大值**：假设第一个是最大值，遇到更大的就更新
45. **数组求和**：遍历数组，累加每个元素
46. **倒序输出**：从 `length-1` 到 `0` 递减遍历
47. **奇偶判断**：`n % 2 == 0` 偶，`n % 2 != 0` 奇

---

## 十一、知识拓展

### 1. 数组的地址值

当直接打印数组变量时，输出的是数组在堆内存中的**地址值**：

```java
int[] a = new int[10];
System.out.println(a);  // 输出类似：[I@b4c966a
```

地址值含义：
- `[` — 一维数组
- `I` — 元素类型为 int
- `@` — 分隔符
- `b4c966a` — 堆内存中的十六进制地址

### 2. 二维数组的内存结构

```
二维数组 arr2 = {arr01, arr02, arr03} 在内存中：
┌──────────┐
│ arr2[0]  │────→ arr01: [1, 3]
├──────────┤
│ arr2[1]  │────→ arr02: [4, 5, 6]
├──────────┤
│ arr2[2]  │────→ arr03: [7, 8, 9, 15]
└──────────┘
```

二维数组的每个元素存储的是**一维数组的引用地址**，而不是元素本身。

### 3. 二分查找原理

`Arrays.binarySearch()` 基于**二分查找算法**（Binary Search）：

- **前提**：数组必须已排序
- **原理**：每次将查找范围缩小一半，与中间元素比较
- **时间复杂度**：O(log n) — 效率远高于线性查找 O(n)

```
已排序数组：[1, 4, 5, 6, 7, 7, 8, 78, 87, 165, 689]
查找目标：689

第1次：比较中间元素 7 → 689 > 7，在右半部分查找
第2次：比较中间元素 87 → 689 > 87，继续在右半部分查找
第3次：比较中间元素 165 → 689 > 165，继续在右半部分查找
第4次：找到 689，返回索引 10
```

### 4. 数组拷贝的深浅问题

`Arrays.copyOf()` 和 `Arrays.copyOfRange()` 执行的是**浅拷贝**：

- 对于基本数据类型：拷贝的是值，互不影响 ✓
- 对于引用数据类型：拷贝的是引用地址，新数组和原数组指向同一个对象 ⚠️

---

## 十二、完整代码汇总

### Demo01.java — 多维数组

```java
/**
 * @author WanJl
 * @version 1.0
 * @title Demo01
 * @description 多维数组
 * @create 2026/7/16
 */
public class Demo01 {
    public static void main(String[] args) {
        /*
            一维数组中所有的元素都是数据类型的变量或常量
            二维数组中所有的元素都是一维数组
            三维数组中所有的元素都是二维数组
            ......

            Java多维数组的本质：其实就是数组的数组，二维数组的元素其实就是一维数组的引用地址值

            二维数组的定义的格式：
            数组类型[][] 数组名=new 数组类型[二维数组长度][一维数组长度];
         */
        //数组类型[][] 数组名=new 数组类型[二维数组长度][一维数组长度];
        int[][] arr = new int[3][2];  //[行数][列数]

        //遍历二维数组需要两层循环嵌套
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr[i].length; j++) {
                System.out.print(arr[i][j] + " ");
            }
            System.out.println();
        }
        // 静态初始化二维数组----三行三列
        int[][] arr1 = {
                {1, 2, 3},
                {4, 5, 6},
                {7, 8, 9}
        };
        for (int i = 0; i < arr1.length; i++) {
            for (int j = 0; j < arr1[i].length; j++) {
                System.out.print(arr1[i][j] + " ");
            }
            System.out.println();
        }

        // 二维数组中的一维数组，不是必须要相同长度的，长度是不一样的。
        int[] arr01 = {1, 3};
        int[] arr02 = {4, 5, 6};
        int[] arr03 = {7, 8, 9, 15};
        int[][] arr2 = {arr01, arr02, arr03}; //二维数组存放的其实就是一维数组的引用地址
        for (int i = 0; i < arr2.length; i++) {
            for (int j = 0; j < arr2[i].length; j++) {
                System.out.print(arr2[i][j] + " ");
            }
            System.out.println();
        }
    }
}
```

### Demo02.java — foreach 增强 for 循环

```java
/**
 * @author WanJl
 * @version 1.0
 * @title Demo02
 * @description foreach迭代-增强for循环
 * @create 2026/7/16
 */
public class Demo02 {
    public static void main(String[] args) {
        //我们之前使用的for循环，是for(int i=0;i<10;i++)
        //可以使用增强for循环--foreach，作用和for循环一样
        //主要是用来进行数组遍历
        /*
            格式：
                for(数据类型 变量 : 数组名){
                    代码块
                }
         */
        int[] arr = {1, 2, 3, 4, 5, 6, 8};
        for (int a : arr) {
            System.out.print(a + " ");
        }
        System.out.println();
        /*
            foreach 遍历的数组，无法获取索引值，所以只适合遍历全部元素。想要获取某个索引位置的元素，需要for循环
         */

        int[] arr01 = {1, 3};
        int[] arr02 = {4, 5, 6};
        int[] arr03 = {7, 8, 9, 15};
        int[][] arr2 = {arr01, arr02, arr03};
        for (int[] a : arr2) { //外层foreach
            for (int a1 : a) { //内层foreach
                System.out.print(a1 + " ");
            }
            System.out.println();
        }
    }
}
```

### Demo03.java — Arrays 数组工具类

```java
import java.util.Arrays;

/**
 * @author WanJl
 * @version 1.0
 * @title Demo03
 * @description 数组工具类 ： java.util.Arrays;
 * @create 2026/7/16
 */
public class Demo03 {
    public static void main(String[] args) {
        /*
            数组工具类 ： java.util.Arrays;
            java中的命名特点，如果一个类的类名 是名词+s，那么大多数情况下，这个类就是一个工具类
         */
        int[] arr = {7, 6, 78, 1, 689, 87, 165, 7, 4, 5, 8};
        //使用工具类对数组进行排序
        Arrays.sort(arr);

        //输出arr数组--Arrays.toString方法 是直接把arr数组转换成字符串
        System.out.println(Arrays.toString(arr));
        // 拷贝（扩展长度，多余补 0）-- 把一个数组的元素复制到一个新的数组并指定新数组的长度，多余补 0
        int[] copy = Arrays.copyOf(arr, 10);
        System.out.println(Arrays.toString(copy));
        // 拷贝指定范围 [1,4)
        int[] range = Arrays.copyOfRange(arr, 1, 4);
        System.out.println(Arrays.toString(range));
        // 二分查找（需先排序），返回索引值
        int index = Arrays.binarySearch(arr, 689);
        System.out.println(index);
    }
}
```

### Demo04.java — 冒泡排序

```java
import java.util.Arrays;

/**
 * @author WanJl
 * @version 1.0
 * @title Demo04
 * @description 数组排序-冒泡排序
 * @create 2026/7/16
 */
public class Demo04 {
    /*
        我们现在已经学了最基础的  数据结构与算法
        数据结构：数组结构--属于线性结构的一种
        算法：比如排序算法（冒泡、选择、插入...）
        冒泡排序，是所有排序【算法】最直观的，最基础的一种。
        冒泡排序就像水里的泡泡一样，大的元素会逐渐的冒到数组的末端。

        冒泡排序的核心思想：
            重复遍历数组，依次比较相邻的两个元素，如果顺序错误就交换。
            每轮遍历把当前未排序的部分的最大值"冒泡"到正确位置。
     */
    public static void main(String[] args) {
        /*
            冒泡排序的基础版
            每一轮把未排序部分的最大值"冒泡"到数组末尾
         */
        int[] arr = {1, 456, 782, 5, 8, 4, 598, 1, 46, 68, 465, 74};
        //冒泡排序需要两层循环
        for (int i = 0; i < arr.length - 1; i++) {  //外层循环，控制的一共冒泡多少轮
            //内层循环 在未排序的区间[0,arr.length-1-i] 中相邻比较
            //因为 i 后面的元素已经有序了，不需要再比较了
            for (int j = 0; j < arr.length - 1 - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    //交换位置（加减法交换）
                    arr[j] = arr[j] + arr[j + 1];
                    arr[j + 1] = arr[j] - arr[j + 1];
                    arr[j] = arr[j] - arr[j + 1];
                }
            }
        }
        System.out.println(Arrays.toString(arr));

        // 优化版
        int[] arr2 = {1, 456, 782, 5, 8, 4, 598, 1, 46, 68, 465, 74};
        for (int i = 0; i < arr2.length - 1; i++) {
            boolean b = false;    //定义boolean类型的变量，用来标记本轮是否有交换
            for (int j = 0; j < arr2.length - 1 - i; j++) {
                if (arr2[j] > arr2[j + 1]) {
                    //交换位置
                    arr2[j] = arr2[j] + arr2[j + 1];
                    arr2[j + 1] = arr2[j] - arr2[j + 1];
                    arr2[j] = arr2[j] - arr2[j + 1];
                    b = true; //本轮有交换
                }
            }
            //判断，如果本轮没有发生任何交换，说明数组已经有序，提前结束
            if (b == false) {
                break;
            }
            System.out.println("第" + (i + 1) + "轮冒泡后：" + Arrays.toString(arr2));
        }
    }
}
```

### Demo05.java — 选择排序

```java
import java.util.Arrays;

/**
 * @author WanJl
 * @version 1.0
 * @title Demo05
 * @description 数组排序-选择排序
 * @create 2026/7/16
 */
public class Demo05 {
    /*
        选择排序：
        选择排序的思路要比冒泡排序更聪明一点，冒泡排序是不断的交换相邻的元素，
        而选择排序，是每轮只做一次交换。
        选择的思路是每一轮从剩余未排序的元素中找出最小的元素，
        直接把它放到未排序部分的第1位。
        核心思路：每一轮从未排序区间选出最小值，与未排序区间的第1个元素进行交换。
        优点：交换次数比冒泡更少
        缺点：不稳定，可能会破坏相对顺序
     */
    public static void main(String[] args) {
        int[] arr = {1, 456, 782, 5, 8, 4, 598, 1, 46, 68, 465, 74};
        //外层循环：i 既是已排序区间的末尾边界，也是当前要确定的位置
        for (int i = 0; i < arr.length - 1; i++) {
            //假设当前未排序区间的第1个元素就是最小的
            int minIndex = i;
            //内层循环：在未排序区间 [i+1, arr.length-1] 找到真正的最小值
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;  //更新最小元素的索引值
                }
            }
            //如果最小元素索引值和当前 i 不相等，就交换位置
            if (minIndex != i) {
                arr[minIndex] = arr[minIndex] + arr[i];
                arr[i] = arr[minIndex] - arr[i];
                arr[minIndex] = arr[minIndex] - arr[i];
            }
            System.out.println("第" + (i + 1) + "轮选择后：" + Arrays.toString(arr));
        }
    }
}
```

### Demo06.java — 插入排序

```java
import java.util.Arrays;

/**
 * @author WanJl
 * @version 1.0
 * @title Demo06
 * @description 数组排序-插入排序
 * @create 2026/7/16
 */
public class Demo06 {
    /*
        插入排序
        是三种基础排序中最实用的一种，也是使用最广泛的一种O(n²)排序算法，
        他的思想非常符合人整理东西的习惯。
        把数组分为"已排序"和"未排序"两部分，每次从未排序部分取一个元素，
        插入到已排序部分的正确位置。
        就类似于打扑克时，抓牌然后插入到手中已排序好的牌中正确的位置。
        核心思想：
            把数组视为"已排序"（前部分）和未排序（后部分）。
            每次从未排序部分中拿取第1个元素，
            从后往前在已排序部分中找到合适的位置（比较）并插入
     */
    public static void main(String[] args) {
        int[] arr = {1, 456, 782, 5, 8, 4, 598, 1, 46, 68, 465, 74};
        //从第2个元素开始，假设 arr[0] 已经是已排序部分
        for (int i = 1; i < arr.length; i++) {
            //获取当前要插入的元素，未排序的第1个
            int current = arr[i];
            //已排序部分的最后一个索引值
            int j = i - 1;
            //从后往前扫描已经排序部分，寻找插入位置
            //如果已排序的元素大于current，就向后移动一位
            while (j >= 0 && arr[j] > current) {
                arr[j + 1] = arr[j];
                j--;
            }
            //此时 j+1 就是current应该插入到的位置
            arr[j + 1] = current;
            System.out.println("第" + i + "轮插入后：" + Arrays.toString(arr));
        }
    }
}
```

### Demo07.java — Scanner 键盘输入

```java
import java.util.Scanner;

/**
 * @author WanJl
 * @version 1.0
 * @title Demo07
 * @description Scanner键盘输入
 * @create 2026/7/16
 */
public class Demo07 {
    /*
        Java中实现键盘输入的标准方式就是 Scanner 类
        它是 java.util 包下的一个工具类，可以读取我们在控制台中输入的数据
        操作步骤：
            1、导包 import java.util.Scanner
            2、创建 Scanner 对象
                格式：Scanner sc = new Scanner(System.in);
            3、调用方法读取数据
                |- nextByte() / nextShort() / nextInt() / nextLong()
                |- nextFloat() / nextDouble()
                |- next()         读取单个字符串（空格/换行符结束）
                |- nextLine()     读取整行字符串（换行符结束）
                |- nextBoolean()  读取布尔值
                |- next().charAt(0)  读取字符
            4、关闭 Scanner sc.close();
     */
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        /*
            Scanner有陷阱：
            nextInt()后面使用nextLine()的问题
            原因：nextInt()读取完数字后，换行符还停留在缓冲区中，
            紧接着的nextLine()遇到了换行符就直接结束了
         */
        // 解决方案：在nextInt()之后调用一次nextLine() 吃掉换行符
        int a = sc.nextInt();
        sc.nextLine();  // 吃掉换行符
        String s = sc.nextLine();

        System.out.println(a);
        System.out.println(s);
        sc.close();
    }
}
```

### Demo08.java — Scanner 练习

```java
import java.util.Scanner;

/**
 * @author WanJl
 * @version 1.0
 * @title Demo08
 * @description Scanner练习
 * @create 2026/7/16
 */
public class Demo08 {
    public static void main(String[] args) {
        // 1、创建Scanner对象
        Scanner sc = new Scanner(System.in);

        // 2、读取不同类型的数据
        System.out.println("请输入您的姓名：");
        String name = sc.next();

        System.out.println("请输入您的年龄：");
        int age = sc.nextInt();

        System.out.println("请输入您的身高（米）：");
        double height = sc.nextDouble();

        System.out.println("您是否是在校学生（true/false）：");
        boolean isStudent = sc.nextBoolean();

        // 3、输出用户信息
        System.out.println("\n========用户信息========");
        System.out.println("姓名：" + name);
        System.out.println("年龄：" + age);
        System.out.println("身高：" + height);
        System.out.println("在校学生：" + isStudent);

        // 4、关闭Scanner 释放资源
        sc.close();
    }
}
```

### Exercises01.java — 首尾对称交换

```java
/**
 * @author WanJl
 * @version 1.0
 * @title Exercises01
 * @description 课堂练习01：首尾对称交换数组元素
 * @create 2026/7/16
 */
public class Exercises01 {
    public static void main(String[] args) {
        int[] arr = {10, 20, 30, 40, 50};
        System.out.print("交换前：");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
        System.out.print("交换后：");
        //首尾交换 第1位和第5位，第2位和第4位
        arr[0] = arr[0] + arr[4];
        arr[4] = arr[0] - arr[4];
        arr[0] = arr[0] - arr[4];

        arr[1] = arr[1] + arr[3];
        arr[3] = arr[1] - arr[3];
        arr[1] = arr[1] - arr[3];

        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }
}
```

### Exercises02.java — 数组循环右移

```java
/**
 * @author WanJl
 * @version 1.0
 * @title Exercises02
 * @description 课堂练习02：数组循环右移
 * @create 2026/7/16
 */
public class Exercises02 {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int k = 2; //右移位数
        //如果k大于数组长度，要取模
        k = k % arr.length;

        System.out.print("移动前：");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
        //整体右移k位（循环右移）
        for (int i = 0; i < k; i++) {
            //保存最后一个元素
            int last = arr[arr.length - 1];
            //从后往前，每个元素向后移动1位
            for (int j = arr.length - 1; j > 0; j--) {
                arr[j] = arr[j - 1];
            }
            //把最后1位给第0位
            arr[0] = last;
        }
        System.out.print("移动后：");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }
}
```

### Exercises03.java — 数组去重

```java
/**
 * @author WanJl
 * @version 1.0
 * @title Exercises03
 * @description 课堂练习03：数组去重
 * @create 2026/7/16
 */
public class Exercises03 {
    public static void main(String[] args) {
        /*
            先统计不重复元素的个数
         */
        int[] arr = {5, 2, 8, 2, 5, 9, 1, 8, 3};
        int count = 0;    //个数
        for (int i = 0; i < arr.length; i++) {
            boolean b = false;    //设置false，表示先认为当前的数组元素是没有重复的
            for (int j = 0; j < i; j++) {    //找 arr[i]元素，在arr数组中，有没有重复的元素。
                if (arr[i] == arr[j]) {    //只要找到一个重复的
                    b = true; //就把b设置为 true
                    break;  //跳出内层循环
                }
            }
            if (b == false) {  //那就说明这个元素是没有重复的，就计数
                count++;
            }
        }
        //创建的新的数组长度就是 count
        int[] newArr = new int[count];    //动态初始化
        int index = 0;    //作为新数组的索引值
        for (int i = 0; i < arr.length; i++) {
            boolean b = false;    //设置false，表示先认为当前的数组元素是没有重复的
            //检查arr[i]在它之前是否有出现过
            for (int j = 0; j < i; j++) {
                if (arr[i] == arr[j]) {
                    b = true;
                    break;
                }
            }
            //判断如果我们设置的标记还是false，说明没有重复
            if (b == false) {
                newArr[index] = arr[i];
                index++;
            }
        }
        for (int i = 0; i < newArr.length; i++) {
            System.out.print(newArr[i] + " ");
        }
    }
}
```

### HomeWork07.java — 数组求和

```java
/**
 * @author WanJl
 * @version 1.0
 * @title HomeWork07
 * @description 7月15日作业：数组元素求和
 * @create 2026/7/16
 */
public class HomeWork07 {
    public static void main(String[] args) {
        int[] arr = {11, 22, 33, 44, 55};
        int sum = 0;
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
        System.out.println(sum);
    }
}
```

### HomeWork08.java — 找数组最大值

```java
/**
 * @author WanJl
 * @version 1.0
 * @title HomeWork08
 * @description 7月15日作业：找数组最大值
 * @create 2026/7/16
 */
public class HomeWork08 {
    public static void main(String[] args) {
        int[] arr = {3, 8, 1, 9, 4, 7, 2};
        int max = arr[0];  //假设第一个元素是最大值
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        System.out.println(max);
    }
}
```

### HomeWork09.java — 数组倒序输出

```java
/**
 * @author WanJl
 * @version 1.0
 * @title HomeWork09
 * @description 7月15日作业：数组倒序输出
 * @create 2026/7/16
 */
public class HomeWork09 {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        for (int i = arr.length - 1; i >= 0; i--) {
            System.out.println(arr[i]);
        }
    }
}
```

### HomeWork10.java — 统计奇偶数个数

```java
/**
 * @author WanJl
 * @version 1.0
 * @title HomeWork10
 * @description 7月15日作业：统计奇偶数个数
 * @create 2026/7/16
 */
public class HomeWork10 {
    public static void main(String[] args) {
        int[] arr = {12, 7, 9, 24, 15, 8, 31, 46};
        int count = 0;    //奇数个数
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] % 2 != 0) {
                count++;
            }
        }
        System.out.println("奇数个数：" + count + "，偶数个数：" + (arr.length - count));
    }
}
```

---

## 十三、拓展阅读

| 概念 | 说明 |
|------|------|
| **多维数组** | Java 中多维数组是数组的数组，二维数组的元素是一维数组的引用地址 |
| **二维数组遍历** | 外层循环遍历行（`arr.length`），内层循环遍历列（`arr[i].length`） |
| **不规则数组** | 二维数组中的每个一维数组长度可以不同 |
| **增强 for 循环** | `for(类型 变量 : 数组)` 语法，简化数组/集合遍历，无法获取索引 |
| **Arrays 工具类** | `java.util.Arrays` 包含 sort、toString、copyOf、binarySearch 等静态方法 |
| **Arrays.sort()** | 对数组进行快速排序（Dual-Pivot Quicksort） |
| **Arrays.toString()** | 将数组转换为可读的字符串格式 `[elem1, elem2, ...]` |
| **Arrays.copyOf()** | 数组拷贝，可指定新长度，扩展部分补默认值，截断部分丢弃 |
| **Arrays.copyOfRange()** | 拷贝数组指定范围 `[from, to)`，包含 from 不包含 to |
| **Arrays.binarySearch()** | 二分查找，数组必须先排序，返回索引值（未找到返回负数） |
| **冒泡排序** | 重复遍历数组，相邻元素两两比较，大的往后冒，每轮把最大值冒泡到末尾 |
| **冒泡排序-外层循环** | 控制冒泡轮数，n 个元素最多需要 n-1 轮 |
| **冒泡排序-内层循环** | 在未排序区间 `[0, arr.length-1-i]` 相邻比较，比较范围随轮数递减 |
| **冒泡排序-优化** | 引入 `boolean` 标记判断本轮是否有交换，无交换则提前结束排序 |
| **冒泡排序-时间复杂度** | 平均 O(n²)，最好 O(n)（优化版已有序），最坏 O(n²) |
| **冒泡排序-空间复杂度** | O(1)，原地排序，不需要额外空间 |
| **选择排序** | 每轮从未排序区间选出最小值，与未排序区间的第 1 个元素交换，每轮最多 1 次交换 |
| **选择排序-外层循环** | `i` 是当前要确定的位置，范围 `[0, arr.length-2]` |
| **选择排序-内层循环** | 在未排序区间 `[i+1, arr.length-1]` 找最小值索引 |
| **选择排序-minIndex** | 记录最小元素的索引值，初始为当前位置 `i`，遇更小值则更新 |
| **选择排序-交换条件** | `minIndex != i` 时才执行交换，每轮最多交换 1 次 |
| **选择排序-时间复杂度** | 平均 O(n²)，最好 O(n²)，最坏 O(n²) — 始终要遍历全部 |
| **选择排序-空间复杂度** | O(1)，原地排序 |
| **选择排序-稳定性** | ❌ 不稳定（交换可能改变相等元素的相对顺序） |
| **冒泡 vs 选择** | 冒泡多次交换，选择最多 1 次；冒泡稳定，选择不稳定 |
| **插入排序** | 将未排序的第 1 个元素插入到已排序部分的正确位置，类似打扑克理牌 |
| **插入排序-分治思想** | 把数组分为"已排序"和"未排序"两部分 |
| **插入排序-外层循环** | `i` 从 1 开始，标记当前要处理的未排序元素 |
| **插入排序-current** | 暂存当前要插入的元素值，防止被覆盖 |
| **插入排序-内层 while** | 从后往前扫描，比 current 大的元素逐一后移，找到插入位置 |
| **插入排序-时间复杂度** | 平均 O(n²)，最好 O(n)（数据基本有序时），最坏 O(n²） |
| **插入排序-空间复杂度** | O(1)，原地排序 |
| **插入排序-稳定性** | ✅ 稳定 |
| **插入排序-实用性** | 三种基础 O(n²) 排序中最实用，小规模数据首选 |
| **三种排序对比** | 实用性：插入 > 选择 > 冒泡；稳定性：冒泡 ✅、插入 ✅、选择 ❌ |
| **Scanner 键盘输入** | `java.util.Scanner` 类，用于读取控制台输入 |
| **Scanner 导包** | `import java.util.Scanner;` |
| **Scanner 创建对象** | `Scanner sc = new Scanner(System.in);` |
| **Scanner 关闭** | `sc.close();` 释放资源 |
| **Scanner-nextInt()** | 读取 int 类型整数 |
| **Scanner-nextDouble()** | 读取 double 类型小数 |
| **Scanner-next()** | 读取单个字符串，以空格/换行结束 |
| **Scanner-nextLine()** | 读取整行字符串，以换行结束 |
| **Scanner-nextBoolean()** | 读取 boolean 类型值（true/false） |
| **Scanner 陷阱** | `nextInt()` 后用 `nextLine()` 会读取到空字符串（缓冲区残留换行符） |
| **Scanner 陷阱解决** | 在 `nextInt()` 后调用一次 `nextLine()` 吃掉换行符 |
| **Scanner 类型不匹配** | 输入类型与接收类型不符时抛出 `InputMismatchException` |
| **变量交换-临时变量** | `tmp = a; a = b; b = tmp;` 最直观的交换方式 |
| **变量交换-加减法** | `a = a + b; b = a - b; a = a - b;` 不借助第三个变量 |
| **数组循环右移** | 从后往前逐位移动，保存最后一位到第一位，外层套循环控制移动次数 |
| **数组去重** | 两层循环 + 布尔标记，检查当前元素在它之前是否出现过 |
| **打擂法** | 先假设第一个是最大值/最小值，遍历数组遇到更值就更新 |
| **数组求和** | `sum += arr[i]` 遍历累加 |
| **倒序遍历** | `for (int i = length-1; i >= 0; i--)` |
| **奇偶判断** | `n % 2 == 0` 为偶数，`n % 2 != 0` 为奇数 |
| **取模优化** | 对循环移动的位数取模，避免无效重复移动 |
