小白算法集训营-大幅提升刷题量,快速逃离新手区

231 阅读6分钟

小白算法集训营-大幅提升刷题量,快速逃离新手区

核心代码,注释必读

// download:3w ukoou com

数据存储结构的四种基本存储方法

顺序存储方法

     结点间的逻辑关系由存储单元的邻接关系来体现,该方法把逻辑上相邻的结点存储在物理位置上相邻的存储单元里。

     通常借助程序语言的数组描述,该方法主要应用于线性的数据结构,由此得到的存储表示称为顺序存储结构,即Sequential Storage Structure,非线性的数据结构也可通过某种线性化的方法实现顺序存储。

     链接存储方法

     结点间的逻辑关系由附加的指针字段表示,该方法不要求逻辑上相邻的结点在物理位置上亦相邻,由此得到的存储表示称为链式存储结构(Linked Storage Structure), 通常借助于程序语言的指针类型描述。

     索引存储方法

     该方法通常在储存结点信息的同时, 索引表由若干索引项组成,还建立附加的索引表。若每个结点在索引表中都有一个索引项,则该索引表称之为稠密索引,即Dense Index ;若一组结点在索引表中只对应一个索引项,则该索引表称为稀疏索引(Spare Index)。

     索引项的一般形式是:(关键字、地址)。稠密索引中索引项的地址指示结点所在的存储位置,稀疏索引中索引项的地址指示一组结点的起始存储位置,关键字是能唯一标识一个结点的那些数据项。

     散列存储方法

     根据结点的关键字直接计算出该结点的存储地址,该方法的基本思想是,四种基本存储方法,既可单独使用,也可组合起来对数据结构进行存储映像。

     选择何种存储结构来表示相应的逻辑结构,视具体要求而定,同一逻辑结构采用不同的存储方法,可以得到不同的存储结构。主要考虑运算方便及算法的时空要求。

     数据结构三方面的关系

     存储结构是数据结构不可缺少的一个方面:同一逻辑结构的不同存储结构可冠以不同的数据结构名称来标识;数据的逻辑结构、数据的存储结构及数据的运算这三方面是一个整体,孤立地去理解一个方面,而不注意它们之间的联系是不可取的。

小白算法集训营-大幅提升刷题量,快速逃离新手区 - 时间复杂度和空间复杂度

目前分析算法主要从时间和空间两个维度进行。时间维度就是算法需要消耗的时间,时间复杂度(time complexity)是常用分析单位。空间维度就是算法需要占用的内存空间,空间复杂度(space complexity)是常用分析单位。

因此,分析算法主要从时间复杂度和空间复杂度进行。很多时候二者不可兼得,有时用时间换空间,有时用空间换时间。

1. 时间复杂度 Time Complexity

现代硬件性能强大,即使是非常昂贵的算法,数据量小时速度也可能很快。但数据量变大时,时间开销就会明显变大。时间复杂度是随着数据量增加,算法耗费时间的度量单位。

1.1 常数时间 Contant Time

常数时间算法不会随数据量变化而变,时间固定。

查看下面方法:

func checkFirst(names: [String]) {
    if let first = names.first {
        print(first)
    } else {
        print("No Names")
    }
}

该函数执行所需时间与 names 数组大小无关。无论数组有十个元素,还是一万个元素,该函数都只检查数组第一个元素。

下图是数据量与时间关系图:

ConstantTime

数据量变大时,算法所需时间保持不变。

为简便起见,使用大O符号(big O notion)表示各种时间复杂度。常数时间大O符号是O(1)

1.2 线性时间 Linear Time

下面代码打印数组中所有元素:

func printNames(names: [String]) {
    for name in names {
        print(name)
    }
}

当数组变大时,for 循环次数也会同步增加,这称为线性时间复杂度。

线性时间复杂度是最好理解的。随着数据量增加,耗费时间同步增加,正如上图中斜线。线性时间复杂度的大O符号是O(n)

如果是两个循环,外加六个O(1),大O符号是O(2n+6)吗?时间复杂度只描述性能曲线,因此,增加一些循环不会改变性能曲线。大O符号会移除所有常量,也就是O(2n+6)等于O(n),但优化绝对性能时,不能忽略这些常量。优化后的 GPU 比 CPU 可能快一百倍,但两者的时间复杂度仍然都是O(n)

1.3 平方时间 Quadratic Time

平方时间(Quadratic Time)也称为n的平方,平方时间复杂度算法耗费时间是数据量的平方。参考以下代码:

func printNames(names: [String]) {
    for _ in names {
        for name in names {
            print(name)
        }
    }
}

如果数组有10个元素,上述函数会把10个元素打印10次,即共打印100次。如果数组有11个元素,就会打印11个元素11次,即共打印121次。数据量变大时,平方时间算法会很快失去控制,所需时间急剧增长。

平方时间的大O符号是O(n²)

无论线性时间算法多么低效,当数据量特别大时,线性时间算法永远比平方时间算法速度快,即使对平方时间算法进行了优化。

1.4 对数时间 Logarithmic Time

线性时间复杂度、平方时间复杂度中数据至少使用一次,但有时只需使用输入数据的一部分,这时运行速度会快一些。如有一个有序整数数组,那查找特定值最快的方法是什么?

有一种方法是循环数组,依次比较,这时是线性时间复杂度。如下面这种最直接的比较方法:

let numbers = [1, 3, 5, 46, 88, 97, 115, 353]

func navieContains(_ value: Int, in array: [Int]) -> Bool {
    for element in array {
        if element == value {
            return true
        }
    }
    
    return false
}

如果查看数字354是否在数组中,上述算法会循环完整数组。由于数组是有序的,可以折半后查找:

func navieContaines(_ value: Int, in array: [Int]) -> Bool {
    guard !array.isEmpty else {
        return false
    }
    
    let middleIndex = array.count / 2
    if value < array[middleIndex] {
        for index in 0...middleIndex {
            if array[index] == value {
                return true
            }
        }
    } else {
        for index in middleIndex..<array.count {
            if array[index] == value {
                return true
            }
        }
    }
    
    return false
}

上述函数进行了一个很小,但却很有效的优化,只比较数组二分之一的元素。先比较数组中间的元素,如果指定值小于中间元素,则只比较前面二分之一的元素;否则,只比较后面二分之一的元素。