排序是我们的日常开发中经常会遇到的需求,例如,在商品的列表页面,我们可以根据各种维度(销量、价格、人气等)对商品的展示顺序进行改变。
所以,对各个排序的性能的了解也是基础且重要的。我们先对排序这一块进行一个整体的把握。
- 冒泡排序
- 选择排序
- 插入排序
- 希尔排序
- 堆排序
- 归并排序
- 快速排序
关于排序的具体过程,本文没有利用图示来具体说明,但是在代码中对每一步执行加上了一些 log,有兴趣的可以复制代码到自己的 Playgroud 中运行查看。
冒泡排序
基本思想
两两比较相邻记录,如果反序则交换,直到没有反序的记录为止。
代码实现
func bubbleSort(_ items: [Int]) -> [Int] {
var arr = items
for i in 0..<arr.count {
print("第\(i + 1)轮冒泡:")
var j = arr.count - 1
while j > i {
if arr[j-1] > arr[j] {
print("\(arr[j-1]) > \(arr[j]): 进行交换")
arr.swapAt(j-1, j)
} else {
print("\(arr[j-1]) <= \(arr[j]): 不进行交换")
}
j -= 1
}
print("第\(i + 1)轮冒泡结束")
print("当前结果为:\n\(arr)\n")
}
return arr
}
复制代码
根据上面的实现过程,可以发现如果数组是类似于 [2, 1, 3, 4, 5] 的话,两次冒泡之后,数组就已经调整为 [1, 2, 3, 4, 5]。此时,再执行后面的循环其实是多余了,所以我们在发现冒泡循环没有进行交换的话,说明排序就已经完成了。下面我们可以将代码优化一下:
func bubbleSort_v2(_ items: [Int]) -> [Int] {
var arr = items
var flag = true
for i in 0..<arr.count {
print("第\(i + 1)轮冒泡:")
if !flag {
return []
}
flag = false
var j = arr.count - 1
while j > i {
if arr[j-1] > arr[j] {
print("\(arr[j-1]) > \(arr[j]): 进行交换")
arr.swapAt(j-1, j)
flag = true
} else {
print("\(arr[j-1]) <= \(arr[j]): 不进行交换")
}
j -= 1
}
print("第\(i + 1)轮冒泡结束")
print("当前结果为:\n\(arr)\n")
}
return arr
}
复制代码
选择排序
基本思想
每一次从待排序的数据元素中选择最小(或最大)的一个元素放在已排好序列的末尾,直到所有元素排完为止。
代码实现
func selectSort(_ items: [Int]) -> [Int] {
var arr = items
for i in 0..<arr.count {
print("第\(i+1)轮选择,x选择下标的范围为\(i)----\(arr.count)")
var j = i + 1
var minValue = arr[i]
var minIndex = i
// 寻找无序部分中最小值
while j < arr.count {
if minValue > arr[j] {
minValue = arr[j]
minIndex = j
}
j = j + 1
}
print("在后半部分乱序数列中,最小值为:\(minValue),下标为:\(minIndex)")
// 与无序表中的第一个值交换,让其成为有序表中的最后一个值
if minIndex != i {
arr.swapAt(minIndex, i)
}
print("本轮结果为:\(arr)\n")
}
return arr
}
复制代码
插入排序
基本思想
每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止,最终可以得到一个有序表。
代码实现
func insertSort(_ items: [Int]) -> [Int] {
var arr = items
for i in 0..<arr.count {
print("第\(i)轮插入:")
print("要选择插入的值为:\(arr[i])")
var j = i
while j > 0 {
if arr[j] < arr[j-1] {
arr.swapAt(j, j-1)
j -= 1
} else {
break
}
}
print("插入的位置:\(j)")
print("本轮插入完毕,插入结果为:\n\(arr)\n")
}
return arr
}
复制代码
希尔排序
基本思想
其实希尔排序是插入排序的升级版,希尔排序根据其排序的特点又叫做缩小增量排序。希尔排序的大体步骤就是先将无序序列按照一定的步长(增量)分为几组,分别将这几组中的数据通过插入排序的方式将其进行排序。然后缩小步长(增量)分组,然后将组内的数据再次进行排序。直到增量为 1 位置。经过上述这些步骤,我们的序列就是有序的了。其实上述的插入排序就是增量为 1 的希尔排序。
代码实现
func hillSort(_ items: [Int]) -> [Int] {
var arr = items
var step: Int = arr.count / 2
while step > 0 {
print("步长为\(step)的插入排序开始:")
for i in 0..<arr.count {
var j = i + step
while j >= step && j < arr.count {
if arr[j] < arr[j-step] {
arr.swapAt(j, j-step)
j = j - step
} else {
break
}
}
}
print("步长为\(step)的插入排序结束")
print("本轮排序结果为:\(arr)\n")
step = step / 2 // 缩小步长
}
return arr
}
复制代码
堆排序
准备知识
堆 是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子。
大顶堆: arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆: arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
基本思想
将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。
基本步骤
- 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆。
- 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端。
- 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整及交换步骤,直到整个序列有序。
代码实现
// 构建小顶堆的层次遍历序列
func heapCreate(items: inout Array<Int>) {
var i = items.count
while i > 0 {
heapAdjast(items: &items, startIndex: i - 1, endIndex: items.count)
i -= 1
}
}
// 对小顶堆j的局部进行调整, 使其该节点的所有父类符合小顶堆的特点
func heapAdjast(items: inout Array<Int>, startIndex: Int, endIndex: Int) {
let temp = items[startIndex]
var fatherIndex = startIndex + 1 //父节点下标
var maxChildIndex = 2 * fatherIndex //左孩子下标
while maxChildIndex <= endIndex {
if maxChildIndex < endIndex && items[maxChildIndex-1] < items[maxChildIndex] {
maxChildIndex = maxChildIndex + 1
}
if temp < items[maxChildIndex-1] {
items[fatherIndex-1] = items[maxChildIndex-1]
} else {
break
}
fatherIndex = maxChildIndex
maxChildIndex = 2 * fatherIndex
}
items[fatherIndex-1] = temp
}
func heapSort(_ items: [Int]) -> [Int] {
var arr = items
var endIndex = arr.count - 1
print("原始堆")
print(arr)
// 创建小顶堆,其实就是讲arr转换成小顶堆层次的遍历结果
heapCreate(items: &arr)
print(arr)
print("堆排序开始")
while endIndex > 0 {
// 将小顶堆的定点与小顶堆的最后一个值进行交换
arr.swapAt(endIndex, 0)
endIndex -= 1 // 缩小小顶堆的范围
// 对交换后的小顶堆进行调整,使其重新成为小顶堆
print("重新调整")
heapAdjast(items: &arr, startIndex: 0, endIndex: endIndex + 1)
print("调整后的结果如下")
print(arr)
}
return arr
}
复制代码
归并排序
基本思想
归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略。
将需要排序的数组分解为一个个单独的子元素,两两比较后将其合并为一个有序数组,不断重复比较及合并的过程,直至合并之后的有序数组的元素数量与原数组相同时,则排序结束。
代码实现
// 合并两个数组
func mergeArray(firstList: Array<Int>, secondList: Array<Int>) -> Array<Int> {
var resultList: Array<Int> = []
var firstIndex = 0
var secondIndex = 0
// 拿出两数组第一个元素,比较之后取小的插入新数组
while firstIndex < firstList.count && secondIndex < secondList.count {
if firstList[firstIndex] < secondList[secondIndex] {
resultList.append(firstList[firstIndex])
firstIndex += 1
} else {
resultList.append(secondList[secondIndex])
secondIndex += 1
}
}
// 将第一个数组剩下的元素插入新数组
while firstIndex < firstList.count {
resultList.append(firstList[firstIndex])
firstIndex += 1
}
// 将第二个数组剩下的元素插入新数组
while secondIndex < secondList.count {
resultList.append(secondList[secondIndex])
secondIndex += 1
}
return resultList
}
func mergeSort(_ items: [Int]) -> [Int] {
let arr = items
// 将数组中的每一个元素放入一个数组中
var tempArray: Array<Array<Int>> = []
for item in arr {
var subArray: Array<Int> = []
subArray.append(item)
tempArray.append(subArray)
}
// 对这个数组中的数组进行合并,直到合并完毕为止
while tempArray.count != 1 {
print(tempArray)
var i = 0
while i < tempArray.count - 1 {
print("将\(tempArray[i])与\(tempArray[i+1])合并")
tempArray[i] = mergeArray(firstList: tempArray[i], secondList: tempArray[i+1])
print("合并结果为:\(tempArray[i])\n")
tempArray.remove(at: i + 1)
i = i + 1
}
}
return tempArray.first!
}
复制代码
快速排序
基本思想
快速排序(Quick Sort)使用分治法策略。
选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序流程:
- 从数列中挑出一个基准值。
- 将所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边);在这个分区退出之后,该基准就处于数列的中间位置。
- 递归地把“基准值前面的子数列”和“基准值后面的子数列”进行排序。
代码实现
/// 将数组以第一个值为准分为两部分,前半部分比该值要小,后半部分比该值要大
///
/// - parameter list: 要二分的数组
/// - parameter low: 数组的下界
/// - parameter high: 数组的上界
///
/// - return: 分界点
func partition(list: inout Array<Int>, low: Int, high: Int) -> Int {
var low = low
var high = high
let temp = list[low]
while low < high {
while low < high && list[high] >= temp {
high -= 1
}
list[low] = list[high]
while low < high && list[low] <= temp {
low += 1
}
list[high] = list[low]
print("====low==\(low)======high==\(high)")
}
list[low] = temp
print("=====temp==\(temp)")
print(list)
return low
}
/// 快速排序
///
/// - parameter list: 要排序的数组
/// - parameter low: 数组的下界
/// - parameter high: 数组的上界
func quickSortHelper(list: inout Array<Int>, low: Int, high: Int) {
if low < high {
let mid = partition(list: &list, low: low, high: high)
quickSortHelper(list: &list, low: low, high: mid - 1)
quickSortHelper(list: &list, low: mid + 1, high: high)
}
}
func quickSort(_ items: Array<Int>) -> Array<Int> {
var list = items
quickSortHelper(list: &list, low: 0, high: list.count - 1)
print("快速排序结束")
return list
}
复制代码
Swift排序用法
在Swift源代码中,sort 函数采用的是一种内审算法(IntroSort)。它由堆排序、插入排序、快速排序三种算法构成,依据输入的深度相应选择最佳的算法来完成。
// 以升序排列为例,array 数组可改变
array.sort
// 以降序排列为例,array 数组不可改变
newArray = array.sorted(by: >)
复制代码
总结
排序方法 | 时间复杂度 | 辅助空间 |
---|---|---|
冒泡排序 | O(n²) | O(1) |
选择排序 | O(n²) | O(1) |
插入排序 | O(n²) | O(1) |
希尔排序 | 与增量选取策略有关 | O(1) |
堆排序 | O(nlogn) | O(1) |
归并排序 | O(nlogn) | O(n) |
快速排序 | O(nlogn) | O(logn)-O(n) |
转载于:https://juejin.im/post/5cf4955df265da1b7401e6da