十大经典排序算法
一、冒泡排序
是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
操作步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
平均时间复杂度:O(n2);最坏时间复杂度:O(n2);最好时间复杂度:O(n);空间复杂度:O(1);稳定性:稳定(即:排序前两个相同元素,在排序后前后顺序不变)
实现代码(swift):
func sort(_ list: [Int]) -> [Int] { if list.count < 2 { return list } var arr = list for i in 0..<list.count-1 { // 只遍历开头至倒数第二个(除了最后一个) for j in 0..<list.count-1-i { // 分别对每个元素进行比较(这里-i的目的是排除比较后的元素) if arr[j] > arr[j+1] { // 交换元素,这里采用数学算法(仅适用于数字),>:表示升序;<:表示降序 arr[j] += arr[j+1] arr[j+1] = arr[j] - arr[j+1] arr[j] = arr[j] - arr[j+1] } } } return arr } let list = [2, 3, 19, 5, 10, 40, 6, 15, 6, 9, 18] let li = sort(list) print("排序结果:\(li)")
二、选择排序
是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
操作步骤:
- 选择比较的起始位置(一般从0开始)分别遍历每个元素,与其他元素做比较,选出最小或最大的元素
- 将最小或最大的元素与当前比较的起始位置进行交换,起始位置+1
- 重复以上步骤,直到比较结束
平均时间复杂度:O(n2);最坏时间复杂度:O(n2);最好时间复杂度:O(n2);空间复杂度:O(1);稳定性:不稳定(即:排序前两个相同元素,在排序后前后顺序发生了变化)
实现代码(swift):
func sort(_ list: [Int]) -> [Int] { if list.count < 2 { return list } var arr = list for i in 0..<list.count-1 { // 只遍历开头至倒数第二个(除了最后一个) var idx = i // 最小或最大值的索引 for j in i+1..<list.count { // 分别对每个元素进行比较 if arr[j] < arr[idx] { // 找到与当前idx比较,最小(升序)(>:最大,降序)的那个元素下标 idx = j } } if i != idx { // 交换两个元素 arr[i] += arr[idx] arr[idx] = arr[i] - arr[idx] arr[i] = arr[i] - arr[idx] } } return arr } let list = [2, 3, 19, 5, 10, 40, 6, 15, 6, 9, 18] let li = sort(list) print("排序结果:\(li)")
三、插入排序
是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
操作步骤:
- 从第一个元素开始,该元素可以被认为是排好序的序列
- 取出下一个元素a,分别与该排好序的序列的每个元素(从后向前)比较
- 如果已排好序中的元素b大于当前取出的元素a,将b后移一位
- 重复3的步骤,直到已排好序中的元素c小于或等于当前取出的元素a
- 将当前取出的元素a插入到元素c之后
- 重复2-5,直到遍历结束
平均时间复杂度:O(n2);最坏时间复杂度:O(n2);最好时间复杂度:O(n);空间复杂度:O(1);稳定性:稳定(即:排序前两个相同元素,在排序后前后顺序不变)
实现代码(swift):
func sort(_ list: [Int]) -> [Int] { if list.count < 2 { return list } var arr = list for i in 1..<list.count { // 从第二个元素开始遍历后续的每个元素 var j = i - 1 // 排好序的有序序列中的最后一个元素 let k = arr[i] // 当前取出的元素 while j >= 0 && arr[j] > k { // 从有序序列中最后一个元素开始向前遍历,>:升序;<:降序 arr[j+1] = arr[j] // 将有序序列中的元素逐个后移 j -= 1 } arr[j+1] = k // 插入取出的元素 } return arr } let list = [2, 3, 19, 5, 10, 40, 6, 15, 6, 9, 18] let li = sort(list) print("排序结果:\(li)")
四、希尔排序
也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。它与插入排序的不同之处在于,它会优先比较距离较远的元素。
希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。
已知的最好步长序列是由Sedgewick提出的(1, 5, 19, 41, 109,...),该序列的项,从第0项开始,偶数来自9x4i - 9x2i + 1,和奇数来自2i+2 x (2i+2 - 3) + 1这两个算式。用这样步长序列的希尔排序比插入排序要快,甚至在小数组中比快速排序和堆排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。
操作步骤:
- 计算增量值,即步长,一般采用n/2,并对步长取半直到步长达到1
- 根据步长将待排序的序列分成若干组,分别对每组进行插入排序
- 重复1-2,直到步长为1结束
平均时间复杂度:O(n1.3);最坏时间复杂度:O(n2);最好时间复杂度:O(n);空间复杂度:O(1);稳定性:不稳定(即:排序前两个相同元素,在排序后前后顺序发生了改变)
实现代码(swift):
func sort(_ list: [Int]) -> [Int] { if list.count < 2 { return list } var arr = list var stepSize = list.count >> 1 // 步长 while stepSize > 0 { for i in stepSize..<list.count { var j = i let currVal = arr[i]
while j-stepSize >= 0, currVal < arr[j-stepSize] { // currVal <:升序;currVal >:降序 arr[j] = arr[j-stepSize] j -= stepSize } arr[j] = currVal } stepSize >>= 1 } return arr } let list = [2, 3, 19, 5, 10, 40, 6, 15, 6, 9, 18] let li = sort(list) print("排序结果:\(li)")
五、归并排序
该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。实际上就是将一个序列分成若干个子序列,使其子序列有序后,将子序列合并,再次得到一组子序列,重复合并,直到成为一个序列。
归并排序离不开归并算法,归并算法指的是将两个已经排序的序列合并成一个序列的算法。
根据遍历方式不同,可以分为两种操作方法:
递归法:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针到达序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
迭代法:
- 将序列每相邻两个数字进行归并操作,形成ceil(n/2)个序列,排序后每个序列包含两/一个元素
- 若此时序列数不是1个则将上述序列再次归并,形成ceil(n/4)个序列,每个序列包含四/三个元素
- 重复步骤2,直到所有元素排序完毕,即序列数为1
平均时间复杂度:O(n * log n);最坏时间复杂度:O(n * log n);最好时间复杂度:O(n * log n);空间复杂度:O(n);稳定性:稳定(即:排序前两个相同元素,在排序后前后顺序不变)
实现代码(swift):
迭代法:
func sort(_ list: [Int]) -> [Int] { if list.count < 2 { return list } let len = list.count var a = list // 用于排序的序列 var arr = list // 用于临时存储归并后的元素 var seg = 1 // 分割下标 while seg < len { var start = 0 while start < len { var k = start let l = start let m = min(start + seg, len) let h = min(start + seg * 2, len) var start1 = l, end1 = m var start2 = m, end2 = h while start1 < end1, start2 < end2 { if a[start1] < a[start2] { arr[k] = a[start1] start1 += 1 }else { arr[k] = a[start2] start2 += 1 } k += 1 } while start1 < end1 { arr[k] = a[start1] start1 += 1 k += 1 } while start2 < end2 { arr[k] = a[start2] start2 += 1 k += 1 } start += seg * 2 } // 此时arr是一个临时排好序的序列,需要让它与a进行交换,使之再次排序 let temp = arr arr = a a = temp seg += seg } return a // 由于每次都会将arr于a进行交换,因此这里应该返回a,而不是arr } let list = [2, 3, 19, 5, 10, 40, 6, 15, 6, 9, 18] let li = sort(list) print("排序结果:\(li)")
以下是一个Array的归并排序的扩展方法:
extension Array { func mergeSorted(isOrderedBefore: (Element, Element) -> Bool) -> [Element] { let n = count var z = [self, self] var d = 0 var width = 1 while width < n { var i = 0 while i < n { var j = i var l = i var r = i + width let lmax = Swift.min(l + width, n) let rmax = Swift.min(r + width, n) while l < lmax, r < rmax { if isOrderedBefore(z[d][l], z[d][r]) { z[1 - d][j] = z[d][l] l += 1 } else { z[1 - d][j] = z[d][r] r += 1 } j += 1 } while l < lmax { z[1 - d][j] = z[d][l] j += 1 l += 1 } while r < rmax { z[1 - d][j] = z[d][r] j += 1 r += 1 } i += width * 2 } width *= 2 d = 1 - d } return z[d] } }
调用,例如:升序排列:
let list = [(idx: 1, v: "a"), (idx: 3, v: "b"), (idx: 3, v: "c"), (idx: 2, v: "d"), (idx: 1, v: "e")] let arr = list.mergeSorted(isOrderedBefore: {$0.idx <= $1.idx}) // 升序排列
print("=====arr: \(arr)")
// 打印结果: [(idx: 1, v: "a"), (idx: 1, v: "e"), (idx: 2, v: "d"), (idx: 3, v: "b"), (idx: 3, v: "c")]
递归法:(递归法容易理解,但是递归存在栈溢出的风险)
func sort(_ list: [Int]) -> [Int] { if list.count < 2 { return list } func merge(left: [Int], right: [Int]) -> [Int] { var arr = [Int]() var l = 0, r = 0 while l < left.count && r < right.count { if left[l] < right[r] { arr.append(left[l]) l += 1 }else { arr.append(right[r]) r += 1 } } while l < left.count { arr.append(left[l]) l += 1 } while r < right.count { arr.append(right[r]) r += 1 } return arr } let left = Array(list.prefix(upTo: list.count/2)) let right = Array(list.suffix(from: list.count/2)) return merge(left: sort(left), right: sort(right)) } let list = [2, 3, 19, 5, 10, 40, 6, 15, 6, 9, 18] let li = sort(list) print("排序结果:\(li)")
六、快速排序
快速排序采用分治法策略来把一个序列分成较小和较大的2个子序列,然后递归的排序两个子序列。
步骤如下:
- 挑选基准值,从一个序列中选出一个元素,这个元素称为“基准值”
- 分割:重新排序列,所有比基准值小的元素,放在基准值前面。所有比基准值大的元素,方法基准值后面(与基准值相等的元素,可以放在任何一侧)。在这个分割结束之后,对基准值的排序就已完成。
- 递归排序子序列:递归的将小于基准值的子序列和大于基准值的子序列排序。
平均时间复杂度:O(n*log n);最坏时间复杂度:O(n2);最好时间复杂度:O(n*log n);空间复杂度:O(n*log n);稳定性:不稳定(即:排序前两个相同元素,在排序后前后顺序发生了改变)
实现代码(swift):
func sort(_ list: [Int]) ->[Int] { if list.count < 2 { return list } func swap(_ li: inout [Int], i: Int, j: Int) { guard i != j else { return } let temp = li[i] li[i] = li[j] li[j] = temp } func partition(_ arr: inout [Int], left: Int, right: Int) -> Int { let pivot = arr[left] var j = left + 1 var i = j while i <= right { if arr[i] < pivot { swap(&arr, i: i, j: j) j += 1 } i += 1 } swap(&arr, i: left, j: j - 1) return j - 1 } func partitionRandom(_ arr: inout [Int], left: Int, right: Int) ->Int { let pivotIndex = left + (right - left) / 2 // 每次取序列中间的数作为基准值,这里也可以采用其他算法计算基准值 swap(&arr, i: pivotIndex, j: left) // 将基准值放在当前序列左侧的第一位 return partition(&arr, left: left, right: right) } func quickSort(_ arr: inout [Int], left: Int, right: Int) { if left < right { let pivot = partitionRandom(&arr, left: left, right: right) quickSort(&arr, left: left, right: pivot - 1) // 这里不包括基准值 quickSort(&arr, left: pivot + 1, right: right) // 这里不包括基准值 } } var li = list quickSort(&li, left: 0, right: list.count - 1) return li } let list = [2, 3, 19, 5, 10, 40, 6, 15, 6, 9, 18] let li = sort(list) print("排序结果:\(li)")
七、堆排序
是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。
通常堆是通过一维数组来实现的。在数组起始位置为0的情形中:
- 父节点i的左子节点在位置(2i+1)
- 父节点i的右子节点在位置(2i+2)
- 子节点i的父节点在位置((i-1)/2)
堆排序是基于完全二叉树实现的,在将一个数组调整成一个堆的时候,关键之一的是确定最后一个非叶子节点的序号,即:最后一个父节点的位置。从上可知,一个子节点i的父节点位于(i-1)/2可知,(加入数组长度为n)最后一个子节点位于:n-1,因此最后一个父节点位于:(n-1-1)/2,即:n/2-1。
步骤如下:
- 初始化构建一个大顶堆(或小顶堆)。
- 将堆顶元素与数组中最后一个元素交换,这样数组中最后的元素就是数组中最大(或最小)的。
- 然后除去最后一个元素,继续构建大顶堆(或小顶堆),重复步骤2,这样数组中后面的元素即为有序的,直到有序序列长度为n-1为止。
平均时间复杂度:O(n*log n);最坏时间复杂度:O(n*log n);最好时间复杂度:O(n*log n);空间复杂度:O(1);稳定性:不稳定(即:排序前两个相同元素,在排序后前后顺序发生了改变)
实现代码(swift):
func sort(_ list: inout [Int]) { if list.count < 2 { return } // 交换元素 func swap(_ arr: inout [Int], i: Int, j: Int) { let temp = arr[i] arr[i] = arr[j] arr[j] = temp } // 构建大顶堆 func buildMaxHeap(_ arr: inout [Int], start: Int, end: Int) { var father = start // 父节点下标 var son = start * 2 + 1 // 左孩子 while son <= end { if son + 1 <= end, arr[son] < arr[son + 1] { // 选出最大的元素下标 son += 1 } if arr[father] > arr[son] { break // 跳出函数 } // 交换父子节点 swap(&arr, i: father, j: son) father = son son = father * 2 + 1 } } // 初始化堆,从最后一个父节点开始 for i in 0...list.count/2-1 { buildMaxHeap(&list, start: i, end: list.count-1) } // 将堆顶元素(最大或最小)与堆的最后一个叶子节点交换,然后剩余元素继续构建大顶堆 var i = list.count - 1 while i > 0 { swap(&list, i: 0, j: i) // 数组中第0个元素即为堆顶元素,将堆顶与堆的最后一个叶子节点交换 buildMaxHeap(&list, start: 0, end: i-1) i -= 1 } } var list = [2, 3, 19, 5, 10, 40, 6, 15, 6, 9, 18] sort(&list) print("排序结果:\(list)")
八、计数排序
是一种稳定的线性时间排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。
步骤如下:
- 找出待排序的数组中最大和最小的元素,计算出计数容器的大小 k
- 统计数组中每个值为 i 的元素出现的次数,存入数组C的第 i 项
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- 反向填充目标数组:将每个元素 i 放在新数组的第C[i]项,每放一个元素就将C[i]减去1
平均时间复杂度:O(n + k);最坏时间复杂度:O(n + k);最好时间复杂度:O(n + k);空间复杂度:O(n + k);稳定性:稳定(即:排序前两个相同元素,在排序后前后顺序不改变)(n:为数组个数, k:为计数容器大小)
实现代码(swift):
func sort(_ list: [Int]) -> [Int] { if list.count < 2 { return list } var arr = Array<Int>(repeating: 0, count: list.count) var max = list.first!, min = list.first! // 选出最大值与最小值 for i in list { if i > max { max = i } if i < min { min = i } } // 计算计数容器的大小 let k = max - min + 1 var c = Array<Int>(repeating: 0, count: k) // 计数容器,用于存储list中每个元素出现的个数 // 开始计数 for i in list { c[i - min] += 1 } // 对所有计数累加 for i in 1..<k { c[i] = c[i] + c[i - 1] }// 反向填充目标数组 var i = list.count - 1 while i >= 0 { let a = list[i] arr[c[a - min] - 1] = a c[a - min] -= 1 i -= 1 } return arr } var list = [2, 3, 19, 5, 10, 40, 6, 15, 6, 9, 18] print("排序结果:\(sort(list))")
九、桶排序
是计数排序的升级版,工作的原理是将数组分到有限数量的桶里,每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。适用于被排序的数组内的数值是均匀分配的。
步骤如下:
- 设置一个定量的数组当作空桶子。
- 遍历序列,并且把项目一个一个放到对应的桶子去。
- 对每个不是空的桶子进行排序。
- 从不是空的桶子里把项目再放回原来的序列中。
平均时间复杂度:O(n + k);最坏时间复杂度:O(n2);最好时间复杂度:O(n);空间复杂度:O(n + k);稳定性:稳定(即:排序前两个相同元素,在排序后前后顺序不改变)(n:为数组个数, k:为桶的数量)
实现代码(swift):
// bucketSize:每个桶的容量 func sort(_ list: [Int], bucketSize size: Int) -> [Int] { if list.count < 2 { return list } // 插入排序 func insertSort(_ arr: inout [Int]) { if arr.count < 2 { return } for i in 1..<arr.count { var j = i - 1 let k = arr[i] while j >= 0, arr[j] > k { arr[j + 1] = arr[j] j -= 1 } arr[j + 1] = k } } var max = list[0], min = list[0] // 计算序列中的最大值与最小值(用于计算所需桶的个数) for i in list { if i > max { max = i }else if i < min { min = i } } // 初始化桶 let bucketCount = (max - min) / size + 1 // 计算所需桶的个数 var buckets = Array<[Int]>(repeating: [], count: bucketCount) // 利用映射函数,将数组分到指定的桶中 for i in list { let idx = (i - min) / size buckets[idx].append(i) } // 分别对每个桶子的元素进行插入排序 for i in 0..<buckets.count { if buckets[i].count < 2 { continue } insertSort(&buckets[i]) } var arr = [Int]() // 从桶中取出元素,放到新数组中 for bucket in buckets { arr.append(contentsOf: bucket) } return arr } var list = [2, 3, 19, 5, 10, 40, 6, 15, 6, 9, 18] print("排序结果:\(sort(list, bucketSize: 10))")
十、基数排序
是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
操作步骤:
MSD:
- 计算序列中,子元素最的位数
- 从最高位开始遍历数组中的每个元素,进行桶排序
- 此时不对桶中的元素进行收集,而是对桶中元素个数>1的再次递归基数排序
- 直到遍历完所有位数,排序结束
LSD:
- 计算序列中,子元素最大的位数
- 从最低位开始遍历数组中的每个元素,取出元素当前位的数子作为桶的索引,分别放入0-9的桶子中
- 取出桶子中的数放入临时数组中
- 重复2、3,直到遍历完每一位,返回临时数组
平均时间复杂度:O(n * k);最坏时间复杂度:O(n * k);最好时间复杂度:O(n * k);空间复杂度:O(n + k);稳定性:稳定(即:排序前两个相同元素,在排序后前后顺序不改变)(n:为数组个数, k:为桶的数量)
MSD算法实现(swift):
func sort(_ list: [Int]) -> [Int] { if list.count < 2 { return list } // 计算最大位数 func maxDigit(_ arr: [Int]) -> Int { var max = arr[0] for i in arr { if i > max { max = i } } var digit = 1 while max / 10 > 0 { digit += 1 max = max / 10 } return digit } // 递归调用 func sort(_ arr: inout [Int], maxDigit: Int) { if maxDigit < 1 || arr.count < 2 { return } var buckets = Array<[Int]>(repeating: [], count: 10) let base = maxDigit - 1 == 0 ? 1 : (maxDigit - 1) * 10 for i in arr { buckets[i / base % 10].append(i) } for i in 0..<buckets.count { if buckets[i].count > 1 { sort(&buckets[i], maxDigit: maxDigit - 1) } } var j = 0 for bucket in buckets { for el in bucket { arr[j] = el j += 1 } } } var arr = list let maxDigit = maxDigit(list) sort(&arr, maxDigit: maxDigit) return arr } var list = [2, 3, 19, 5, 10, 40, 6, 15, 6, 9, 18] print("排序结果:\(sort(list))")
LSD算法实现(swift):
func sort(_ list: [Int]) -> [Int] { if list.count < 2 { return list } // 计算最大位数 func maxDigit(_ arr: [Int]) -> Int { var max = arr[0] for i in arr { if i > max { max = i } } var digit = 1 while max / 10 > 0 { digit += 1 max = max / 10 } return digit } var arr = list let maxDigit = maxDigit(list) for i in 0..<maxDigit { // 初始化桶,0-9 var buckets = Array<[Int]>(repeating: [], count: 10) let base = i == 0 ? 1 : i * 10 for el in arr { buckets[el / base % 10].append(el) } var j = 0 for bucket in buckets { for el in bucket { arr[j] = el j += 1 } } } return arr } var list = [2, 3, 19, 5, 10, 40, 6, 15, 6, 9, 18] print("排序结果:\(sort(list))")