美文网首页算法基础-打开算法之门
三个示例学习如何使用渐近符号描述算法的运行时间(θ、Ο、Ω)

三个示例学习如何使用渐近符号描述算法的运行时间(θ、Ο、Ω)

作者: 红酥手黄藤酒丶 | 来源:发表于2018-12-18 00:29 被阅读0次

三个示例学习如何描述算法的运行时间

1. 准备工作

  • 从数组中取出目标值所在的索引位置(数组中可能含有多个目标元素)
  • 创建接口 FindElementUtil
  • 创建实现类 FirstWay SecondWay ThirdWay
  • 测试
import java.util.List;

public interface FindElementUtil {
    /**
     *
     * @param elements 目标数组
     * @param n 数组中的元素个数
     * @param target 查找目标
     * @return 目标值在数字中的索引位置,若没找到则返回-1
     */
    int findEle(List<Integer> elements, int n, int target);
}

import java.util.List;

/**
 * @author : zyf
 * @since : 2018-12-17 23:19
 **/
public class FirstWay implements FindElementUtil {
    @Override
    public int findEle(List<Integer> elements, int n, int target) {
        int index = -1;
        
        /**
         * 当前循环会执行 n 次迭代(循环体执行n次)
         * n + 1 次越界判断(每次迭代都会判断当前索引是否小于数组长度)
         */
        for (int i = 0; i < n; i++) {
            if(elements.get(i) == target){
                index = i;
            }
        }
        return index;
    }
}
import java.util.List;

/**
 * @author : zyf
 * @since : 2018-12-17 23:22
 **/
public class SecondWay implements FindElementUtil {
    @Override
    public int findEle(List<Integer> elements, int n, int target) {
        
        /**
         * 与 FirstWay 相比,迭代次数与判断次数均减少
         * 因为找到了就会立即返回
         *
         * 迭代次数:1 to n
         * 判断次数:0 to n-1
         */
        for (int i = 0; i < n; i++) {
            if(elements.get(i) == target){
                return i;
            }
        }
        return -1;
    }
}

import java.util.List;

/**
 * @author : zyf
 * @since : 2018-12-17 23:27
 **/
public class ThirdWay implements FindElementUtil {
    @Override
    public int findEle(List<Integer> elements, int n, int target) {

        //将数组中的最后一个元素保存到一个局部变量中
        int lastEle = elements.get(n - 1);

        //将目标值赋值到数组的最后一个元素
        elements.set(n - 1, target);

        int index = 0;
        while (elements.get(index) != target) {
            index++;
        }

        //将原本保存的最后一个元素归位
        elements.set(n - 1, lastEle);
        /**
         * 判断
         * 1. 迭代数 index 是否小于 数组长度
         * 2. 数组最后一个元素是否为目标值
         */

        if (index < n - 1 || elements.get(n - 1) == target) {
            //说明找到了
            return index;
        } else {
            //没找到则赋值为 -1
            index = -1;
        }

        return index;
    }
}
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Function;

/**
 * @author : zyf
 * @since : 2018-12-17 23:19
 **/
public class Main {
    private static List<Integer> elements;

    public static void main(String[] args) {

        initData();

        speedMeasure(new FirstWay());
        speedMeasure(new SecondWay());
        speedMeasure(new ThirdWay());

    }

    private static void initData() {
        elements = new ArrayList<>();
        
        Random random = new Random(50);

        for (int i = 0; i <= 800000; i++) {
            int anInt = random.nextInt();
            elements.add(anInt);
        }
    }

    private static void speedMeasure(FindElementUtil util) {


        long start = System.currentTimeMillis();

        int index = util.findEle(elements, elements.size(), 500000);

        long end = System.currentTimeMillis();

        long timeCost = end - start;

        System.out.println(util.getClass().getSimpleName() + "-------timeCost:" + timeCost + "     index=" + index);
    }
}

2. 如何描述运行时间

1)计算 FirstWay 的运行时间:

为了表示完成一项任务所花的时间,我们必须做一些简单的假设。我们假定每步单独的操作---无
论它是一个算术运算(例如加减乘除)、比较、变量赋值、给数组标记索引操作,或者程序调用、
从程序中返回结果---均会花掉不依赖于输入规模的固定时间(输入规模指的就是本例中的数组中
的元素数目),操作不同,各个操作所花费的时间也不同,例如除法操作可能比加法操作花费更长
时间。一个步骤可能含有多个简单操作,由于多个步骤所执行的操作所花费的时间不同,执行不同
步骤所花费的时间也是不一样的。

