算法复杂度之时间复杂度

时间复杂度

 

概念定义

根据定义,时间复杂度指输入数据大小为 NN 时,算法运行所需花费的时间。需要注意:

统计的是算法的「计算操作数量」,而不是「运行的绝对时间」。计算操作数量和运行绝对时间呈正相关关系,并不相等。算法运行时间受到「编程语言 、计算机处理器速度、运行环境」等多种因素影响。例如,同样的算法使用 Python 或 C++ 实现、使用 CPU 或 GPU 、使用本地 IDE 或力扣平台提交,运行时间都不同。

体现的是计算操作随数据大小 NN 变化时的变化情况。假设算法运行总共需要「 11 次操作」、「 100100 次操作」,此两情况的时间复杂度都为常数级 O(1)O(1) ;需要「 NN 次操作」、「 100N100N 次操作」的时间复杂度都为 O(N)O(N) 。

符号表示

根据输入数据的特点,时间复杂度具有「最差」、「平均」、「最佳」三种情况,分别使用 OO , \ThetaΘ , \OmegaΩ 三种符号表示。以下借助一个查找算法的示例题目帮助理解。

 

题目: 输入长度为 NN 的整数数组 nums ,判断此数组中是否有数字 77 ,若有则返回 true ,否则返回 falsefalse 。

 

解题算法: 线性查找,即遍历整个数组,遇到 77 则返回 true 。

 

代码:

 

PythonJavaC++

 

def find_seven(nums):

    for num in nums:

        if num == 7:

            return True

    return False

最佳情况 \Omega(1)Ω(1) : nums = [7, a, b, c, ...] ,即当数组首个数字为 77 时,无论 nums 有多少元素,线性查找的循环次数都为 11 次;

最差情况 O(N)O(N) : nums = [a, b, c, ...] 且 nums 中所有数字都不为 77 ,此时线性查找会遍历整个数组,循环 NN 次;

平均情况 \ThetaΘ : 需要考虑输入数据的分布情况,计算所有数据情况下的平均时间复杂度;例如本题目,需要考虑数组长度、数组元素的取值范围等;

大 OO 是最常使用的时间复杂度评价渐进符号,下文示例与本 LeetBook 题目解析皆使用 OO 。

 

常见种类

根据从小到大排列,常见的算法时间复杂度主要有:

497aa2154440403caede56f91af0683f.png

 

O(1) < O(\log N) < O(N) < O(N\log N) < O(N^2) < O(2^N) < O(N!)

O(1)<O(logN)<O(N)<O(NlogN)<O(N 

2

 )<O(2 

N

 )<O(N!)

 

Picture1.png

 

示例解析

对于以下所有示例,设输入数据大小为 NN ,计算操作数量为 countcount 。图中每个「蓝色方块」代表一个单元计算操作。

 

常数 O(1)O(1) :

运行次数与 NN 大小呈常数关系,即不随输入数据大小 NN 的变化而变化。

 

PythonJavaC++

 

def algorithm(N):

    a = 1

    b = 2

    x = a * b + N

    return 1

对于以下代码,无论 aa 取多大,都与输入数据大小 NN 无关,因此时间复杂度仍为 O(1)O(1) 。

 

PythonJavaC++

 

def algorithm(N):

    count = 0

    a = 10000

    for i in range(a):

        count += 1

    return count

Picture2.png

 

线性 O(N)O(N) :

循环运行次数与 NN 大小呈线性关系,时间复杂度为 O(N)O(N) 。

 

PythonJavaC++

 

def algorithm(N):

    count = 0

    for i in range(N):

        count += 1

    return count

对于以下代码,虽然是两层循环,但第二层与 NN 大小无关,因此整体仍与 NN 呈线性关系。

 

PythonJavaC++

 

def algorithm(N):

    count = 0

    a = 10000

    for i in range(N):

        for j in range(a):

            count += 1

    return count

Picture3.png

 

平方 O(N^2)O(N 

2

 ) :

两层循环相互独立,都与 NN 呈线性关系,因此总体与 NN 呈平方关系,时间复杂度为 O(N^2)O(N 

2

 ) 。

 

PythonJavaC++

 

def algorithm(N):

    count = 0

    for i in range(N):

        for j in range(N):

            count += 1

    return count

以「冒泡排序」为例,其包含两层独立循环:

 

第一层复杂度为 O(N)O(N) ;

第二层平均循环次数为 \frac{N}{2} 

2

