Files
hustoj/web/doc/2.2 数组的插入与删除.md

451 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2.2 数组的插入与删除
---
## 一、先把数组想成一排固定座位
今天我们只学两件事:
- 往数组里放进一个新元素,这叫**插入**。
- 从数组里拿走一个元素,这叫**删除**。
数组可以想成教室里一排**紧紧挨着的固定座位**,每个位置都不能乱跳。
比如下面这个数组:
| 位置(第几个) | 第1个 | 第2个 | 第3个 | 第4个 | 第5个 |
|:---:|:---:|:---:|:---:|:---:|:---:|
| 下标 | 0 | 1 | 2 | 3 | 4 |
| 内容 | 12 | 18 | 23 | 31 | 40 |
这里要特别注意:
- 平时说“第 1 个位置、第 2 个位置”,是按照人的习惯数的。
- 在 C++ 数组里,下标从 `0` 开始。
所以:
- 第 1 个位置,对应下标 `0`
- 第 3 个位置,对应下标 `2`
-`pos` 个位置,对应下标 `pos - 1`
> 这节课讲图的时候,我们主要说“第几个位置”;写代码的时候,再换成下标。
---
## 二、为什么数组的插入和删除要“搬家”?
数组里的元素是**连续排在一起**的。
这就像一排已经坐好的同学:
- 想在中间插入一个人,就得有人挪一挪,腾出空位。
- 想删掉中间一个人,就会空出一个洞,后面的人要补上来。
所以数组的插入和删除,最关键的不是“放进去”或“拿出来”,而是:
> **怎样移动其他元素,才能既不丢数据,也不弄乱顺序。**
这就是本节最重要的地方。
---
## 三、数组插入:先腾位置,再放新元素
### 1. 例子:在第 3 个位置插入 20
原数组:
```text
位置: 1 2 3 4 5 6
内容: [12] [18] [23] [31] [40] [ ]
```
注意:这里第 6 个位置先留空,表示数组容量足够大,可以插入新元素。
现在要把 `20` 插入到**第 3 个位置**。
插入后应该变成:
```text
位置: 1 2 3 4 5 6
内容: [12] [18] [20] [23] [31] [40]
```
### 2. 插入时,元素是怎样移动的?
要想让第 3 个位置空出来,原来第 3、4、5 个位置上的元素都要向后移动一格。
但一定要记住:
> **插入时,要从后往前移。**
移动过程如下:
```text
原来: [12] [18] [23] [31] [40] [ ]
第1步 [12] [18] [23] [31] [40] [40] 把 40 往后移
第2步 [12] [18] [23] [31] [31] [40] 把 31 往后移
第3步 [12] [18] [23] [23] [31] [40] 把 23 往后移
第4步 [12] [18] [20] [23] [31] [40] 把 20 放进第 3 个位置
```
### 3. 为什么插入要从后往前移?
因为如果你从前往后移,前面的数据会把后面的数据盖掉。
来看一个错误示范。还是在第 3 个位置插入 `20`
```text
原来: [12] [18] [23] [31] [40] [ ]
错误地从前往后移:
第1步 [12] [18] [23] [23] [40] [ ]
第2步 [12] [18] [23] [23] [23] [ ]
第3步 [12] [18] [23] [23] [23] [23]
```
你会发现:原来的 `31``40` 都不见了!
原因是:
- 你先把 `23` 复制到了后面。
- 后面的位置被改掉以后,再继续搬,就只能搬到已经改坏的数据。
所以插入时一定要:
> **从最后一个元素开始,倒着往前挪。**
### 4. 插入的步骤口诀
可以记成一句话:
> **先留空,从后搬,最后放。**
具体就是:
1. 先保证数组后面有空位置。
2. 从最后一个元素开始,依次向后移动一格。
3. 移到目标位置后停下。
4. 把新元素放进去。
5. 数组长度 `n` 加 1。
### 5. 插入的课堂版 C++ 代码
下面的代码约定:
- 数组现在有 `n` 个元素。
- 要把 `x` 插入到第 `pos` 个位置。
- `pos``1` 开始数。
- 数组容量足够大。
```cpp
for (int i = n; i >= pos; i--) {
a[i] = a[i - 1];
}
a[pos - 1] = x;
n++;
```
你可以这样理解这段循环:
- `i = n` 时,把最后一个元素搬到新位置。
- `i = n - 1` 时,把倒数第二个元素往后搬。
- 一直搬到第 `pos` 个位置为止。
---
## 四、数组删除:先补空位,再缩短长度
### 1. 例子:删除第 3 个位置的元素
原数组:
```text
位置: 1 2 3 4 5
内容: [ 8] [11] [14] [17] [20]
```
现在要删除第 3 个位置上的 `14`
删除后应该变成:
```text
位置: 1 2 3 4
内容: [ 8] [11] [17] [20]
```
### 2. 删除时,元素是怎样移动的?
第 3 个位置删掉以后,会空出一个位置。后面的元素要依次往前补。
注意:
> **删除时,要从前往后移。**
移动过程如下:
```text
原来: [ 8] [11] [14] [17] [20]
第1步 [ 8] [11] [17] [17] [20] 把 17 往前移
第2步 [ 8] [11] [17] [20] [20] 把 20 往前移
最后: [ 8] [11] [17] [20] 长度减 1
```
最后那个多出来的 `20` 虽然还留在原来的地方,但因为数组长度已经减 1所以它已经**不算数组的一部分了**。
### 3. 为什么删除要从前往后移?
因为空位在前面,应该让后面的元素一个一个补上来。
如果你反过来,从后往前移,也会出问题。还是删除第 3 个位置:
```text
原来: [ 8] [11] [14] [17] [20]
错误地从后往前移:
第1步 [ 8] [11] [14] [20] [20]
第2步 [ 8] [11] [20] [20] [20]
```
原来的 `17` 被弄丢了。
所以删除时要记住:
> **从缺口的后面开始,按顺序往前补。**
### 4. 删除的步骤口诀
可以记成一句话:
> **先删掉,从前搬,最后减。**
具体就是:
1. 找到要删除的位置。
2. 让后面的元素依次向前移动一格。
3. 数组长度 `n` 减 1。
### 5. 删除的课堂版 C++ 代码
下面的代码约定:
- 数组现在有 `n` 个元素。
- 删除第 `pos` 个位置上的元素。
- `pos``1` 开始数。
```cpp
for (int i = pos; i < n; i++) {
a[i - 1] = a[i];
}
n--;
```
你可以这样理解这段循环:
- `i = pos` 时,把后一个元素补到空位上。
- `i = pos + 1` 时,再把更后面的元素补上来。
- 一直补到最后一个元素为止。
---
## 五、插入和删除放在一起比较
| 操作 | 会发生什么 | 正确移动方向 | 为什么 |
|:---:|:---|:---:|:---|
| 插入 | 中间要腾出一个空位 | 从后往前 | 先保护后面的数据,不让它们被覆盖 |
| 删除 | 中间会出现一个缺口 | 从前往后 | 让后面的数据按顺序补上来 |
再记一遍:
- **插入:从后往前移**
- **删除:从前往后移**
这是本节最重要的结论。
---
## 六、两个最容易忽略的小细节
### 1. 插入前,数组要有空位
如果数组已经装满了,就不能直接插入。
比如数组最多只能放 5 个数,现在已经有 5 个数了,再插入第 6 个,就没有地方放了。
所以写程序时,常常会这样做:
- 开一个更大的数组,比如最多放 `100` 个数。
- 实际已经用了多少个,用变量 `n` 记录。
### 2. 删除后,不是把数“擦掉”,而是把长度减 1
数组删除一个元素后,最后那个位置里可能还保留着旧数值,但这已经不重要了。
因为程序只看前 `n` 个元素:
- 删除前看前 `n`
- 删除后只看前 `n - 1`
所以真正关键的是:
> **长度变了,数组有效范围也变了。**
---
## 七、特殊情况也要会判断
### 1. 在最后面插入
如果要在第 `n + 1` 个位置插入,也就是直接接到数组末尾:
```text
[12] [18] [23] [31] [40] -> [12] [18] [23] [31] [40] [50]
```
这时不需要搬动任何元素,直接放进去即可。
### 2. 删除最后一个元素
如果删除的是最后一个元素:
```text
[12] [18] [23] [31] [40] -> [12] [18] [23] [31]
```
这时也不需要搬动任何元素,只要把长度减 1 就行。
---
## 八、最容易犯的 5 个错误
### 错误 1插入时移动方向写反
结果:后面的数据会被覆盖。
### 错误 2删除时移动方向写反
结果:中间的数据会丢失。
### 错误 3忘记更新 `n`
插入后忘记 `n++`,新元素虽然放进去了,但程序可能不把它算进去。
删除后忘记 `n--`,旧数据虽然应该消失了,但程序还会把它输出出来。
### 错误 4把“第几个位置”和“下标”搞混
记住:
-`1` 个位置,对应下标 `0`
-`pos` 个位置,对应下标 `pos - 1`
### 错误 5插入时没有预留空间
结果:数组越界,程序可能出错。
---
## 九、一张总图记住本节内容
```text
数组插入:先腾空位
[12] [18] [23] [31] [40] [ ]
← ← ←
[12] [18] [20] [23] [31] [40]
数组删除:先补缺口
[ 8] [11] [14] [17] [20]
→ →
[ 8] [11] [17] [20]
```
记忆口诀:
> **插入看后面,从后往前搬;删除看前面,从前往后搬。**
---
## 十、课堂版完整示例
下面是一份把“插入”和“删除”都写进去的课堂版程序。为了简单起见,输入位置 `pos` 按“第几个位置”来理解,也就是从 `1` 开始数。
```cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 105;
int a[MAXN];
int n;
void insertValue(int pos, int x) {
for (int i = n; i >= pos; i--) {
a[i] = a[i - 1];
}
a[pos - 1] = x;
n++;
}
void deleteValue(int pos) {
for (int i = pos; i < n; i++) {
a[i - 1] = a[i];
}
n--;
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
int pos, x;
cin >> pos >> x;
insertValue(pos, x);
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << "\n";
cin >> pos;
deleteValue(pos);
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << "\n";
return 0;
}
```
---
## 十一、自己试一试
### 1. 观察移动过程
`9` 插入到下面数组的第 2 个位置,请你把每一步移动都画出来:
```text
[3] [7] [12] [15] [ ]
```
### 2. 观察删除过程
删除下面数组的第 4 个位置,请你把每一步移动都画出来:
```text
[5] [8] [11] [14] [20]
```
### 3. 想一想
- 为什么插入和删除都可能需要移动很多个元素?
- 如果总是在数组最前面插入,会不会很麻烦?
如果你能清楚地回答这两个问题,就说明你已经真正理解数组的插入和删除了。