考点分析
- 顺序、折半、分块查找:会求平均查找长度(注意不考B、B+树)
- 【重点】 散列表/哈希表:构造方法、求平均查找长度
- 八大排序的特点、时间、空间复杂度、稳定性;插入、快排、归并、堆排序是重点,除基数排序外都要能写出算法。
顺序查找
- 基本思想: 从线性表的一端开始,逐个查询关键字是否满足给定条件。
- 实现代码:
typedef struct {
int *Elem; //动态数组
int length;
}SSTable;
/**
* 顺序查找
* @param S 线性表
* @param key 查找的元素
* @return 返回查找元素的下标,失败返回-1
*/
int Search_Seq(SSTable S,int key){
for (int i = 0; i < S.length; ++i) {
if (S.Elem[i] == key){
return i;
}
}
return -1;
}
-
平均查找长度:
(1)成功:对于又n个元素的表,给定值key与表中第 i i i个元素相等,要进行 i i i次关键字比较,即 C i = i C_{i}=i Ci=i 查找成功时,顺序查找的平均长度为: A S L 成 功 = ∑ i = 1 n P i C i ASL_{成功}= \sum_{i=1}^nP_{i}C_{i} ASL成功=i=1∑nPiCi
每个数的查询概率是相同的,即 P i = 1 n P_{i}=\frac{1}{n} Pi=n1,计算得: A S L 成 功 = n + 1 2 ASL_{成功}=\frac{n+1}{2} ASL成功=2n+1
(2)失败:比较n次。 A S L 失 败 = n ASL_{失败}=n ASL失败=n -
有序表的顺序查找(平均查找长度):
(1)成功: 同上。
(2)失败: 主要是减少了失败的比较次数,当我们发现第 i i i个关键字大于key时,即可停止比较,返回失败信息。
以上图为例分析:共有 n + 1 n+1 n+1查询失败的情况,注意我们key大于第n个关键字也是失败的一种。 A S L 失 败 = ( 1 + 2 + . . . + n + n ) / ( n + 1 ) = n 2 + n n + 1 ASL_{失败}=(1+2+...+n+n)/(n+1)=\frac{n}{2}+\frac{n}{n+1} ASL失败=(1+2+...+n+n)/(n+1)=2n+n+1n
折半查找
-
前提: 有序的顺序表。【需要具有随机访问的特性,链表没有】
-
基本思想: 搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜 素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组 为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。折半搜索每次把搜索区域减少一半,时间复杂度为Ο(logn) 。
-
代码实现:
/**
* 折半查找
* @param S 顺序表
* @param key 查找的关键字
* @return 返回下标
*/
int Binary_Search(SSTable S,int key){
int low = 0;
int high = S.length-1;
int mid;
while (low <= high){
mid = (low + high)/2;
if (S.Elem[mid] == S.Elem[key]){
return mid;
} else if (S.Elem[key] > S.Elem[mid]) {
low = mid+1;
} else{
high = mid-1;
}
}
//失败返回-1
return -1;
}
- 平均查找长度:
我们可以画出折半查找过程的判定树:
分析: 给定值的比较次数最多不会超过树的高度,在等概率查找时
A
S
L
成
功
=
1
n
(
1
×
2
0
+
2
×
2
1
+
.
.
.
+
h
×
N
)
ASL_{成功}=\frac{1}{n}(1×2^{0}+2×2^{1}+...+h×N)
ASL成功=n1(1×20+2×21+...+h×N)
折半查找的判定树一定是平衡二叉树,折半查找的判定树中,只有最下面一层是不满的因此,元素个数为n时树高
h
=
⌈
l
o
g
2
(
n
+
1
)
⌉
h =\lceil log_2(n+1) \rceil
h=⌈log2(n+1)⌉【计算方法同完全二叉树】,N为最后一层结点树。
失败的结点数为n+1【等于成功结点的空链域数量】
- 时间复杂度:
每次规模减半 n z k = 1 , k = l o g 2 n \frac{n}{z^k}=1,k=log_{2}n zkn=1,k=log2n时间复杂度为O(logN)
分块查找
- 分块查找又称索引顺序查找。
- 基本思想:
- 索引表中记录每个分块的最大关键字、分块的区间
- 先查索引表(顺序或折半)、再对分块内进行顺序查找
(1)找到第一块大于等于关键字的块【可以采用顺序查找、折半查找,值得注意:对索引表进行折半查找时,若索引表中不包含目标关键字,则折半查找最终停在low>high,要在low所指分块中查找】
(2)根据索引查找关键字
- 平均查找长度:
ASL=查索引表的平均查找长度+查分块的平均查找长度
散列表
- 散列表(Hash Table),又称哈希表。是一种数据结构,特点是∶数据元素的关键字与其存储地址直接相关。
- 处理冲突的方法——拉链法
- 散列查找
散列函数的构造方法
设计目标:设计目标―—让不同关键字的冲突尽可能地少。
除留余数法——H(key) = key % p
- 散列表表长为m,取一个不大于m但最接近或等于m的质数p
- 用质数取模,分布更均匀,冲突更少。
直接定址法 —— H(key) = key 或 H(key) = a*key + b
其中,a和b是常数。这种⽅法计算最简单,且不会产⽣冲突。它适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费。
数字分析法 —— 选取数码分布较为均匀的若⼲位作为散列地址
设关键字是
r
r
r进制数(如⼗进制数),⽽
r
r
r个数码在各位上出现的频率不⼀定相同,可能在某些位上分布均匀⼀些,每种数码出现的机会均等;⽽在某些位上分布不均匀,只有某⼏种数码经常出现,此时可选取数码分布较为均匀的若⼲位作为散列地址。这种⽅法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。
平⽅取中法——取关键字的平⽅值的中间⼏位作为散列地址。
具体取多少位要视实际情况⽽定。这种⽅法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布⽐较均匀,适⽤于关键字的每位取值都不够均匀或均⼩于散列地址所需的位数。
处理冲突的方法
- 拉链法
- 开放定址法
所谓开放定址法,是指可存放新表项的空闲地址既向它的同义词表项开放,⼜向它的⾮同义词表项开放。其数学递推公式为:
H
i
=
(
H
(
k
e
y
)
+
d
i
)
%
m
,
i
=
0
,
1
,
2
,
…
,
k
(
k
≤
m
−
1
)
H_i = (H(key) + d_i) \%m,i = 0, 1, 2,…, k(k≤m - 1)
Hi=(H(key)+di)%m,i=0,1,2,…,k(k≤m−1)
m表示散列表表⻓;
d
i
d_i
di为增量序列;i 可理解为“第
i
i
i次发⽣冲突”
(1)线性探测法 ——
d
i
d_i
di = 0, 1, 2, 3, …, m-1;即发⽣冲突时,每次往后探测相邻的下⼀个单元是否为空
查找操作:
成功:
失败:【遇到空位置,查找失败】
删除操作:
如果像上面一样查找的话会查找失败:
注意:采⽤“开放定址法”时,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填⼊散列表的同义词结点的查找路径,可以做⼀个“删除标记”,进⾏逻辑删除
平均查找长度分析:
小结:
线性探测法很容易造成同义词、⾮同义词的“聚集(堆积)”现象,严重影响查找效率
产⽣原因——冲突后再探测⼀定是放在某个连续的位置
(2)平方探测法 —— d i d_i di = 02, 12, -12, 22, -22, …, k2, -k2时,称为平⽅探测法,⼜称⼆次探测法其中k≤m/2
需要注意: 负数取模, ( − 3 ) % 27 = 24 (-3)\% 27 = 24 (−3)%27=24,不是3,规则:a MOD m == (a+km) MOD m , 其中,k为任意整数
查找操作:
H
0
=
71
%
13
=
6
⟶
冲
突
H
1
=
(
H
0
+
1
)
%
27
=
7
⟶
冲
突
H
2
=
(
H
0
−
1
)
%
27
=
5
⟶
冲
突
H
3
=
(
H
0
+
4
)
%
27
=
10
⟶
冲
突
H
4
=
(
H
0
−
4
)
%
27
=
2
⟶
冲
突
H
5
=
(
H
0
+
9
)
%
27
=
15
H_0=71\%13=6\stackrel{冲突}{\longrightarrow}H_1=(H_0+1)\%27=7\stackrel{冲突}{\longrightarrow}H_2=(H_0-1)\%27=5\stackrel{冲突}{\longrightarrow}H_3=(H_0+4)\%27=10\stackrel{冲突}{\longrightarrow}H_4=(H_0-4)\%27=2\stackrel{冲突}{\longrightarrow}H_5=(H_0+9)\%27=15
H0=71%13=6⟶冲突H1=(H0+1)%27=7⟶冲突H2=(H0−1)%27=5⟶冲突H3=(H0+4)%27=10⟶冲突H4=(H0−4)%27=2⟶冲突H5=(H0+9)%27=15
⽐起线性探测法更不易产⽣“聚集(堆积)”问题
注意点:
(3)伪随机序列法 —— d i d_i di 是⼀个伪随机序列,如 di= 0, 5, 24, 11, …
- 再散列法:
再散列法(再哈希法)︰除了原始的散列函数H(key)之外,多准备几个散列函数,当散列函数冲突时,用下一个散列函数计算一个新地址,直到不冲突为止︰
小结
八大排序
插入排序
-
算法思想∶每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。
-
算法过程∶
假设前面 n-1(其中 n>=2)个数已经是排好顺序的,现将第 n 个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。
按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序。
从小到大的插入排序整个过程如图示:
第一轮:从第二位置的 6 开始比较,比前面 7 小,交换位置。
第二轮:第三位置的 9 比前一位置的 7 大,无需交换位置。
第三轮:第四位置的 3 比前一位置的 9 小交换位置,依次往前比较。
第四轮:第五位置的 1 比前一位置的 9 小,交换位置,再依次往前比较。
…
就这样依次比较到最后一个元素。
动图演示:
- 实现代码∶
/**
* 直接插入排序
* @param A 需要排序的数组
* @param n 数组的长度
*/
void InsertSort(int A[],int n){
int temp,j;
for (int i = 1; i < n; ++i) {
//第(i+1)个插入到前面1~i有序序列中:
// 1.第(i+1)个 大于等于 第i个(A[i]>=A[i-1]),直接插入到最后,无需调换
// 2.第(i+1)个 小于 第i个(A[i]<A[i-1]),第i+1个需要插入到前面去,记录
// 第i+1元素,有序序列比A[j]大的往后移一个位置
if (A[i] < A[i-1]){
temp = A[i];
for (j = i-1; j >= 0 && A[j] > temp; --j) {
A[j+1] = A[j];
}
A[j+1] = temp;
}
}
}
- 空间复杂度∶ O ( 1 ) O(1) O(1)
- 时间复杂度∶主要来⾃对⽐关键字、移动元素。若有 n 个元素,则需要 n-1 趟处理。
- 最好的时间复杂度:共n-1趟处理,每⼀趟只需要对⽐关键字1次,不⽤移动元素【最好情况:原本就有序】 — — O ( n ) ——O(n) ——O(n)
- 最坏的情况:【原本就是逆序】需要外循环 n-1 次,每次排序中待插入的元素都要和 [0, i-1] 中的 i 个元素进行比较且要将这 i 个元素后移 i 次,加上 temp = A[i] 与 arr[j] = A的两次移动,每趟移动次数为 i+2,此时比较次数和移动次数达到最大值。
—
—
O
(
n
2
)
——O(n^2)
——O(n2)
- 平均时间复杂度: — — O ( n 2 ) ——O(n^2) ——O(n2)
-
稳定性∶
稳定排序算法。因为相同元素的相对位置不变,如果两个元素相同,插入元素放在相同元素后面。 -
优化∶先⽤折半查找找到应该插⼊的位置,再移动元素。
(1)过程: A [ 0 ] A[0] A[0]不存放数据当做哨兵。
当 low>high 时折半查找停⽌,应将 [low, i-1] 内的元素全部右移,并将 A[0] 复制到 low 所指位置
上面是查找失败的情况下的插入,查找成功的话,即当 A[mid]==A[0] 时,为了保证算法的“稳定性”,应继续在 mid 所指位置右边寻找插⼊位置
==总结:==当 low>high 时折半查找停⽌,应将 [low, i-1] 内的元素全部右移,并将 A[0] 复制到 low 所指位置,当 A[mid]==A[0] 时,为了保证算法的“稳定性”,应继续在 mid 所指位置右边寻找插⼊位置
(2)实现代码:
//折半插入优化
void InsertSort3(int A[],int n){
int low,high,mid;
for (int i = 2; i <= n; ++i) {
if (A[i]<A[i-1]){
A[0] = A[i];
//找到插入的位置
low = 1;
high = i-1;
while(low <= high){
mid = (low+high)/2;
if (A[mid] <= A[0]){
low = mid + 1;
} else {
high = mid - 1;
}
}
for (int j = i-1; j >= low; --j) {
A[j+1] = A[j];
}
A[low] = A[0];
}
}
}
- 总结∶
希尔排序
-
算法思想∶先将待排序表分割成若⼲形如 L [ i , i + d , i + 2 d , … , i + k d ] L[i, i + d, i + 2d,…, i + kd] L[i,i+d,i+2d,…,i+kd] 的“特殊”⼦表,对各个⼦表分别进⾏直接插⼊排序。缩⼩增量d,重复上述过程,直到d=1为⽌。【先追求表中元素部分有序,再逐渐逼近全局有序】
-
算法过程∶
选择一个增量序列,根据d分成4个子表
对子表进行排序
第一趟排序后的序列:
缩小d,进行第二趟排序:
第二趟排序后的序列:
缩小d,进行第三趟排序:
整个表已呈现出“基本有序”,对整体再进⾏⼀次“直接插⼊排序”
- 实现代码∶
根据增量序列d,分成多个子表,对子表进行直接插入排序,直到d=1,序列看起来基本有序,再进行一趟直接插入排序排序即可。
/**
* 希尔排序
* @param A 需要排序的数组,A[0]不存放数据
* @param n 数组的长度
*/
void shellSort(int A[],int n){
//步长d
int d;
int j;
for (d = n/2; d >= 1; d = d/2) {
//子表进行直接插入排序
for (int i = d+1; i <= n; i = i+d) {
if (A[i] < A[i-d]){
A[0] = A[i];
for (j = i-d; j > 0 && A[j] > A[0]; j = j-d ) {
A[j+d] = A[j];
}
A[j+d] = A[0];
}
}
}
}
-
空间复杂度∶ O ( 1 ) O(1) O(1)
-
时间复杂度∶和增量序列 d1, d2, d3… 的选择有关,⽬前⽆法⽤数学⼿段证明确切的时间复杂度最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2),当n在某个范围内时,可达 O ( n 1.3 ) O(n^{1.3}) O(n1.3)
-
稳定性∶
-
小结∶
简单选择排序
- 算法思想∶
选择排序:每⼀趟在待排序元素中选取关键字最⼩(或最⼤)的元素加⼊有序⼦序列。
- 算法过程∶
- 实现代码∶
/**
* 选择排序
* @param A 需要排序的数组
* @param n 数组的长度
*/
void SelectionSort(int A[],int n){
//记录最小的下标
int min;
//暂时保存数据
int temp;
for (int i = 0; i < n-1; ++i) {
min = i;
for (int j = i+1; j < n; ++j) {
if (A[j] < A[min]){
min = j;
}
}
if (i != min){
temp = A[i];
A[i] = A[min];
A[min] = temp;
}
}
}
-
空间复杂度∶ O ( 1 ) O(1) O(1)
-
时间复杂度∶ O ( n 2 ) O(n^2) O(n2)
- 稳定性∶
- 小结∶
堆排序
- 堆的相关概念∶
若n个关键字序列 L [ 1 … n ] L[1…n] L[1…n] 满⾜下⾯某⼀条性质,则称为堆(Heap):
① 若满⾜: L ( i ) ≥ L ( 2 i ) 且 L ( i ) ≥ L ( 2 i + 1 ) ( 1 ≤ i ≤ n / 2 ) L(i)≥L(2i)且L(i)≥L(2i+1) (1 ≤ i ≤n/2 ) L(i)≥L(2i)且L(i)≥L(2i+1)(1≤i≤n/2)—— ⼤根堆(⼤顶堆)
② 若满⾜: L ( i ) ≤ L ( 2 i ) 且 L ( i ) ≤ L ( 2 i + 1 ) ( 1 ≤ i ≤ n / 2 ) L(i)≤L(2i)且L(i)≤L(2i+1) (1 ≤ i ≤n/2 ) L(i)≤L(2i)且L(i)≤L(2i+1)(1≤i≤n/2)—— ⼩根堆(⼩顶堆)
可以放到二叉树的顺序存储中来理解记忆:
- 大根堆:非叶子结点的值都大于等于它左右孩子的值。
- 小根堆:非叶子结点的值都小于等于它左右孩子的值。
-
算法思想∶
把所有⾮终端结点都检查⼀遍,是否满⾜⼤根堆的要求,如果不满⾜,则进⾏调整。【在顺序存储的完全⼆叉树中,⾮终端结点编号 i ≤ ⌊ n / 2 ⌋ i≤⌊n/2⌋ i≤⌊n/2⌋】 -
算法过程∶
(1)检查当前结点是否满⾜
根
≥
左
、
右
根≥左、右
根≥左、右若不满⾜,将当前结点与更⼤的⼀个孩⼦互换。从最后一个非终端节点开始,即编号为
i
≤
⌊
n
/
2
⌋
i≤⌊n/2⌋
i≤⌊n/2⌋的节点。
(2)更⼩的元素“下坠”,可能导致下⼀层的⼦树不符合⼤根堆的要求,若元素互换破坏了下⼀级的堆,则采⽤相同的⽅法继续往下调整(⼩元素不断“下坠”)
(3)构造大根堆完成
(4)开始进行排序【每⼀趟将堆顶元素加⼊有序⼦序列(与待排序序列中的最后⼀个元素交换)】
交换后,长度减一,前面的无序序列破坏了大根堆,对第一个结点进行调整,小元素下坠
重复堆顶元素与待排序序列中的最后⼀个元素交换,调整堆顶
…
- 实现代码∶
//建立大根堆
void BuildMaxHeap(int A[],int len){
for (int i = len/2; i > 0; --i) {
HeadAdjust(A,i,len);
}
}
void HeadAdjust(int A[],int k,int len){
//待调整的节点保存到A[0]
A[0] = A[k];
//调整
for (int i = 2*k; i <= len; i = i*2) {
if (i < len && A[i] < A[i+1]){
i++;
}
if (A[0] >= A[i]){
//根 大于等于 左右孩子,无需调整
break;
} else {
//大的元素放到根节点
A[k] = A[i];
//小元素继续下坠
k = i;
}
}
A[k] = A[0];
}
//堆排序
void HeapSort(int A[],int len){
//建立大根堆
BuildMaxHeap(A,len);
//排序
for (int i = len; i > 1; --i) {
A[0] = A[i];
A[i] = A[1];
A[1] = A[0];
//对堆顶节点进行调整
HeadAdjust(A,1,i-1);
}
}
-
空间复杂度∶ O ( 1 ) O(1) O(1)
-
时间复杂度∶O()
- 稳定性∶
堆排序是不稳定的
- 小结∶
- 在堆中插⼊新元素∶
小根堆:对于⼩根堆,新元素放到表尾,与⽗节点对⽐,若新元素⽐⽗节点更⼩,则将⼆者互换。新元素就这样⼀路“上升”,直到⽆法继续上升为⽌
- 在堆中删除元素∶
小根堆:被删除的元素⽤堆底元素替代,然后让该元素不断“下坠”,直到⽆法下坠为⽌
冒泡排序
-
算法思想∶
基于“交换”的排序:根据序列中两个元素关键字的⽐较结果来对换这两个记录在序列中的位置 -
算法过程∶
从后往前(或从前往后)两两⽐较相邻元素的值,若为逆序(即A[i-1]>A[i]),则交换它们,直到序
列⽐较完。称这样过程为“⼀趟”冒泡排序。
- 实现代码∶
void bubbleSort(int A[],int n){
int temp;
bool flag;
for (int i = n-1; i >= 1; --i) {
flag = false;
for (int j = 0; j < i; ++j) {
if (A[j] > A[j+1]){
temp = A[j];
A[j] = A[j+1];
A[j+1] = temp;
flag = true;
}
}
//一趟下来没有交换,说明已经有序
if (flag == false){
return;
}
}
}
- 空间复杂度∶ O ( 1 ) O(1) O(1)
- 时间复杂度∶
-
稳定性∶稳定。
-
小结∶
快速排序
- 算法思想∶
在待排序表L[1…n]中任取⼀个元素pivot作为枢轴(或基准,通常取⾸元素),通过⼀趟排序将待排序表划分为独⽴的两部分L[1…k-1]和L[k+1…n],使得L[1…k-1]中的所有元素⼩于pivot,L[k+1…n]中的所有元素⼤于等于pivot,则pivot放在了其最终位置L(k)上,这个过程称为⼀次“划分”。然后分别递归地对两个⼦表重复上述过程,直⾄每部分内只有⼀个元素或空为⽌,即所有元素放在了其最终位置上。
- 算法过程∶
- 实现代码∶
//用一个元素划分成左右两个部分
int Partition(int A[],int low,int high){
int pivot = A[low];//第一个作为基准
while (low < high){
while (low < high && A[high] >= pivot){
high--;
}
A[low] = A[high];
while (low < high && A[low] <= pivot){
low++;
}
A[high] = A[low];
}
A[low] = pivot;
return low;
}
//快速排序
void QuickSort(int A[],int n,int low,int high){
if (low < high){
int pivotpos = Partition(A,low,high);//划分
QuickSort(A,n,low,pivotpos-1);//划分左子表
QuickSort(A,n,pivotpos+1,high);//划分右子表
}
}
- 空间复杂度∶
- 时间复杂度∶ O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
具体分析:
最坏情况:
比较好情况:
小结:
-
稳定性∶不稳定
-
小结∶
归并排序
- 算法思想∶
归并:把两个或多个已经有序的序列合并成⼀个
- 算法过程∶
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
合并相邻有序子序列
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
- 实现代码∶
int *B = (int *)malloc(n*sizeof (int));//辅助数组
/**
* 合并两个有序序列
* @param A 数组
* @param low low~mid
* @param mid 分割
* @param high mid+1~high
*/
void Merge(int A[],int low,int mid,int high){
//将A复制到B
for (int i = low; i <= high; ++i) {
B[i] = A[i];
}
int i = low,j = mid+1,k = low;
while (i <= mid&&j <= high){
if (B[i] <= B[j]){
A[k++] = B[i++];
} else {
A[k++] = B[j++];
}
}
while (i <= mid){
A[k++] = B[i++];
}
while (j <= high){
A[k++] = B[j++];
}
}
void MergeSort(int A[],int low,int high){
if (low < high){
int mid = (low+high)/2;
MergeSort(A,low,mid);//对左边归并排序
MergeSort(A,mid+1,high);//对右边进行归并排序
Merge(A,low,mid,high);//合并
}
}
-
空间复杂度∶ O ( n ) O(n) O(n)
-
时间复杂度∶
-
稳定性∶ 稳定
-
小结∶
基数排序
- 算法过程∶
(1)第一趟:以个位进行分配(按个位递减排序)
分配结果:
收集:
(2)第二趟:以十位进行分配
收集:
(3)第三趟:以百位进行分配
收集:
排序结束:
- 空间复杂度和时间复杂度∶
-
稳定性∶稳定
-
小结∶