热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:Java程序员能看的懂算法复杂度分析:大O符号你都搞不懂,所以只能搬砖到秃顶?

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java程序员能看的懂算法复杂度分析:大O符号你都搞不懂,所以只能搬砖到秃顶?相关的知识,希望对你有一定的参考价值。如果

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java程序员能看的懂算法复杂度分析:大O符号你都搞不懂,所以只能搬砖到秃顶?相关的知识,希望对你有一定的参考价值。


如果你连算法复杂度分析都不会,或者没有这种意思,你学各种排序算、查找等算法有何用,因为你根本不知道或者没有意识什么时候应该使用它。当然,好处还是有的,能提高面试通过机率。


时间复杂度

大O符号背后的思想

大O符号是我们用来讨论算法运行所需时间的语言,用来表示我们如何比较不同方法解决问题的效率。

它就像数学,只是它是一种令人敬畏的、又不枯燥的数学,对于细节东西你可以挥一挥衣袖,而专注于正在发生的事情。

使用大O符号,我们可以通过以下方式表示运行时—当输入变得任意大时,它相对于输入的增长率。

我们来分析一下:



  1. 运行时增长率—很难确定算法的确切运行时间。 这取决于处理器的速度,计算机还在运行什么等等。因此,我们不是直接讨论运行时间,而是使用大O表示法来讨论运行时增长率。

  2. 相对输入—由于我们正在衡量运行时增长率,因此我们需要以其他方式表达我们的速度。对于大O符号,输入的大小,称之为“n”。所以可以这么说,运行时间的增长是“按照输入的大小的顺序”(O(n))或者“按照输入大小的平方的顺序”(O(技术图片))。

  3. 当输入变得任意大时—n很小的时候,我们的算法可能会有一些步骤看起来很昂贵,但是当n变得很大的时候,我们的算法最终会被其他步骤所掩盖。对于大O分析,我们最关心的是那些随着输入的增长而增长最快的因素,因为当n变得非常大时,其他所有因素都会接近消失。(如果你知道渐近线是什么,你可能会理解为什么“大O分析”有时被称为“渐近线分析”。)

如果你认为这看起来太抽象了,好吧,确实是如此。下面通过一些例子来更加形象的理解。


示例说明


public static void printFirstItem(int[] items) {
System.out.println(items[0]);
}

以上代码,相对于其输入,此方法在O(1)时间(“常数阶”)中运行,输入数组长度可以是1或1000,但是这个方法仍然只需要一个步骤就执行结束,因为始终返回数组的第一个值。


public static void printAllItems(int[] items) {
for (int item : items) {
System.out.println(item);
}
}

以上代码,该方法在O(n)时间(“线性阶”)内运行,其中n是数组中的项数或者长度。如果数组有10项,就要打印10次,如果它有1000项,就要1000次。


public static void printAllPossibleOrderedPairs(int[] items) {
for (int firstItem : items) {
for (int secondItem : items) {
System.out.println(firstItem + ", " + secondItem);
}
}
}

以上代码,这里我们嵌套了两个循环,如果我们的数组有n项,那么外层循环运行n次,而内层循环每次循环运行n次,总共输出技术图片次。因此,该方法在O(技术图片)时间内运行(或“二次方时间”)。如果数组有10项,就要打印100次。如果有1000项,就要打印100万次。


N可以是实际的输入项,或者输入的大小

这两种方法都是O(n)运行时间,尽管一个以整数作为输入,另一个以数组作为输入:


public static void sayHiNTimes(int n) {
for (int i = 0; i System.out.println("hi");
}
}
public static void printAllItems(int[] items) {
for (int item : items) {
System.out.println(item);
}
}

因此,有时n是作为方法输入的实际数字或大小,有时n是一个输入数组(或一个map、或一个object等)中的项数。


省略常数

这是大O符号的规则之一,当你计算某个任务的大O复杂度时,要护额略常数,如:


public static void printAllItems(int[] items) {
for (int item : items) {
System.out.println(item);
}
}
public static void printAllItemsTwice(int[] items) {
for (int item : items) {
System.out.println(item);
}
// 再遍历一次
for (int item : items) {
System.out.println(item);
}
}

上面这段代码的时间复杂度是O(2n),但我们也叫它O(n)。


public static void printFirstItemThenFirstHalfThenSayHi100Times(int[] items) {
System.out.println(items[0]);
int middleIndex = items.length / 2;
int index = 0;
while (index System.out.println(items[index]);
index++;
}
for (int i = 0; i <100; i++) {
System.out.println("hi");
}
}

上面这段代码的时间复杂度是O(1+n/2+100) ,我们也叫它O(n)。

为什么我们能这么理解呢?记住,对于大O符号,我们考虑的是当n趋于无穷大时会发生什么。当n变得越来越大,甚至趋于无穷大时,加100或除以2的效果可以忽略不计。


只保留增长最快的项


public static void printAllNumbersThenAllPairSums(int[] numbers) {
System.out.println("these are the numbers:");
for (int number : numbers) {
System.out.println(number);
}
System.out.println("and these are their sums:");
for (int firstNumber : numbers) {
for (int secondNumber : numbers) {
System.out.println(firstNumber + secondNumber);
}
}
}