第一步 int index = -1; 和第三步 return index; 只会执行一次,但是第二步 for(){} 呢?我们必须对 i (循环增量) 和 n(数组元素数量) 的值进行比较 (i 是否小于 n),比较多少次呢?比较 n + 1 次。(for循环,从第一次开始算,每次执行一次循环体前都会执行判断条件语句

/**
 * @author : zyf
 * @since : 2018-12-18 00:39
 **/
public class Test {
    public static void main(String[] args) {
        //不会输出
        //所以比较了 一次
        //所以一定会比较 n + 1 次
        // 0 + 1 = 1
        for (int i = 0; i < 0; i++) {
            System.out.println(1);
        }
    }
}

二(1)if(elements.get(i) == target){ index = i; } 执行了 n 次,当循环增量 i 从 0 变化到 n 时,对于每个 i 值,我们都执行了第 二(1) 步,我们不知道这里到底会执行多少次,因为不确定数组中有多少个目标值。所以第二步(循环)会执行两个具有不同循环次数的操作:

  • 比较:n + 1 次 使用 T2′ 表示

  • 递增:n 次 使用 T2″ 表示

  • 我们将第 二(1) 步中判断元素是否等于目标值的操作时间记为 T2(1)′

  • 我们将第 二(1) 步中把 i 赋值给 index 的操作时间记为 T2(1)″

将 FirstWay 分为三步,约定执行第 i 步所需花费的时间为 Ti,其中 Ti 是不依赖于输入规模 n 的常量。

所以 FirstWay 的运行时间介于以下 bottomtop 之间。

  • bottom = T1 + T2′ * (n + 1) + T2″ * n + T2(1)′ * n + T2(1)″ * 0 + T3
  • top = T1 + T2′ * (n + 1) + T2″ * n + T2(1)′ * n + T2(1)″ * n + T3
  • bottom = 定义index的时间 + 循环判断比较的时间 * (n + 1) + 循环增量递增的时间 * n + 判断元素是否等于目标值的时间 * n + 把i赋值为index的时间 * 一个没有则为0 + 返回的时间
  • top = 定义index的时间 + 循环判断比较的时间 * (n + 1) + 循环增量递增的时间 * n + 判断元素是否等于目标值的时间 * n + 把i赋值为index的时间 * 所有元素均为目标值则为n + 返回的时间

乘法分配律后:

  • bottom = (T2′ + T2″ + T2(1)′) * n + (T1 + T2′ + T3)
  • top = (T2′ + T2″ + T2(1)′ + T2(1)″) * n + (T1 + T2′ + T3)

所以实际上这就是两个 线性函数 ,可表示成 c * n + d 的形式。其中 c 和 d 均为不依赖于 n 的常量。

所以:FirstWay 的运行时间可表示为 介于关于 n 的一个线性函数和关于 n 的另一个线性函数之间。

在算法中使用一个特殊符号来表示运行时间大于关于 n 的一个线性函数,同时小于关于 n 的另一个线性函数。将这样的运行时间表示为 θ(n)。(读:theta n)这样表示去除了低阶项(T1 + T2′ + T3) 和 n 的系数。尽管这样表示会降低精度,但是它强调了 运行时间的增长阶数,弱化了不必要的细节
**( ①确实,数据量低的时候,是否使用正确的算法不会对程序造成过多的影响,但是数据量较大时,正确的算法便可以提搞运行效率) **

θ 符号可以表示任何通用的函数,假如我们有两个函数,f(n) g(n) ,且当 n 足够大时,f(n)g(n) 的常数倍之内,此时我们成 f(n) = θ(g(n))

注释:f(n) 就是小时候学习xy函数时候的y,n 就是 x。
f(n) = 2x  与 y = 2x 是等价的,表示方式不同而已。

所以说 `f(n)` 在 `g(n)` 的常数倍之内的意思就是 
一个函数:f(n) = 常量 * x 的值 
在另一个函数:g(n) = 常量 * x 的值的常数倍之内。

f(n) = y = 4x
g(n) = z = x

y 始终在 z 的常数倍之内,(零也是常数,常数就是常量)
所以 y = θ(z),说 z = θ(y) 也是一样的

所以说当 n 足够大时,FirstWay 的运行时间小于等于 n 的某个常数倍。

在使用 θ 符号时,我们仅仅关心 主阶项 而忽略 次阶项 和 主阶项的常量因子。也就是说 y = 2x²; 和 z = 10x²; 是等价的,忽略次阶项和主阶项的常量因子后,y和z都等于x²。所以可以说 y 和 z 都等于:θ(n²)。

假设:f(n) = n²/4 + 100n + 100 那么忽略过后:f(n) = θ(n²)

如果在思考当 n = 1 的时候,明明 100n 起到了主导作用的话,请参看 ①

2)计算 SecondWay 的运行时间

如果 elements.get(0) 等于 target,那么循环只迭代了一次。如果 elements 中没有 target,那么循环会迭代 n 次,此时称:在最坏的情况下,SecondWay 在一个包含 n 个元素的数组中进行查找操作需要花费 θ(n) 的时间(最坏情况下也是一个关于 n 的线性函数)。

在最好的情况下,当 elements.get(0) 等于 target 时,SecondWay 只会花费常量时间(因为运行时间与 n 无关,所以称之为常量时间),此时使用 θ(1) 来表示运行时间是一个不依赖于 n 的常量。

此时问题就出来了,我们不能用 θ(n) 来表示 SecondWay 的运行时间,因为在 最好情况下 SecondWay 的运行时间与 n 无关,是一个常量 θ(1)

