Files
hsa/Html/apps/static/68bacdfadf5aeae0912f7f18-第四章:查询-分位统计量.html
2025-11-17 14:04:52 +08:00

198 lines
8.2 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>
<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>第K大的数</h3>
<p>在许多算法与数据分析任务中,我们不仅需要排序整个数组,还需要找出某个特定顺序统计量,例如:
• 一个数组中第 K 大的元素;
• 所有元素的中位数K = n/2
• 某个分位点25%、75%等)…
<br>
最直接的办法是先对数组进行排序,然后取第 K 个位置的元素。这种方法的时间复杂度为 <eq>O(n \log n)</eq>,因为我们需要对所有元素进行排序。
<br>
但我们真正关心的,只是一个位置的元素 —— 我们是否有更快的方法?
<br></p>
<h4>快速选择QuickSelect</h4>
<p><strong>快排中的“轴枢”启发</strong>
我们在快速排序中,每次选一个枢轴 pivot通过划分操作把数组分成两部分
• 左边元素 &lt;= pivot
• 右边元素 &gt;= pivot。
最终 pivot 会被放置固定位置上。
<br>
可见:</p>
<blockquote>
<p>若 pivot 恰好是我们要找的第 K 大元素,就不必继续递归。</p>
</blockquote>
<br>
因此可得快速选择的具体做法先令target等于N-K即找第N-K+1小的数其下标为target
1. 随机选一个枢轴;
2. 利用快速排序的 partition 划分函数,找出 pivot 的位置 idx
3. 比较该位置与目标索引:
• 若 idx == target返回 pivot
• 若 idx > target在左半边递归查找
• 否则在右半边递归查找。
<br>
理解做法并分析时间复杂度。
### 快速选择的实现
<p>实现一个 <code>quickselect(arr, k)</code> 函数,返回第 K 大的元素
也就是增序列下标N-K位置的数</p>
<pre><code class="language-python">import random
def partition(arr, low, high):
&quot;&quot;&quot;
对arr序列从low到high下标找到一个枢轴并返回其下标。在arr中比枢轴小的数都在idx左边大的在右边
&quot;&quot;&quot;
def quickselect(arr, k):
&quot;&quot;&quot;
快速选择第K大元素转换为第n-k小
:param arr: 输入数组
:param k: 要找的第K大元素
:return: 该元素值
&quot;&quot;&quot;
n = len(arr)
target = n - k # 第k大转为第n-k小序列下标
low, high = 0, n - 1
while low &lt;= high:
idx = partition(arr, low, high)
if idx == target:
return arr[idx]
elif idx &lt; target:
low = idx + 1
else:
high = idx - 1
if __name__ == &quot;__main__&quot;:
#测试1随机数组
random.seed(42) # 设置随机种子,确保结果可重现
arr1 = [random.randint(1, 100) for _ in range(10)]
k1 = 3
result1 = quickselect(arr1.copy(), k1) # 使用副本避免修改原数组
sorted_arr1 = sorted(arr1)
expected1 = sorted_arr1[-k1]
print(f&quot;测试1 - 随机数组:&quot;)
print(f&quot;原数组: {arr1}&quot;)
print(f&quot;第{k1}大元素: {result1}, 预期值: {expected1}&quot;)
print(f&quot;测试{'通过' if result1 == expected1 else '失败'}\n&quot;)
#测试2包含重复元素的数组
arr2 = [5, 3, 8, 8, 1, 5, 9, 3, 5]
k2 = 2
result2 = quickselect(arr2.copy(), k2)
sorted_arr2 = sorted(arr2)
expected2 = sorted_arr2[-k2]
print(f&quot;测试2 - 包含重复元素:&quot;)
print(f&quot;原数组: {arr2}&quot;)
print(f&quot;第{k2}大元素: {result2}, 预期值: {expected2}&quot;)
print(f&quot;测试{'通过' if result2 == expected2 else '失败'}\n&quot;)
#测试3边界条件 - k=1最大元素
arr3 = [10, 20, 30, 40, 50]
k3 = 1
result3 = quickselect(arr3.copy(), k3)
sorted_arr3 = sorted(arr3)
expected3 = sorted_arr3[-k3]
print(f&quot;测试3 - 最大元素:&quot;)
print(f&quot;原数组: {arr3}&quot;)
print(f&quot;第{k3}大元素: {result3}, 预期值: {expected3}&quot;)
print(f&quot;测试{'通过' if result3 == expected3 else '失败'}\n&quot;)
</code></pre>
<h3>BFPRT 算法:中位数的中位数算法</h3>
<p>快速选择的平均复杂度为 <eq>O(n)</eq>,但可能退化为 <eq>O(n^2)</eq></p>
<h4>BFPRT分治法</h4>
<p>BFPRT算法和二分查找中对“矩阵”进行分治的做法很像也是通过矩阵的中心数来排除一部分数据。
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_4d626b28-8e7e-4c19-8238-daaaba079a5f" alt="image" />
<br>
我们通过以下几个步骤确保选出的“轴枢”可以高概率地“削减”足够多的元素,从而保证递归过程每次至少减少 30% 左右的输入规模:
<br></p>
<ol>
<li>将原始数组划分为若干个 5 个元素的组
请看图中所有的竖列(粉红色背景小矩形):每个小矩形内有 5 个点,这就是我们将原始数据分成的小组。
组的个数 ≈ n / 5设 n 是总元素数量)
不足 5 个的组,可以用一个全局最小值(如 -1补全。
<br></li>
<li>对每组内部排序,取出中位数
图中每个竖列的小组中,绿色圆点表示每组的元素。中间处于红框中的数为每组中位数。
每列上两个数小于中位数,下两个大于。
<br></li>
<li>将所有组的中位数组成新数组,对其递归调用本算法找中位数。
图中红色矩形框住的内容,正是“所有组中位数”的集合。这些点来自所有小组的中位点,组成新的长度为 n/5 的数组。
对这个新数组再次使用本算法,递归找出它的中位数,作为我们最终要用的“枢轴”。即图中棕色点。
<br></li>
<li>这个中位数(棕色圆点)就是我们分治的“枢轴”
有如下重要性质:
a. 棕点及左上角(图中蓝框的点)均小于等于枢轴
b. 棕点及右下角(图中黑框的点)均大于等于枢轴
<br></li>
<li>进行排除分解先令x=n-k+1找第k大数就是找第x小数
a. 若x&gt;蓝框数数量则在非蓝框数中找第k大
b. 若k&gt;黑框数数量则在非黑框数中找第k-黑框数数量)大
c. 当k=1或n'n'是当前子问题的数数量或n'&lt;=5均可在不大于O(n')的用时完成找数。
可以证明在c不成立的情况下a,b两点至少成立一个。
<br></li>
</ol>
<p>与AI教师讨论每一步在做的事和最终想要实现的效果。可以联想你之前学习到的二分查找中的矩阵分治。</p>
<h3>BFPRT 复杂度分析</h3>
<p>在理解了“中位数的中位数”算法的流程之后,我们接下来要正式分析它的<strong>时间复杂度</strong>,并证明该算法的整体运行时间为 <eq>O(n)</eq></p>
<hr />
<h4>写出递推式</h4>
<p>根据 BFPRT 算法的执行过程依次与AI教师讨论每步的时间复杂度
<br></p>
<ol>
<li><strong><eq>n</eq> 个元素分组、找出每组中位数</strong>
<ul>
<li>分成 <eq>n/5</eq> 组,每组排序并取中位数。
<br></li>
</ul>
</li>
<li><strong>递归地找所有中位数的中位数(枢轴)</strong>
<ul>
<li>子问题规模为 <eq>n/5</eq>
<br></li>
</ul>
</li>
<li><strong>额外一次线性 partition</strong>
<ul>
<li>对全数组按枢轴划分:蓝框部分、黑框部分,此外递归时是对非蓝框或非黑框的补集部分。
<br></li>
</ul>
</li>
<li><strong>将原始数组按枢轴划分,并在一侧继续递归</strong>
<ul>
<li>对非蓝框或非黑框数进行递归查询。
<br></li>
</ul>
</li>
</ol>
<p><img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_4d626b28-8e7e-4c19-8238-daaaba079a5f" alt="image" /></p>
<p>从而得到最终的递推式。</p>
<hr />
<h4>证明 <eq>T(n) = O(n)</eq></h4>
<p>这里的证明并不复杂,使用高中学习的数学归纳法即可。
提示先假设满足T(n)&lt;=cn然后证明递推式&lt;=d*cn&lt;=cn其中d小于1。</p>
<p>与AI教师探讨证明流程。</p>
</body>
</html>