关于数组排序的问题,在以前的文章有很详细的介绍(连接:《iOS面试之道》算法基础学习(下))。在这篇文章中,笔者会对经典的冒泡算法进行优化。先来看下未经优化的冒泡算法:面试
//经典版
func bubbleSort( _ array: inout [Int]) -> [Int]? {
//i表示数组中依次比较的总次数
for i in 0..<array.count {
for j in 0..<array.count - i - 1) {
//j表示下标,判断第j位和j+1位元素大小,进行排序
if array[j] > array[j+1] {
//元素左右进行交换
let tmp = array[j]
array[j] = array[j+1]
array[j+1] = tmp
}
}
}
return array
}
复制代码
咱们都知道传统的冒泡算法是经过2个for循环来比较相邻元素的值,每次一轮大的循环结束,都会固定一个值,而后对已固定数值外的元素接着遍历,最终使得数组有序。这样的写法看上去是没有问题,可是时间复杂度却很是高达到了O(n^2),因此说这个算法不是一个“优秀”的算法。算法
怎么才能优化这个算法呢,咱们仍是经过个例子来看下。假设如今有一个数组array = [7,6,1,2,3,4],不难看出这个数组其实经历过2次循环就已经使得数组有序了,可是程序仍是会继续进行无心义的比较。那么这时咱们能够经过一个Bool值,来肯定当前数组是否已经有序,如有序则中断循环,直接看代码:数组
//进化版
func bubbleSort( _ array: inout [Int]) -> [Int]? {
for i in 0..<array.count {
//设置bool判断是否有序
var isSorted = true
for j in 0..<array.count - (i+1) {
if array[j] > array[j+1] {
let tmp = array[j]
array[j] = array[j+1]
array[j+1] = tmp
//有交换 数组依旧无序
isSorted = false
}
}
if(isSorted) {
//有序 中断循环
break
}
}
return array
}
复制代码
解决方法简单粗暴,每次外层循环开始isSorted为true,若元素未进行位置交换,则证实数组已有序,结束循环。学习
那这个算法还有继续优化的空间么,答案是确定的。用另外一个例子来解释一下,假设如今数组array = [1,3,2,4,5,6]。按照上面的写法咱们的确能够在3轮判断后就结束循环,很是高效。但问题是后面的4,5,6元素已经有序,每次的循环仍是会对后三个元素进行判断。优化
问题的所在是由于咱们对已排序好的数组进行了不少无心义的判断,因此须要咱们对已经排好序的元素进行边界限定,来减小判断,看下代码:spa
//超级进化版
func bubbleSort( _ array: inout [Int]) -> [Int]? {
//记录数组边界
var arrayBorder = array.count - 1
for i in 0..<array.count {
//设置bool判断是否有序
var isSorted = true
for j in 0..<arrayBorder {
if array[j] > array[j+1] {
let tmp = array[j]
array[j] = array[j+1]
array[j+1] = tmp
//有交换 数组依旧无序
isSorted = false
//记录最后一次交换的位置
arrayBorder = j
}
}
if(isSorted) {
//有序 中断循环
break
}
}
return array
}
复制代码
若是说上面进化版的方法是减小外层i循环的无用判断,那么添加数组边界,就是减小内部j循环的无用判断。经过记录须要交换位置的边界值,来避免没必要要的判断。code
通过了2个版本的进化,冒泡排序已经实现了大蜕变,减小了不少没必要要的操做。可是此时的冒泡排序还不是那么的优秀,好比数组array = [2,3,4,5,6,1],能够看出除了元素1之外,其余元素都已经完成排序。可是把该数组带入上面超级进化版本,你会发现它仍是进行了5轮的判断才完成任务。排序
出现上述问题的主要缘由仍是循环一直是从左至右进行,每次的起始都是从第0位开始。若是说能让循环从右至左再进行一轮循环,就能很快的把元素1放到首位。这就要介绍一种新的排序方法--鸡尾酒排序。get
鸡尾酒排序:也就是定向冒泡排序, 鸡尾酒搅拌排序, 搅拌排序 (也能够视做选择排序的一种变形), 涟漪排序, 来回排序 or 快乐小时排序, 是冒泡排序的一种变形。此演算法与冒泡排序的不一样处在于排序时是以双向在序列中进行排序。for循环
知道了解决方法,直接来看下最终的究极进化版本:
//究极进化版
func cocktailSort( _ array: inout [Int]) -> [Int]? {
//数组左边界
var arrayLeftBorder = 0
//数组右边界
var arrayRightBorder = array.count - 1
for i in 0..<array.count/2 {
//设置bool判断是否有序
var isSorted = true
//第一轮循环 左->右
for j in arrayLeftBorder..<arrayRightBorder {
if array[j] > array[j+1] {
let tmp = array[j]
array[j] = array[j+1]
array[j+1] = tmp
//有交换 数组依旧无序
isSorted = false
//记录最后一次交换的位置
arrayRightBorder = j
}
}
if(isSorted) {
//有序 中断循环
break
}
//再次初始化isSorted位true
isSorted = true
//第二轮循环 右->左
for j in (arrayLeftBorder+1...arrayRightBorder).reversed() {
if array[j] < array[j-1] {
let tmp = array[j]
array[j] = array[j-1]
array[j-1] = tmp
//有交换 数组依旧无序
isSorted = false
//记录最后一次交换的位置
arrayLeftBorder = j
}
}
if(isSorted) {
//有序 中断循环
break
}
}
return array
}
复制代码
鸡尾酒排序将以前完整的一轮循环拆分为从左->右和从右->左两个子循环,这就保证了排序的双向进行,效率较单项循环来讲更高。 与此同时在这两个循环中加入了以前两个版本的特性isSorted和有序边界,使得排序更加高效。
随着对冒泡排序的不断升入理解,发现了实际排序中的问题,经过将几种方法的组合使用,使得改进后的冒泡排序更加高效,当数据量很是巨大时效率提高很是明显。