mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 08:46:40 +08:00
1099 lines
26 KiB
HTML
1099 lines
26 KiB
HTML
<!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>13 复杂度:如何利用数学推导对程序进行优化?.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 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 似然估计:如何利用 MLE 对参数进行估计?.md.html">09 似然估计:如何利用 MLE 对参数进行估计?.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 class="current-tab" 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 AI 入门:利用 3 个公式搭建最简 AI 框架.md.html">18 AI 入门:利用 3 个公式搭建最简 AI 框架.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/程序员的数学课/19 逻辑回归:如何让计算机做出二值化决策?.md.html">19 逻辑回归:如何让计算机做出二值化决策?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/程序员的数学课/20 决策树:如何对 NP 难复杂问题进行启发式求解?.md.html">20 决策树:如何对 NP 难复杂问题进行启发式求解?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/程序员的数学课/21 神经网络与深度学习:计算机是如何理解图像、文本和语音的?.md.html">21 神经网络与深度学习:计算机是如何理解图像、文本和语音的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/程序员的数学课/22 面试中那些坑了无数人的算法题.md.html">22 面试中那些坑了无数人的算法题.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/程序员的数学课/23 站在生活的十字路口,如何用数学抉择?.md.html">23 站在生活的十字路口,如何用数学抉择?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/程序员的数学课/24 结束语 数学底子好,学啥都快.md.html">24 结束语 数学底子好,学啥都快.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>13 复杂度:如何利用数学推导对程序进行优化?</h1>
|
||
|
||
<p>这一讲开始,我们进入到这个专栏“模块三 数据结构与算法”的学习,在这个模块,我们会重点学习数学与算法、代码之间的关系。</p>
|
||
|
||
<p>在一个程序开发的过程中,常常需要我们去关注程序的复杂度。这一讲,我们就先从复杂度出发,来看看数学的思想是如何应用在程序复杂度优化的。</p>
|
||
|
||
<h3>程序的时间损耗</h3>
|
||
|
||
<p><strong>程序</strong>就是计算机执行运算动作的指令,<strong>运算</strong>就是对数据进行的处理。</p>
|
||
|
||
<p>例如,1+2 这样的加法运算,就是对两个数据 1 和 2 执行加法的处理。同样地,加法运算还可以针对更多的数据,比如 1+2+3+...+50,这就是对 1~50 这 50 个数据,执行加法运算的处理。</p>
|
||
|
||
<p>当我们用计算机指令,也就是程序,执行 1+2 这样的运算时,可能在毫秒,甚至更短的时间内就能得到结果。然而,当数据量变大时,执行的时间就会越来越长。</p>
|
||
|
||
<p>我们看一个例子,下面一段代码的任务,是给定一个正整数 n,计算从 1~n 之间所有整数之和。</p>
|
||
|
||
<pre><code>import time
|
||
|
||
|
||
|
||
import sys
|
||
|
||
|
||
|
||
t1 = int(time.time()*1000000)
|
||
|
||
|
||
|
||
n = int(sys.argv[1])
|
||
|
||
|
||
|
||
result = 0
|
||
|
||
|
||
|
||
for i in range(n):
|
||
|
||
|
||
|
||
result += i
|
||
|
||
|
||
|
||
t2 = int(time.time()*1000000)
|
||
|
||
|
||
|
||
print t2 - t1
|
||
|
||
</code></pre>
|
||
|
||
<p>我们对代码进行走读:</p>
|
||
|
||
<ul>
|
||
|
||
<li>第 4 行,记录了程序开始执行的毫秒级时间戳;</li>
|
||
|
||
<li>第 5 行,得到输入参数 n;</li>
|
||
|
||
<li>第 7~8 行,执行 1 加到 n 的循环求和;</li>
|
||
|
||
<li>第 9 行,记录了程序结束计算的毫秒级时间戳;</li>
|
||
|
||
<li>最后,第 10 行打印出程序执行的时间损耗。</li>
|
||
|
||
</ul>
|
||
|
||
<p>当输入分别是 100、1000 和 10000 时,程序的执行结果如下图所示:</p>
|
||
|
||
<p><img src="assets/Ciqc1F_KBzyAeUPcAAMh0u-3bgI704.png" alt="Lark20201204-175328.png" /></p>
|
||
|
||
<p>由图可见,数据量越大,程序的时间损耗也就越大。</p>
|
||
|
||
<h3>程序的复杂度</h3>
|
||
|
||
<p>开发者在编写代码时,除了实际的时间损耗外,还有个重要概念就是<strong>复杂度</strong>。复杂度是衡量程序效率的重要指标,也是工程师的必备技能。</p>
|
||
|
||
<blockquote>
|
||
|
||
<p>在实际工作中,通常会重点关注时间方面的复杂度,也叫时间复杂度。这一讲,我们为了简便行文,就把时间复杂度简称为复杂度。</p>
|
||
|
||
</blockquote>
|
||
|
||
<p>从本质上来看,<strong>复杂度描述的是程序时间损耗和数据总量之间的变化关系</strong>。</p>
|
||
|
||
<p>【例 1】我们先举一个例子说明,看下面这段代码:</p>
|
||
|
||
<pre><code>a = [1,2,2,3,4,5]
|
||
|
||
|
||
|
||
result = 0
|
||
|
||
|
||
|
||
for i in range(len(a)):
|
||
|
||
|
||
|
||
result += a[i]
|
||
|
||
|
||
|
||
print result
|
||
|
||
</code></pre>
|
||
|
||
<p>这段代码执行的内容是采用了一个 for 循环,来求 a 数组所有元素之和。</p>
|
||
|
||
<p>根据代码执行的顺序可知,第 1~2 行分别执行 1 次后,进入了第 3~4 行的 for 循环;这个 for 循环需要被反复执行 len(a) 次,也就是 6 次;最后,再执行 1 次第 5 行的代码。</p>
|
||
|
||
<p>可以估算出,程序执行的时间损耗为 t(总时间) = t(第1,2,5行) + 6t(第3,4行),更泛化的写法是 t=c+n×b。</p>
|
||
|
||
<blockquote>
|
||
|
||
<p>其中 t 代表代码执行损耗的时间,c 和 b 分别是两个常数,而 n 是决定循环次数的数据量的大小。可见,随着 n 的变大,t 以线性的关系变大。</p>
|
||
|
||
</blockquote>
|
||
|
||
<p>【例 2】我们再看一个例子,代码如下:</p>
|
||
|
||
<pre><code>a = [1,2,2,3,4,5]
|
||
|
||
|
||
|
||
result = 0
|
||
|
||
|
||
|
||
result = a[0] + a[-1]
|
||
|
||
|
||
|
||
print result
|
||
|
||
</code></pre>
|
||
|
||
<p>这段代码计算的是数组 a 第一个元素与最后一个元素之和。</p>
|
||
|
||
<p>具体来看,第 1 行定义数组 a,第 2 行定义变量 result;第 3 行,直接取出数组的第一个元素和最后一个元素,并且求和;最后,第 4 行打印结果。</p>
|
||
|
||
<p>可以估算出,程序执行的时间损耗为 t(总时间) = t(第1,2,3,4行),更泛化的写法是 t = c。</p>
|
||
|
||
<blockquote>
|
||
|
||
<p>其中 t 代表代码执行的时间损耗,c 是个与数组 a 大小无关的常数。可见,无论数组 a 的长度很大还是很小,执行的时间损耗都不会受到影响。</p>
|
||
|
||
</blockquote>
|
||
|
||
<p>从上面的两个例子,我们就能对复杂度有更深入的理解了。</p>
|
||
|
||
<h4>【深入理解复杂度】</h4>
|
||
|
||
<p>复杂度是程序时间损耗和数据总量之间的变化关系,通常用 O(f(n)) 来表示,其中 f(n) 就是复杂度函数。</p>
|
||
|
||
<p>如果程序的时间损耗和数据量的关系是 t=c+n×b,也就是说复杂度函数为 f(n)=c+n×b。复杂度通常不关注常数,因为它是个固定的时间损耗,与输入的数据总量没有任何的关系。因此,复杂度函数 c+n×b 可以忽略常数 c 和 b,直接缩写为 f(n) = n,即第一个例子的复杂度为 O(n)。</p>
|
||
|
||
<p>如果程序的时间损耗和数据量没有关系,即 t=c,我们依然会忽略这个常数,直接用 O(1) 来表示。</p>
|
||
|
||
<h3>复杂度的性质和代码结构</h3>
|
||
|
||
<p>有时候,复杂度函数会非常复杂,例如下面的代码:</p>
|
||
|
||
<pre><code>a = [1,2,2,3,4,5]
|
||
|
||
|
||
|
||
index_max = 0
|
||
|
||
|
||
|
||
times_max = -1
|
||
|
||
|
||
|
||
for i in range(len(a)):
|
||
|
||
|
||
|
||
times_temp = 0
|
||
|
||
|
||
|
||
for j in range(len(a)):
|
||
|
||
|
||
|
||
if a[i] == a[j]:
|
||
|
||
|
||
|
||
times_temp += 1
|
||
|
||
|
||
|
||
if times_temp > times_max:
|
||
|
||
|
||
|
||
times_max = times_temp
|
||
|
||
|
||
|
||
index_max = i
|
||
|
||
|
||
|
||
result = a[index_max]
|
||
|
||
|
||
|
||
for k in range(len(a)):
|
||
|
||
|
||
|
||
result += a[k]
|
||
|
||
|
||
|
||
print result
|
||
|
||
</code></pre>
|
||
|
||
<p>这段代码的任务是寻找出数组 a 中出现次数最多的元素 a[index_max],再计算出 a[index_max] 与数组 a 中所有元素的求和。</p>
|
||
|
||
<p>我们对代码进行走读。</p>
|
||
|
||
<ul>
|
||
|
||
<li>第 4~11 行,有两层 for 循环。我们具体算一下时间损耗,t(4~11行) = 6×[t(第4,5行)+t(6~8行)+t(9~11行)]。</li>
|
||
|
||
<li>而程序的第 6~8 行,又是一个 for 循环,则有 t(6~8行) = 6×t(第6,7,8行)</li>
|
||
|
||
<li>因此,整体的时间损耗为 t(4~11 行)= 6×[t(第4,5行) + 6×t(第6,7,8行)+ t(9~11行)] = n×n×b + n×c + n×d。</li>
|
||
|
||
</ul>
|
||
|
||
<blockquote>
|
||
|
||
<p>其中,n 为数组 a 的长度,即数据量;b、c、d 分别是第 6、7、8 行执行的时间,第 4、5 行执行的时间,以及第 9~11 行执行的时间,并且它们与输入的数据量无关,可以视作常数。</p>
|
||
|
||
<p>利用忽略常数的原则,则有 t = n2 + n + n = n2 + 2n;还可以继续忽略常数“2”,则有
|
||
|
||
t =n2+ n;根据数学中的平方公式,还有 t =n2 + n = (n + 1/2)2 - 1/4。此时,仍然可以把与 n 无关的系数“1/2”和“1/4”忽略掉,则有 t = n2。因此,程序的第 4~11 行是 O(n2) 的时间复杂度。</p>
|
||
|
||
</blockquote>
|
||
|
||
<ul>
|
||
|
||
<li>而第 14~15 行,根据前面所学是 O(n) 的时间复杂度。所以,整个代码的时间复杂度就是 O(n2+n)。仍然可以继续使用刚刚平方公式的化简方法,得到最终的时间复杂度是 O(n2)。</li>
|
||
|
||
</ul>
|
||
|
||
<p>从这个例子,我们可以发现,<strong>多项式级的复杂度相加时,可以选择高者作为结果。</strong> 例如,O(n2+n) 的时间复杂度,可以直接写为 O(n2)。</p>
|
||
|
||
<p>复杂度的性质都来自数学的推导,与此同时,复杂度的计算还与程序的结构有着密切关系。通常而言,一个<strong>顺序结构</strong>或<strong>选择结构</strong>的代码的执行时间与数据量无关,复杂度就是 O(1);而对于<strong>循环结构</strong>而言,如果循环的次数与输入数据量的多少有关,就会产生复杂度了。</p>
|
||
|
||
<blockquote>
|
||
|
||
<p>程序的三大基本结构是顺序结构、选择结构和循环结构,如果忘了,可以复习一下 C 语言。</p>
|
||
|
||
</blockquote>
|
||
|
||
<p>通常,一层循环的时间复杂度是 O(n);如果是两个循环的嵌套,时间复杂度是 O(n2);如果是三个循环的嵌套,则是 O(n3);依次类推。</p>
|
||
|
||
<h3>利用数学来优化时间复杂度</h3>
|
||
|
||
<p>设想一下,如果一段线上代码在输入变量很多的时候就会“卡死”,那么这一定是一款无法上线的系统。因此,时间复杂度的优化,是每个开发者必须具备的技能。</p>
|
||
|
||
<p>其实,时间复杂度的优化有很多办法。除了优化数据结构、优化代码结构、减少程序中不必要的计算等通用方法以外,还可以利用强大的数学知识来进行时间复杂度的优化。</p>
|
||
|
||
<p>我们来举几个例子。</p>
|
||
|
||
<p>我们在开篇词中讲了一个异或的案例。在一个无序的数组中,只有一个数字 obj 出现了一次,其他数字都出现了两次,尝试去查找出这个出现了一次的 obj。绝大多数程序员的代码逻辑,应该都是设计两层 for 循环:一层遍历每个数字,一层计算每个数字出现的次数,直到找到 obj。</p>
|
||
|
||
<p>代码如下:</p>
|
||
|
||
<pre><code>a = [2,1,4,3,4,2,3]
|
||
|
||
|
||
|
||
for i in range(0,len(a)):
|
||
|
||
|
||
|
||
times = 0
|
||
|
||
|
||
|
||
for j in range(0,len(a)):
|
||
|
||
|
||
|
||
if a[i] == a[j]:
|
||
|
||
|
||
|
||
times += 1
|
||
|
||
|
||
|
||
if times == 1:
|
||
|
||
|
||
|
||
print a[i]
|
||
|
||
|
||
|
||
break
|
||
|
||
</code></pre>
|
||
|
||
<p>我们对代码进行走读:</p>
|
||
|
||
<ul>
|
||
|
||
<li>第 2 行,开始 for 循环,并把计数的变量 times 置为 0;</li>
|
||
|
||
<li>第 4 行,嵌套了一个 for 循环;</li>
|
||
|
||
<li>第 5 行开始,判断里外两层循环的值是否相等。如果相等,则 times 加 1;</li>
|
||
|
||
<li>第 7 行,判断 times 是否为 1,如果为 1 说明 a[i] 在数组中只出现了一次,则打印并 break 跳出循环结束。</li>
|
||
|
||
</ul>
|
||
|
||
<p>根据我们前面的结论,这段代码的复杂度是 O(n2),而且单独借助数据结构等思想已经很难再进行程序的优化了。</p>
|
||
|
||
<p>然而,如果从数学视角来看,这段代码就可以进行如下优化:</p>
|
||
|
||
<pre><code>a = [2,1,4,3,4,2,3]
|
||
|
||
|
||
|
||
result = a[0]
|
||
|
||
|
||
|
||
for i in range(1,len(a)):
|
||
|
||
|
||
|
||
result = result ^ a[i]
|
||
|
||
|
||
|
||
print result
|
||
|
||
</code></pre>
|
||
|
||
<p>在这里,利用了异或运算的性质:</p>
|
||
|
||
<ul>
|
||
|
||
<li>第一,满足交换律和结合律;</li>
|
||
|
||
<li>第二,可以把相同元素计算为 0;</li>
|
||
|
||
<li>第三,0 异或任何数字都是其本身。</li>
|
||
|
||
</ul>
|
||
|
||
<p>这样,只要把数组 a 中所有元素都异或在一起,就得到了 obj。此时,只需要一层 for 循环,复杂度是 O(n)。</p>
|
||
|
||
<p>我们再看下面一个例子。输入一个正整数 n,求不大于 n 的所有偶数之和。例如输入 6,则输出 2、4、6 之和,为 12;输入5,则输出 2、4 之和,为 6。</p>
|
||
|
||
<p>这个题目的常规解法,是采用 for 循环,让 i 从 1 遍历到 n。如果 i 为奇数,则 continue;如果为偶数,则加到 result 变量中。不难发现,复杂度是 O(n),代码如下:</p>
|
||
|
||
<pre><code>import sys
|
||
|
||
|
||
|
||
n = int(sys.argv[1])
|
||
|
||
|
||
|
||
result = 0
|
||
|
||
|
||
|
||
for i in range(n+1):
|
||
|
||
|
||
|
||
if i % 2 == 0:
|
||
|
||
|
||
|
||
result += i
|
||
|
||
|
||
|
||
print result
|
||
|
||
</code></pre>
|
||
|
||
<p>我们再从数学的视角来看待这个问题,你就会发现这是个等差数列求和的问题,等差数列求和的公式为
|
||
|
||
<img src="assets/Ciqc1F_KB4qACGq3AAAh0smYQ_M453.png" alt="Lark20201204-175336.png" /></p>
|
||
|
||
<blockquote>
|
||
|
||
<p>其中 a1 为首项,n 为项数,d 为公差,前 n 项和为 Sn。</p>
|
||
|
||
</blockquote>
|
||
|
||
<p>利用这个公式,我们可以直接写出下面的代码:</p>
|
||
|
||
<pre><code>import sys
|
||
|
||
|
||
|
||
n = int(sys.argv[1])
|
||
|
||
|
||
|
||
a1 = 0
|
||
|
||
|
||
|
||
d = 2
|
||
|
||
|
||
|
||
nn = n/2 + 1
|
||
|
||
|
||
|
||
print nn * a1 + 2 * nn * (nn - 1) / d
|
||
|
||
</code></pre>
|
||
|
||
<p>我们对代码进行走读。</p>
|
||
|
||
<ul>
|
||
|
||
<li>第 2 行,获得输入变量 n。</li>
|
||
|
||
<li>第 3 行,求和的第一项,直接赋值为 0。</li>
|
||
|
||
<li>第 4 行,公差 d 为 2。</li>
|
||
|
||
<li>第 5 行,求项数。例如,输入 6,则项数为 0、2、4、6,6/3+1 = 4 项;输入 5,则项数为 0、2、4,5/2+1 = 3 项。</li>
|
||
|
||
<li>最后第 6 行,调用等差数列求和公式,直接得到结果,运行截图如下:</li>
|
||
|
||
</ul>
|
||
|
||
<p><img src="assets/Ciqc1F_KB3mAMqH8AAIw0hMvzoo693.png" alt="Lark20201204-175339.png" /></p>
|
||
|
||
<p>这段代码的执行与输入数据量 n 毫无关系,因此复杂度是 O(1)。</p>
|
||
|
||
<p>同样的道理,等比数列求和的代码,如果用计算机程序开发的思想,是需要一个 for 循环在 O(n) 复杂度下完成计算的。但借助等比数列求和公式,你只需要 O(1) 的复杂度就能得到结果。在这里,我们作为课后习题不再赘述。</p>
|
||
|
||
<h3>小结</h3>
|
||
|
||
<p>复杂度是程序开发中老生常谈的话题了。时间复杂度衡量的是程序执行时间与数据量之间的关系。在计算复杂度的时候,通常常数是可以被忽略掉的。如果是多项式的求和,通常只保留最高次幂一项,其他都可以省略。</p>
|
||
|
||
<p>复杂度与代码结构息息相关。for 循环嵌套的越多,复杂度就会越高。如果你的数学知识非常渊博,从数学的角度来降低代码复杂度也是一个不错的选择。</p>
|
||
|
||
<p>最后,我们留一个练习题:输入一个正整数 n,求不大于 n 的所有 2 的正整数次幂的数字之和。例如,输入 17,则输出 1+2+4+8+16 = 31;输入 8,则输出 1+2+4+8 = 15。你可以尝试两种方法来开发,分别是 O(n) 复杂度的 for 循环,和 O(1) 复杂度的等比数列求和公式。</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/程序员的数学课/12 统计学方法:如何证明灰度实验效果不是偶然得到的?.md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/程序员的数学课/14 程序的循环:如何利用数学归纳法进行程序开发?.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":"70997bda088a3cfa","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>
|
||
|