N

​ 

  ,复杂度为 O(N)O(N) ,推导过程如下:

O(\frac{N}{2}) = O(\frac{1}{2})O(N) = O(1)O(N) = O(N)

O( 

2

N

​ 

 )=O( 

2

1

​ 

 )O(N)=O(1)O(N)=O(N)

 

因此,冒泡排序的总体时间复杂度为 O(N^2)O(N 

2

 ) ,代码如下所示。

 

PythonJavaC++

 

def bubble_sort(nums):

    N = len(nums)

    for i in range(N - 1):

        for j in range(N - 1 - i):

            if nums[j] > nums[j + 1]:

                nums[j], nums[j + 1] = nums[j + 1], nums[j]

    return nums

Picture4.png

 

指数 O(2^N)O(2 

N

 ) :

生物学科中的 “细胞分裂” 即是指数级增长。初始状态为 11 个细胞,分裂一轮后为 22 个,分裂两轮后为 44 个,……,分裂 NN 轮后有 2^N2 

N 个细胞。

 

算法中,指数阶常出现于递归,算法原理图与代码如下所示。

 

PythonJavaC++

 

def algorithm(N):

    if N <= 0: return 1

    count_1 = algorithm(N - 1)

    count_2 = algorithm(N - 1)

    return count_1 + count_2

Picture5.png

 

阶乘 O(N!)O(N!) :

阶乘阶对应数学上常见的 “全排列” 。即给定 NN 个互不重复的元素,求其所有可能的排列方案,则方案数量为:

 

N \times (N - 1) \times (N - 2) \times \cdots \times 2 \times 1 = N!

N×(N−1)×(N−2)×⋯×2×1=N!

 

如下图与代码所示,阶乘常使用递归实现,算法原理:第一层分裂出 NN 个,第二层分裂出 N - 1N−1 个,…… ,直至到第 NN 层时终止并回溯。

 

PythonJavaC++

 

def algorithm(N):

    if N <= 0: return 1

    count = 0

    for _ in range(N):

        count += algorithm(N - 1)

    return count

Picture6.png

 

对数 O(\log N)O(logN) :

对数阶与指数阶相反,指数阶为 “每轮分裂出两倍的情况” ,而对数阶是 “每轮排除一半的情况” 。对数阶常出现于「二分法」、「分治」等算法中,体现着 “一分为二” 或 “一分为多” 的算法思想。

 

设循环次数为 mm ,则输入数据大小 NN 与 2 ^ m2 

m

  呈线性关系,两边同时取 log_2log 

 

​ 

  对数,则得到循环次数 mm 与 \log_2 Nlog 

 

​ 

 N 呈线性关系,即时间复杂度为 O(\log N)O(logN) 。

 

PythonJavaC++

 

def algorithm(N):

    count = 0

    i = N

    while i > 1:

        i = i / 2

        count += 1

    return count

如以下代码所示,对于不同 aa 的取值,循环次数 mm 与 \log_a Nlog 

 

​ 

 N 呈线性关系 ,时间复杂度为 O(\log_a N)O(log 

 

​ 

 N) 。而无论底数 aa 取值,时间复杂度都可记作 O(\log N)O(logN) ,根据对数换底公式的推导如下:

 

O(\log_a N) = \frac{O(\log_2 N)}{O(\log_2 a)} = O(\log N)

O(log 

​ 

 N)= 

O(log 

2

​ 

 a)

O(log 

2

​ 

 N)

​ 

 =O(logN)

 

PythonJavaC++

 

def algorithm(N):

    count = 0

    i = N

    a = 3

    while i > 1:

        i = i / a

        count += 1

    return count

如下图所示,为二分查找的时间复杂度示意图,每次二分将搜索区间缩小一半。

8cf7a53d9c33496abc6384f6e9750c5f.png

 

线性对数 O(N \log N)O(NlogN) :

两层循环相互独立,第一层和第二层时间复杂度分别为 O(\log N)O(logN) 和 O(N)O(N) ,则总体时间复杂度为 O(N \log N)O(NlogN) ;

 

PythonJavaC++

 

def algorithm(N):

    count = 0

    i = N

    while i > 1:

        i = i / 2

        for j in range(N):

            count += 1

线性对数阶常出现于排序算法,例如「快速排序」、「归并排序」、「堆排序」等,其时间复杂度原理如下图所示。

4b61ecd8253d4f9ea93ea911731f37f7.png

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

绽放情花

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值