排序算法是《数据结构与算法》中最基本的算法之一。
排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。用一张图概括:
在开始之前我们先来写一个帮助测试的函数
// 生成有n个元素的随机数组,每个元素的随机范围为[rangeL, rangeR]
func generateRandomArray(count:Int,rangL:Int,rangR:Int) -> Array<Int> {
if rangR <= rangL{
fatalError("取值范围不准确")
}
var arr:[Int] = Array()
for _ in 0..<count {
let arc = rangR - rangL + 1
let item:Int = Int(arc4random()) % arc + rangL
arr.append(item)
}
return arr
}
// 判断arr数组是否有序
func isSorted(arr:[Int]) -> Bool {
for i in 0..<arr.count-1 {
if arr[i] > arr[i+1] {
return false
}
}
return true
}
排序算法优越评价有三个指标,执行效率、内存消耗、稳定性,一般来讲,在分析效率时会从几个方面来衡量:
时间复杂度。会从最好、最坏和平均情况三个来分析;
时间复杂度的系数、常数 、低阶。在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。
比较次数和交换(或移动)次数。
1、冒泡排序(Bubble Sort)
冒泡排序是最基本最简单的排序了,在大家刚开始学习 C 语言的时候就会接触到。基本的思想就是,对于一个数比较与之相邻的数字,例如要把一个数列按从小到大的顺序排列,就拿左边第一个数,和第二的比,若小于第二个数两个交换,否则不换,再比较第二个和第三个,按照同样的规则,继续第三第四…直到最后。这样就算一次冒泡,每次冒泡都会有一个数被放到了最终的位置。
总的来说冒泡排序就是把最大的数放到最后面的位置
创建BubbleSorting类
//冒泡排序
func sorting(arr:[Int]) -> Array<Int>{
var sortArr:[Int] = arr
for i in 0..<sortArr.count {
for j in 0..<sortArr.count-1-i{
if sortArr[j] > sortArr[j+1]{
sortArr.swapAt(j, j+1)
}
}
print("-------------------")
print(sortArr)
}
return sortArr
}
我们调用这个函数
let arr = [5, 4, 6, 3, 2, 1]
let bubble = BubbleSorting().sorting(arr: arr)
我们查看打印发现,其实最后1次遍历其实是不用了,因为已经排序号结果了,但是因为我们没有进行判断,所以多运行了1次。我们的数据量比较小,所以多一次少一次没太大的性能差异,但是当数据量比较大的时候性能差异就显示出来了。为此我们继续进行优化
func sorting1(arr:[Int]) -> Array<Int>{
var sortArr:[Int] = arr
print(sortArr)
var swapped = false
for i in 0..<sortArr.count {
swapped = false
for j in 0..<sortArr.count-1-i{
if sortArr[j] > sortArr[j+1]{
sortArr.swapAt(j, j+1)
swapped = true
}
}
if swapped == false{
break
}
print("-------------------")
print(sortArr)
}
return sortArr
}
在判断语句中,如果一次都没有进行位置交换的话,证明已经排序完成了,这个时候可以停止遍历,结束循环
2、选择排序(Selection Sort)
选择排序也会把数组分为已排序区和未排序区。但是与插入排序不同的是,它每次找到未排序区的最小值,与未排序区的首个元素交换,这样就变成了已排序区的末尾元素了
class SelectionSort: NSObject {
func sorting(arr:[Int]) -> Array<Int>{
var sortArr = arr
let n = sortArr.count
for i in 0..<n {
// 寻找[i, n)区间里的最小值的索引
var minIndex = i;
for j in (i+1)..<n{
if sortArr[minIndex] > sortArr[j]{
minIndex = j
}
}
sortArr.swapAt(i, minIndex)
}
return sortArr
}
}
3、插入排序(Insertion Sort)
插入排序把数组分为已排序区和未排序区。取未排序区的元素,在已排序区上找到一个正确的位置插上去。还是希望对一个数据进行从小到大的排序。我们从未排序区上拿一个元素,按从右到左与已排序区的元素对比,如果如果当前元素 A 小于已排序区中的元素 B,让 B 往后移,即让 B 后面的位置等于 B,继续比 B 前面的数,也叫它为 B,它是新的一个 B,重复操作直到 A 大于 B,就让 A 插进当前 B 的前面。
class InsertionSort: NSObject {
func sorting(arr:[Int]) -> Array<Int>{
var sortArr = arr
for i in 0..<sortArr.count {
for j in stride(from: i, to: 0, by: -1) {
if sortArr[j] < sortArr[j-1]{
sortArr.swapAt(j, j-1)
}
}
}
return sortArr
}
}
4、希尔排序(ShellSort)
希尔排序(ShellSort)是以发明者Donald Shell名字命名的
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法,对于中等数据的性能表现还不错。
希尔排序是基于插入排序的以下两点性质而提出改进方法的
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
逻辑
首先它把较大的数据集合分割成若干个小组(逻辑上分组),然后对每一个小组分别进行插入排序,此时,插入排序所作用的数据量比较小(每一个小组),插入的效率比较高
可以看出,他是按下标相隔距离为4分的组,也就是说把下标相差4的分到一组,比如这个例子中a[0]与a[4]是一组、a[1]与a[5]是一组...,这里的差值(距离)被称为增量
每个分组进行插入排序后,各个分组就变成了有序的了(整体不一定有序)
此时,整个数组变的部分有序了(有序程度可能不是很高)
然后缩小增量为上个增量的一半:2,继续划分分组,此时,每个分组元素个数多了,但是,数组变的部分有序了,插入排序效率同样比高
同理对每个分组进行排序(插入排序),使其每个分组各自有序
最后设置增量为上一个增量的一半:1,则整个数组被分为一组,此时,整个数组已经接近有序了,插入排序效率高
class ShellSort: NSObject {
private
var data:[Int] = Array()
func shellSort(data:[Int]) -> [Int] {
self.data = data
let incremental = data.count / 2
recursiveShellSort(incremental: incremental)
self.data = sorting(arr: self.data)
return self.data
}
/// 递归
///
/// - Parameter incremental: 增量
private
func recursiveShellSort(incremental:Int) {
if incremental == 0 {
return
}
for index in 0..<data.count - incremental {
let a = data[index]
let b = data[index + incremental]
if a > b{
data.swapAt(index, index+incremental)
}
}
recursiveShellSort(incremental: incremental / 2)
}
//插入排序
func sorting(arr:[Int]) -> Array<Int>{
var sortArr = arr
for i in 0..<sortArr.count {
for j in stride(from: i, to: 0, by: -1) {
if sortArr[j] < sortArr[j-1]{
sortArr.swapAt(j, j-1)
}
}
}
return sortArr
}
}
5、归并排序(Merge sort)
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法
1、自上而下的递归
2、自下而上的迭代
Merge
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
合并相邻有序子序列
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤
Merge1
class MergeSort: NSObject {
private
var sortArr:[Int] = Array()
func sort(arr:[Int]) ->Array<Int>{
sortArr = arr
recursiveSort(l: 0, r: arr.count-1)
return sortArr
}
func recursiveSort(l:Int,r:Int) {
if l >= r {
return
}
let mid = (l+r)/2
//左边归并排序,使得左子序列有序
recursiveSort(l: l, r: mid)
//右边归并排序,使得右子序列有序
recursiveSort(l: mid+1, r: r)
//将两个有序子数组合并操作
merge(l: l, mid: mid, r: r)
}
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
func merge(l:Int,mid:Int,r:Int) {
var aux:[Int] = Array()
for i in l..<r+1 {
aux.append(sortArr[i])
}
// 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
var i = l
var j = mid+1
for k in l..<r+1 {
if (i > mid){// 如果左半部分元素已经全部处理完毕
sortArr[k] = aux[j-l]
j += 1
}
else if( j > r ){ // 如果右半部分元素已经全部处理完毕
sortArr[k] = aux[i-l];
i += 1
}
else if(aux[i-l] - aux[j-l] < 0 ){ // 左半部分所指元素 < 右半部分所指元素
sortArr[k] = aux[i-l]
i += 1
}
else{ // 左半部分所指元素 >= 右半部分所指元素
sortArr[k] = aux[j-l]
j += 1
}
}
}
}
我们跟选择排序对比一下时间消耗
let arr = generateRandomArray(count: 10000, rangL: 0, rangR: 100)
let starttime1 = Date().timeIntervalSince1970
let sort1 = SelectionSort().sorting(arr: arr)
let endTime1 = Date().timeIntervalSince1970
let starttime2 = Date().timeIntervalSince1970
let sort2 = MergeSort().sort(arr: arr)
let endTime2 = Date().timeIntervalSince1970
print("选择排序:\(endTime1 - starttime1)")
print("归并排序:\(endTime2 - starttime2)")
Merge2
6、快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
我们假设对4、6、2、3、1、5、7、8进行快速排序,我们在排序之前把数组分成两部分,一部分是大于4(第一个元素)的部分,一部分是小于4的部分。我们假设对数组左边都是小于4的部分, 数组右边都是大于4的部分
quickSort1
里面有两个步骤需要考虑,在大于4时和小于4时
1、在大于4时,不做不处理
2、在小于4的时候,我们需要把小于4的部分,挪移到右边数组中,也就是和第一个大于4的位置交换
代码实现
private
var arr:[Int] = Array()
func quickSort(arr:[Int]) ->[Int]{
self.arr = arr
recursiveQuickSort(l: 0, r: arr.count-1)
return self.arr
}
//递归排序
private
func recursiveQuickSort(l:Int,r:Int) {
if l >= r {
return
}
let p = partition(l: l, r: r)
recursiveQuickSort(l: l, r: p-1)
recursiveQuickSort(l: p+1, r: r)
}
//对arr[l...r]部分进行partition
//返回p,使得arr[l...p-1] < arr[p] arr[p+1...r] > arr[p]
private
func partition(l:Int,r:Int) -> Int {
let v = arr[l]
//arr[l+1...j] < v arr[j+1...i] > v
var j = l
for i in l+1..<r+1 {
if arr[i] < v {
arr.swapAt(j+1, i)
j += 1
}
}
arr.swapAt(l, j)
return j
}
测试
let arr = [4,6,2,1,5,3,7,8]
let res = quickSort().quickSort(arr: arr)
print(res) // 1,2,3,4,5,6,7,8
优化
上面我们实现了基础的快速排序,但是上面的实现是有一点问题的,我们来使用一个1万数量级的,一个顺序数组,一个随机数组来测试一下排序时间
var arr:[Int] = []
for i in 0..<10000{
arr.append(i)
}
let starttime1 = Date().timeIntervalSince1970
let res = quickSort().quickSort(arr: arr)
let endTime1 = Date().timeIntervalSince1970
let arr1 = generateRandomArray(count: 10000, rangL: 0, rangR: 100000)
let starttime2 = Date().timeIntervalSince1970
let _ = quickSort().quickSort(arr: arr1)
let endTime2 = Date().timeIntervalSince1970
print("顺序数组快速排序:\(endTime1 - starttime1)")
print("随机数组快速排序:\(endTime2 - starttime2)")
image.png
我们发现时间效率相差了十几倍,如果数量级更大的话,相差也更大
我们知道快速排序最好的结果就是O(nlnn)
,最坏的结果就是在顺序打印的时候,效率变成了O(n*n)
image.png
对于上面的我们可以做一个简单的优化,那就是我们在选择中间值的时候,不是固定选择第一个,而是随机产生一个,那么最坏的那种情况的可能性就小了很多。
image.png
我们仅仅需要修改上面那一句
我们重新再来看打印结果
image.png
完整代码
class quickSort: NSObject {
private
var arr:[Int] = Array()
func quickSort(arr:[Int]) ->[Int]{
self.arr = arr
recursiveQuickSort(l: 0, r: arr.count-1)
return self.arr
}
//递归排序
private
func recursiveQuickSort(l:Int,r:Int) {
if l >= r {
return
}
let p = partition(l: l, r: r)
recursiveQuickSort(l: l, r: p-1)
recursiveQuickSort(l: p+1, r: r)
}
//对arr[l...r]部分进行partition
//返回p,使得arr[l...p-1] < arr[p] arr[p+1...r] > arr[p]
private
func partition(l:Int,r:Int) -> Int {
//=======================================
let rand = Int(arc4random()) % (r - l + 1) + l
arr.swapAt(rand, l)
//======================================
let v = arr[l]
//arr[l+1...j] < v arr[j+1...i] > v
var j = l
for i in l+1..<r+1 {
if arr[i] < v {
arr.swapAt(j+1, i)
j += 1
}
}
arr.swapAt(l, j)
return j
}
}
7、堆排序(Heapsort)
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为Ο(nlogn)
。
8、计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
它是一个不基于比较的排序算法。不管是快排,归并,还是堆排,它们都难以突破NlogN的运行时间下限,而计数排序是一个线性时间级别的排序算法。对NlogN的突破凭借的就是不基于比较对元素进行排序,当然了,它也有很大的局限性,比如它只能对整数进行排序。总之,计数排序是一种对整数进行排序非常有效的排序算法。
1、找出待排序的数组中最大和最小的元素
2、统计数组中每个值为i的元素出现的次数,存入数组C的第i项
3、对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
4、反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
计数排序.gif
class CountingSort: NSObject {
func sort(arr:[Int]) -> [Int] {
var max:Int = arr[0] //最大值
var min:Int = arr[0] //最小值
for item in arr {
//找出最大值
max = max < item ? item : max
//找出最小值
min = min < item ? min : item
}
//创建一个元素个数为 max-min 的数组
var list:[Int] = Array()
for _ in 0..<max-min+1 {
list.append(0)
}
//排序[3,1,1,2,1,1]
for item in arr {
let a = item - min
let count = list[a] + 1
list[a] = count
}
//返回结果
var resultList:[Int] = Array()
for (index,item) in list.enumerated() {
if item == 0{
continue
}
for _ in 0..<item{
let num = index + min
resultList.append(num)
}
}
return resultList
}
}
9、桶排序(BucketSort)
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
在额外空间充足的情况下,尽量增大桶的数量
使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
算法步骤
设置固定数量的空桶
把数据放到对应的桶中
对每个不为空的桶中数据进行排序。
拼接不为空的桶中数据,得到结果
桶排序.gif
class BucketSort: NSObject {
///桶容量大小
private
var bucket = 5
func sort(arr:[Int]) -> [Int] {
var max:Int = arr[0] //最大值
var min:Int = arr[0] //最小值
for item in arr {
//找出最大值
max = max < item ? item : max
//找出最小值
min = min < item ? min : item
}
//获取桶的个数
let buckets = bucketCount(min: min, max: max, arr: arr)
var bucketList:[[Int]] = Array()
for _ in 0..<buckets {
bucketList.append([Int]())
}
// 将数据分配到各个桶中
for item in arr {
let index = (item - min) / bucket
var items = bucketList[index]
items.append(item)
bucketList[index] = items
}
// 对每个桶进行排序,这里使用了插入排序
var resultList:[Int] = Array()
for items in bucketList {
let sorts = insertSorting(arr: items)
resultList += sorts
}
return resultList
}
///获取桶的个数
func bucketCount(min:Int,max:Int,arr:[Int]) -> Int {
let num1 = (max - min + 1) / bucket
let num2 = (max - min + 1) % 5 > 0 ? 1 : 0
return num1 + num2
}
//插入排序
func insertSorting(arr:[Int]) -> [Int]{
var sortArr = arr
for i in 0..<sortArr.count {
for j in stride(from: i, to: 0, by: -1) {
if sortArr[j] < sortArr[j-1]{
sortArr.swapAt(j, j-1)
}
}
}
return sortArr
}
}
10、基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零
从最低位开始,依次进行一次排序
从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列
radixSort.gif
基数排序.png
class radixSort: NSObject {
private
var data:[Int] = Array()
func sort(arr:[Int]) -> [Int] {
data = arr
recursiveSort(num: 1)
return data
}
//递归
private
func recursiveSort(num:Int) {
let mode = num * 10
var buckets = [[Int]]()
for _ in 0..<10 {
buckets.append([Int]())
}
//判断递归是否结束,默认结束,
//当取余的所有值都为0的时候证明已经遍历到了最高位,递归结束
var end = true
for item in data {
let temp = (item % mode) / num
if temp != 0{
end = false
}
var tempArr = buckets[temp]
tempArr.append(item)
buckets[temp] = tempArr
}
if end {
return
}
//取出结果
data.removeAll()
for item in buckets {
data += item
}
recursiveSort(num: mode)
}
}
结尾
相关代码实现,请点击这里
参考
五分钟学算法
十大经典排序算法