learn.lianglianglee.com/专栏/程序员的数学课/22 面试中那些坑了无数人的算法题.md.html
2022-09-06 22:30:37 +08:00

370 lines
24 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>22 面试中那些坑了无数人的算法题.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 开篇词 数学,编程能力的营养根基</a>
</li>
<li>
<a href="/专栏/程序员的数学课/01 从计数开始,程序员必知必会的数制转换法.md.html">01 从计数开始,程序员必知必会的数制转换法</a>
</li>
<li>
<a href="/专栏/程序员的数学课/02 逻辑与沟通,怎样才能讲出有逻辑的话?.md.html">02 逻辑与沟通,怎样才能讲出有逻辑的话?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/03 用数学决策,如何规划好投入、转化和产出?.md.html">03 用数学决策,如何规划好投入、转化和产出?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/04 万物可数学,经典公式是如何在生活中应用的?.md.html">04 万物可数学,经典公式是如何在生活中应用的?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/05 求极值:如何找到复杂业务的最优解?.md.html">05 求极值:如何找到复杂业务的最优解?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/06 向量及其导数:计算机如何完成对海量高维度数据计算?.md.html">06 向量及其导数:计算机如何完成对海量高维度数据计算?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/07 线性回归:如何在离散点中寻找数据规律?.md.html">07 线性回归:如何在离散点中寻找数据规律?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/08 加乘法则:如何计算复杂事件发生的概率?.md.html">08 加乘法则:如何计算复杂事件发生的概率?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/09 似然估计:如何利用 MLE 对参数进行估计?.md.html">09 似然估计:如何利用 MLE 对参数进行估计?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/10 信息熵:事件的不确定性如何计算?.md.html">10 信息熵:事件的不确定性如何计算?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/11 灰度实验:如何设计灰度实验并计算实验的收益?.md.html">11 灰度实验:如何设计灰度实验并计算实验的收益?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/12 统计学方法:如何证明灰度实验效果不是偶然得到的?.md.html">12 统计学方法:如何证明灰度实验效果不是偶然得到的?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/13 复杂度:如何利用数学推导对程序进行优化?.md.html">13 复杂度:如何利用数学推导对程序进行优化?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/14 程序的循环:如何利用数学归纳法进行程序开发?.md.html">14 程序的循环:如何利用数学归纳法进行程序开发?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/15 递归:如何计算汉诺塔问题的移动步数?.md.html">15 递归:如何计算汉诺塔问题的移动步数?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/16 二分法:如何利用指数爆炸优化程序?.md.html">16 二分法:如何利用指数爆炸优化程序?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/17 动态规划:如何利用最优子结构解决问题?.md.html">17 动态规划:如何利用最优子结构解决问题?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/18 AI 入门:利用 3 个公式搭建最简 AI 框架.md.html">18 AI 入门:利用 3 个公式搭建最简 AI 框架</a>
</li>
<li>
<a href="/专栏/程序员的数学课/19 逻辑回归:如何让计算机做出二值化决策?.md.html">19 逻辑回归:如何让计算机做出二值化决策?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/20 决策树:如何对 NP 难复杂问题进行启发式求解?.md.html">20 决策树:如何对 NP 难复杂问题进行启发式求解?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/21 神经网络与深度学习:计算机是如何理解图像、文本和语音的?.md.html">21 神经网络与深度学习:计算机是如何理解图像、文本和语音的?</a>
</li>
<li>
<a class="current-tab" href="/专栏/程序员的数学课/22 面试中那些坑了无数人的算法题.md.html">22 面试中那些坑了无数人的算法题</a>
</li>
<li>
<a href="/专栏/程序员的数学课/23 站在生活的十字路口,如何用数学抉择?.md.html">23 站在生活的十字路口,如何用数学抉择?</a>
</li>
<li>
<a href="/专栏/程序员的数学课/24 结束语 数学底子好,学啥都快.md.html">24 结束语 数学底子好,学啥都快</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>22 面试中那些坑了无数人的算法题</h1>
<p>前面的课时,我们学习了“代数与统计”“算法与数据结构”,至今这门课程的主体知识已告一段落,下面我们进入彩蛋环节,我会向你介绍两个应用到数学的场景,第一个是求职面试,第二个是做人生规划。</p>
<p>这一讲,我们先聊一聊求职面试时常见的数学题。</p>
<p>毕业后,相信你一定参加过不少的面试吧。在求职面试的时候,即使目标工作岗位很少需要直接使用数学知识,也依然有不少面试官非常注重候选人的数学水平,而这并不是没有依据的。因为绝大多数的岗位,都需要候选人具有逻辑推理能力和解决问题的能力。而这些能力在数学上都能有所体现。</p>
<p>下面,我们通过三个例题,带大家体验一下面试中的数学。</p>
<h3>例题1 抛硬币问题</h3>
<p>假设你和大漂亮在玩抛硬币游戏。硬币的正面朝上可得 1 分,背面朝上则分数不变。如果大漂亮可以抛 51 次硬币,而你只能抛 50 次硬币,那么大漂亮分数比你高的概率是多少?</p>
<p>这个问题如果用计算机进行仿真求解,就会非常容易,我们给出下面的代码。</p>
<pre><code>import random
dapiaoliang = 0
you = 0
win = 0
for _ in range(1000):
for _ in range(51):
if random.randint(0,1) == 1:
dapiaoliang += 1
for _ in range(50):
if random.randint(0,1) == 1:
you += 1
if dapiaoliang &gt; you:
win += 1
dapiaoliang = 0
you = 0
print win
</code></pre>
<p>我们对代码进行走读:</p>
<ul>
<li>第 3、4 行,分别定义两个变量来保存大漂亮和你的得分;</li>
<li>第 5 行,用 win 变量来记录大漂亮获胜的次数;</li>
<li>第 6 行开始,执行一个重复 1000 次的循环;</li>
<li>在每次的循环内部,先在第 79 行,通过 51 次的循环,模拟出大漂亮的得分;</li>
<li>再在第 1012 行,通过 50 次的循环,模拟出你的得分;</li>
<li>在 13、14 行判断,如果大漂亮分数比你高,则大漂亮获胜一局。</li>
</ul>
<p>最终,打印出大漂亮获胜的局数。我们运行代码的结果如下图。</p>
<p><img src="assets/CgpVE1_tlQqAHftLAABE5ruo4OM414.png" alt="png" /></p>
<p>可见,在 1000 次的游戏中,大漂亮获胜了 502 次。这样,我们可以估算出,大漂亮获胜的概率为 0.502。</p>
<h4>【数学角度解答】</h4>
<p>我们再从数学的角度重新计算一下这道题。在这里,我们需要通过加乘法则去拆解一下事件。假设 A 事件代表大漂亮抛 51 次硬币的得分B 事件代表你抛 50 次硬币的得分,要计算的目标是 A 大于 B 的概率 P(A&gt;B)。</p>
<p>每次抛硬币是独立的事件,独立事件共同发生的概率满足乘法法则。因此,可以把大漂亮的得分,拆解为前 50 次抛硬币的得分M 事件)和最后一次抛硬币的得分( N 事件)。</p>
<blockquote>
<p>而其中,最后一次抛硬币,只有正面得 1 分或者背面得 0 分两种情况。</p>
</blockquote>
<p>对于一个事件的两个可能的结果分支,可以通过加法法则来求概率,因此有下面的公式。</p>
<p>P(A&gt;B)=P(M+N&gt;B)=
P(N=0)·P(M+0&gt;B)+P(N=1)·P(M+1&gt;B)=
0.5·P(M+0&gt;B) + 0.5·P(M+1&gt;B)</p>
<p>对于最后一项 P(M+1&gt;B) 等价于 P(M≥B)。这是因为,如果 M 大于或等于 B则 M+1 必然是大于 B 的反过来M 和 B 是抛硬币正面朝上的次数,所以必然是整数。如果 M+1 比 B 大,那么 M 必然会大于或等于 B。因此有二者概率相等即 P(M+1&gt;B) = P(M≥B)。</p>
<p>我们把这个关系带入到 P(A&gt;B) 中,则有 P(A&gt;B)=0.5·P(M&gt;B)+0.5·P(M&gt;=B)</p>
<p>再根据加法法则,则有 P(A&gt;B)=0.5·P(M&gt;B)+0.5·P(M&gt;B)+0.5·P(M=B)</p>
<p>别忘了M 事件代表“大漂亮前 50 次抛硬币的得分”,而 B 事件是“你抛 50 次硬币的得分”。区别只剩下了抛硬币的人不一样。不管是谁抛硬币正面朝上的概率始终都是1/2。所以从结果来看这两个事件是完全等价的</p>
<p>则有 P(M&gt;B) = P(M&lt;B)。</p>
<p>因此 P(A&gt;B)
= 0.5·P(M&gt;B)+0.5·P(M&lt;B)+0.5·P(M=B) = 0.5·[P(M&gt;B)+P(M&lt;B)+P(M=B)]</p>
<p>注意M 和 B 的关系只有大于、小于或者等于,所以 P(M&gt;B)+P(M&lt;B)+P(M=B) 之和为 1。因此可以得到结果为 P(A&gt;B) = 0.5·[P(M&gt;B)+P(M&lt;B)+P(M=B)] = 0.5</p>
<p>这与我们用代码仿真计算的结果是一致的。</p>
<h3>例题2 数据上溢问题</h3>
<p>对于一个 Sigmoid 函数y=1/(1+e-x)。假设输入的自变量 x 很小,为 -1000000。因为要先计算 e-x 的值,即 e1000000如下图所示直接计算就会先得到一个非常大的数字而抛出异常。那么在线上代码中该如何规避这种情况计算出输出值呢</p>
<p><img src="assets/CgqCHl_tlRyAGEVyAAOiGgi9i8A083.png" alt="png" /></p>
<p>其实这里可以用到一个非常简单的技巧对公式做个变形就能让程序适应这种情况了。我们知道Sigmoid 函数的结果是一个在 0~1 之间的连续值。而之所以产生数据溢出是因为要先计算e-x 的值。处理这种情况,我们可以从数学的角度,对分子和分母都乘以 ex 这一项,则有</p>
<p>y = 1/(1+e-x) = ex/(ex+1)。</p>
<p>此时,输入 x=-1000000则需要计算 ex得到结果为 0.0。再带入到 Sigmoid 函数中,就可以得到结果啦。</p>
<p>可能你还会问,对公式做了变形之后,如果 x 为很大的正数,如 1000000岂不是又数据溢出抛异常了吗如果 x 为很大的正数,我们直接用 Sigmoid 函数的原始形态 y=1/(1+e-x) 就可以了。</p>
<p>综合上面两种情况我们可将x分正数及非正数分别计算来避免数据的溢出。即</p>
<ul>
<li>如果 x&gt;0则 y = 1/(1+e-x)</li>
<li>如果 x&lt;0则 y = ex/(1+ex)</li>
</ul>
<p>实现的代码如下:</p>
<pre><code>import math
def sigmoid(x):
if x &lt; 0:
y = math.pow(math.e,x) / (1 + math.pow(math.e,x))
else:
y = 1 / (1 + math.pow(math.e,-x))
return y
a = -1000000
b = 1000000
print sigmoid(a)
print sigmoid(b)
</code></pre>
<p>我们对代码进行走读:</p>
<ul>
<li>在 Sigmoid 函数的代码中,第 4 行,判断 x 和 0 的大小关系;</li>
<li>如果 x 为负数,则通过第 5 行的公式计算 y</li>
<li>如果 x 不是负数,则通过第 7 行的公式来计算 y。</li>
</ul>
<p>我们在主函数中,分别输入了非常小和非常大的两个数字,并顺利得到结果分别为 0.0 和 1.0,如下图所示。</p>
<p><img src="assets/CgqCHl_tlSeAJpQ6AABCtn1OaL8521.png" alt="png" /></p>
<h3>例题3 投点距离期望问题</h3>
<p>假设在墙上有一个半径为 10 厘米的圆形区域,现在大迷糊用飞镖向这个圆形区域进行均匀随机的投射。假设大迷糊不会“脱靶”,求大迷糊扎到的点到圆形区域圆心距离的期望。</p>
<p>这个题用代码仿真会非常容易,我们给出下面的代码。</p>
<pre><code>import random
import math
inCircle = 0
distance = 0.0
for _ in range(1000):
x = 1.0 * random.randint(0,1000) / 100
y = 1.0 * random.randint(0,1000) / 100
if x * x + y * y &gt; 100:
continue
else:
inCircle += 1
distance += math.sqrt(x * x + y * y)
print distance / inCircle
</code></pre>
<p>我们对代码进行走读:</p>
<ul>
<li>第 4 行,保存合法的投射次数变量;</li>
<li>第 5 行,是累计的距离之和变量;</li>
<li>第 6 行,通过 for 循环执行多次的投射动作;</li>
<li>每次投射,第 7 行和第 8 行,随机地生成投射点的坐标变量 x 和 y在这里我们精确到小数点后两位</li>
<li>第 9 行,如果坐标点的平方和超过 100也就是投射点在 10 厘米的圆形之外;</li>
<li>那么第 10 行,执行 continue继续下一轮循环</li>
<li>否则,说明投射点在圆内,执行第 11 行的代码;</li>
<li>第 12 行,合法投射次数加 1</li>
<li>第 13 行,通过本次投射点到圆心的距离,更新累计的距离之和;</li>
<li>最后第 14 行,打印累计距离和合法投射次数的比值,得到了平均距离。</li>
</ul>
<p>这也是投射点到圆心距离的期望,我们运行代码的结果为 6.66 厘米,如下图所示。</p>
<p><img src="assets/Ciqc1F_tlS6AK2f7AABGjfsXxYU116.png" alt="png" /></p>
<p>接下来,我们再从数学的角度来计算一下这个题目。</p>
<h4>【数学角度解答】</h4>
<p>题目中,要求解的是一个点到圆心距离这个随机变量的期望。很显然,点到圆心的距离是个连续值。要求某个连续型随机变量的期望,可以用期望的定义式来计算,即</p>
<p><img src="assets/Cip5yF_0HUqAOF9BAAAjCCYcaoY309.png" alt="图片8.png" /></p>
<p>所以,当你在工作中遇到“某连续型变量的期望”时,它一定可以写成上面的积分形式,这是定义式,也是公理。</p>
<p>在我们这个问题中,随机变量 x 是点到圆心的距离。由于投射点不可以在圆形以外,所以这个距离的取值范围是 010。因此我们可以把上面的公式改写为</p>
<p><img src="assets/Cip5yF_0HVKAbUCQAAAicNJwXKI890.png" alt="图片9.png" /></p>
<p>那么问题来了,这里的概率密度函数 f(x) 的表达式是什么呢?别忘了,概率论告诉我们,概率密度函数是概率分布函数的导数。</p>
<p>我们不妨试着求一下投射问题的概率分布函数。假设在圆内有一个小圆,半径是 x0。那么投射点恰好也在小圆内的概率为 P(x&lt;x0) = π·x02/π·102 = x02/102。</p>
<p>因此,概率分布函数为 F(x) = x2/102又因为概率密度函数是概率分布函数的导数所以概率密度函数为 f(x) = 2x/102。</p>
<p>我们把这些条件都带入到期望的公式中,则有</p>
<p><img src="assets/CgqCHl_0HaWAJ0mbAADyboYaUTE926.png" alt="png" /></p>
<p>这与我们用代码求解的 6.66 厘米是一致的。</p>
<h3>小结</h3>
<p>我们对这一讲进行总结。这一课时的内容是面试中的数学,面试官会通过一个简单的数学题,考察候选人解决问题的思考路径。</p>
<p>数学题的魅力就在于活学活用,你很难遇到同一道题,所以靠死记硬背是不行的。只有深入理解数学原理,才能做到在面试的数学考察中游刃有余。在备考的时候,应该注意在基本功方面多花时间去做到深入理解。对于每个知识点的适用范围,来龙去脉做到掌握。</p>
<p>如果你遇到了一个让你束手无策的题目,不妨试着从下面两个角度寻找突破口。</p>
<ul>
<li>第一个角度,从问题出发去寻找突破口。</li>
</ul>
<p>例如,本课时的投点距离期望问题。这个题目要计算的是连续型随机变量的期望,那么它一定可以用连续型随机变量期望的定义式表示。接下来,问题就变成了对这个定义式的未知量进行计算求解。</p>
<ul>
<li>第二个角度,从已知条件出发去寻找突破口。</li>
</ul>
<p>例如,在抛硬币问题中,已知条件是大漂亮抛了 51 次,你抛了 50 次。抛 51 次,可以拆分为抛 50 次和抛 1 次。这样,我们就得到了大漂亮抛 50 次和你抛 50 次,这样等价的两个事件。基于这两个事件,就能推导出大漂亮得分比你高的概率。</p>
<p>这些寻找突破口的方法,是候选人解决问题能力的集中体现;也是数学题、算法题千变万化后,唯一不变的规律。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/程序员的数学课/21 神经网络与深度学习:计算机是如何理解图像、文本和语音的?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/程序员的数学课/23 站在生活的十字路口,如何用数学抉择?.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":"70997bef7eda3cfa","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>