style fix
This commit is contained in:
@@ -0,0 +1,178 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Document</title>
|
||||||
|
<!-- 你的其他样式 -->
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||||
|
<script>
|
||||||
|
MathJax.config = {
|
||||||
|
tex: {
|
||||||
|
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||||
|
},
|
||||||
|
svg: {
|
||||||
|
fontCache: 'global'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>效率的重要性与实践验证</h1>
|
||||||
|
<h2>效率</h2>
|
||||||
|
<h3>算法是什么</h3>
|
||||||
|
<h4>算法就是解决某一个问题的做法,其实它在生活中无处不在</h4>
|
||||||
|
<p>比如从学校宿舍走到食堂:</p>
|
||||||
|
<ol>
|
||||||
|
<li>要先宿舍下楼</li>
|
||||||
|
<li>然后到食堂楼之间可能有3条路,
|
||||||
|
A. 直线方向穿过曲折难走的小路,
|
||||||
|
B. 先走远路到平坦的道上,
|
||||||
|
C. 等一会校车,</li>
|
||||||
|
<li>从3者选择一条走过去,最后再上楼。</li>
|
||||||
|
</ol>
|
||||||
|
<h4>算法的5大组成</h4>
|
||||||
|
<ol>
|
||||||
|
<li>输入</li>
|
||||||
|
<li>输出</li>
|
||||||
|
<li>有穷性</li>
|
||||||
|
<li>确定性</li>
|
||||||
|
<li>可行性</li>
|
||||||
|
</ol>
|
||||||
|
<p>这5大组成其实暗示了一个特性
|
||||||
|
其实对于所有的可以被算法描述的问题,
|
||||||
|
一定会有一种算法有解的。
|
||||||
|
至少有一种方法称为”暴力搜索“”穷举法“,穷尽一切可能。</p>
|
||||||
|
<h3>效率的重要性与验证</h3>
|
||||||
|
<h4>任务:分析与决策</h4>
|
||||||
|
<p>项目组目前有两套备选的交通信号灯同步算法,它们将在不同性能的服务器上运行:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p><strong>方案A</strong>:部署在超级服务器上(10^9 次运算/秒),采用的是一种较为简单的算法,处理n个路口需要 <eq>2n^2</eq> 次计算。</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>方案B</strong>:部署在普通服务器上(10^7 次运算/秒),但采用的是一种更优化的算法,处理n个路口需要 <eq>50nlog_2n</eq> 次计算。</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>你的项目经理(右侧的Agent)希望你通过分析,来判断哪个方案更具前景。请与TA对话,逐一回答以下问题。</p>
|
||||||
|
<h4>问题</h4>
|
||||||
|
<p>与右侧的Agent对话,回答以下问题:</p>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<p><strong>概念回顾</strong>:首先,请向你的“经理”解释,根据课程所学,一个合格的“算法”应具备哪些基本特征?</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>小规模测试</strong>:对于一个包含100个路口的小型城区(n=100),计算并说明方案A和方案B分别需要多长时间完成计算?在这种情况下,你会推荐哪个方案?</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>大规模应用</strong>:现在,我们需要为一座拥有100万个路口的大都市(n=1,000,000)进行规划。再次计算并说明两个方案的耗时。你的推荐会改变吗?为什么?</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>总结陈词</strong>:综合以上分析,向你的“经理”总结:为什么一个更优的算法设计,其重要性远超硬件性能的提升? 这验证了课程中提到的哪个核心观点?</p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<h3>编程实践:验证算法的真实性能</h3>
|
||||||
|
<h4>任务:编码与分析</h4>
|
||||||
|
<p>理论分析让你认识到了算法效率的重要性。现在,你需要通过编程来亲身感受不同交通状况对同一算法性能的巨大影响。
|
||||||
|
我们将以“插入排序”为例,来处理三种典型的交通流量数据,这分别对应算法分析中的<strong>最好</strong>,<strong>最坏</strong>和<strong>平均</strong>情况。</p>
|
||||||
|
<h5>题目:模拟交通流量排序</h5>
|
||||||
|
<p>实现插入排序算法,并验证其在处理“畅通无阻”(数据有序)、“交通大堵塞”(数据逆序)和“随机车流”(数据随机)三种模式时的效率差异。</p>
|
||||||
|
<h5>代码框架</h5>
|
||||||
|
<p>在代码编辑区,完成 <code>insertion_sort(arr)</code> 函数的实现后,运行完整代码,并与Agent讨论结果。
|
||||||
|
<strong>请创建任意文件,将下面代码写入到编辑器中</strong></p>
|
||||||
|
<pre><code class="language-python">import random
|
||||||
|
import time
|
||||||
|
|
||||||
|
def insertion_sort(arr):
|
||||||
|
"""
|
||||||
|
实现插入排序算法
|
||||||
|
参数arr: 待排序的交通数据数组(整数代表车辆通行次序)
|
||||||
|
返回: 排序后的数组
|
||||||
|
"""
|
||||||
|
# TODO: 请在此处实现你的插入排序逻辑
|
||||||
|
|
||||||
|
def generate_traffic_data(n):
|
||||||
|
"""
|
||||||
|
生成模拟交通数据
|
||||||
|
参数n: 数据规模(路口数量或监控点数量)
|
||||||
|
返回: 三种不同交通状况的数据
|
||||||
|
"""
|
||||||
|
random_data = [random.randint(0, 10**6) for _ in range(n)]
|
||||||
|
# 模拟“畅通无阻”:交通流按次序进入,数据基本有序 (Best Case)
|
||||||
|
best_case_data = sorted(random_data)
|
||||||
|
# 模拟“交通大堵塞”:疏散时情况完全反转,数据逆序 (Worst Case)
|
||||||
|
worst_case_data = sorted(random_data, reverse=True)
|
||||||
|
# 模拟“随机车流”:正常但无规律的交通状况 (Average Case)
|
||||||
|
average_case_data = random_data
|
||||||
|
return best_case_data, worst_case_data, average_case_data
|
||||||
|
|
||||||
|
def measure_performance(func, data):
|
||||||
|
"""
|
||||||
|
测量算法性能
|
||||||
|
参数func: 排序函数
|
||||||
|
参数data: 交通数据
|
||||||
|
返回: 执行时间(毫秒)
|
||||||
|
"""
|
||||||
|
start_time = time.perf_counter_ns()
|
||||||
|
func(data.copy()) # 使用副本避免影响其他测试
|
||||||
|
end_time = time.perf_counter_ns()
|
||||||
|
return (end_time - start_time) / 10**6 # 转换为毫秒
|
||||||
|
|
||||||
|
#测试不同规模的路口网络
|
||||||
|
network_sizes = [1000, 5000, 10000]
|
||||||
|
print("交通数据处理算法性能测试:")
|
||||||
|
for size in network_sizes:
|
||||||
|
best, worst, avg = generate_traffic_data(size)
|
||||||
|
|
||||||
|
time_best = measure_performance(insertion_sort, best)
|
||||||
|
time_worst = measure_performance(insertion_sort, worst)
|
||||||
|
time_avg = measure_performance(insertion_sort, avg)
|
||||||
|
|
||||||
|
print(f"网络规模 n={size}:")
|
||||||
|
print(f" - 畅通无阻 (Best Case): {time_best:.2f} ms")
|
||||||
|
print(f" - 交通大堵塞 (Worst Case): {time_worst:.2f} ms")
|
||||||
|
print(f" - 随机车流 (Average Case): {time_avg:.2f} ms")
|
||||||
|
</code></pre>
|
||||||
|
<h4>分析与讨论</h4>
|
||||||
|
<p>完成编程并得到输出后,请与右侧Agent讨论以下问题,以检验你的理解:</p>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<p><strong>结果分析</strong>:当网络规模从1000增加到10000时,“交通大堵塞”(最坏情况)的处理时间增长了大约多少倍?这更符合O(n)(线性)还是O(n2)(二次)的增长模式?</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>原因探究</strong>:为什么“畅通无阻”(最好情况)的处理速度如此之快?它的时间复杂度是什么?请结合你的代码逻辑来解释。</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>实践应用</strong>:根据你的实验结果,你认为插入排序是否适合用于需要快速响应大规模交通拥堵的实时预警系统?为什么?</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>融会贯通</strong>:结合第一关的理论分析和第二关的编程实验,你对“算法是解决问题的核心”这句话有了怎样更深的理解?</p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<h3>规模与增长率</h3>
|
||||||
|
<h4>算法复杂度定义</h4>
|
||||||
|
<p>为了更好地描述算法优化的效果,定义为当问题规模趋于无穷大时算法运行时间(算法复杂度,也可以理解为计算机运行的步骤数)</p>
|
||||||
|
<p>符号:<code>Θ( f(n) )</code> 相对常用,
|
||||||
|
称为“渐进等于”,表示算法复杂度随着问题规模n的增大而增大的速率,和函数f(n)在常数倍率上相同。</p>
|
||||||
|
<h4>算法复杂度的计算</h4>
|
||||||
|
<p>计算时间复杂度是一件比较重要的技能,我们来用一些例子试着计算:
|
||||||
|
假设你是一个收银员,有n个人排队,1分钟你只能收银1个人,随着n增大,你收银的时间复杂度(时长)增长,和哪一个函数增长“渐进等于”呢?</p>
|
||||||
|
<h5>增加一点难度</h5>
|
||||||
|
<p>你这个收银员有超能力,可以越来越熟练,第1个人用时1分钟,第2人用时1/2分钟,第3人只用1/4分钟,随着n增大,时间复杂度怎么样呢?</p>
|
||||||
|
<h5>再难一点</h5>
|
||||||
|
<p>如果你这个超能力是这样的:第1个人用时1分钟,后面2个人用时1分钟,后面4个人用时1分钟,后面8个人用时1分钟,那么这种情况下,随着用户n的增大,时间复杂度可以用那个函数描述呢?</p>
|
||||||
|
<h4>另外两个符号</h4>
|
||||||
|
<p>最后还有两个符号:</p>
|
||||||
|
<ul>
|
||||||
|
<li>O 记号:渐近 “小于”:f (n)“≤”g (n)</li>
|
||||||
|
<li>Ω 记号:渐近 “大于”:f (n)“≥”g (n)
|
||||||
|
比如上面Θ( n ) > Θ( log_2(n) ) > Θ( 1 ),就可以写作Θ( 1 )= O ( log_2(n) ) = O ( n ),或者写作Θ( n ) = Ω( log_2(n) ) = Ω( 1 )</li>
|
||||||
|
</ul>
|
||||||
|
<p>当然事实上,上面的写法比较不常见,只是让大家理解一下,常函数的增长渐进小于log_2(n),也渐进小于n。</p>
|
||||||
|
<p>在具体的使用中,由于渐进大于没什么意义(我们不会去找一个更差的算法),我们常混用Θ、O,也就是只研究函数上界(研究一个算法复杂度渐进小于哪一个函数)</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Document</title>
|
||||||
|
<!-- 你的其他样式 -->
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||||
|
<script>
|
||||||
|
MathJax.config = {
|
||||||
|
tex: {
|
||||||
|
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||||
|
},
|
||||||
|
svg: {
|
||||||
|
fontCache: 'global'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>算法设计范式:分而治之与归并排序</h1>
|
||||||
|
<blockquote>
|
||||||
|
<p>Auto-generated at 2025-09-22 15:38</p>
|
||||||
|
</blockquote>
|
||||||
|
<h2>算法设计范式:分而治之与归并排序</h2>
|
||||||
|
<h3>分治法:分而治之</h3>
|
||||||
|
<h4>引入</h4>
|
||||||
|
<p>在算法分析与设计中,有一些设计算法解决问题的通用方法可供大家选取尝试,其中一个很常用的方法就叫做“分治法”,字如其意,
|
||||||
|
分治法就是将大问题分解为小问题(或说子问题),并将子问题持续分解下去,直到子问题可以用常数时间进行求解。
|
||||||
|
最基础的问题:何时可以用分治法?</p>
|
||||||
|
<ol>
|
||||||
|
<li>找最值问题</li>
|
||||||
|
<li>回文字符串</li>
|
||||||
|
</ol>
|
||||||
|
<h4>分治法的组成</h4>
|
||||||
|
<p>分治法有3个重要组成:分解、解决、合并。</p>
|
||||||
|
<p>有一些帮助我们进行这些操作的分析与设计原则:</p>
|
||||||
|
<ol>
|
||||||
|
<li><strong>子问题相似</strong>:分解出的子问题与原问题类型一致,可复用相同解法,保证递归或分治逻辑的一致性。</li>
|
||||||
|
<li><strong>子问题互不干扰</strong>:子问题边界清晰、数据独立,避免相互影响,简化问题拆解与求解逻辑。</li>
|
||||||
|
<li><strong>合并代价可控</strong>:子问题解合并为原问题解的时间 / 空间开销在可接受范围,避免抵消分治带来的效率提升。</li>
|
||||||
|
</ol>
|
||||||
|
<h4>分治法的时间复杂度迭代公式</h4>
|
||||||
|
<p>T(n)= 子问题数量 * T( 子问题规模 ) + 合并这些子问题的解的用时</p>
|
||||||
|
<h3>归并算法理论分析</h3>
|
||||||
|
<p><strong>案例背景:智慧城市交通优化系统</strong></p>
|
||||||
|
<p>在上周的工作中,你已经实现并分析了插入排序在交通数据处理中的性能。虽然它在数据量较小或基本有序的情况下表现不错,但在应对大规模拥堵(逆序数据)时,其性能瓶颈非常明显。本周,你的项目经理(Agent)将向你介绍一种更高效的算法设计思想——“分而治之”,并要求你掌握其代表算法:归并排序。</p>
|
||||||
|
<h4>任务:理解与分析</h4>
|
||||||
|
<h5>场景介绍</h5>
|
||||||
|
<p>归并排序(Merge Sort)是解决大规模排序问题的利器。在投入编码之前,你必须先从理论上理解它为何如此高效。</p>
|
||||||
|
<h5>思考题</h5>
|
||||||
|
<p>请与右侧的Agent(你的项目经理)对话,清晰地回答以下问题,以证明你已理解核心思想:</p>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<p><strong>分治思想</strong>:根据课程资料,请解释什么是“分而治之”(Divide-and-Conquer)设计范式?它包含哪三个基本步骤?</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>递推关系</strong>:归并排序的时间复杂度可以用递推式 T(n)=2T(n/2)+Θ(n) 来表示。请解释这个式子各部分的含义:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>2</code> 代表什么?</li>
|
||||||
|
<li><code>T(n/2)</code> 代表什么?</li>
|
||||||
|
<li><code>Θ(n)</code> 代表什么?</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>效率对比</strong>:你的同事认为,插入排序经过极致优化后,处理n个数据的耗时为 <eq>0.1n^2</eq>;而归并排序由于递归和合并开销,耗时为 1000nlog_2n。在处理超大规模城市交通数据时(即n趋于无穷大时),哪个算法最终会胜出?请从渐进增长的角度解释你的理由。</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>增长排序</strong>:将下列函数的渐进增长率从低到高排序,并将它们划分到等价类(即 Θ 关系相同的函数)。这有助于你理解不同算法效率的所在层级。</p>
|
||||||
|
<ul>
|
||||||
|
<li><eq>n^2</eq> (插入排序最坏情况)</li>
|
||||||
|
<li>nlogn (归并排序)</li>
|
||||||
|
<li>n (一种非常低效的蛮力算法)</li>
|
||||||
|
<li><eq>2^n</eq> (另一种指数级算法)</li>
|
||||||
|
<li>logn (类似二分查找的效率)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<h3>归并排序的编程实现</h3>
|
||||||
|
<h4>任务:编码与对比</h4>
|
||||||
|
<h5>场景介绍</h5>
|
||||||
|
<p>现在你已经从理论上理解了归并排序的威力。是时候亲手实现它,并与上周的插入排序进行一次性能上的正面较量了!</p>
|
||||||
|
<h5>题目:实现归并排序并对比性能</h5>
|
||||||
|
<p>你需要实现归并排序的核心函数,并利用上周的测试框架来直观对比它与插入排序在处理不同交通数据时的表现。</p>
|
||||||
|
<h5>代码框架</h5>
|
||||||
|
<p>在下方代码编辑区,完成 <code>merge_sort(arr)</code> 和 <code>merge(left, right)</code> 两个函数的实现。</p>
|
||||||
|
<pre><code class="language-python">import random
|
||||||
|
import time
|
||||||
|
|
||||||
|
#上周实现的插入排序(用于对比)
|
||||||
|
def insertion_sort(arr):
|
||||||
|
for i in range(1, len(arr)):
|
||||||
|
key = arr[i]
|
||||||
|
j = i - 1
|
||||||
|
while j >= 0 and arr[j] > key:
|
||||||
|
arr[j + 1] = arr[j]
|
||||||
|
j -= 1
|
||||||
|
arr[j + 1] = key
|
||||||
|
return arr
|
||||||
|
|
||||||
|
#--- 本周任务:请在下方实现归并排序 ---
|
||||||
|
|
||||||
|
def merge_sort(arr):
|
||||||
|
"""
|
||||||
|
实现归并排序算法
|
||||||
|
参数arr: 待排序的交通数据数组
|
||||||
|
返回: 一个新的、排序好的数组
|
||||||
|
"""
|
||||||
|
# TODO: 实现归并排序的递归逻辑
|
||||||
|
# 提示:当数组元素个数小于等于1时,递归终止
|
||||||
|
|
||||||
|
|
||||||
|
def merge(left, right):
|
||||||
|
"""
|
||||||
|
合并两个已排序的子数组
|
||||||
|
参数left: 左侧已排序数组
|
||||||
|
参数right: 右侧已排序数组
|
||||||
|
返回: 合并后的一个有序数组
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
#--- 测试与对比部分 ---
|
||||||
|
|
||||||
|
def measure_performance(sort_func, data):
|
||||||
|
start_time = time.perf_counter_ns()
|
||||||
|
sort_func(data.copy())
|
||||||
|
end_time = time.perf_counter_ns()
|
||||||
|
return (end_time - start_time) / 10**6
|
||||||
|
|
||||||
|
network_sizes = [1000, 5000, 10000, 50000]
|
||||||
|
print("算法性能对比测试:")
|
||||||
|
for size in network_sizes:
|
||||||
|
# 生成逆序数据,这是插入排序的最坏情况
|
||||||
|
worst_case_data = list(range(size, 0, -1))
|
||||||
|
|
||||||
|
time_insertion = measure_performance(insertion_sort, worst_case_data)
|
||||||
|
time_merge = measure_performance(merge_sort, worst_case_data)
|
||||||
|
|
||||||
|
print(f"--- 网络规模 n={size} (最坏情况) ---")
|
||||||
|
print(f" - 插入排序: {time_insertion:.2f} ms")
|
||||||
|
print(f" - 归并排序: {time_merge:.2f} ms")
|
||||||
|
</code></pre>
|
||||||
|
<h5>分析与讨论</h5>
|
||||||
|
<p>完成编程并得到输出后,请与右侧Agent讨论以下问题:</p>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<p><strong>性能验证</strong>:观察“交通大堵塞”(最坏情况)的测试结果,归并排序的性能表现与插入排序有何天壤之别?这是否验证了你在第一关的理论分析?</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>性能稳定性</strong>:如果我们将测试数据换成“畅通无阻”(最好情况)或“随机车流”(平均情况),你认为归并排序的运行时间会发生巨大变化吗?为什么?</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>空间成本</strong>:在实现 <code>merge</code> 函数时,你可能创建了一个新的数组 <code>merged</code> 来存放结果。这在算法分析中被称为“空间复杂度”。与在原数组上进行交换的插入排序相比,归并排序的这个特点是优点还是缺点?</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>最终决策</strong>:作为项目工程师,你会选择哪种排序算法用于我们的大规模交通调度系统?请综合考虑<strong>时间效率</strong>和<strong>空间成本</strong>来陈述你的理由。</p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<h3>进一步研究分治问题的时间复杂度分析</h3>
|
||||||
|
<p>分治问题的两种时间复杂度分析方法:主方法和递归树法。</p>
|
||||||
|
<h4>主方法</h4>
|
||||||
|
<p>主方法本质是一种速算公式,针对于分治迭代式是这样的形式的:T(n)=aT(n/b)+f(n),a>=1,b>1。
|
||||||
|
主方法概念上理解,就是比较分治法的“子问题总数”和“合并总数”,在问题复杂度上,哪个为“主”,就按照这个主部分计算时间复杂度。
|
||||||
|
(因为时间复杂度公式上,我们只会保留增长最快的那一项,也就是“主项”)</p>
|
||||||
|
<p>严格定义这里就略了,直接看口诀:</p>
|
||||||
|
<ol>
|
||||||
|
<li>算log_b (a),得到n^log_b (a);</li>
|
||||||
|
<li>比f(n)和n^log_b (a)的增长速度:</li>
|
||||||
|
</ol>
|
||||||
|
<ul>
|
||||||
|
<li>前者慢 → O(n^log_b (a));</li>
|
||||||
|
<li>差不多 → O(n^log_b (a) · log n)(k=0 时);</li>
|
||||||
|
<li>前者快且满足正则条件 → O(f(n))。</li>
|
||||||
|
<li>其中正则条件是存在0<c<1,使得:a·f(n/b)≤ c·n²</li>
|
||||||
|
</ul>
|
||||||
|
<p>试着计算一下:T(n) = 3T(n/2) + O(n²) 这个迭代式的时间复杂度</p>
|
||||||
|
<h4>递归树法</h4>
|
||||||
|
<p>递归树相对更容易理解(可能之前的问题你已经在不知觉中使用递归树法),因为这种方法完全就是对分治方法本身的全流程模拟。</p>
|
||||||
|
<p>步骤 1:画树 —— 把问题拆解成 “节点”,标记每个节点的分解与合并耗时(父节点的解决耗时其实就是自己的所有子节点的分解与合并耗时)
|
||||||
|
a. 根节点:写原问题的规模 n 和分解 / 合并耗时 f (n)
|
||||||
|
b. 子节点:根节点拆成几个子问题,就画几个子节点,每个子节点写 “子问题规模 + 子问题的分解 / 合并耗时”
|
||||||
|
c. 叶子节点:当子问题规模小到能直接解决(比如 n=1),就停止拆解,叶子节点写 “规模 1,耗时 Θ(1)”
|
||||||
|
步骤 2:算层 —— 求每一层的总耗时
|
||||||
|
同一层的节点,代表 “同一轮拆解的所有问题”,它们的耗时总和就是这一层的总代价
|
||||||
|
步骤 3:求和 —— 总耗时 = 所有层的代价之和
|
||||||
|
先确定树有多少层,再把每层的总代价加起来</p>
|
||||||
|
<p>试着计算:无法用主方法解决的问题: T (n)=T (n/3)+T (2n/3)+2*n</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
112
Html/apps/static/68bacdfadf5aeae0912f7f18-第四章:查询-二分查找.html
Normal file
112
Html/apps/static/68bacdfadf5aeae0912f7f18-第四章:查询-二分查找.html
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Document</title>
|
||||||
|
<!-- 你的其他样式 -->
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||||
|
<script>
|
||||||
|
MathJax.config = {
|
||||||
|
tex: {
|
||||||
|
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||||
|
},
|
||||||
|
svg: {
|
||||||
|
fontCache: 'global'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>二分查找</h1>
|
||||||
|
<h2>二分查找</h2>
|
||||||
|
<h3>二分查找的原理</h3>
|
||||||
|
<p>二分查找是一个基础但很重要的知识点,也是一种特殊的分治,为以后许多高级的数据结构与算法铺垫。</p>
|
||||||
|
<p>下面是一个用二分的简单场景:</p>
|
||||||
|
<p>假设小明从0到1000之间选择了一个数字但不告诉你,你可以不断猜测这个数,每次猜测小明会告知你的猜测得过大还是过小,问最多几次就一定能猜中?</p>
|
||||||
|
<p>答案是利用二分查找的原理,猜测11次即可。</p>
|
||||||
|
<ol>
|
||||||
|
<li>对于0到1000的答案备选区,猜测中位数500,假设过小,</li>
|
||||||
|
<li>则对于501到1000的答案备选区,猜测750,假设过大</li>
|
||||||
|
<li>则对于501到749的答案备选区,猜测625,假设过小,</li>
|
||||||
|
<li>则对于626到749区间......</li>
|
||||||
|
<li>(688-749)</li>
|
||||||
|
<li>(718-749)</li>
|
||||||
|
<li>(734-749)</li>
|
||||||
|
<li>(742-749)</li>
|
||||||
|
<li>(746-749)</li>
|
||||||
|
<li>(748-749)</li>
|
||||||
|
<li>(749-749)</li>
|
||||||
|
</ol>
|
||||||
|
<p>在最差的情况下,第11次的答案备选区就一定长度为1了,也就是必然是答案。</p>
|
||||||
|
<p>因此如果序列是有序的,就可以通过二分查找快速定位所需要的数据。</p>
|
||||||
|
<h4>例题</h4>
|
||||||
|
<p>对于上面那个题目,如果问题区间是1到4000,最差情况下需要猜测几次?这个值可以怎么迅速地算出来,你可以用时间复杂度的公式建立一下并推导一下么?</p>
|
||||||
|
<h3>练习:二分查找</h3>
|
||||||
|
<p>试试对于下面的题目,用代码实现一下二分查找。</p>
|
||||||
|
<h4>题目:有序数组寻址</h4>
|
||||||
|
<p>给出一个长度为n的有序数组(从小到大),有q次询问,对于每次询问,输出指定数在数组中的下标。如果不存在则输出-1。</p>
|
||||||
|
<h5>输入</h5>
|
||||||
|
<p>第一行一个整数n。(1<=n<=10^5)</p>
|
||||||
|
<p>第二行n个用空格分开的整数ai。(0<=ai<=10^8)</p>
|
||||||
|
<p>第三行一个整数q,表示询问的次数。(1<=q<=10^4)</p>
|
||||||
|
<p>后q行,每行一个整数b,表示询问的数。(0<=b<=10^8)</p>
|
||||||
|
<h5>输出</h5>
|
||||||
|
<p>q行,每行一个整数,对应每次询问的返回结果</p>
|
||||||
|
<h5>提示:</h5>
|
||||||
|
<p>完成代码后,通知Agent进行评测。</p>
|
||||||
|
<p>如果你还不完全会这个算法,询问Agent获取提示并进行学习。</p>
|
||||||
|
<h3>二维二分查找</h3>
|
||||||
|
<h4>二分查找的直觉</h4>
|
||||||
|
<p>通过之前的原理和实现,二分查找本质上是通过取中的方式,尽可能排除多(一半)的备选数。
|
||||||
|
<br>
|
||||||
|
为进一步理解,除了序列,本节我们来尝试一下在矩阵(二维数组)上进行分治和查找。</p>
|
||||||
|
<h4>问题建模</h4>
|
||||||
|
<p>给出一个n*n的矩阵,其中每一行都是一个从小到大的序列,每一列都是从小到大的序列。从中找到指定的一个数target。</p>
|
||||||
|
<h5>例</h5>
|
||||||
|
<p>[
|
||||||
|
[1, 2, 4, 5]
|
||||||
|
[2, 5, 7, 11]
|
||||||
|
[3, 8, 10, 12]
|
||||||
|
[4, 10, 17, 20]
|
||||||
|
]
|
||||||
|
从中找到"8"这个数。</p>
|
||||||
|
<h5>线性做法</h5>
|
||||||
|
<p>这里先介绍线性做法。
|
||||||
|
一维序列的线性做法就是逐个比对一下,
|
||||||
|
二维做法最差是逐个扫描n*n所有的数,可以聪明一些降低到线性成本,称为“楼梯式”查找:</p>
|
||||||
|
<ol>
|
||||||
|
<li>从右上角看一个元素 x = M[r][c]</li>
|
||||||
|
<li>查找与排除
|
||||||
|
a. 若 x > target,则这一列里 x 下方都 ≥ x,更不可能是 target,所以安全地左移(排除一整列)。
|
||||||
|
b. 若 x < target,则这一行里 x 左边都 ≤ x,更不可能是 target,所以安全地下移(排除一整行)。</li>
|
||||||
|
<li>持续直到排除所有行和列,从而找到目标元素或告知找不到。</li>
|
||||||
|
</ol>
|
||||||
|
<p>做法正确性分析,时间复杂度分析。</p>
|
||||||
|
<h4>分治做法:二维二分</h4>
|
||||||
|
<p>接下来试着通过二分的技术,找找复杂度更低的做法。
|
||||||
|
试着回答这些问题,并与AI教师进行讨论:</p>
|
||||||
|
<ol>
|
||||||
|
<li>分解中点在哪里?</li>
|
||||||
|
<li>二分后排除掉的部分是哪些?</li>
|
||||||
|
<li>如何划分子问题?</li>
|
||||||
|
<li>时间复杂度是多少?</li>
|
||||||
|
</ol>
|
||||||
|
<h3>二维二分查找实现</h3>
|
||||||
|
<h4>题目:有序矩阵查找</h4>
|
||||||
|
<p>给出一个 n×n 的矩阵,其中每一行的元素都按照从小到大的顺序排列,每一列的元素也都按照从小到大的顺序排列。现需要判断指定的数 target 是否在该矩阵中,若存在则输出其所在的行下标和列下标(行和列均从 0 开始计数);若不存在则输出 - 1 -1。</p>
|
||||||
|
<h4>输入</h4>
|
||||||
|
<p>第一行一个整数 n。(1<=n<=10^3)
|
||||||
|
接下来 n 行,每行 n 个用空格分开的整数,表示矩阵的元素。
|
||||||
|
最后一行一个整数 target,表示需要查找的数。</p>
|
||||||
|
<h4>输出</h4>
|
||||||
|
<p>一行两个整数,分别表示 target 所在的行下标和列下标,中间用空格隔开。若不存在则输出 - 1 -1。</p>
|
||||||
|
<h4>示例</h4>
|
||||||
|
<p>输入:41 2 4 52 5 7 113 8 10 124 10 17 208
|
||||||
|
输出:2 1</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
height: calc(100vh - 220px);
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.md-preview-pane h1, .md-preview-pane h2, .md-preview-pane h3 {
|
.md-preview-pane h1, .md-preview-pane h2, .md-preview-pane h3 {
|
||||||
|
|||||||
Reference in New Issue
Block a user