这里运行时间是O(n+技术图片),大O也就是O(技术图片),即使它是O(技术图片/2+100n) ,大O表示仍然是O(技术图片)。

类似的:



  • O(技术图片 + 50技术图片 + 10000) 大O表示是 O(技术图片)

  • O((n + 30) * (n + 5)) 大O表示是 O(技术图片)

这是大O符号的规则之二,几项之和我们只保留增长最快(通常是阶最高)的项,其他项省略。


只讨论最坏的情况

通常这种“最坏情况”条件是隐含的,但是如果你明确地说出来,会给面试官留下更深刻的印象。

因为有时最坏的情况要比最好的情况严重得多:


public static boolean contains(int[] haystack, int needle) {
// haystack 是否包含 needle?
for (int n : haystack) {
if (n == needle) {
return true;
}
}
return false;
}

以上的代码中,我们的haystack中可能有100个项,但是第一个项可能是想要needle,在这种情况下,我们只需要循环一次就能将结果返回。

但实际上,我们会说运行时间是O(n),这是“最坏情况”。更具体些,我们可以说最坏情况是O(n)和最好情况是O(1)。对于某些算法,我们还可以取“平均情况”的运行时间。


空间复杂度

有时,我们希望优化使用更少的内存,而不是运行更少的时间。讨论内存成本(或“空间复杂度”)与谈论时间成本非常相似,

我们只需查看正在分配的任何新变量的总大小(相对于输入的大小)。

下面这个方法占用O(1)空间(变量只占用一个内存空间):


public static void sayHiNTimes(int n) {
for (int i = 0; i System.out.println("hi");
}
}

下面这个方法占用O(n)空间(hiArray是数组,随着n的输入分配相应的内存空间):


public static String[] arrayOfHiNTimes(int n) {
String[] hiArray = new String[n];
for (int i = 0; i hiArray[i] = "hi";
}
return hiArray;
}

通常当我们谈论空间复杂度,我们谈论的是额外的空间,所以不包括输入占用的空间。例如,即使输入有n项,该方法仍然占用常数空间::


public static int getLargestItem(int[] items) {
int largest = Integer.MIN_VALUE;
for (int item : items) {
if (item > largest) {
largest = item;
}
}
return largest;
}

技术图片 技术图片

有时候在节省时间和节省空间之间会有一个权衡,所以得决定你要优化哪一个。


总结

当然,大O的不只是文中提到两种,本文不讨论大O各个函数阶或者渐进符号。实际程序员面试常遇到最多用到这几种函数阶:





























符号名称
O(1)常数阶
O(n)线性阶
O(技术图片)对数阶
O(n技术图片)对数线性阶
O(技术图片)平方阶

你应该养成在设计算法时考虑时间和空间复杂性的习惯。不久之后,这将成为你的固性思维,是你在编码时能够立即看到潜在的性能问题与待优化的问题。

渐进分析是一种强大的工具,但要理性地使用它。

大O分析忽略常数,但有时候常数的影响很重要。如果我们有一个需要5个小时运行的脚本,那么将运行时间除以5的优化可能不会影响大O,但它仍然会为你节省4个小时的等待时间。

谨慎过早的优化,有时过早优化时间或空间会对代码可读性或编码消耗时间带来负面影响。对于一个年轻的初创公司来说,编写易于快速发布或对以后易于理解的代码可能更重要,即使算法时间和空间效率要被降低。国内互联网产品基本都是这个思路,就是先快速野蛮生长,任何问题等遇到了或者有足够人力时间剩余了再去优化,当然这个优化不只包括编码,还有比如政策(钻政策漏洞的,可能做大了会被相关机构部门注意到)、盈利模式(先烧资本的钱占领市场再去考虑盈利)等。

但这并不意味着初创公司不关心大O分析,一个优秀有经验的程序员一般会知道如何在运行时间、空间、编码实现时间、可维护性和可读性之间取得适当的平衡。

所以,作为程序员,你应该培养观察出时间和空间优化的技能,以及判断这些优化是否值得进行的思维。

推荐:网站交互设计的特点

南宁SEO(优化推广技术)


