10分钟看懂10大经典算法(Swift代码实现)
排序算法是《数据结构与算法》中最基本的算法之一。
排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳一律的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。用一张图概括:
sort.png
在开始之前我们先来写一个帮助测试的函数
// 生成有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 语言的时候就会接触到。基本的思想就是,对于一个数比较与之相邻的数字,例如要把一个数列按从小到大的顺序排列,就拿左边第一个数,和第二的比,若小于第二个数两个交换,否则不换,再比较第二个和第三个,按照同样的规则,继续第三第四…直到最后。这样就算一次冒泡,每次冒泡都会有一个数被放到了最终的位置。
总的来说冒泡排序就是把最大的数放到最后面的位置
bubbleSort.gif
创立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)
bubbleSort.png
我们查看打印发现,其实最后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 }在判断语句中,假如一次都没有进行位置交换的话,证实已经排序完成了,这个时候可以中止遍历,结束循环
bubbleSort1.png
2、选择排序(Selection Sort)
选择排序也会把数组分为已排序区和未排序区。但是与插入排序不同的是,它每次找到未排序区的最小值,与未排序区的首个元素交换,这样就变成了已排序区的末尾元素了
selectionSort.gif
selectionSort.png
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 的前面。
insertionSort.gif
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名字命名的
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳固排序算法,对于中等数据的性能体现还不错。
希尔排序是基于插入排序的以下两点性质而提出改进方法的
- 插入排序在对几乎已经排好序的数据操作时,效率高,就可以达到线性排序的效率
- 但插入排序一般来说是低效的,由于插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录”基本有序”时,再对全体记录进行依次直接插入排序。
逻辑
首先它把较大的数据集合分割成若干个小组(逻辑上分组),而后对每一个小组分别进行插入排序,此时,插入排序所作用的数据量比较小(每一个小组),插入的效率比较高
希尔排序1.jpeg
可以看出,他是按下标相隔距离为4分的组,也就是说把下标相差4的分到一组,比方这个例子中a[0]与a[4]是一组、a[1]与a[5]是一组…,这里的差值(距离)被称为增量
希尔排序2.jpeg
每个分组进行插入排序后,各个分组就变成了有序的了(整体不肯定有序)
希尔排序3.jpeg
此时,整个数组变的部分有序了(有序程度可能不是很高)
希尔排序4.jpeg
而后缩小增量为上个增量的一半:2,继续划分分组,此时,每个分组元素个数多了,但是,数组变的部分有序了,插入排序效率同样比高
希尔排序5.jpeg
同理对每个分组进行排序(插入排序),使其每个分组各自有序
希尔排序6.jpeg
最后设置增量为上一个增量的一半:1,则整个数组被分为一组,此时,整个数组已经接近有序了,插入排序效率高
希尔排序7.jpeg
希尔排序8.gif
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().timeIntervalSince1970let sort1 = SelectionSort().sorting(arr: arr)let endTime1 = Date().timeIntervalSince1970let starttime2 = Date().timeIntervalSince1970let sort2 = MergeSort().sort(arr: arr)let endTime2 = Date().timeIntervalSince1970print("选择排序:\(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().timeIntervalSince1970let res = quickSort().quickSort(arr: arr)let endTime1 = Date().timeIntervalSince1970let arr1 = generateRandomArray(count: 10000, rangL: 0, rangR: 100000)let starttime2 = Date().timeIntervalSince1970let _ = quickSort().quickSort(arr: arr1)let endTime2 = Date().timeIntervalSince1970print("顺序数组快速排序:\(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) } }结尾
相关代码实现,请点击这里
参考
- 五分钟学算法
- 十大经典排序算法
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 10分钟看懂10大经典算法(Swift代码实现)