前言
关于数组排序的问题,在之前的文章有很详细的介绍(链接:《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元素已经有序,每次的循环还是会对后三个元素进行判断。
解决方法
问题的所在是因为我们对已排序好的数组进行了很多无意义的判断,所以需要我们对已经排好序的元素进行边界限定,来减少判断,看下代码:
//超级进化版
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循环的无用判断。通过记录需要交换位置的边界值,来避免不必要的判断。
经过了2个版本的进化,冒泡排序已经实现了大蜕变,减少了很多不必要的操作。但是此时的冒泡排序还不是那么的优秀。我们可以再来看一个例子,比如数组array = [2,3,4,5,6,1],可以看出除了元素“1”以外,其他元素都已经完成排序。但是把该数组带入上面超级进化版本,你会发现它还是进行了5轮的判断才完成任务。
解决方法
出现上述问题的主要原因还是循环一直是从左至右进行,每次的起始都是从第0位开始。如果说能让循环从右至左再进行一轮循环,就能很快的把元素1放到首位。这就要介绍一种新的排序方法--鸡尾酒排序。
鸡尾酒排序:也就是定向冒泡排序, 鸡尾酒搅拌排序, 搅拌排序 (也可以视作选择排序的一种变形), 涟漪排序, 来回排序 or 快乐小时排序, 是冒泡排序的一种变形。此演算法与冒泡排序的不同处在于排序时是以双向在序列中进行排序。
知道了解决方法,直接来看下最终的究极进化版本:
//究极进化版
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和有序边界,使得排序更加高效。
总结
随着对冒泡排序的不断升入理解,发现了实际排序中的问题,通过将几种方法的组合使用,使得改进后的冒泡排序更加高效,当数据量非常巨大时效率提升非常明显。