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

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

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)。

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