Merge branch 'main' into feature/paste_detecte_new
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>
|
||||
|
||||
172
Html/apps/static/68bacdfadf5aeae0912f7f18-第三周-分治法实践与优化.html
Normal file
172
Html/apps/static/68bacdfadf5aeae0912f7f18-第三周-分治法实践与优化.html
Normal file
@@ -0,0 +1,172 @@
|
||||
|
||||
<!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:44</p>
|
||||
</blockquote>
|
||||
<h2>分治策略进阶与主方法</h2>
|
||||
<h3>最大子数组问题</h3>
|
||||
<h4>任务:编码与分析</h4>
|
||||
<h5>场景介绍</h5>
|
||||
<p>我们的任务是分析每日交通流量的<strong>变化数组</strong>(正数代表流量增加,负数代表减少),并找到哪一个<strong>连续时段</strong>的流量总增量最大。这在算法上被称为“最大子数组问题” 。</p>
|
||||
<p>例如,对于流量变化数组<code>[13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]</code>,总增量最大的连续时段是从第8天到第11天,总增量为 <code>18 + 20 - 7 + 12 = 43</code> 。
|
||||
虽然这个问题可以通过O(n2) 的暴力法求解,但我们将使用更高效的分治策略。</p>
|
||||
<h5>题目:最大交通流量增量</h5>
|
||||
<p>你需要实现一个分治算法来解决最大子数组问题。
|
||||
同时,为了对比,你也会实现一个暴力求解算法。</p>
|
||||
<h5>代码框架</h5>
|
||||
<p>在下方代码编辑区,完成 <code>find_maximum_subarray</code>(分治法)和 <code>find_max_crossing_subarray</code> 以及 <code>find_maximum_subarray_brute</code>(暴力法)三个函数。</p>
|
||||
<pre><code class="language-python">import time
|
||||
import random
|
||||
|
||||
def find_maximum_subarray_brute(arr):
|
||||
"""
|
||||
使用暴力法求解最大子数组问题
|
||||
返回: (最大和, 开始索引, 结束索引)
|
||||
"""
|
||||
|
||||
#--- 本周任务:请在下方实现分治算法 ---
|
||||
|
||||
def find_max_crossing_subarray(arr, low, mid, high):
|
||||
"""
|
||||
找到跨越中点的最大子数组
|
||||
返回: (最大和, 开始索引, 结束索引)
|
||||
"""
|
||||
# TODO: 实现寻找跨越中点的最大子数组的逻辑
|
||||
# 提示: 从mid向左和向右分别扫描,找到各自的最大和
|
||||
|
||||
def find_maximum_subarray(arr, low, high):
|
||||
"""
|
||||
使用分治法求解最大子数组问题
|
||||
返回: (最大和, 开始索引, 结束索引)
|
||||
"""
|
||||
# TODO: 实现分治递归逻辑
|
||||
# 提示: 递归基是当数组只有一个元素时
|
||||
|
||||
#--- 测试与对比部分 --- 以下内容无需修改
|
||||
traffic_changes = [13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]
|
||||
print(f"交通流量变化数据: {traffic_changes}")
|
||||
|
||||
#使用分治法
|
||||
max_sum_dc, start_dc, end_dc = find_maximum_subarray(traffic_changes, 0, len(traffic_changes) - 1)
|
||||
print(f"分治法结果: 最大增量 = {max_sum_dc}, 时段 = Day {start_dc} to Day {end_dc}")
|
||||
|
||||
#使用暴力法验证
|
||||
max_sum_brute, start_brute, end_brute = find_maximum_subarray_brute(traffic_changes)
|
||||
print(f"暴力法结果: 最大增量 = {max_sum_brute}, 时段 = Day {start_brute} to Day {end_brute}")
|
||||
|
||||
#性能测试
|
||||
large_data = [random.randint(-50, 50) for _ in range(2000)]
|
||||
start_time = time.perf_counter()
|
||||
find_maximum_subarray(large_data, 0, len(large_data) - 1)
|
||||
dc_time = (time.perf_counter() - start_time) * 1000
|
||||
print(f"\n在 n=2000 的数据集上,分治法耗时: {dc_time:.2f} ms")
|
||||
|
||||
start_time = time.perf_counter()
|
||||
find_maximum_subarray_brute(large_data)
|
||||
brute_time = (time.perf_counter() - start_time) * 1000
|
||||
print(f"在 n=2000 的数据集上,暴力法耗时: {brute_time:.2f} ms")
|
||||
</code></pre>
|
||||
<h3>分治经典算法:快速排序</h3>
|
||||
<h4>快速排序原理</h4>
|
||||
<p>快速排序(Quick Sort)是分治法的经典应用之一。与归并排序不同的是,快速排序通过原地划分数组来完成排序,无需额外的同规模辅助存储空间。它在平均情况下表现极为高效,是实际应用中常用的排序算法之一。</p>
|
||||
<p>快速排序的分治核心思路非常好理解,对于一个未排序的数组,希望分解为左区间和一个“枢轴(pivot)”元素和右区间,其中左区间的数都小于等于枢轴,右区间的都大于等于枢轴。
|
||||
我们这里用一种固定枢轴法来将序列变成上述的区间情况:
|
||||
”选取数组第一个元素作为枢轴,用一个移动变量指向数组末尾元素并不断向前移动,直到找到第一个小于枢轴的元素,将该元素与枢轴位置交换,然后从枢轴原位置的下一位个元素开始往后找第一大于的交替,重复上述过程,最终实现以枢轴为界划分出左小右大的区间,再对左右区间递归执行此操作完成排序“</p>
|
||||
<h4>任务:编码与实验</h4>
|
||||
<p>现在你需要亲手实现快速排序算法,并通过实验验证其在不同输入情况下的性能差异。</p>
|
||||
<p>实现 quick_sort(arr) 函数,使用分治策略对传入的数组进行排序。为简单起见,可选择每次固定使用数组的第一个元素作为枢轴,将比枢轴小的元素放在左侧,比枢轴大的放在右侧,然后递归地排序左右子数组。完成实现后,我们将利用性能测试函数对比有序输入(近似最坏情况)和随机输入(平均情况)下算法的运行时间。
|
||||
代码框架
|
||||
请在下方代码编辑区完成 quick_sort(arr) 的实现。</p>
|
||||
<pre><code>import random
|
||||
import time
|
||||
|
||||
def quick_sort(arr):
|
||||
"""
|
||||
实现快速排序算法(固定枢轴策略)
|
||||
参数arr: 待排序的数组
|
||||
返回: 排序后的数组
|
||||
"""
|
||||
# TODO: 实现快速排序的分治逻辑
|
||||
# 提示:选取第一个元素为枢轴,递归排序左右子数组
|
||||
|
||||
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 # 转换为毫秒
|
||||
|
||||
#性能测试:对比有序输入和随机输入
|
||||
sizes = [1000, 5000, 10000]
|
||||
print("快速排序性能测试(固定枢轴):")
|
||||
for n in sizes:
|
||||
sorted_data = list(range(n))
|
||||
random_data = [random.randint(0, 10**6) for _ in range(n)]
|
||||
time_sorted = measure_performance(quick_sort, sorted_data)
|
||||
time_random = measure_performance(quick_sort, random_data)
|
||||
print(f"数据规模 n={n}: 有序输入 {time_sorted:.2f} ms, 随机输入 {time_random:.2f} ms")
|
||||
</code></pre>
|
||||
<p>完成编码并运行测试,报告时间差异。</p>
|
||||
<h3>快速排序的复杂度分析</h3>
|
||||
<h4>时间复杂度</h4>
|
||||
<p>基于 “划分 - 递归” 核心逻辑,时间复杂度由<strong>划分效率(枢轴选择)<strong>和</strong>递归深度</strong>共同决定
|
||||
三种典型场景:最坏情况、平均情况、最佳情况</p>
|
||||
<p>最坏情况:每次划分得规模 n-1 和 0 的子数组
|
||||
平均情况:输入随机 / 等概率选枢轴
|
||||
最佳情况:每次划分平分数组</p>
|
||||
<h4>空间复杂度</h4>
|
||||
<p>快速排序比归并排序更常用的一点在于,快排是“原地的”。</p>
|
||||
<h3>注入随机进行优化:随机快排与期望复杂度</h3>
|
||||
<h4>分析</h4>
|
||||
<p>为了避免固定枢轴选择导致的最坏情况,我们可以引入随机化策略优化快速排序。
|
||||
随机快排在每次划分时随机选择枢轴,从概率上保证划分均衡,从而将最坏情况表现转化为极低概率事件。
|
||||
理论上可以证明,随机快排对任何输入的期望时间复杂度为<eq>O(n \log n)</eq>,且大幅降低了出现<eq>O(n^2)</eq>耗时的概率。</p>
|
||||
<h4>实践</h4>
|
||||
<pre><code>import random
|
||||
import time
|
||||
|
||||
def random_quick_sort(arr):
|
||||
"""
|
||||
实现随机快速排序算法(随机选择枢轴)
|
||||
参数arr: 待排序的数组
|
||||
返回: 排序后的数组
|
||||
"""
|
||||
|
||||
|
||||
def quick_sort(arr):
|
||||
"""
|
||||
你在上一章实现的快排内容
|
||||
"""
|
||||
|
||||
#验证随机快排在极端有序输入下的性能
|
||||
n = 10000
|
||||
sorted_data = list(range(n))
|
||||
time_fixed = measure_performance(quick_sort, sorted_data)
|
||||
time_random = measure_performance(random_quick_sort, sorted_data)
|
||||
print(f"数据规模 n={n}: 固定枢轴快排 {time_fixed:.2f} ms, 随机枢轴快排 {time_random:.2f} ms")
|
||||
|
||||
</code></pre>
|
||||
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
159
Html/apps/static/68bacdfadf5aeae0912f7f18-第四周-基于比较的排序.html
Normal file
159
Html/apps/static/68bacdfadf5aeae0912f7f18-第四周-基于比较的排序.html
Normal file
@@ -0,0 +1,159 @@
|
||||
|
||||
<!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><strong>优先队列(Priority Queue)</strong> 是一种抽象数据结构,它支持以最高(或最低)优先级为先进行元素的插入和取出操作。
|
||||
<br>
|
||||
在优先队列中,每次检索得到的都是当前权重最大(或最小)的元素。
|
||||
<br><br>
|
||||
为了实现优先队列,最直接的思路是维护一个内部元素有序的数组:每次取数就是第一个数,存数时将元素放入合适位置,并调整序列。
|
||||
<br><br>
|
||||
<strong>堆(Heap)结构</strong>是实现优先队列的首选。</p>
|
||||
<h3>数组模拟堆实现的优先队列</h3>
|
||||
<h4>堆结构与取存原理</h4>
|
||||
<p>堆(Heap)本质上是一个完全二叉树结构:
|
||||
(当然也可以是多叉树,但没有必要)
|
||||
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_b73d7df5-018e-4542-b891-b3711c42c56a" alt="图1:大根堆的二叉树结构图示" /></p>
|
||||
<p>这里用“大根堆”为例,从图中可以看到,每一个节点都比它的子节点更大。
|
||||
既然“优先队列”可以理解为一种特殊的“队列”,那么我们先用堆实现这个队列的出和入:</p>
|
||||
<h5>取数</h5>
|
||||
<p>从优先队列中取数,显然堆顶的数就是要的那个最大者。
|
||||
但是将这个数取出后还不能结束,因为需要维护堆的性质。
|
||||
<br>
|
||||
为了维护堆性质,一般通过将末尾的元素放到堆顶,然后将其不断与左右儿子进行替换,直到他比两个儿子都大或儿子不存在为止,称为“下滤”。
|
||||
<br></p>
|
||||
<h5>存数</h5>
|
||||
<p>与取数类似,重要的是维护堆的性质。将新数放到最后(上图中的第一个黑色节点中)后,将这个数进行“上浮”。</p>
|
||||
<h4>数组模拟堆</h4>
|
||||
<p>为了实现堆,其实不需要真的写一个二叉树,用数组就可以做到。
|
||||
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_b4142d35-630c-4d12-a147-d514cca9e0d5" alt="图2:左边是堆的数组表示,右边是其对应的二叉树" />
|
||||
如上图,左边是堆的数组表示,右边是其对应的二叉树。</p>
|
||||
<p>数组下标从1开始,任意一个下标i,其左右儿子下标刚好就是"i*2"和“i*2+1”。这样在后续代码实现时,代码写起来就会简单很多。</p>
|
||||
<h6>建堆</h6>
|
||||
<p>用数组模拟堆还有一个好处,就是可以“原地建堆”。
|
||||
对于一个元素随机的数组,只需要O(n)的时间复杂度就可以完成随机数组向堆的转化。</p>
|
||||
<p>具体做法为“从后向前”遍历,对于每一个非叶子节点,就将其进行“下滤”,这样以它为根的子树就变成一个小堆。往前遍历即可。</p>
|
||||
<h3>优先队列的算法实现</h3>
|
||||
<h4>练习:堆操作</h4>
|
||||
<p>在进行代码实现之前,做一个问题练习:
|
||||
对于一个随机队列"[3, 32, 6, 43, 5, 8, 0, 9]",经过一轮反向扫描下滤建大根堆操作,得到的堆的序列是什么?
|
||||
将答案告知AI教师。</p>
|
||||
<h4>任务:城市事件优先调度</h4>
|
||||
<p>现在,让我们通过编程实践来掌握堆排序的实现。假设我们需要对城市中发生的一系列事件按照紧急程度(以数值大小表示优先级)排序,从而依次处理最高优先级的事件。这相当于将一组数字按从大到小排序的过程,与堆排序的机制完全一致。</p>
|
||||
<p><br><br></p>
|
||||
<h4>题目:实现堆排序</h4>
|
||||
<p>请你实现一个堆排序算法 heap_sort(arr),将传入的数组利用堆排序方法排序(从大到小)。
|
||||
<br>
|
||||
你可以通过实现max_heapify和build_max_heap等函数来完成这一任务。
|
||||
<br>
|
||||
完成编码后,我们将对算法的性能进行测试,比较不同规模输入下堆排序运行时间的增长情况。
|
||||
<br></p>
|
||||
<h5>代码框架</h5>
|
||||
<p>请在下方代码编辑区完成 max_heapify、build_max_heap 和 heap_sort 函数的实现。</p>
|
||||
<pre><code class="language-python">import random
|
||||
import time
|
||||
|
||||
def max_heapify(arr, n, i):
|
||||
"""
|
||||
维护最大堆性质:假设结点 i 的左右子树已经是最大堆,
|
||||
调整结点 i 使以其为根的子树成为最大堆
|
||||
参数:
|
||||
arr: 存储堆的数组
|
||||
n: 堆的有效大小(长度)
|
||||
i: 需要下滤调整的节点索引
|
||||
"""
|
||||
# TODO: 在此处实现 "下滤" 操作,将 arr[i] 下沉到正确位置
|
||||
|
||||
|
||||
def build_max_heap(arr):
|
||||
"""
|
||||
将无序数组原地建成最大堆,从后往前进行下滤
|
||||
参数:
|
||||
arr: 待调整的数组
|
||||
"""
|
||||
# TODO: 调用 max_heapify 将 arr 调整为堆
|
||||
|
||||
|
||||
def heap_sort(arr):
|
||||
"""
|
||||
利用堆排序算法排序数组(降序)
|
||||
参数:
|
||||
arr: 待排序数组
|
||||
返回:
|
||||
排序后的数组(从大到小)
|
||||
"""
|
||||
# TODO: 完成堆排序的实现
|
||||
n = len(arr)
|
||||
# 1. 原地建堆
|
||||
build_max_heap(arr)
|
||||
# 2. 依次将当前堆顶(最大值)交换到数组末尾,并缩小堆的范围,然后下滤
|
||||
|
||||
|
||||
#性能测试:对比不同规模输入的堆排序用时
|
||||
def measure(sort_func, data):
|
||||
start = time.perf_counter_ns()
|
||||
sort_func(data.copy())
|
||||
end = time.perf_counter_ns()
|
||||
return (end - start) / 10**6 # 毫秒
|
||||
|
||||
sizes = [1000, 5000, 10000]
|
||||
print("堆排序性能测试:")
|
||||
for n in sizes:
|
||||
data = [random.randint(0, 1000000) for _ in range(n)]
|
||||
t = measure(heap_sort, data)
|
||||
print(f"数据规模 n={n}: 排序耗时 {t:.2f} ms")
|
||||
</code></pre>
|
||||
<h5>实验结果分析</h5>
|
||||
<p>请完成并运行上述代码,观察不同输入规模下算法的执行时间。理论上,堆排序的时间复杂度为<eq>O(n \log n)</eq>,当输入规模增大时,运行时间应呈现近似线性乘以对数的增长趋势。具体来说,若将输入规模扩大10倍,运行时间将增加约<eq>10 \times \log_2(10) \approx 10 \times 3.3 \approx 33</eq>倍左右。
|
||||
<br>
|
||||
相比之下,简单的<eq>O(n^2)</eq>排序算法在相同扩大量级下耗时会增加约100倍。通过与之前插入排序实验的对比,你会发现堆排序对规模扩大的响应增长显著缓慢得多。
|
||||
<br>
|
||||
这印证了堆排序的效率优势:在最坏情况和平均情况下它都能维持<eq>O(n \log n)</eq>的性能,不会出现如快速排序在极端情况下退化为<eq>O(n^2)</eq>的尴尬局面。
|
||||
<br>
|
||||
此外,堆排序是一种原地排序(只需要常数级别的额外空间),这也是相对于归并排序的一个优势。综合来看,利用优先队列实现的堆排序在效率和空间上都表现出色,是一种成熟可靠的排序方法。</p>
|
||||
<h3>比较排序的决策树模型</h3>
|
||||
<p>前面的内容介绍了多种基于元素比较的排序算法(比较排序),包括快速排序、堆排序等。接下来,我们讨论一个重要的理论结果:
|
||||
<br>
|
||||
在比较模型下,任意排序算法的最优时间复杂度下界为<eq>Ω(n \log n)</eq>。
|
||||
这个结论意味着,无论设计何种巧妙的比较排序算法,都无法突破这一定义上的效率极限。证明这一点的经典工具是决策树模型。</p>
|
||||
<p>决策树是描述比较排序过程的一种抽象模型。
|
||||
在排序过程中,每进行一次比较(例如“<eq>A[i] \le A[j]</eq>?”)就相当于根据结果(二叉决策:是/否)将可能的输入情况划分到两个分支。
|
||||
<br>
|
||||
整个排序算法的运行过程可以被看作是在这样一棵决策树上从根节点走向某个叶节点的过程。决策树的每个叶节点对应一种可能的输入集合及其确定的输出顺序。
|
||||
<br>
|
||||
当有<eq>n</eq>个待排序元素时,假设它们两两各不相同,则可能的输入排列情况共有<eq>n!种(所有元素的全排列)。为了正确地将每种输入排列映射到唯一的输出(即排好序的有序序列),排序算法的决策树必须至少具备</eq>n!个叶节点——每个叶子对应一种输入排列的判别结果。</p>
|
||||
<p>对于一棵二叉决策树,若包含<eq>L</eq>个叶节点,其高度<eq>h</eq>满足<eq>L \le 2^h</eq>,因此<eq>h \ge \lceil \log_2 L \rceil</eq>。在排序问题中,<eq>L</eq>最少取<eq>n!</eq>,因此最优情况下决策树高度也满足:
|
||||
<br>
|
||||
<eq>$h_{\min} \geq \lceil \log_2(n!) \rceil.</eq>$
|
||||
利用对数运算的性质,可以估计<eq>\log_2(n!)</eq>的数量级。根据斯特林公式近似,<eq>n!</eq>大约为<eq>(n/e)^n</eq>的数量级,那么:
|
||||
<br>
|
||||
<eq>$\log_2(n!) \approx \log_2\left((n/e)^n\sqrt{2\pi n}\right) = n\log_2 n - n\log_2 e + O(\log n).</eq>$
|
||||
<br>
|
||||
可以看出,当<eq>n</eq>较大时,<eq>\log_2(n!) = Θ(n \log n)</eq>。这意味着决策树的高度下界<eq>h_{\min} = Ω(n \log n)</eq>。换言之,任何基于比较的排序算法在最理想情况下也需要执行与<eq>n \log n</eq>同数量级的比较操作。
|
||||
<br>
|
||||
例如,对于$ n=3$的简单情况,<eq>3! = 6</eq>,满足<eq>2^2 < 6 < 2^3</eq>,因此判定3个元素的任意排列需要至少3次比较。这与我们已知的事实相符:对三个无任何特殊性质的数进行排序,最少需要3次比较才能确定它们的正确顺序。</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>
|
||||
|
||||
6
Html/apps/static/cdnback/all.min.css
vendored
Normal file
6
Html/apps/static/cdnback/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
Html/apps/static/cdnback/axios.min.js
vendored
Normal file
3
Html/apps/static/cdnback/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2018
Html/apps/static/cdnback/bootstrap-icons.css
vendored
Normal file
2018
Html/apps/static/cdnback/bootstrap-icons.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
Html/apps/static/cdnback/bootstrap.min.css
vendored
Normal file
6
Html/apps/static/cdnback/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
Html/apps/static/cdnback/easymde.min.css
vendored
Normal file
7
Html/apps/static/cdnback/easymde.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
Html/apps/static/cdnback/easymde.min.js
vendored
Normal file
7
Html/apps/static/cdnback/easymde.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
Html/apps/static/cdnback/jquery.min.js
vendored
Normal file
2
Html/apps/static/cdnback/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
Html/apps/static/cdnback/markdown-it.min.js
vendored
Normal file
2
Html/apps/static/cdnback/markdown-it.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Html/apps/static/cdnback/marked.min.js
vendored
Normal file
1
Html/apps/static/cdnback/marked.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
Html/apps/static/cdnback/purify.min.js
vendored
Normal file
3
Html/apps/static/cdnback/purify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
Html/apps/static/cdnback/socket.io.min.js
vendored
Normal file
7
Html/apps/static/cdnback/socket.io.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
Html/apps/static/cdnback/split.min.js
vendored
Normal file
3
Html/apps/static/cdnback/split.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -125,7 +125,7 @@
|
||||
.tab-btn.active { background:#f0f8ff; border-color:#a0c4ff; }
|
||||
.actions { margin-left:auto; }
|
||||
.actions button { padding:6px 12px; border:none; border-radius:8px; background:#4CAF50; color:#fff; cursor:pointer; }
|
||||
.tab-content { flex:1; padding:8px; }
|
||||
.tab-content { flex:1; padding:8px;display: flex; }
|
||||
.tab { display:none; height:100%; }
|
||||
.tab.active { display:block; height:100%; }
|
||||
textarea {
|
||||
|
||||
63
Html/apps/static/css/lesson_markdown.css
Normal file
63
Html/apps/static/css/lesson_markdown.css
Normal file
@@ -0,0 +1,63 @@
|
||||
/* 让左侧编辑区真正占满并允许内部滚动 */
|
||||
.tab-content {
|
||||
height: calc(100vh - 220px);
|
||||
/* 不要隐藏滚动:*/
|
||||
overflow: visible; /* 或者干脆删掉 overflow 这行 */
|
||||
}
|
||||
|
||||
/* EasyMDE 容器按列铺开,编辑器本体撑满剩余空间 */
|
||||
.EasyMDEContainer {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 关键:让 CodeMirror 吃满容器高度 */
|
||||
.EasyMDEContainer .CodeMirror {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* 关键:滚动发生在 CodeMirror-scroll 里 */
|
||||
.EasyMDEContainer .CodeMirror-scroll {
|
||||
height: 100% !important;
|
||||
overflow: auto !important; /* 启用内部滚动条(也支持鼠标滚轮) */
|
||||
}
|
||||
|
||||
/* 右侧预览保持原样独立滚动 */
|
||||
.md-preview-pane {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.md-preview-pane h1, .md-preview-pane h2, .md-preview-pane h3 {
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
padding-bottom: .3em;
|
||||
margin-top: 1.2em;
|
||||
}
|
||||
.md-preview-pane img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
/* 只影响右侧 Markdown 预览里的表格 */
|
||||
.md-preview-pane .table {
|
||||
/* 如果你想完全摆脱 bootstrap.table 的影响 */
|
||||
all: revert; /* 或者 all: unset; 视需求选择 */
|
||||
display: table;
|
||||
border-collapse: collapse;
|
||||
/* 下面写你自己的表格样式 */
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.md-preview-pane .table th,
|
||||
.md-preview-pane .table td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.md-preview-pane .table thead th {
|
||||
background: #f8fafc;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -13,7 +13,39 @@
|
||||
font-size: 32px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
#chapter-list {
|
||||
/* 设置固定高度,超出范围将出现滚动 */
|
||||
max-height: 75vh;
|
||||
/* 超出高度时显示垂直滚动条 */
|
||||
overflow-y: auto;
|
||||
|
||||
/* 隐藏水平滚动条(如果不需要) */
|
||||
overflow-x: hidden;
|
||||
|
||||
/* 可选:添加一些内边距和边框增强视觉效果 */
|
||||
padding: 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 可选:美化滚动条样式(仅WebKit浏览器有效) */
|
||||
#chapter-list::-webkit-scrollbar {
|
||||
width: 6px; /* 滚动条宽度 */
|
||||
}
|
||||
|
||||
#chapter-list::-webkit-scrollbar-track {
|
||||
background: #f1f1f1; /* 滚动条轨道背景 */
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#chapter-list::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1; /* 滚动条滑块颜色 */
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#chapter-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8; /* 鼠标悬停时的滑块颜色 */
|
||||
}
|
||||
.course-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -801,10 +801,11 @@ function ValidTextareaNoBlankHead(textarea) {
|
||||
// 更新 textarea 的值(如果有变化)
|
||||
textarea.value = lines.join("\n");
|
||||
}
|
||||
const editors = document.querySelectorAll("textarea");
|
||||
const editorsDoc = document.querySelectorAll("textarea");
|
||||
const PREFIX = "### ";
|
||||
// 记录每个 textarea 最新的“标题内容”(不含前缀)
|
||||
const lastTitleMap = new Map();
|
||||
|
||||
// 防止循环触发
|
||||
let isProgrammaticUpdate = false;
|
||||
function getFirstLine(text) {
|
||||
@@ -823,7 +824,7 @@ const lastTitleMap = new Map();
|
||||
return { line: normalized, title };
|
||||
}
|
||||
// 初始化 lastTitleMap(把当前值规范化一次)
|
||||
editors.forEach(ed => {
|
||||
editorsDoc.forEach(ed => {
|
||||
const rawFirst = getFirstLine(ed.value || "");
|
||||
const { line, title } = normalizeFirstLine(rawFirst);
|
||||
if (line !== rawFirst) {
|
||||
@@ -837,7 +838,7 @@ const lastTitleMap = new Map();
|
||||
function syncOthers(sourceEditor, newTitle) {
|
||||
isProgrammaticUpdate = true;
|
||||
try {
|
||||
editors.forEach(ed => {
|
||||
editorsDoc.forEach(ed => {
|
||||
if (ed === sourceEditor) return;
|
||||
const first = getFirstLine(ed.value || "");
|
||||
const { line } = normalizeFirstLine(first);
|
||||
@@ -851,56 +852,204 @@ const lastTitleMap = new Map();
|
||||
isProgrammaticUpdate = false;
|
||||
}
|
||||
}
|
||||
editors.forEach(editor => {
|
||||
editor.addEventListener("input", (event) => {
|
||||
const textarea = event.target
|
||||
const value = event.target.value;
|
||||
const type = event.target.dataset.type;
|
||||
// 在这里做提示或校验
|
||||
console.log(`内容已更改: ${event.target.id} => ${value}`);
|
||||
|
||||
// ==== 前置:你的三编辑器实例与 id 映射(按你实际为准)====
|
||||
const ID_LESSON = 'editor-lesson';
|
||||
const ID_PROMPT = 'editor-prompt';
|
||||
const ID_SCORE = 'editor-score';
|
||||
|
||||
ValidTextareaNoBlankHead(event.target)
|
||||
// editors: { [textareaId]: EasyMDEInstance }
|
||||
const editorIds = [ID_LESSON, ID_PROMPT, ID_SCORE];
|
||||
|
||||
const rawFirst = getFirstLine(value);
|
||||
// 全局防抖/重入保护:避免程序化更新再触发同步
|
||||
|
||||
// // 记录上一次各编辑器的标题文本(不含 "### " 前缀)
|
||||
// const lastTitleMap = new Map();
|
||||
|
||||
// —— 工具:从 EasyMDE 拿值 / 设值(会同步 textarea,因为 forceSync:true)——
|
||||
const getMD = (id) => editors[id].value();
|
||||
const setMD = (id, text) => editors[id].value(text);
|
||||
|
||||
// —— 工具:仅替换“首行”为 normalizedLine,尽量不破坏光标与撤销栈 ——
|
||||
// line 是完整首行文本(含 "### ")
|
||||
function setFirstLineInEditor(ed, normalizedLine) {
|
||||
const cm = ed.codemirror;
|
||||
cm.operation(() => {
|
||||
const doc = cm.getDoc();
|
||||
const firstLine = doc.getLine(0) ?? "";
|
||||
if (firstLine === normalizedLine) return;
|
||||
|
||||
// 记住用户光标
|
||||
const cursors = doc.listSelections();
|
||||
|
||||
// 用 replaceRange 仅替换首行
|
||||
doc.replaceRange(
|
||||
normalizedLine,
|
||||
{ line: 0, ch: 0 },
|
||||
{ line: 0, ch: firstLine.length }
|
||||
);
|
||||
|
||||
// 尽力恢复光标(如果原先在首行,偏移可能要校正)
|
||||
doc.setSelections(cursors);
|
||||
});
|
||||
}
|
||||
|
||||
// —— 工具:从 Markdown 文本取首行(兼容你现有函数)
|
||||
function getFirstLineFromText(text) {
|
||||
const idx = text.indexOf('\n');
|
||||
return idx === -1 ? text : text.slice(0, idx);
|
||||
}
|
||||
|
||||
// —— 工具:把 normalizedLine 写回文本首行(备用:整段 setValue)——
|
||||
function setFirstLineInText(text, normalizedLine) {
|
||||
const idx = text.indexOf('\n');
|
||||
return idx === -1 ? normalizedLine : (normalizedLine + text.slice(idx));
|
||||
}
|
||||
|
||||
// —— 同步另外两个编辑器的首行(仅标题行),避免全文覆盖 ——
|
||||
function syncOthersTitle(fromId, titleText /*不含前缀*/, normalizedLine /*含### */) {
|
||||
editorIds.forEach((id) => {
|
||||
if (id === fromId) return;
|
||||
const ed = editors[id];
|
||||
if (!ed) return;
|
||||
|
||||
const currentFirst = ed.codemirror.getDoc().getLine(0) ?? "";
|
||||
if (currentFirst !== normalizedLine) {
|
||||
setFirstLineInEditor(ed, normalizedLine);
|
||||
lastTitleMap.set(ed, titleText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// —— 更新 tab 按钮上的 “*” 修改状态(保持你原逻辑)——
|
||||
function refreshTabDirtyBadges() {
|
||||
let btn = document.getElementById("tab-btn-lesson");
|
||||
if (CURRENT.active.lesson !== getMD(ID_LESSON)) {
|
||||
btn.innerText = AddModifiedStat(btn.innerText);
|
||||
} else {
|
||||
btn.innerText = ClearModifiedStat(btn.innerText);
|
||||
}
|
||||
|
||||
btn = document.getElementById("tab-btn-prompt");
|
||||
if (CURRENT.active.prompt !== getMD(ID_PROMPT)) {
|
||||
btn.innerText = AddModifiedStat(btn.innerText);
|
||||
} else {
|
||||
btn.innerText = ClearModifiedStat(btn.innerText);
|
||||
}
|
||||
|
||||
btn = document.getElementById("tab-btn-score");
|
||||
if (CURRENT.active.score !== getMD(ID_SCORE)) {
|
||||
btn.innerText = AddModifiedStat(btn.innerText);
|
||||
} else {
|
||||
btn.innerText = ClearModifiedStat(btn.innerText);
|
||||
}
|
||||
}
|
||||
|
||||
// —— 单个编辑器的 change 处理器工厂 ——
|
||||
function bindEditorChangeHandler(textareaId) {
|
||||
const ed = editors[textareaId];
|
||||
const cm = ed.codemirror;
|
||||
|
||||
// 初始化 lastTitleMap
|
||||
lastTitleMap.set(ed, "");
|
||||
|
||||
cm.on('change', () => {
|
||||
if (isProgrammaticUpdate) return;
|
||||
|
||||
const value = ed.value();
|
||||
const rawFirst = getFirstLineFromText(value);
|
||||
|
||||
// 你自己的校验(如果要继续保留)
|
||||
// ValidTextareaNoBlankHead(ed.element.nextSibling /* textarea DOM? */);
|
||||
|
||||
// 归一化首行,要求 "### " 前缀
|
||||
const { line: normalizedLine, title: currentTitle } = normalizeFirstLine(rawFirst);
|
||||
|
||||
// 若首行不规范,程序化改回去(仅替换首行)
|
||||
if (normalizedLine !== rawFirst) {
|
||||
isProgrammaticUpdate = true;// 把首行改回以 '### ' 开头
|
||||
isProgrammaticUpdate = true;
|
||||
try {
|
||||
textarea.value = setFirstLine(value, normalizedLine);
|
||||
setFirstLineInEditor(ed, normalizedLine);
|
||||
} finally {
|
||||
isProgrammaticUpdate = false;
|
||||
}
|
||||
}
|
||||
const lastTitle = lastTitleMap.get(textarea) ?? "";// —— 2) 如标题内容有变化,则同步到其他两个 —— //
|
||||
|
||||
// 标题变化则同步到其他两个编辑器(只同步首行)
|
||||
const lastTitle = lastTitleMap.get(ed) ?? "";
|
||||
if (currentTitle !== lastTitle) {
|
||||
lastTitleMap.set(textarea, currentTitle);// 更新自身记录
|
||||
syncOthers(textarea, currentTitle);
|
||||
lastTitleMap.set(ed, currentTitle);
|
||||
isProgrammaticUpdate = true;
|
||||
try {
|
||||
syncOthersTitle(textareaId, currentTitle, normalizedLine);
|
||||
} finally {
|
||||
isProgrammaticUpdate = false;
|
||||
}
|
||||
}
|
||||
|
||||
let btn = document.getElementById("tab-btn-lesson");
|
||||
if (CURRENT.active.lesson != document.getElementById("editor-lesson").value){
|
||||
btn.innerText = AddModifiedStat(btn.innerText)
|
||||
}else{
|
||||
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||
}
|
||||
btn = document.getElementById("tab-btn-prompt");
|
||||
if (CURRENT.active.prompt != document.getElementById("editor-prompt").value){
|
||||
btn.innerText = AddModifiedStat(btn.innerText)
|
||||
}else{
|
||||
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||
}
|
||||
btn = document.getElementById("tab-btn-score");
|
||||
if (CURRENT.active.score != document.getElementById("editor-score").value){
|
||||
btn.innerText = AddModifiedStat(btn.innerText)
|
||||
}else{
|
||||
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||
// 刷新三个 Tab 的“修改中 * ”标识
|
||||
refreshTabDirtyBadges();
|
||||
|
||||
// 如果你还有右侧预览:
|
||||
if (typeof renderToPreview === 'function' && textareaId === activeId) {
|
||||
renderToPreview(ed.value());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定三个编辑器
|
||||
editorIds.forEach(bindEditorChangeHandler);
|
||||
|
||||
// editorsDoc.forEach(editor => {
|
||||
// editor.addEventListener("input", (event) => {
|
||||
// const textarea = event.target
|
||||
// const value = event.target.value;
|
||||
// const type = event.target.dataset.type;
|
||||
// // 在这里做提示或校验
|
||||
// console.log(`内容已更改: ${event.target.id} => ${value}`);
|
||||
|
||||
// ValidTextareaNoBlankHead(event.target)
|
||||
|
||||
// const rawFirst = getFirstLine(value);
|
||||
// const { line: normalizedLine, title: currentTitle } = normalizeFirstLine(rawFirst);
|
||||
|
||||
// if (normalizedLine !== rawFirst) {
|
||||
// isProgrammaticUpdate = true;// 把首行改回以 '### ' 开头
|
||||
// try {
|
||||
// textarea.value = setFirstLine(value, normalizedLine);
|
||||
// } finally {
|
||||
// isProgrammaticUpdate = false;
|
||||
// }
|
||||
// }
|
||||
// const lastTitle = lastTitleMap.get(textarea) ?? "";// —— 2) 如标题内容有变化,则同步到其他两个 —— //
|
||||
// if (currentTitle !== lastTitle) {
|
||||
// lastTitleMap.set(textarea, currentTitle);// 更新自身记录
|
||||
// syncOthers(textarea, currentTitle);
|
||||
// }
|
||||
|
||||
// let btn = document.getElementById("tab-btn-lesson");
|
||||
// if (CURRENT.active.lesson != document.getElementById("editor-lesson").value){
|
||||
// btn.innerText = AddModifiedStat(btn.innerText)
|
||||
// }else{
|
||||
// btn.innerText = ClearModifiedStat(btn.innerText)
|
||||
// }
|
||||
// btn = document.getElementById("tab-btn-prompt");
|
||||
// if (CURRENT.active.prompt != document.getElementById("editor-prompt").value){
|
||||
// btn.innerText = AddModifiedStat(btn.innerText)
|
||||
// }else{
|
||||
// btn.innerText = ClearModifiedStat(btn.innerText)
|
||||
// }
|
||||
// btn = document.getElementById("tab-btn-score");
|
||||
// if (CURRENT.active.score != document.getElementById("editor-score").value){
|
||||
// btn.innerText = AddModifiedStat(btn.innerText)
|
||||
// }else{
|
||||
// btn.innerText = ClearModifiedStat(btn.innerText)
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
|
||||
// 把“某个步骤”对应的三个片段载入到三个编辑器
|
||||
// 把“某个步骤”对应的三个片段载入到三个编辑器
|
||||
function loadStepToEditors(phaseText, stepText) {
|
||||
// 三份文档里都取对应 phase/step 的片段(不存在则回退)
|
||||
const L = CURRENT.parsed.lesson, P = CURRENT.parsed.prompt, S = CURRENT.parsed.score;
|
||||
@@ -918,9 +1067,9 @@ function loadStepToEditors(phaseText, stepText) {
|
||||
const pText = sliceSection(subP, stepText, 3) || promptPhase;
|
||||
const sText = sliceSection(subS, stepText, 3) || scorePhase;
|
||||
|
||||
document.getElementById('editor-lesson').value = lText;
|
||||
document.getElementById('editor-prompt').value = pText;
|
||||
document.getElementById('editor-score').value = sText;
|
||||
editors['editor-lesson'].value(lText);
|
||||
editors['editor-prompt'].value(pText);
|
||||
editors['editor-score'].value(sText);
|
||||
|
||||
CURRENT.active={ //保存打开时的三个文本
|
||||
phase:phaseText,
|
||||
@@ -938,16 +1087,32 @@ function loadStepToEditors(phaseText, stepText) {
|
||||
|
||||
}
|
||||
|
||||
// Tab 切换
|
||||
document.addEventListener('click', (e)=>{
|
||||
if (e.target.classList.contains('tab-btn')) {
|
||||
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
||||
e.target.classList.add('active');
|
||||
const id = e.target.dataset.target;
|
||||
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
||||
document.getElementById(id).classList.add('active');
|
||||
}
|
||||
});
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('tab-btn')) {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
e.target.classList.add('active');
|
||||
|
||||
const id = e.target.dataset.target;
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.getElementById(id).classList.add('active');
|
||||
|
||||
// 广播 tab 变更(告诉别人当前激活的 tab id / 以及要用哪个 editorId)
|
||||
const editorIdMap = {
|
||||
'tab-lesson' : 'editor-lesson',
|
||||
'tab-prompt' : 'editor-prompt',
|
||||
'tab-score' : 'editor-score',
|
||||
};
|
||||
const activeBtnId = e.target.id; // 如 'tab-btn-lesson'
|
||||
const editorId =
|
||||
activeBtnId === 'tab-btn-lesson' ? 'editor-lesson' :
|
||||
activeBtnId === 'tab-btn-prompt' ? 'editor-prompt' :
|
||||
'editor-score';
|
||||
|
||||
document.dispatchEvent(new CustomEvent('tab:change', {
|
||||
detail: { tabId: id, editorId }
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// 保存全部:将三个编辑器中的文本保存回相应的 markdown 文件
|
||||
document.getElementById('save-all-btn')?.addEventListener('click', async () => {
|
||||
|
||||
198
Html/apps/static/js/lesson_markdown.js
Normal file
198
Html/apps/static/js/lesson_markdown.js
Normal file
@@ -0,0 +1,198 @@
|
||||
const editors = {}; // id -> EasyMDE
|
||||
(function(){
|
||||
// ====== 配置:图片上传接口 ======
|
||||
const UPLOAD_URL = '/api/upload'; // 返回 { url: 'https://...' }
|
||||
|
||||
// ====== Markdown 渲染器 ======
|
||||
const md = window.markdownit({
|
||||
html: false, // 禁止原生HTML,安全些(如需支持可设为 true)
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: true
|
||||
});
|
||||
|
||||
const $preview = document.getElementById('md-preview');
|
||||
|
||||
// ====== 三个 textarea 的 EasyMDE 实例 ======
|
||||
const textareas = [
|
||||
{ id: 'editor-lesson', tabBtnId: 'tab-btn-lesson' },
|
||||
{ id: 'editor-prompt', tabBtnId: 'tab-btn-prompt' },
|
||||
{ id: 'editor-score', tabBtnId: 'tab-btn-score' },
|
||||
];
|
||||
|
||||
let activeId = 'editor-lesson'; // 默认激活
|
||||
|
||||
// 工具:将 Markdown 渲染到右侧预览
|
||||
const renderToPreview = (markdownStr) => {
|
||||
const rawHtml = md.render(markdownStr || '');
|
||||
const safe = DOMPurify.sanitize(rawHtml);
|
||||
$preview.innerHTML = safe;
|
||||
};
|
||||
|
||||
// 工具:节流/防抖
|
||||
function debounce(fn, delay){
|
||||
let t;
|
||||
return function(...args){
|
||||
clearTimeout(t);
|
||||
t = setTimeout(()=>fn.apply(this,args), delay);
|
||||
}
|
||||
}
|
||||
const debouncedPreview = debounce(renderToPreview, 120);
|
||||
|
||||
// 工具:插入文本到当前光标
|
||||
function insertAtCursor(cm, text){
|
||||
const doc = cm.getDoc();
|
||||
const cursor = doc.getCursor();
|
||||
doc.replaceRange(text, cursor);
|
||||
}
|
||||
|
||||
// 图片上传:给粘贴/拖拽/按钮选择共用
|
||||
async function uploadFile(file){
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const { data } = await axios.post(UPLOAD_URL, form, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
if(!data || !data.url){
|
||||
throw new Error('上传返回缺少 url 字段');
|
||||
}
|
||||
return data.url;
|
||||
}
|
||||
|
||||
// 为一个 EasyMDE 实例注入图片粘贴/拖拽/按钮上传
|
||||
function bindImageHandlers(easy){
|
||||
const cm = easy.codemirror;
|
||||
|
||||
// 1) 粘贴图片
|
||||
cm.on('paste', async (cmInst, e)=>{
|
||||
const items = (e.clipboardData || e.originalEvent?.clipboardData)?.items || [];
|
||||
for(const it of items){
|
||||
if(it.kind === 'file'){
|
||||
const file = it.getAsFile();
|
||||
if(file && file.type.startsWith('image/')){
|
||||
e.preventDefault();
|
||||
try{
|
||||
const url = await uploadFile(file);
|
||||
insertAtCursor(cmInst, ``);
|
||||
}catch(err){
|
||||
console.error(err);
|
||||
alert('图片上传失败,请重试');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2) 拖拽图片
|
||||
easy.codemirror.getWrapperElement().addEventListener('drop', async (e)=>{
|
||||
const files = e.dataTransfer?.files || [];
|
||||
if(!files.length) return;
|
||||
const img = Array.from(files).find(f=>f.type.startsWith('image/'));
|
||||
if(!img) return;
|
||||
e.preventDefault();
|
||||
try{
|
||||
const url = await uploadFile(img);
|
||||
insertAtCursor(cm, ``);
|
||||
}catch(err){
|
||||
console.error(err);
|
||||
alert('图片上传失败,请重试');
|
||||
}
|
||||
});
|
||||
|
||||
// 3) 工具栏“上传图片”按钮:使用隐藏 input[type=file]
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
fileInput.addEventListener('change', async ()=>{
|
||||
const file = fileInput.files?.[0];
|
||||
if(!file) return;
|
||||
try{
|
||||
const url = await uploadFile(file);
|
||||
insertAtCursor(cm, ``);
|
||||
}catch(err){
|
||||
console.error(err);
|
||||
alert('图片上传失败,请重试');
|
||||
}finally{
|
||||
fileInput.value = '';
|
||||
}
|
||||
});
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
// 将 input 触发器挂到 toolbar 按钮上(下面初始化里有 name: 'upload-image')
|
||||
easy.toolbar?.forEach(btn=>{
|
||||
if(btn?.name === 'upload-image'){
|
||||
btn.action = () => fileInput.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 创建并初始化一个 EasyMDE
|
||||
function createMDEFor(textareaId){
|
||||
const el = document.getElementById(textareaId);
|
||||
if(!el) return null;
|
||||
|
||||
const mde = new EasyMDE({
|
||||
element: el,
|
||||
autoDownloadFontAwesome: false,
|
||||
spellChecker: false,
|
||||
status: false,
|
||||
autofocus: textareaId === activeId,
|
||||
minHeight: '100%',
|
||||
renderingConfig: { singleLineBreaks: false },
|
||||
uploadImage: false, // 我们自定义上传
|
||||
toolbar: [
|
||||
'bold','italic','heading','|',
|
||||
'quote','unordered-list','ordered-list','table','|',
|
||||
'code','link',
|
||||
{ name:'upload-image', action:null, className:'fa fa-image', title:'插入图片(上传)' },
|
||||
'|','preview','side-by-side','fullscreen',
|
||||
'|','guide'
|
||||
],
|
||||
// 保证 textarea 同步(默认就会同步 value;保存时你的原逻辑仍可读取)
|
||||
forceSync: true
|
||||
});
|
||||
|
||||
// 内容变化 -> 刷新预览(仅激活编辑器触发)
|
||||
mde.codemirror.on('change', ()=>{
|
||||
if(textareaId === activeId){
|
||||
debouncedPreview(mde.value());
|
||||
}
|
||||
});
|
||||
|
||||
bindImageHandlers(mde);
|
||||
return mde;
|
||||
}
|
||||
|
||||
// 初始化三个编辑器
|
||||
for(const t of textareas){
|
||||
editors[t.id] = createMDEFor(t.id);
|
||||
}
|
||||
|
||||
// 初次渲染预览
|
||||
debouncedPreview(editors[activeId]?.value() || '');
|
||||
|
||||
// 监听 Tab 切换按钮,让预览跟随当前激活编辑器
|
||||
function setActiveEditor(id){
|
||||
activeId = id;
|
||||
// 确保 EasyMDE 的编辑区获得焦点(可选)
|
||||
const ed = editors[id];
|
||||
if(ed){
|
||||
ed.codemirror.refresh();
|
||||
ed.codemirror.focus();
|
||||
renderToPreview(ed.value());
|
||||
}
|
||||
}
|
||||
document.addEventListener('tab:change', (e) => {
|
||||
const { editorId } = e.detail || {};
|
||||
if (editorId) setActiveEditor(editorId);
|
||||
});
|
||||
|
||||
// 如果你已有 tab 切换逻辑(例如给 .tab-btn 切换 active 类),保留原逻辑即可;
|
||||
// 我们只需在切换时调用 setActiveEditor 对齐预览内容。
|
||||
})();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -450,6 +450,7 @@ async function persistOrderNow(chapters) {
|
||||
});
|
||||
if (!res.ok) throw new Error('保存新顺序失败');
|
||||
console.log('已保存顺序')
|
||||
console.log(payload);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
BIN
Html/apps/static/webfonts/fa-regular-400.woff2
Normal file
BIN
Html/apps/static/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
Html/apps/static/webfonts/fa-solid-900.woff2
Normal file
BIN
Html/apps/static/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
Reference in New Issue
Block a user