learn.lianglianglee.com/专栏/重学数据结构与算法-完/01 复杂度:如何衡量程序运行的效率?.md.html
2022-05-11 19:04:14 +08:00

689 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>01 复杂度:如何衡量程序运行的效率?.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/重学数据结构与算法-完/00 数据结构与算法,应该这样学!.md.html">00 数据结构与算法,应该这样学!.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/重学数据结构与算法-完/01 复杂度:如何衡量程序运行的效率?.md.html">01 复杂度:如何衡量程序运行的效率?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/02 数据结构:将“昂贵”的时间复杂度转换成“廉价”的空间复杂度.md.html">02 数据结构:将“昂贵”的时间复杂度转换成“廉价”的空间复杂度.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/03 增删查:掌握数据处理的基本操作,以不变应万变.md.html">03 增删查:掌握数据处理的基本操作,以不变应万变.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/04 如何完成线性表结构下的增删查?.md.html">04 如何完成线性表结构下的增删查?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/05 栈:后进先出的线性表,如何实现增删查?.md.html">05 栈:后进先出的线性表,如何实现增删查?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/06 队列:先进先出的线性表,如何实现增删查?.md.html">06 队列:先进先出的线性表,如何实现增删查?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/07 数组:如何实现基于索引的查找?.md.html">07 数组:如何实现基于索引的查找?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/08 字符串:如何正确回答面试中高频考察的字符串匹配算法?.md.html">08 字符串:如何正确回答面试中高频考察的字符串匹配算法?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/09 树和二叉树:分支关系与层次结构下,如何有效实现增删查?.md.html">09 树和二叉树:分支关系与层次结构下,如何有效实现增删查?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/10 哈希表:如何利用好高效率查找的“利器”?.md.html">10 哈希表:如何利用好高效率查找的“利器”?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/11 递归:如何利用递归求解汉诺塔问题?.md.html">11 递归:如何利用递归求解汉诺塔问题?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/12 分治:如何利用分治法完成数据查找?.md.html">12 分治:如何利用分治法完成数据查找?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/13 排序:经典排序算法原理解析与优劣对比.md.html">13 排序:经典排序算法原理解析与优劣对比.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/14 动态规划:如何通过最优子结构,完成复杂问题求解?.md.html">14 动态规划:如何通过最优子结构,完成复杂问题求解?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/15 定位问题才能更好地解决问题:开发前的复杂度分析与技术选型.md.html">15 定位问题才能更好地解决问题:开发前的复杂度分析与技术选型.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/16 真题案例(一):算法思维训练.md.html">16 真题案例(一):算法思维训练.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/17 真题案例(二):数据结构训练.md.html">17 真题案例(二):数据结构训练.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/18 真题案例(三):力扣真题训练.md.html">18 真题案例(三):力扣真题训练.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/19 真题案例(四):大厂真题实战演练.md.html">19 真题案例(四):大厂真题实战演练.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/20 代码之外,技术面试中你应该具备哪些软素质?.md.html">20 代码之外,技术面试中你应该具备哪些软素质?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/21 面试中如何建立全局观,快速完成优质的手写代码?.md.html">21 面试中如何建立全局观,快速完成优质的手写代码?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/加餐 课后练习题详解.md.html">加餐 课后练习题详解.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>01 复杂度:如何衡量程序运行的效率?</h1>
<p>前面我说到了,咱们这个专栏的目标是想教会你利用数据结构的知识,建立算法思维,并完成代码效率的优化。为了达到这个目标,在第一节课,我们先来讲一讲<strong>如何衡量程序运行的效率</strong></p>
<p>当你在大数据环境中开发代码时,你一定遇到过程序执行好几个小时、甚至好几天的情况,或者是执行过程中电脑几乎死机的情况:</p>
<ul>
<li>如果这个效率低下的系统是<strong>离线</strong>的,那么它会让我们的开发周期、测试周期变得很长。</li>
<li>如果这个效率低下的系统是<strong>在线</strong>的,那么它随时具有时间爆炸或者内存爆炸的可能性。</li>
</ul>
<p>因此,衡量代码的运行效率对于一个工程师而言,是一项非常重要的基本功。本课时我们就来学习程序运行效率相关的度量方法。</p>
<h4>复杂度是什么</h4>
<p><strong>复杂度是衡量代码运行效率的重要度量因素</strong>。在介绍复杂度之前,有必要先看一下复杂度和计算机实际任务处理效率的关系,从而了解降低复杂度的必要性。</p>
<p>计算机通过一个个程序去执行计算任务,也就是对输入数据进行加工处理,并最终得到结果的过程。每个程序都是由代码构成的。可见,编写代码的核心就是要完成计算。但对于同一个计算任务,不同计算方法得到结果的过程复杂程度是不一样的,这对你实际的任务处理效率就有了非常大的影响。</p>
<p>举个例子,你要在一个在线系统中实时处理数据。假设这个系统平均每分钟会新增 300M 的数据量。如果你的代码不能在 1 分钟内完成对这 300M 数据的处理,那么这个系统就会发生时间爆炸和空间爆炸。表现就是,电脑执行越来越慢,直到死机。因此,我们需要讲究合理的计算方法,去通过尽可能低复杂程度的代码完成计算任务。</p>
<p><img src="assets/CgqCHl7CRGiAe-NpAR0S70dSC2M990.gif" alt="1.gif" /></p>
<p>那提到降低复杂度我们首先需要知道怎么衡量复杂度。而在实际衡量时我们通常会围绕以下2 个维度进行。<strong>首先,这段代码消耗的资源是什么</strong>。一般而言,代码执行过程中会消耗计算时间和计算空间,那需要衡量的就是时间复杂度和空间复杂度。</p>
<p>我举一个实际生活中的例子。某个十字路口没有建立立交桥时,所有车辆通过红绿灯分批次行驶通过。当大量汽车同时过路口的时候,就会分别消耗大家的时间。但建了立交桥之后,所有车辆都可以同时通过了,因为立交桥的存在,等于是消耗了空间资源,来换取了时间资源。</p>
<p><img src="assets/CgqCHl7CRMaAO_oEAJfz6fjfMNQ403.gif" alt="2.gif" /></p>
<p><strong>其次,这段代码对于资源的消耗是多少</strong>。我们不会关注这段代码对于资源消耗的绝对量,因为不管是时间还是空间,它们的消耗程度都与输入的数据量高度相关,输入数据少时消耗自然就少。为了更客观地衡量消耗程度,我们通常会关注时间或者空间消耗量与输入数据量之间的关系。</p>
<p>好,现在我们已经了解了衡量复杂度的两个纬度,那应该如何去计算复杂度呢?</p>
<p><strong>复杂度是一个关于输入数据量 n 的函数</strong>。假设你的代码复杂度是 f(n),那么就用个大写字母 O 和括号,把 f(n) 括起来就可以了,即 O(f(n))。例如O(n) 表示的是,复杂度与计算实例的个数 n 线性相关O(logn) 表示的是,复杂度与计算实例的个数 n 对数相关。</p>
<p>通常,复杂度的计算方法遵循以下几个原则:</p>
<ul>
<li>首先,<strong>复杂度与具体的常系数无关</strong>,例如 O(n) 和 O(2n) 表示的是同样的复杂度。我们详细分析下O(2n) 等于 O(n+n),也等于 O(n) + O(n)。也就是说,一段 O(n) 复杂度的代码只是先后执行两遍 O(n),其复杂度是一致的。</li>
<li>其次,<strong>多项式级的复杂度相加的时候,选择高者作为结果</strong>,例如 O(n²)+O(n) 和 O(n²) 表示的是同样的复杂度。具体分析一下就是O(n²)+O(n) = O(n²+n)。随着 n 越来越大,二阶多项式的变化率是要比一阶多项式更大的。因此,只需要通过更大变化率的二阶多项式来表征复杂度就可以了。</li>
</ul>
<p>值得一提的是,<strong>O(1) 也是表示一个特殊复杂度</strong>,含义为某个任务通过有限可数的资源即可完成。此处有限可数的具体意义是,<strong>与输入数据量 n 无关</strong></p>
<p>例如,你的代码处理 10 条数据需要消耗 5 个单位的时间资源3 个单位的空间资源。处理 1000 条数据,还是只需要消耗 5 个单位的时间资源3 个单位的空间资源。那么就能发现资源消耗与输入数据量无关,就是 O(1) 的复杂度。</p>
<p>为了方便你理解不同计算方法对复杂度的影响,我们来看一个代码任务:对于输入的数组,输出与之逆序的数组。例如,输入 a=[1,2,3,4,5],输出 [5,4,3,2,1]。</p>
<p>先看<strong>方法一</strong>,建立并初始化数组 b得到一个与输入数组等长的全零数组。通过一个 for 循环,从左到右将 a 数组的元素,从右到左地赋值到 b 数组中,最后输出数组 b 得到结果。</p>
<p><img src="assets/Ciqc1F7CRP6ARwDTAGHL-opG6Bk835.gif" alt="3.gif" /></p>
<p>代码如下:</p>
<pre><code>public void s1_1() {
int a[] = { 1, 2, 3, 4, 5 };
int b[] = new int[5];
for (int i = 0; i &lt; a.length; i++) {
b[i] = a[i];
}
for (int i = 0; i &lt; a.length; i++) {
b[a.length - i - 1] = a[i];
}
System.out.println(Arrays.toString(b));
}
</code></pre>
<p>这段代码的输入数据是 a数据量就等于数组 a 的长度。代码中有两个 for 循环作用分别是给b 数组初始化和赋值,其执行次数都与输入数据量相等。因此,代码的<strong>时间复杂度</strong>就是 O(n)+O(n),也就是 O(n)。</p>
<p>空间方面主要体现在计算过程中,对于存储资源的消耗情况。上面这段代码中,我们定义了一个新的数组 b它与输入数组 a 的长度相等。因此,空间复杂度就是 O(n)。</p>
<p><strong>接着我们看一下第二种编码方法</strong>,它定义了缓存变量 tmp接着通过一个 for 循环,从 0 遍历到a 数组长度的一半(即 len(a)/2。每次遍历执行的是什么内容就是交换首尾对应的元素。最后打印数组 a得到结果。</p>
<p><img src="assets/Ciqc1F7CR22AIbSuABc0Rwl-t3w666.gif" alt="4.gif" /></p>
<p>代码如下:</p>
<pre><code>public void s1_2() {
int a[] = { 1, 2, 3, 4, 5 };
int tmp = 0;
for (int i = 0; i &lt; (a.length / 2); i++) {
tmp = a[i];
a[i] = a[a.length - i - 1];
a[a.length - i - 1] = tmp;
}
System.out.println(Arrays.toString(a));
}
</code></pre>
<p>这段代码包含了一个 for 循环,执行的次数是数组长度的一半,时间复杂度变成了 O(n/2)。根据复杂度与具体的常系数无关的性质,这段代码的时间复杂度也就是 O(n)。</p>
<p>空间方面,我们定义了一个 tmp 变量,它与数组长度无关。也就是说,输入是 5 个元素的数组,需要一个 tmp 变量;输入是 50 个元素的数组,依然只需要一个 tmp 变量。因此,空间复杂度与输入数组长度无关,即 O(1)。</p>
<p>可见,<strong>对于同一个问题,采用不同的编码方法,对时间和空间的消耗是有可能不一样的</strong>。因此,工程师在写代码的时候,一方面要完成任务目标;另一方面,也需要考虑时间复杂度和空间复杂度,以求用尽可能少的时间损耗和尽可能少的空间损耗去完成任务。</p>
<h4>时间复杂度与代码结构的关系</h4>
<p>好了,通过前面的内容,相信你已经对时间复杂度和空间复杂度有了很好的理解。从本质来看,时间复杂度与代码的结构有着非常紧密的关系;而空间复杂度与数据结构的设计有关,关于这一点我们会在下一讲进行详细阐述。接下来我先来系统地讲一下时间复杂度和代码结构的关系。</p>
<p>代码的<strong>时间复杂度,与代码的结构有非常强的关系</strong>,我们一起来看一些具体的例子。</p>
<p>例 1定义了一个数组 a = [1, 4, 3],查找数组 a 中的最大值,代码如下:</p>
<pre><code>public void s1_3() {
int a[] = { 1, 4, 3 };
int max_val = -1;
for (int i = 0; i &lt; a.length; i++) {
if (a[i] &gt; max_val) {
max_val = a[i];
}
}
System.out.println(max_val);
}
</code></pre>
<p>这个例子比较简单,实现方法就是,暂存当前最大值并把所有元素遍历一遍即可。因为代码的结构上需要使用一个 for 循环,对数组所有元素处理一遍,所以时间复杂度为 O(n)。</p>
<p>例2下面的代码定义了一个数组 a = [1, 3, 4, 3, 4, 1, 3],并会在这个数组中查找出现次数最多的那个数字:</p>
<pre><code>public void s1_4() {
int a[] = { 1, 3, 4, 3, 4, 1, 3 };
int val_max = -1;
int time_max = 0;
int time_tmp = 0;
for (int i = 0; i &lt; a.length; i++) {
time_tmp = 0;
for (int j = 0; j &lt; a.length; j++) {
if (a[i] == a[j]) {
time_tmp += 1;
}
if (time_tmp &gt; time_max) {
time_max = time_tmp;
val_max = a[i];
}
}
}
System.out.println(val_max);
}
</code></pre>
<p>这段代码中,我们采用了双层循环的方式计算:第一层循环,我们对数组中的每个元素进行遍历;第二层循环,对于每个元素计算出现的次数,并且通过当前元素次数 time_tmp 和全局最大次数变量 time_max 的大小关系,持续保存出现次数最多的那个元素及其出现次数。由于是双层循环,这段代码在时间方面的消耗就是 n*n 的复杂度,也就是 O(n²)。</p>
<p>在这里,我们给出一些经验性的结论:</p>
<ul>
<li>一个顺序结构的代码,时间复杂度是 O(1)。</li>
<li>二分查找,或者更通用地说是采用分而治之的二分策略,时间复杂度都是 O(logn)。这个我们会在后续课程讲到。</li>
<li>一个简单的 for 循环,时间复杂度是 O(n)。</li>
<li>两个顺序执行的 for 循环,时间复杂度是 O(n)+O(n)=O(2n),其实也是 O(n)。</li>
<li>两个嵌套的 for 循环,时间复杂度是 O(n²)。</li>
</ul>
<p>有了这些基本的结论,再去分析代码的时间复杂度将会轻而易举。</p>
<h4>降低时间复杂度的必要性</h4>
<p>很多新手的工程师,对降低时间复杂度并没有那么强的意识。这主要是在学校或者实验室中,参加的课程作业或者科研项目,普遍都不是实时的、在线的工程环境。</p>
<p>实际的在线环境中,用户的访问请求可以看作一个流式数据。假设这个数据流中,每个访问的平均时间间隔是 t。如果你的代码无法在 t 时间内处理完单次的访问请求,那么这个系统就会一波未平一波又起,最终被大量积压的任务给压垮。这就要求工程师必须通过优化代码、优化数据结构,来降低时间复杂度。</p>
<p>为了更好理解,我们来看一些数据。假设某个计算任务需要处理 10 万 条数据。你编写的代码:</p>
<ul>
<li>如果是 O(n²) 的时间复杂度,那么计算的次数就大概是 100 亿次左右。</li>
<li>如果是 O(n),那么计算的次数就是 10 万 次左右。</li>
<li>如果这个工程师再厉害一些,能在 O(log n) 的复杂度下完成任务,那么计算的次数就是 17 次左右log 100000 = 16.61,计算机通常是二分法,这里的对数可以以 2 为底去估计)。</li>
</ul>
<p>数字是不是一下子变得很悬殊?通常在小数据集上,时间复杂度的降低在绝对处理时间上没有太多体现。但在当今的大数据环境下,时间复杂度的优化将会带来巨大的系统收益。而这是优秀工程师必须具备的工程开发基本意识。</p>
<h4>总结</h4>
<p>OK今天的内容到这儿就结束了。相信你对复杂度的概念有了进一步的认识。</p>
<p>复杂度通常包括时间复杂度和空间复杂度。在具体计算复杂度时需要注意以下几点。</p>
<ol>
<li><strong>它与具体的常系数无关</strong>O(n) 和 O(2n) 表示的是同样的复杂度。</li>
<li><strong>复杂度相加的时候,选择高者作为结果</strong>,也就是说 O(n²)+O(n) 和 O(n²) 表示的是同样的复杂度。</li>
<li><strong>O(1) 也是表示一个特殊复杂度</strong>,即任务与算例个数 n 无关。</li>
</ol>
<p>复杂度细分为时间复杂度和空间复杂度,其中时间复杂度与<strong>代码的结构设计</strong>高度相关;空间复杂度与代码中<strong>数据结构的选择</strong>高度相关。会计算一段代码的时间复杂度和空间复杂度,是工程师的基本功。这项技能你在实际工作中一定会用到,甚至在参加互联网公司面试的时候,也是面试中的必考内容。</p>
<h3>练习题</h3>
<p>下面的练习题,请你独立思考。评估一下,如下的代码片段,时间复杂度是多少?</p>
<pre><code>for (i = 0; i &lt; n; i++) {
for (j = 0; j &lt; n; j++) {
for (k = 0; k &lt; n; k++) {
}
for (m = 0; m &lt; n; m++) {
}
}
}
</code></pre>
<p>关于复杂度的评估,需要你深入理解本节课的知识点。最后,你工作中有遇到过关于计算复杂度的哪些实际问题吗?你又是如何解决的?欢迎你在留言区和我分享。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/重学数据结构与算法-完/00 数据结构与算法,应该这样学!.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/重学数据结构与算法-完/02 数据结构:将“昂贵”的时间复杂度转换成“廉价”的空间复杂度.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997dbd9a5a3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>