推荐阅读
  • 全面解析JavaScript代码注释技巧与标准规范
    在Web前端开发中,JavaScript代码的可读性和维护性至关重要。本文将详细介绍如何有效地使用注释来提高代码的可读性,并探讨JavaScript代码注释的最佳实践和标准规范。通过合理的注释,开发者可以更好地理解和维护复杂的代码逻辑,提升团队协作效率。 ... [详细]
  • 在《Cocos2d-x学习笔记:基础概念解析与内存管理机制深入探讨》中,详细介绍了Cocos2d-x的基础概念,并深入分析了其内存管理机制。特别是针对Boost库引入的智能指针管理方法进行了详细的讲解,例如在处理鱼的运动过程中,可以通过编写自定义函数来动态计算角度变化,利用CallFunc回调机制实现高效的游戏逻辑控制。此外,文章还探讨了如何通过智能指针优化资源管理和避免内存泄漏,为开发者提供了实用的编程技巧和最佳实践。 ... [详细]
  • 基于Net Core 3.0与Web API的前后端分离开发:Vue.js在前端的应用
    本文介绍了如何使用Net Core 3.0和Web API进行前后端分离开发,并重点探讨了Vue.js在前端的应用。后端采用MySQL数据库和EF Core框架进行数据操作,开发环境为Windows 10和Visual Studio 2019,MySQL服务器版本为8.0.16。文章详细描述了API项目的创建过程、启动步骤以及必要的插件安装,为开发者提供了一套完整的开发指南。 ... [详细]
  • 当使用 `new` 表达式(即通过 `new` 动态创建对象)时,会发生两件事:首先,内存被分配用于存储新对象;其次,该对象的构造函数被调用以初始化对象。为了确保资源管理的一致性和避免内存泄漏,建议在使用 `new` 和 `delete` 时保持形式一致。例如,如果使用 `new[]` 分配数组,则应使用 `delete[]` 来释放内存;同样,如果使用 `new` 分配单个对象,则应使用 `delete` 来释放内存。这种一致性有助于防止常见的编程错误,提高代码的健壮性和可维护性。 ... [详细]
  • 开发日志:201521044091 《Java编程基础》第11周学习心得与总结
    开发日志:201521044091 《Java编程基础》第11周学习心得与总结 ... [详细]
  • C++ 开发实战:实用技巧与经验分享
    C++ 开发实战:实用技巧与经验分享 ... [详细]
  • 如何利用Java 5 Executor框架高效构建和管理线程池
    Java 5 引入了 Executor 框架,为开发人员提供了一种高效管理和构建线程池的方法。该框架通过将任务提交与任务执行分离,简化了多线程编程的复杂性。利用 Executor 框架,开发人员可以更灵活地控制线程的创建、分配和管理,从而提高服务器端应用的性能和响应能力。此外,该框架还提供了多种线程池实现,如固定线程池、缓存线程池和单线程池,以适应不同的应用场景和需求。 ... [详细]
  • 本文提出了一种基于栈结构的高效四则运算表达式求值方法。该方法能够处理包含加、减、乘、除运算符以及十进制整数和小括号的算术表达式。通过定义和实现栈的基本操作,如入栈、出栈和判空等,算法能够准确地解析并计算输入的表达式,最终输出其计算结果。此方法不仅提高了计算效率,还增强了对复杂表达式的处理能力。 ... [详细]
  • Delphi XE Rtti单元深入解析:TRttiContext的应用与实践
    Delphi XE Rtti单元深入解析:TRttiContext的应用与实践 ... [详细]
  • 阿里巴巴终面技术挑战:如何利用 UDP 实现 TCP 功能?
    在阿里巴巴的技术面试中,技术总监曾提出一道关于如何利用 UDP 实现 TCP 功能的问题。当时回答得不够理想,因此事后进行了详细总结。通过与总监的进一步交流,了解到这是一道常见的阿里面试题。面试官的主要目的是考察应聘者对 UDP 和 TCP 在原理上的差异的理解,以及如何通过 UDP 实现类似 TCP 的可靠传输机制。 ... [详细]
  • 在《数字图像处理及应用(MATLAB)第4章》中,详细探讨了“逢七必过”游戏规则的实现方法,并结合数字图像处理技术进行了深入分析。本章通过丰富的实例和代码示例,展示了如何利用MATLAB实现这一游戏规则,并介绍了数字图像处理的基本原理和技术应用。内容涵盖了图像增强、滤波、边缘检测等多个方面,为读者提供了全面的技术支持和实践指导。 ... [详细]
  • Python 序列图分割与可视化编程入门教程
    本文介绍了如何使用 Python 进行序列图的快速分割与可视化。通过一个实际案例,详细展示了从需求分析到代码实现的全过程。具体包括如何读取序列图数据、应用分割算法以及利用可视化库生成直观的图表,帮助非编程背景的用户也能轻松上手。 ... [详细]
  • 如何撰写适应变化的高效代码:策略与实践
    编写高质量且适应变化的代码是每位程序员的追求。优质代码的关键在于其可维护性和可扩展性。本文将从面向对象编程的角度出发,探讨实现这一目标的具体策略与实践方法,帮助开发者提升代码效率和灵活性。 ... [详细]
  • 在iOS开发中,基于HTTPS协议的安全网络请求实现至关重要。HTTPS(全称:HyperText Transfer Protocol over Secure Socket Layer)是一种旨在提供安全通信的HTTP扩展,通过SSL/TLS加密技术确保数据传输的安全性和隐私性。本文将详细介绍如何在iOS应用中实现安全的HTTPS网络请求,包括证书验证、SSL握手过程以及常见安全问题的解决方法。 ... [详细]
  • NOIP2000的单词接龙问题与常见的成语接龙游戏有异曲同工之妙。题目要求在给定的一组单词中,从指定的起始字母开始,构建最长的“单词链”。每个单词在链中最多可出现两次。本文将详细解析该题目的解法,并分享学习过程中的心得体会。 ... [详细]
author-avatar
浪奔西安
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有