新概念:关于 n 的一个线性函数是所有情况的上界,使用 Ο(n) 来表示。(读:ou of n)

如果一个函数 f(n) 是 Ο(g(n)) (意思就是 一旦 n 非常大,一个函数的上界 是另一个函数的常量因子倍)

f(n) = 4n

g(n) = 2n

则 f(n) 是 g(n) 的 1/2 倍,1/2就是常量因子

对于 SecondWay ,我们可以称在所有情况下,该算法的运行时间均满足 Ο(n),尽管它的运行时间可能会比关于 n 的线性函数运行时间好(elements.get(0)==target 为 true 时),但是它一定不会比关于 n 的线性函数运行时间差。这就是 Ο(n) 存在的意义。

那么我们如何表示一个函数的运行时间不会比关于 n 的某个线性函数好呢?这是一个下界问题。就是如何描述下界?此时使用的是 Ω 符号,它与 Ο 符号的含义恰恰相反。

如果 f(n) 是 Ω(g(n)) ,即一旦 n 变得非常大,f(n) 的下界是 g(n) 的常数倍,就写作:f(n) = Ω(g(n))。

Ο 符号给出了上界,Ω 符号给出了下界,Θ 符号即给出了上界也给出了下界,我们能推断出:
②f(n) 是 θ(g(n))的必要条件,当且仅当 f(n) 是 Ο(g(n)) 且 f(n) 是 Ω(g(n))。

把运行时间看成一个区间,上界就表示最大值,下界表示最小值。运行时间最小,则表示速度最快,也就是最好情况。

解释一下②
f(n) = 5n
g(n) = 7n

使用 f(n) 表示下界,此时下界又可以使用 Ω(n) 表示,使用 g(n) 表示上界,此时上界又可
以使用 Ο(n) 表示,而 f(n) 和 g(n) 都是线性函数,Θ(n) 的定义是 运行时间大于某个线
性函数又小于某个线性函数,所以此时 就可以使用 Θ(n) 表示

所以描述 SecondWay 最恰当的方式就是给出一个满足所有情况的下界:Ω(1),就是至少要花费常量时间。(常量运行时间就是指与 n 无关的运行时间)

Θ 、Ο、Ω 符号均为 渐近符号,这些符号记录了 随着变量近似趋向于无穷大时函数的增长趋势。这些渐近符号使得我们去掉了低阶项和高阶项的常量因子,以便淡化不必要的细节,专注于主要方面:函数是如何随着 n 增长而变化的!

3) 计算 ThirdWay 运行时间

至少会执行一次迭代,最多执行 n 次迭代。ThirdWaySecondWay 最大的不同是,ThirdWay 每次迭代的时间均小于 SecondWay(少了判断数组越界)。两者最坏的情况下均需要花费线性时间(n的线性函数)。虽然 ThirdWay 的常量因子小一些,但是当我们使用 渐近符号 表示时,它们是相等的,即在最坏情况下均是 θ(n) 最好情况下均是 θ(1),满足所有条件时均为 Ο(n)。

相关文章

  • 三个示例学习如何使用渐近符号描述算法的运行时间(θ、Ο、Ω)

    三个示例学习如何描述算法的运行时间 1. 准备工作 从数组中取出目标值所在的索引位置(数组中可能含有多个目标元素)...

  • 《python算法教程》Day1- 渐近表示法

    算法的时间复杂度一般使用渐近表示法表示。 渐近表示法的表示符号 使用的符号主要有这三个:O f(n))、Ω (f(...

  • 算法和数据结构

    算法和数据结构 [TOC] 算法 函数的增长 渐近记号 用来描述算法渐近运行时间的记号,根据定义域为自然数集$N=...

  • 大O表示法

    一、简介 表示时间的大O符号,是用来描述算法效率的语言和度量单位。大O表示法分析了算法的运行时间如何随列表的增长而...

  • 函数的渐近的界&阶的比较

    一、函数的渐近的界   我们在研究算法性能的时候,往往会在意算法的运行时间,而运行时间又与算法输入的规模相关,对于...

  • 快速过一遍数据结构

    一、如何衡量算法? 时间复杂度 一个函数,用 大O表示,比如O(1)、O(n),定型描述算法运行时间,即描述时间的...

  • 渐进符号

    在表示算法的时间复杂度上 , 我们通常会使用渐进符号 常用的渐进符号有 :O ,Ω ,θ那么这三个符号分别表示了什...

  • 时间复杂度

    什么是时间复杂度 在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。时间复杂度常用大O符号...

  • 关于时间复杂度介绍

    知道算法的速度和它需要多少空间是很有用的。这允许您为工作选择正确的算法。O符号给出了一个算法运行时间和它使用的内存...

  • 大O符号基础

    大O符号(Big O notation), 又称渐进符号,是用于描述函数的渐近行为的数学符号。它是指用另一个(通常...

网友评论

    本文标题:三个示例学习如何使用渐近符号描述算法的运行时间(θ、Ο、Ω)

    本文链接:https://www.haomeiwen.com/subject/cyohkqtx.html