[TOC]
算法 - 十大经典排序算法(动图演示)
在计算机科学与数学中,一个**排序算法**(英语:Sorting algorithm)是一种能将一串资料依照特定排序方式进行排列的一种算法。最常用到的排序方式是数值顺序以及字典顺序。排序算法也用在处理文字资料以及产生人类可读的输出结果。
基本上,排序算法的输出必须遵守下列两个原则:
- 输出结果为递增序列(递增是针对所需的排序顺序而言)
- 输出结果是原输入的一种排列或是重组
算法基本介绍
十种排序算法一般分为两大类:
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。(冒泡、选择、插入、归并、快速、希尔、堆排序)
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。 (计数、基数、桶)
十种排序算法复杂度一览:
- n:数据规模
- k:进制数量(eg:十进制 k=1)
- d:最大值的位数
- m:桶的数量
名词解释:
- 稳定性:如果相等的两个元素,在排序前后的相对位置保持不变,那么这称之为稳定的排序算法
- In-place:不依赖额外的资源或者依赖少数的额外资源,仅依靠输出来覆盖输入
- 时间复杂度:指执行算法所需要的计算工作量。也就是说程序需要执行的次数
- 空间复杂度:对一个算法在运行过程中临时占用存储空间大小的量度
baseCode
为方便其他算法的测试与实现 搞了一写base代码 可忽略
class BaseSort: NSObject {
var arrayList = [NSInteger]() // 需要排序的数组
var swapCount = 0 // 交换次数
var cmpCount = 0 // 比较次数
fileprivate func sort(array: [NSInteger]) {
arrayList = array
let startTime = CFAbsoluteTimeGetCurrent()
sortAction()
let endTime = CFAbsoluteTimeGetCurrent()
let sortTitle = self.className
print("""
【\(sortTitle)】
执行时间:\(endTime - startTime) 比较次数:\(cmpCount) 交换次数:\(swapCount)
---------------------------------------------------------------------------------
""")
print(arrayList)
}
func sortAction(){ }
/// 比较两个元素的大小 等于0:v1=v2 小于0:v1<v2 大于0:v1>v2
func cmpValue(_ v1: NSInteger, _ v2: NSInteger) -> Int {
return v1 - v2
}
func cmpIndex(_ index1: NSInteger, _ index2: NSInteger) -> Int {
let v1 = arrayList[index1]
let v2 = arrayList[index2]
cmpCount += 1
return cmpValue(v1, v2)
}
func swap(_ index1: NSInteger, _ index2: NSInteger) {
let temp = arrayList[index1]
arrayList[index1] = arrayList[index2]
arrayList[index2] = temp
swapCount += 1
}
}
/// 创建一个随机数组
func createRandom(count: NSInteger, min: NSInteger, max: NSInteger) -> [NSInteger]{
var array = [NSInteger]()
for _ in 0..<count {
let v = Int(arc4random_uniform(UInt32(max)))+min
array.append(v)
}
return array
}
func testSorts(array:[NSInteger], _ objs:BaseSort...){
for obj in objs {
obj.sort(array: array)
}
}
1. 冒泡排序(Bubble Sort)
冒泡排序又称为**泡式排序**,是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误则交换两个元素,走访数列的工作是重复的进行直到没有需要交换的,也就是说该数列已经排序完成,这个算法名字的由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
又称为**泡式排序**,是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
1.1 算法描述
- 从头开始比较每一对相邻的元素,如果第一个比第二个大,就交换他们的位置
- 执行完一轮后,最末尾那个元素就是最大的元素
- 忽略(1)中曾经找到的最大元素,重复执行步骤(1),直到元素全部有序。
1.2 动图演示
1.3 代码实现
-
方案一:
class BubbleSort01: BaseSort { override func sortAction() { if arrayList.count <= 1 { return } let end = arrayList.count for j in 0..<end { for i in 1..<end-j { if cmpIndex(i, i-1) < 0 { swap(i, i-1) } } } } }
-
优化方案:
思考:
如果一次内层的for循环 执行一次之后并没有发生交换的操作,
那么就可以证明整个数据是已经有序的了,这个时候就完全可以终止for循环。
这种操作,在数据越接近有序的情况下越明显。
class BubbleSort02: BaseSort { override func sortAction() { if arrayList.count <= 1 { return } let end = arrayList.count for j in 0..<end { var exchanged = true for i in 1..<end-j { if cmpIndex(i, i-1) < 0 { swap(i, i-1) exchanged = false // 没有交换过 说明剩余的数据已经是有序的了 } } if exchanged { break } } } }
-
方案对比结果
var sortArray = createRandom(count: 1000, min: 0, max: 10000) testSorts(array: sortArray, BubbleSort01(), BubbleSort02() ) 结果: 【算法.BubbleSort01】 执行时间:0.6306749582290649 比较次数:499500 交换次数:240953 --------------------------------------------------------------------------------- 【算法.BubbleSort02】 执行时间:0.6284579038619995 比较次数:498870 交换次数:240953 --------------------------------------------------------------------------------- 可以看到 优化后的方案 比较次数是比较少的,交换次数不变,整体时间上也略有差异。
2. 选择排序(Selection sort)
选择排序是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最大(小)元素,存放到排序序列的起始位置,然后在从剩余的未排序的元素中据需寻找最大(小)元素,然后放到已排序的序列的末尾,一次类推,知道所有元素均排序完毕。
选择排序的主要优点与数据移动有关,如果某个元素位于正确的最终位置上,则他不会移动。选择排序每次交换一对元素,它们当中至少有一个将被移到最终的位置上,因此对n个元素的表进行排序总共进行至多(n-1)次交换。在所有的完钱依靠交换去移动元素的排序方法中,选择排序算是非常好的一种。
2.1 算法描述
- 将序列中的第一个元素作为,
- 逐个比较其余元素,从队列中找出最小的元素 然后与第一个元素进行交换
- 一次循环之后会得到最小的值在最前面
- 忽略已经找到的元素,然后循环执行2、3步操作
2.2 动图演示
1.3 代码实现
-
方案:正常原地算法
override func sortAction() { if arrayList.count <= 1 { return } let end = arrayList.count for i in 0..<end { var min = i for begin in i..<end-1 { if cmpIndex(min, begin+1) > 0 { min = begin+1 } } swap(i, min) } }
-
方案对比结果
3. 堆排序(Heapsort)
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子节点的键值或者索引总是小于(或者大于)他的父节点
堆排序也可以看做是选择排序的一种优化方案
1.1 算法描述
涉及到原地建堆
的基本公式及概念:
- 父节点
i
的左子节点的位置:2i+1 - 父节点
i
的右子节点的位置:2i+2 - 子节点
i
的父节点的位置: (i-1)/2 - 完全二叉树中第一个非叶子节点的位置:元素个数/2
算法描述
- 原地建堆
- 交换堆顶元素与尾部元素
- 对的元素数量减少1
- 对0的位置进行一次siftDown(恢复堆的特性)
- 重复 2、3、4步骤
1.2 动图演示
1.3 代码实现
-
方案
/// 堆排序 class HeapSort: BaseSort { var heapSize = 0 override func sortAction() { heapSize = arrayList.count var i = heapSize >> 1 - 1 while i >= 0 { siftDown(i) i-=1 } while heapSize >= 1 { heapSize-=1 // 忽略已经交换的值 swap(0, heapSize) // 交换尾部和顶部元素 siftDown(0) // 恢复堆的性质 } } // 原地建堆 func siftDown(_ downIndex: Int) { let half = heapSize >> 1 let value = arrayList[downIndex] var index = downIndex while index < half { // index 必须是非叶子节点 var childIndex = (index << 1)+1 var child = arrayList[childIndex] let rightIndex = childIndex + 1 if rightIndex < heapSize && (cmpValue(arrayList[rightIndex], child) > 0){ childIndex = rightIndex child = arrayList[rightIndex] } if cmpValue(value, child) >= 0 { break } arrayList[index] = child index = childIndex } arrayList[index] = value } }
-
方案对比结果
4. 插入排序(Insertion Sort)
插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序的数据,在已排序的序列中从后向前扫描,找到响应的位置并插入。插入排序在实现上,通常采用In-place排序(即只需要使用O(1)的额外空间),因而在从后向前的扫描过程中,需要反复把已排序的元素逐步向后挪位,为最新的元素提供插入空间。
插入排序类似于扑克牌的排序 可以根据日常打扑克时的思维去思考
4.1 算法描述
- 从第一个元素开始,默认为第一个元素是已经被排序的
- 取出下一个元素,在已排序的序列中从后向前扫描
- 如果该元素大于取出元素,则向后挪动
- 重复步骤3 直到在已排序好的序列中找到小于或等于取出的元素的位置
- 将取出的元素插入带该元素的位置
- 重复步骤 2~5
4.2 动图演示
4.3 代码实现
-
方案:
override func sortAction() { for i in 1..<arrayList.count { var begin = i let value = arrayList[begin] while begin > 0 && cmpValue(arrayList[begin-1], value) > 0 { arrayList[begin] = arrayList[begin-1] begin -= 1 } arrayList[begin] = value } }
5. 归并排序(Merge sort)
归并排序是创建在归并操作上的一种有效的排序算法,效率为O(nlogn).1945年由[约翰·冯·诺伊曼](https://zh.wikipedia.org/wiki/约翰·冯·诺伊曼)首次提出。该算法是采用分治法的一个非常典型的应用,且各层分支递归可同时进行。
归并操作:指的是将两个已经排序的序列合并成一个序列的操作,归并排序算法依赖归并操作
5.1 算法描述
<img src="https://tva1.sinaimg.cn/large/008i3skNly1gsgsoueveuj30yq0u0n0p.jpg" alt="image-20210714212322508" style="zoom: 33%;" />
-
不断的将当前序列分割成两个子序列
- 直到不能分割(序列中只有一个元素)
-
不断的将2个子序列合并成一个有序的序列
-
合并需要临时变量:
- li:左侧序列开始位置 le:左侧序列截止位置
- ri:右侧序列开始位置 re:右侧序列截止位置
- ai:当前插入位置
提前将左侧的序列拿出来,用来循环拿去当前左侧序列的值
-
一次对比左右序列中 值的大小 之后想[ai]处覆盖。
<img src="https://tva1.sinaimg.cn/large/008i3skNly1gshoxsycgmj31ay0p876f.jpg" alt="image-20210715155913889" style="zoom:33%;" />
-
直到最终只剩下一个序列
5.2 动图演示
5.3 代码实现
-
方案
/// 归并排序 class MergeSort: BaseSort { var leftArray = [NSInteger]() override func sortAction() { leftArray = [NSInteger](repeating: 0, count: arrayList.count >> 1) divide(0, arrayList.count) } // 分割 func divide(_ begin: NSInteger, _ end: NSInteger){ if end - begin < 2 { return } let mid = (begin+end) >> 1 divide(begin, mid) divide(mid, end) merge(begin, mid, end) } // 合并 func merge(_ begin: NSInteger, _ mid: NSInteger, _ end: NSInteger){ var li = 0, le = mid-begin var ri = mid, re = end var ai = begin for i in li..<le { leftArray[i] = arrayList[begin+i] } while li<le { if ri<re && cmpValue(arrayList[ri], leftArray[li])<0 { arrayList[ai] = arrayList[ri] ai+=1; ri+=1 }else{ arrayList[ai] = leftArray[li] ai+=1; li+=1 } } } }
6. 快速排序(Quicksort)
快速排序,又称分区交换排序,简称快排,一种排序算法,最早由[东尼·霍尔](https://zh.wikipedia.org/wiki/東尼·霍爾)提出。在平均状况下,排序n个项目要O(nlogn)次比较,最欢情况下则需要O(n^2)次比较,但这种情况并不常见,事实上,快速排序O(nlogn)通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率的达成。
快速排序使用分治法策略来把一个序列分为较小和较大的2个子序列,然后递归的排序两个子系列
5.1 算法描述
- 从序列中选择一个基准值(pivot)
- 假设每次选择第0个位置的元素作为基准值
- 利用pivot将序列分割成2个子序列
- 将随机一个基准值与第0个位置交换 ( 为避免出现最坏的时间复杂度 )
- 将小于pivot的元素放到pivot的前面(左侧)
- 将大于pivot的元素放到pivot的后面(右侧)
- 将等于pivot的元素放在那边都可以
- 对子序列进行1、2操作
- 直到不能在分割位置(自列元素中只剩下1个元素)
5.2 动图演示
5.3 代码实现
-
方案
class QuickSort: BaseSort { override func sortAction() { sort(0, arrayList.count) } func sort(_ begin: NSInteger, _ end: NSInteger) { if end-begin < 2 { return } // 确定基准值的元素 let mid = pivotIndex(begin, end-1) sort(begin, mid) sort(mid+1, end) } func pivotIndex(_ begin: NSInteger, _ end: NSInteger) -> NSInteger { // 随机一个基准值元素与第一个交换 let random = Int(arc4random_uniform(UInt32(end-begin)))+begin swap(begin, random) let pivotValue = arrayList[begin] var beginIndex = begin var endIndex = end while beginIndex < endIndex { while beginIndex < endIndex { if cmpValue(pivotValue, arrayList[endIndex]) < 0 { endIndex-=1 }else{ arrayList[beginIndex] = arrayList[endIndex] beginIndex+=1 break } } while beginIndex < endIndex { if cmpValue(pivotValue, arrayList[beginIndex]) > 0 { beginIndex+=1 }else{ arrayList[endIndex] = arrayList[beginIndex] endIndex-=1 break } } } arrayList[beginIndex] = pivotValue return beginIndex } }
7. 希尔排序(Shellsort)
希尔排序,也称递减增量排序算法,是插入排序的一种高效改进版本,希尔排序是非常稳定的排序算法
希尔排序是基于插入排序的以下两点性质而提出的改进方法:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
步长序列
步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为普通插入排序,这就保证了数据一定会被排序。
Donald Shell最初建议步长选择为n/2并且对步长取半直到步长达到1。虽然这样取可以比O(n^2)类的算法(插入排序)更好,但这样仍然有减少平均时间和最差时间的余地
目前已知的最好步长序列是由Sedgewick提出的(1, 5, 19, 41, 109,...)
.
7.1 算法描述
希尔排序是吧序列看成一个矩阵,分成m列,逐列进行排序
- m从某个整数逐渐减为1
- 当m为1 时 整个序列将完全有序
矩阵的列数取决于步长序列
- 如果步长序列为[1,5,19,41,109...],就代表一次分成 109列,41列,19列,5列 ,1列进行排序
- 不同的步长序列执行的效率也不同
概念
-
假设元素在第col列,第row行,步长(总列数)是step
<img src="/Users/dian1/Library/Application Support/typora-user-images/image-20210715174154155.png" alt="image-20210715174154155" style="zoom:33%;" />
- 这个元素在数组中的索引 col+row*step
- 例如 9在 序列的第二列,第0行 , 2+0*5 = 2
7.2 动图演示
7.3 代码实现
-
方案
class ShellSort: BaseSort { override func sortAction() { let stepSequence = sedgewickStepSequence() // 步长序列 for step in stepSequence { sort(step) } } func sort(_ step:NSInteger){ for col in 0..<step { var begin = col + step while begin < arrayList.count { var cur = begin let value = arrayList[begin] while cur > col && cmpValue(value,arrayList[cur-step]) < 0 { arrayList[cur] = arrayList[cur-step] cur -= step } arrayList[cur] = value begin += step } } } /// 步长序列实现方式 /// - Returns: [1,5,19,41,109...] func sedgewickStepSequence() -> [NSInteger] { var stepSequence = [NSInteger]() var k = 0, step = 0 while true { if k % 2 == 0 { let powNum:Int = Int(pow(CGFloat(2), CGFloat(k>>1))) step = 1+9*(powNum * powNum - powNum) }else{ let powNum1:Int = Int(pow(CGFloat(2), CGFloat((k-1)>>1))) let powNum2:Int = Int(pow(CGFloat(2), CGFloat((k+1)>>1))) step = 1 + 8 * powNum1 * powNum2 - 6 * powNum2 } if step >= arrayList.count { break } stepSequence.insert(step, at: 0) k+=1 } return stepSequence } }
8. 计数排序(Counting sort)
计数排序是一种稳定的线性时间排序算法。该算法于1954年由 Harold H. Seward 提出。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序算法中,能够更有效的排序数据范围很大的数组。
8.1 算法描述
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
8.2 动画演示
8.3 代码实现
-
方式
class CountingSort: BaseSort { override func sortAction() { guard arrayList.count > 0 else { return } let maxElement = arrayList.max() ?? 0 var countArray = [Int](repeating: 0, count: Int(maxElement + 1)) for element in arrayList { countArray[element] += 1 } for index in 1 ..< countArray.count { let sum = countArray[index] + countArray[index - 1] countArray[index] = sum } // 输出 for element in arrayList { countArray[element] -= 1 arrayList[countArray[element]] = element } } }
9. 基数排序(Radix sort)
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
9.1 算法描述
- 一次对个位数、十位数、百位数、千位数...进行排序(从低位到高位)
- 个位十位百位数的取值范围都是0~9,可以使用计数排序对他们进行排序
9.2 动图演示
9.3 代码实现
-
方式
class RadixStor: BaseSort { override func sortAction() { var tempArray = [Int]() var maxValue = 0 var maxDigit = 0 var level = 0 repeat { var buckets = [[Int]]() for _ in 0..<10 { buckets.append([Int]()) } for i in 0..<arrayList.count { let elementValue = arrayList[i] let num = pow(10.0, Double(level)) let divident = level < 1 ? 1 : NSDecimalNumber(decimal:Decimal(num)).intValue let mod = elementValue / divident % 10 buckets[mod].append(elementValue) if maxDigit == 0 { if elementValue > maxValue { maxValue = elementValue } } } tempArray.removeAll() for element in buckets { tempArray.append(contentsOf: element) } if maxDigit == 0 { while maxValue > 0 { maxValue = maxValue / 10 maxDigit += 1 } } arrayList = tempArray level += 1 } while (level < maxDigit) } }
10. 桶排序(Bucket sort)
桶排序或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。但桶排序并不是比较排序,他不受到O(nlogn)下限的影响。
10.1 算法描述
- 创建一定数量的桶(比如用数组链表作为桶)
- 按照一定规则(不同类型的数据,规则不同)将序列中的元素均匀分配到对应的桶中
- 分别对每个桶进行单独排序
- 将所有非空桶元素合并成有序序列
10.2 动图演示
10.3 代码实现
-
方案
override func sortAction() { let maxNum = arrayList.max() var bucket:[Int] = Array.init(repeatElement(0, count: maxNum! + 1)) var newNum:[Int] = Array.init() for index in arrayList { let numId = index bucket[numId] += 1 } for index in bucket.indices { while bucket[index] > 0 { newNum.append(index) bucket[index] -= 1 } } arrayList = newNum }
效率对比:
非比较类:
var sortArray = createRandom(count: 10000, min: 0, max: 100000)
testSorts(array: sortArray,
CountingSort(), // 计数排序
RadixStor(), // 基数排序
BucketSort() // 桶排序
)
【算法.CountingSort】
执行时间:0.09470701217651367
---------------------------------------------------------------------------------
【算法.RadixStor】
执行时间:0.07344508171081543
---------------------------------------------------------------------------------
【算法.BucketSort】
执行时间:0.168626070022583
---------------------------------------------------------------------------------
计数排序需要占用大量空间,它仅适用于数据比较集中的情况。比如 [0100],[1000019999] 这样的数据。
桶排序可用于最大最小值相差较大的数据情况,比如[9012,19702,39867,68957,83556,102456]。
但桶排序要求数据的分布必须均匀,否则可能导致数据都集中到一个桶中。比如[104,150,123,132,20000], 这种数据会导致前4个数都集中到同一个桶中。导致桶排序失效。
基数排序一般用于长度相同的元素组成的数组。基数排序可以看做是进行多趟桶排序。每个有效数字都在0-9之间,很适合桶排序,建10个桶很方便
比较类算法:
冒泡排序01、冒泡排序02、 选择排序、插入排序 一起比较 因为这个耗时比较长
-
数据规模1000
var sortArray = createRandom(count: 1000, min: 0, max: 10000) testSorts(array: sortArray, BubbleSort01(), BubbleSort02(), SelectSort01(), InsertionSort01() ) 【算法.BubbleSort01】 执行时间:0.6120259761810303 比较次数:499500 交换次数:248498 --------------------------------------------------------------------------------- 【算法.BubbleSort02】 执行时间:0.6053379774093628 比较次数:499500 交换次数:248498 --------------------------------------------------------------------------------- 【算法.SelectSort01】 执行时间:0.4609769582748413 比较次数:499500 交换次数:1000 --------------------------------------------------------------------------------- 【算法.InsertionSort01】 执行时间:0.12423503398895264 比较次数:249485 交换次数:0 ---------------------------------------------------------------------------------
**堆排序、归并排序。快速排序、希尔排序 **
-
数据规模1万
var sortArray = createRandom(count: 10000, min: 0, max: 100000) testSorts(array: sortArray, HeapSort(), MergeSort(), QuickSort(), ShellSort() ) 【算法.HeapSort】 执行时间:0.13575303554534912 比较次数:235379 交换次数:10000 --------------------------------------------------------------------------------- 【算法.MergeSort】 执行时间:0.2747499942779541 比较次数:120561 交换次数:0 --------------------------------------------------------------------------------- 【算法.QuickSort】 执行时间:0.1792229413986206 比较次数:150138 交换次数:6625 --------------------------------------------------------------------------------- 【算法.ShellSort】 执行时间:0.3527510166168213 比较次数:196090 交换次数:0 ---------------------------------------------------------------------------------
最后还是这张图 大家可以根据需求酌情选择~~
END~
** 友情链接:**
参考: