算法基础--时间复杂度,三个常规O(N²)的排序算法(冒泡、选择、插入)

本文只是自己的笔记,并不具备过多的指导意义。

代码的初衷是便于理解,网上大神优化过的代码很多,也不建议在项目中copy本文代码。


目录

  • 时间复杂度
    • 常数时间的操作
    • 时间复杂度的计算
    • 常数操作表达式类型的时间复杂度
    • 时间复杂度相同的比对
  • 冒泡排序
    • 改进版的冒泡排序
  • 选择排序
    • 二元选择排序
  • 插入排序
  • 时间复杂度的最差情况,最好情况,平均情况
  • *对数器

时间复杂度

衡量代码的好坏,包括两个非常重要的指标:运行时间与占用空间。

而时间复杂度正代表前者,后者由空间复杂度(即算法在运行过程中临时占用存储空间大小的量度)表示。

  • 常数时间的操作

一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
比如数组下标的寻址,一对下标交换。

  • 常数操作数量

单次常数时间的操作,写作做O(1)。读作big O 1 。

  • 时间复杂度的计算

法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示。
若有一个函数,f(N)。当N趋近于无穷大时,使得T(n)/f(n)趋近于一个不为0的常数。
则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。

  • 线性的时间复杂度

比如一个算法共需要执行N次循环,每次循环内部都是常数操作O(1)

for i in 1..<N+1 {
    //常数操作
    let firstItem = arr[i-1]
    let secondItem = arr[i]
    if firstItem > secondItem {
        arr.swapAt(i-1, i)
    }
}

的T(N)=F(N)=N,时间复杂度为O(F(N))=O(N)。

  • 常数操作表达式类型的时间复杂度

对于T(N)为达式类型的时间复杂度,F(N)简而言之就是要简化成当N趋近无穷大时,表达式中对其影响最大的一项的表达式。

具体来说。在常数操作数量的表达式中, 只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分 如果记为f(N),那么时间复杂度为O(f(N))。

借用百度百科上的例子:

for(i=1; i<=n; ++i)
{
    c[i];//该步骤属于基本操作执行次数:n
    for(j=1; j<=n; ++j)
    {
        c[i][j] = 0;//该步骤属于基本操作执行次数:n的平方次
        for(k=1; k<=n; ++k)
            c[i][j] += a[i][k] * b[k][j];//该步骤属于基本操作执行次数:n的三次方次
    }
}

T(N) = A×N³+B×N²+C×N。当N趋近于无穷大时,三次方的影响远大于二次方以及一次方。当然也大于常数项A的影响。
所以表达式f(N)=N³。
时间复杂度为O(N)=O(f(N))=O(N³)

领附一张图方便理解高阶项在基数变大时的变化:


  • 时间复杂度相同的比对

评价一个算法流程的好坏,先看时间复杂度的指标,然后再分
析不同数据样本下的实际运行时间,也就是常数项时间。


冒泡排序

在冒泡排序过程中会将数组分成两部分,前半部分是无序的数列,后半部分为有序的数列。无序数列中不断的将其中最大的值往有序序列中冒泡,泡冒完后,我们的序列就创建好了。

具体操作上,如果相邻的两个数字前者较大,则将二者交换,到达无序数组边界则停止。

func bubbleSort(arr: inout [Int]) {
    if arr.count < 2 {
        return
    }
    
    for i in 0..<arr.count {
        for j in 0..<arr.count - (i+1) {
            if arr[j] > arr[j+1] {
                let temp = arr[j]
                arr[j] = arr[j+1]
                arr[j+1] = temp
            }
        }
    }
}

时间复杂度O(N²),额外空间复杂度O(1)。

时间复杂度的来源f(N) = N +( N -1) + (N-2) + ...+ 2 + 1 为一个等差数列。前N项和的通用公式为:N*(N-1)/2化简后f(N)=N²。

  • 改进版的冒泡排序

经典的冒泡排序,无论数组是否已经有序。都会一次次的遍历,从这一点上我们可以进行改进

func bubbleSort2(arr: inout [Int]) {
    if arr.count < 2 {
        return
    }
    var swapped = false //记录是否有交换动作的变量
    for i in 0..<arr.count {
        swapped = false
        for j in 0..<arr.count - (i+1) {
            if arr[j] > arr[j+1] {
                let temp = arr[j]
                arr[j] = arr[j+1]
                arr[j+1] = temp
                swapped = true //有交换动作则记录
            }
        }
        if swapped == false {
            break //没有交换动作,说明已经有序
        }
    }
}

极端情况下(对于一个已经有序的数组),算法完成第一次外层循环后就会返回。
实际上只发生了 N - 1次比较,所以最好的情况下,该算法复杂度是O(N)。


选择排序

基本思想与冒泡排序相同。前半部分为序的数列,只不过后半部分是无序的数列。无序数列中不断的将其中最大的值往有序序列中冒泡,泡冒完后,我们的序列就创建好了。

具体操作上,每次遍历记录无序序列中最小值的位置,并在结束时与无序序列的首位置交换,使其变成有序序列的最后一位。

func selectionSort(arr : inout [Int]) {
    if arr.count<2 {
        return
    }
    var minIndex :Int
    for i in 0..<arr.count {
        minIndex = i
        for j in i+1..<arr.count { //循环从i+1开始,也就是无序数组的第二位开始
            minIndex = arr[j]<arr[minIndex] ? j:minIndex //比对当前位置与记录位置的值,记录其中最小的。
        }
        arr.swapAt(i, minIndex) //将无序数组的第一位与最小一位交换
    }
}
  • 二元选择排序

选择排序本身没有什么可改进的,但是我们可以左右开弓。

将序列分成三个部分,前段有序部分,中段无序部分,后端有序部分。
每次循环,将最大值与最小值分别置入前后两个有序序列。

func selectionSort2(arr : inout [Int]) {
    if arr.count<2 {
        return
    }
    var minIndex :Int
    var maxIndex :Int
    for i in 0..<arr.count {
        minIndex = i
        maxIndex = i
        if i+1 >= arr.count - i {
            return // 由于这一步的存在,实际上会在i=arr.count/2处结束循环
        }
        for j in i+1..<arr.count - i { //循环从i+1开始,也就是无序数组的第二位开始。并且在后端有序序列的前一位停止
            minIndex = arr[j]<arr[minIndex] ? j:minIndex //比对当前位置与记录位置的值,记录其中最小的。
            maxIndex = arr[j]>arr[maxIndex] ? j:maxIndex //比对当前位置与记录位置的值,记录其中最大的。
        }

        if maxIndex == i && minIndex == arr.count - (i+1) {
            //如果最大值与最小值恰好处于边界,直接交换会导致乱序。需要手动赋值
            let maxValue = arr[maxIndex];
            let minValue = arr[minIndex];
            let maxToValue = arr[arr.count - (i+1)]
            let minToValue = arr[i]
            
            arr[maxIndex] = maxToValue
            arr[arr.count - (i+1)] = maxValue
            arr[minIndex] = minToValue
            arr[i] = minValue
            
        }else if maxIndex == i{
            //如果最大值位置处于最小值将要交换的位置,先交换最大值
            arr.swapAt(arr.count - (i+1) , maxIndex) //将无序数组的最后一位与最大一位交换
            arr.swapAt(i, minIndex) //将无序数组的第一位与最小一位交换
        }else  {
            arr.swapAt(i, minIndex) //将无序数组的第一位与最小一位交换
            arr.swapAt(arr.count - (i+1) , maxIndex) //将无序数组的最后一位与最大一位交换
        }
    }
}

这样虽然复杂度还是O(N²),但实际上的表达式系数比经典选择排序不止缩小了1/2。


插入排序

基本思想也是前半部分为序的数列,后半部分是无序的数列。无序数列不断将其首位元素推给有序数列,有序数列将其插入适当的位置。

具体操作上,会从有序数列的尾部依次向前比较,若前位大于后位则进行交换。

func insertionSort(arr : inout [Int]) {
    if arr.count<2 {
        return
    }
    for i in 1..<arr.count { //无序数组从i=1到末尾
        for j in (0...i-1).reversed() {  //从 i-1 位置到 0位置的有序数组内进行循环
            if arr[j+1] > arr[j] {  //j+1当第一次执行的时候,正位于无序数组的首位置
                break //如果后位置大于前位置,说明已经有序。退出当前循环
            }
            arr.swapAt(j, j+1)//否则交换
        }
    }
}

改进的话,或许可以试试用二分法确定具体位置然后进行整体后移并插入。


时间复杂度的最差情况,最好情况,平均情况

对于插入排序这种有明确终止条件的排序,实际的时间复杂度与数据的实际状况有关。
最好情况是最开始便有序,我们只需要执行一次大循环,复杂度为O(N)。
最差情况是将整个数组倒序排列一次,复杂度为O(N²)。
平均情况是指在大数状况下的平均期望复杂度。

在数据的实际状况对算法流程存在影响时,使用最差情况作为时间复杂度。

不过,我们可以利用主动打乱数据状况影响的方式。将复杂度易数学期望的方式表达(参考随机快排)。


对数器

对数器是用来测试代码正确性的,我们在找不到合适的oj系统测试自己的代码时,可以自己写一个对数器对代码进行测试。

  • 设计对数器的一般步骤为:

1.有一个你要测的方法a; 自己写的方法
2.实现一个绝对正确即使复杂度不好的方法b; 系统自带方法即可
3.实现一个随机样本产生器;
4.实现比对的方法; 比对两个结果最后是否一致
5.把方法a和方法b比对很多次来验证方法a是否正确
6.如果有一个样本使得比对出错,打印样本分析是哪个方法出错
7.当样本数量很多时比对测试依然正确,可以确定方法a已经正确

  • 实现对数器

其中1,2,4已经说了。6,7也没啥好说的。

3.实现一个随机样本产生器
/// 随机数组生成器
///
/// - Parameters:
///   - size: 最大长度
///   - value: 最大值
/// - Returns: 随机数组
func generateRandomArray(size : Int ,value : Int) -> [Int] {
    var arr :[Int]
    arr = Array.init()
 
    for i in 0..<Int(arc4random() % 10) * size / 10  {
        let item = Int(arc4random() % 10)*value/10
        arr.append(item)
    }
    print(arr)
    return arr
}
  • 把方法a和方法b比对很多次来验证方法a是否正确
var checkOK = true
for i in 0..<10000 {
    var arr1 = generateRandomArray(size: 5, value: 20)
    var arr2 = arr1 //数组在swift里属于值类型,赋值动作会自动copy
    let originalArr = arr1
    arr1.sort()
    heapSort(arr: &arr2)
    
    if arr1 != arr2 {
        checkOK = false
        print(originalArr)
        print(arr2)
        break
    }
}

print(checkOK ? "比对成功":"比对失败")


//打印
[0, 6, 2, 12, 18]
[0, 6, 12, 2, 18]
比对失败

错误的原始数据已经打印出来了,你就可以随意重现这个数据进行调试了。

var arr = [0, 6, 12, 2, 18];
print(arr)
heapSort(arr: &arr)
print(arr)

参考资料

左神牛课网算法课
时间复杂度和空间复杂度的简单讲解
选择排序及改进方法
十大经典排序算法(动图演示)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 排序(上):为什么插入排序比冒泡排序更受欢迎? 排序对于任何一个程序员来说,可能都不会陌生。你学的第一个算法,可能...
    GhostintheCode阅读 3,350评论 4 27
  • 简单排序 插入排序 想象一下插队的过程... 一个人通过插队的方式排到前面,而将原本在他前面的人挤到了后面的位置。...
    Kasheem_Lew阅读 1,491评论 0 4
  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,654评论 0 13
  • 听了今晚仙草的分享,我变得有些焦灼,她的很多以前得信念系统都和我一样,看着她走出来了,我迫切的希望自己也可以赶快走...
    臻悉阅读 143评论 0 0
  • 08年经历了一次不痛不痒的高考,在那个火热的盛夏体验了初恋的苦涩。可能是命里安排,第二年的成绩依旧没有太多的...
    Nicole凝阅读 212评论 0 0