分治算法在信息学奥赛中的地位
分治算法(Divide and Conquer)是一种重要的算法设计范式,它通过将复杂问题分解为规模较小的相似子问题,递归地解决子问题,最后合并子问题的解得到原问题的解。
在信息学奥赛中,分治算法是解决大型复杂问题的利器,尤其在处理排序、查找、矩阵运算、几何问题等方面表现出色。掌握分治算法不仅能帮助你解决特定问题,更能培养你将复杂问题分解为简单问题的思维能力。
分治算法基本概念
分治算法(Divide and Conquer)的核心思想是"分而治之",即将一个复杂的问题分解成两个或更多个相同或相似的子问题,直到子问题可以简单地直接求解,然后将子问题的解合并,得到原问题的解。
分治算法的特点
-
•
问题可分解:原问题可以分解为若干个规模较小的相同类型子问题
-
•
子问题独立:子问题之间相互独立,不包含重叠的子问题
-
•
递归求解:子问题的解法与原问题相同,可以递归求解
-
•
合并解:需要将子问题的解合并,得到原问题的解
分治算法的基本步骤
分解(Divide)
将原问题分解为若干个规模较小、相互独立、与原问题形式相同的子问题
解决(Conquer)
若子问题规模较小而容易解决则直接解决,否则递归解决各个子问题
合并(Combine)
将各个子问题的解合并为原问题的解
分治与其他算法的对比
算法类型 | 核心思想 | 主要应用 | 时间复杂度 |
---|---|---|---|
分治算法 | 分解为子问题,递归求解后合并 | 排序、查找、矩阵运算、几何问题 | 通常为O(n log n) |
贪心算法 | 每步选择局部最优,不回溯 | 优化问题、区间问题、图论问题 | 通常为O(n log n) |
动态规划 | 存储子问题结果,解决重叠子问题 | 最优子结构问题、计数问题 | 通常为O(n²)或O(n³) |
回溯算法 | 尝试所有可能,失败则回溯 | 排列组合、子集问题、搜索问题 | 通常为指数级 |
分治算法的核心原理
分治算法的有效性基于问题的两个关键性质:问题可分解性和最优子结构性质。理解这些性质有助于我们判断一个问题是否适合用分治算法解决。
分治算法的适用条件
-
1.
问题规模缩小后更容易解决
当问题规模足够小时,可以直接求解
-
2.
问题可以分解为形式相同的子问题
原问题和子问题具有相同的结构和求解方法
-
3.
子问题的解可以合并为原问题的解
存在有效的方法将子问题的解合并
-
4.
子问题相互独立
子问题之间不包含重叠的子问题(否则更适合动态规划)
分治算法的时间复杂度分析
分治算法的时间复杂度通常可以用递归关系式表示,对于一个规模为n的问题:
其中:
- n:问题规模
- a:子问题数量
- n/b:每个子问题的规模(假设均匀分割)
- f(n):分解和合并子问题的时间
可以使用主方法(Master Theorem)求解这类递归关系式:
- 若f(n) = O(n^log_b a-ε),则T(n) = Θ(n^log_b a)
- 若f(n) = Θ(n^log_b a),则T(n) = Θ(n^log_b a log n)
- 若f(n) = Ω(n^log_b a+ε),且a·f(n/b) ≤ c·f(n),则T(n) = Θ(f(n))
递归与分治的关系
分治算法通常与递归紧密相关,但两者并不等同:
递归是实现分治的常用手段
递归为分治算法提供了自然的实现方式,通过函数自身调用来处理规模逐渐缩小的子问题。但分治也可以用非递归方式实现(如使用栈)。
分治是一种算法设计思想
递归是一种编程技术,而分治是一种算法设计范式。分治关注如何将问题分解为子问题并合并结果,而递归关注如何通过自我调用来解决问题。
分治算法的递归实现框架
// 分治算法的递归实现框架
Result divideAndConquer(Problem problem) {
// 基本情况:问题规模足够小,直接求解
if (problem.size <= threshold) {
return solveBaseCase(problem);
}
// 分解:将问题分解为子问题
vector<Problem> subproblems = splitProblem(problem);
// 解决:递归求解每个子问题
vector<Result> subresults;
for (auto& subproblem : subproblems) {
subresults.push_back(divideAndConquer(subproblem));
}
// 合并:将子问题的解合并为原问题的解
return combineResults(subresults);
}
分治算法经典例题解析
归并排序
问题描述:使用分治算法实现归并排序,对一个数组进行升序排序。归并排序是分治算法的经典应用,具有稳定的O(n log n)时间复杂度。
解题思路:
归并排序的核心思想是将数组不断二分,直到子数组长度为1,然后将两个有序子数组合并为一个有序数组。
- 分解:将数组分成两个规模大致相等的子数组
- 解决:递归地对两个子数组进行归并排序
- 合并:将两个已排序的子数组合并成一个有序数组
代码实现
// 归并排序的分治实现
#include <iostream>
#include <vector>
using namespace std;
// 合并两个有序数组
void merge(vector<int>& arr, int left, int mid, int right) {
int n1 = mid - left + 1; // 左子数组大小
int n2 = right - mid; // 右子数组大小
// 创建临时数组
vector<int> L(n1), R(n2);
// 复制数据到临时数组
for (int i = 0; i < n1; i++)
L[i] = arr[left + i];
for (int j = 0; j < n2; j++)
R[j] = arr[mid + 1 + j];
// 合并临时数组到原数组
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
// 复制剩余元素
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}
// 归并排序主函数
void mergeSort(vector<int>& arr, int left, int right) {
// 基本情况:如果子数组长度为1或0,直接返回
if (left >= right)
return;
// 分解:找到中间点
int mid = left + (right - left) / 2; // 避免溢出
// 解决:递归排序左右子数组
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
// 合并:合并两个已排序的子数组
merge(arr, left, mid, right);
}
// 辅助函数:打印数组
void printArray(const vector<int>& arr) {
for (int num : arr) {
cout << num << " ";
}
cout << endl;
}
int main() {
vector<int> arr = {12, 11, 13, 5, 6, 7};
cout << "排序前的数组: ";
printArray(arr);
mergeSort(arr, 0, arr.size() - 1);
cout << "排序后的数组: ";
printArray(arr);
return 0;
}
算法分析
- 时间复杂度:O(n log n),在最好、最坏和平均情况下均为此值
- 空间复杂度:O(n),需要额外的存储空间来合并数组
- 稳定性:稳定排序,相等元素的相对顺序不会改变
- 递归关系式:T(n) = 2T(n/2) + O(n),根据主方法,解为O(n log n)
快速排序
问题描述:使用分治算法实现快速排序,对一个数组进行升序排序。快速排序是实践中最常用的排序算法之一,平均时间复杂度为O(n log n)。
解题思路:
快速排序的核心思想是选择一个基准元素,将数组分为两部分,一部分所有元素小于等于基准,另一部分所有元素大于基准,然后递归地对这两部分进行排序。
- 分解:选择一个基准元素,将数组分为两个子数组
- 解决:递归地对两个子数组进行快速排序
- 合并:子数组已经有序,不需要显式合并
代码实现
// 快速排序的分治实现
#include <iostream>
#include <vector>
#include <algorithm> // 用于swap
using namespace std;
// 划分函数:选择基准并划分数组
int partition(vector<int>& arr, int low, int high) {
// 选择最右边的元素作为基准
int pivot = arr[high];
// i是小于基准区域的边界
int i = low - 1;
// 遍历数组,将小于基准的元素放到左边
for (int j = low; j <= high - 1; j++) {
if (arr[j] <= pivot) {
i++;
swap(arr[i], arr[j]);
}
}
// 将基准放到正确的位置
swap(arr[i + 1], arr[high]);
return i + 1; // 返回基准的位置
}
// 快速排序主函数
void quickSort(vector<int>& arr, int low, int high) {
// 基本情况:如果子数组长度为1或0,直接返回
if (low >= high)
return;
// 分解:划分数组,获取基准位置
int pi = partition(arr, low, high);
// 解决:递归排序基准左右的子数组
quickSort(arr, low, pi - 1); // 排序左半部分
quickSort(arr, pi + 1, high); // 排序右半部分
// 不需要显式合并,因为子数组已经有序且基准在正确位置
}
// 辅助函数:打印数组
void printArray(const vector<int>& arr) {
for (int num : arr) {
cout << num << " ";
}
cout << endl;
}
int main() {
vector<int> arr = {10, 7, 8, 9, 1, 5};
cout << "排序前的数组: ";
printArray(arr);
quickSort(arr, 0, arr.size() - 1);
cout << "排序后的数组: ";
printArray(arr);
return 0;
}
算法分析
- 平均时间复杂度:O(n log n)
- 最坏时间复杂度:O(n²)(当数组已经有序或逆序时)
- 空间复杂度:O(log n)(递归调用栈)
- 稳定性:不稳定排序
- 优化技巧:随机选择基准元素或三数取中法选择基准,避免最坏情况
二分查找
问题描述:使用分治算法实现二分查找,在一个有序数组中查找目标元素的位置。如果找到,返回其索引;否则,返回-1。
解题思路:
二分查找的核心思想是将有序数组不断二分,通过比较目标元素与中间元素的大小,缩小查找范围,直到找到目标元素或确定目标元素不存在。
- 分解:计算中间位置,将数组分为两部分
- 解决:
- 如果中间元素等于目标,查找成功
- 如果中间元素大于目标,在左半部分查找
- 如果中间元素小于目标,在右半部分查找
- 合并:不需要合并步骤,找到目标后直接返回
代码实现
// 二分查找的分治实现
#include <iostream>
#include <vector>
using namespace std;
// 递归实现二分查找
int binarySearchRecursive(const vector<int>& arr, int left, int right, int target) {
// 基本情况:查找范围为空,目标不存在
if (left > right)
return -1;
// 分解:计算中间位置
int mid = left + (right - left) / 2; // 避免溢出
// 解决:比较中间元素与目标
if (arr[mid] == target)
return mid; // 找到目标,返回索引
// 如果中间元素大于目标,在左半部分查找
if (arr[mid] > target)
return binarySearchRecursive(arr, left, mid - 1, target);
// 否则,在右半部分查找
return binarySearchRecursive(arr, mid + 1, right, target);
}
// 非递归实现二分查找
int binarySearchIterative(const vector<int>& arr, int target) {
int left = 0;
int right = arr.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target)
return mid;
if (arr[mid] > target)
right = mid - 1;
else
left = mid + 1;
}
return -1; // 目标不存在
}
int main() {
vector<int> arr = {2, 3, 4, 10, 40};
int target = 10;
// 递归方式查找
int resultRecursive = binarySearchRecursive(arr, 0, arr.size() - 1, target);
if (resultRecursive != -1)
cout << "递归查找:元素 " << target << " 存在于索引 " << resultRecursive << endl;
else
cout << "递归查找:元素 " << target << " 不存在于数组中" << endl;
// 非递归方式查找
int resultIterative = binarySearchIterative(arr, target);
if (resultIterative != -1)
cout << "非递归查找:元素 " << target << " 存在于索引 " << resultIterative << endl;
else
cout << "非递归查找:元素 " << target << " 不存在于数组中" << endl;
return 0;
}
算法分析
- 时间复杂度:O(log n),每次查找都会将问题规模减半
- 空间复杂度:递归实现为O(log n),非递归实现为O(1)
- 适用条件:必须是有序数组,且支持随机访问
- 递归关系式:T(n) = T(n/2) + O(1),根据主方法,解为O(log n)
最大子数组和(Kadane算法的分治实现)
问题描述:给定一个整数数组,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。这是LeetCode第53题的分治解法。
解题思路:
使用分治算法解决最大子数组问题,核心思想是最大子数组要么在左半部分,要么在右半部分,要么跨越左右两部分。
- 分解:将数组分为左右两个子数组
- 解决:
- 递归求解左子数组的最大子数组和
- 递归求解右子数组的最大子数组和
- 求解跨越左右子数组的最大子数组和
- 合并:返回三个结果中的最大值
代码实现
// 最大子数组和的分治实现
#include <iostream>
#include <vector>
#include <climits> // 用于INT_MIN
#include <algorithm> // 用于max
using namespace std;
// 查找跨越中点的最大子数组和
int maxCrossingSum(const vector<int>& arr, int left, int mid, int right) {
// 从中间向左查找最大和
int sum = 0;
int leftSum = INT_MIN;
for (int i = mid; i >= left; i--) {
sum += arr[i];
if (sum > leftSum)
leftSum = sum;
}
// 从中间向右查找最大和
sum = 0;
int rightSum = INT_MIN;
for (int i = mid + 1; i <= right; i++) {
sum += arr[i];
if (sum > rightSum)
rightSum = sum;
}
// 返回跨越中点的最大和
return leftSum + rightSum;
}
// 分治算法求解最大子数组和
int maxSubarraySum(const vector<int>& arr, int left, int right) {
// 基本情况:子数组只有一个元素
if (left == right)
return arr[left];
// 分解:找到中间点
int mid = left + (right - left) / 2;
// 解决:递归求解左右子数组和跨越子数组
int leftSum = maxSubarraySum(arr, left, mid);
int rightSum = maxSubarraySum(arr, mid + 1, right);
int crossSum = maxCrossingSum(arr, left, mid, right);
// 合并:返回三个结果中的最大值
return max({leftSum, rightSum, crossSum});
}
int main() {
vector<int> arr = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
cout << "数组: ";
for (int num : arr) {
cout << num << " ";
}
cout << endl;
int maxSum = maxSubarraySum(arr, 0, arr.size() - 1);
cout << "最大子数组和: " << maxSum << endl; // 应该输出6,对应子数组[4,-1,2,1]
return 0;
}
算法分析
- 时间复杂度:O(n log n),每次递归将问题规模减半,合并步骤需要O(n)时间
- 空间复杂度:O(log n),递归调用栈的深度
- 对比Kadane算法:Kadane算法时间复杂度为O(n),但分治算法展示了分治思想的应用
- 递归关系式:T(n) = 2T(n/2) + O(n),根据主方法,解为O(n log n)
最近点对问题
问题描述:给定平面上n个点,找到距离最近的一对点。这是计算几何中的经典问题,分治算法可以高效地解决这个问题。
解题思路:
最近点对问题的分治解法将平面上的点分为左右两部分,分别求解左右两部分的最近点对,然后检查是否存在跨越两部分的更近点对。
- 分解:将点集按x坐标排序,分为左右两个子集
- 解决:
- 递归求解左子集的最近点对及距离d1
- 递归求解右子集的最近点对及距离d2
- 令d = min(d1, d2),检查在距离分割线d范围内的点是否存在更近的点对
- 合并:返回d和跨越点对距离中的最小值
代码实现
// 最近点对问题的分治实现
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <limits>
using namespace std;
// 定义点结构
struct Point {
double x, y;
Point(double x = 0, double y = 0) : x(x), y(y) {}
};
// 计算两点之间的距离
double distance(const Point& a, const Point& b) {
double dx = a.x - b.x;
double dy = a.y - b.y;
return sqrt(dx*dx + dy*dy);
}
// 按x坐标排序
bool compareX(const Point& a, const Point& b) {
return a.x < b.x;
}
// 按y坐标排序
bool compareY(const Point& a, const Point& b) {
return a.y < b.y;
}
// 暴力求解最近点对距离(用于小规模点集)
double bruteForce(const vector<Point>& points, int left, int right) {
double minDist = numeric_limits<double>::max();
for (int i = left; i <= right; i++) {
for (int j = i + 1; j <= right; j++) {
minDist = min(minDist, distance(points[i], points[j]));
}
}
return minDist;
}
// 寻找跨越左右两部分的最近点对
double closestCrossing(const vector<Point>& points, int left, int mid, int right, double d) {
vector<Point> strip;
double midX = points[mid].x;
// 收集距离中线小于d的点
for (int i = left; i <= right; i++) {
if (abs(points[i].x - midX) < d) {
strip.push_back(points[i]);
}
}
// 按y坐标排序
sort(strip.begin(), strip.end(), compareY);
// 检查每个点与后续最多6个点的距离
double minDist = d;
int n = strip.size();
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n && (strip[j].y - strip[i].y) < minDist; j++) {
minDist = min(minDist, distance(strip[i], strip[j]));
}
}
return minDist;
}
// 分治算法求解最近点对
double closestUtil(const vector<Point>& points, int left, int right) {
// 基本情况:点的数量较少时,使用暴力法
if (right - left + 1 <= 3) {
return bruteForce(points, left, right);
}
// 分解:找到中间点
int mid = left + (right - left) / 2;
Point midPoint = points[mid];
// 解决:递归求解左右两部分
double leftDist = closestUtil(points, left, mid);
double rightDist = closestUtil(points, mid + 1, right);
// 找到左右两部分中的最小距离
double d = min(leftDist, rightDist);
// 检查跨越左右两部分的点对
double crossDist = closestCrossing(points, left, mid, right, d);
// 合并:返回三者中的最小值
return min(d, crossDist);
}
// 计算最近点对距离
double closestPair(vector<Point> points) {
// 先按x坐标排序
sort(points.begin(), points.end(), compareX);
return closestUtil(points, 0, points.size() - 1);
}
int main() {
vector<Point> points = {
Point(2, 3), Point(12, 30), Point(40, 50),
Point(5, 1), Point(12, 10), Point(3, 4)
};
cout << "点的数量: " << points.size() << endl;
cout << "最近点对的距离: " << closestPair(points) << endl;
return 0;
}
算法分析
- 时间复杂度:O(n log n),排序耗时O(n log n),递归部分T(n) = 2T(n/2) + O(n)
- 空间复杂度:O(n),用于存储排序后的点和中间条带
- 关键优化:在检查跨越点对时,每个点最多需要与后续6个点比较,使合并步骤保持O(n)时间
- 实际应用:地理信息系统、计算机图形学、模式识别等领域
分治算法解题策略与技巧
分治算法的应用范围广泛,但如何正确地设计和实现分治算法需要掌握一定的策略和技巧。以下是一些实用的经验总结:
如何判断问题是否适合分治算法
问题是否可分解
原问题能否分解为结构相似的子问题,且子问题规模更小
子问题是否独立
子问题之间是否存在重叠,如果存在大量重叠,可能更适合动态规划
是否存在有效的合并方法
能否将子问题的解有效合并为原问题的解,合并步骤的复杂度是否可接受
小规模问题是否容易解决
当问题规模足够小时,是否存在简单直接的解法
设计分治算法的技巧
合理分解问题
通常将问题分解为规模大致相等的子问题,以获得最佳的时间复杂度
选择合适的基本情况
基本情况的选择会影响算法效率,通常选择易于求解且规模较小的情况
优化合并步骤
合并步骤往往是分治算法的性能瓶颈,需要精心设计以降低复杂度
考虑预处理
如排序等预处理步骤可以显著简化分治过程,提高算法效率
注意边界条件
分治过程中容易出现边界错误,需要特别注意子问题的划分和索引处理
分治算法的常见错误与避免方法
错误类型:分解不合理
子问题规模差异过大,导致时间复杂度退化
避免方法:
- 尽量将问题均匀分解为规模相近的子问题
- 对于数组问题,通常选择中点作为分解点
- 对于树问题,通常按子树自然分解
错误类型:合并步骤低效
合并子问题解的步骤复杂度太高,抵消了分治的优势
避免方法:
- 精心设计合并算法,尽量将复杂度控制在O(n)
- 利用问题特性减少合并时需要考虑的元素数量
- 考虑预处理步骤,为合并创造有利条件
错误类型:递归边界处理不当
基本情况定义错误,导致无限递归或结果错误
避免方法:
- 明确界定基本情况的条件
- 对小规模问题进行充分测试
- 注意处理空集、单元素等特殊情况
分治与其他算法的结合
分治算法常与其他算法技术结合使用,以解决更复杂的问题:
分治 + 排序
排序往往是分治算法的预处理步骤(如最近点对问题),或本身就是分治算法的应用(如归并排序、快速排序)
分治 + 贪心
在某些问题中,分治用于分解问题,而贪心用于解决子问题或合并子问题的解
分治 + 动态规划
对于某些复杂问题,可以先用分治分解问题,在子问题层面使用动态规划求解
分治 + 剪枝
在求解过程中,通过剪枝技术减少不必要的子问题计算,提高算法效率
分治算法常见模型
1. 排序与查找模型
分治算法在排序和查找领域有广泛应用,常见问题包括:
归并排序
将数组二分,递归排序后合并,稳定的O(n log n)排序算法
快速排序
选择基准元素划分数组,递归排序子数组,平均O(n log n)排序算法
二分查找
在有序数组中不断二分查找范围,O(log n)时间复杂度
选择问题
查找数组中第k小元素,平均O(n)时间复杂度
核心技巧
排序和查找问题的分治解法通常需要对数据进行合理划分,选择合适的划分点(如中点、基准元素)对算法效率至关重要。预处理(如排序)可以显著提升后续分治过程的效率。
2. 数组与序列问题模型
分治算法可以有效解决多种数组和序列相关问题:
最大子数组和
寻找具有最大和的连续子数组,分治解法时间复杂度O(n log n)
逆序对计数
统计数组中逆序对的数量,基于归并排序的分治解法时间复杂度O(n log n)
矩阵乘法(Strassen算法)
将矩阵分块相乘,时间复杂度优于传统的O(n³)算法
大数乘法(Karatsuba算法)
将大数分解为较小数相乘,减少乘法次数,时间复杂度O(n^log₂3)
核心技巧
数组和序列问题的分治解法关键在于如何将问题分解为子问题,并设计高效的合并步骤。对于涉及"跨越"子问题边界的情况(如最大子数组和),需要特别处理。
3. 几何问题模型
分治算法在计算几何中有重要应用:
最近点对问题
寻找平面上距离最近的一对点,分治解法时间复杂度O(n log n)
凸包问题
寻找包含所有点的最小凸多边形,分治解法时间复杂度O(n log n)
线段相交问题
判断平面上是否存在相交的线段,分治解法可高效解决
平面点集的中位数
寻找平面点集在x或y坐标上的中位数,可用于划分点集
核心技巧
几何问题的分治解法通常需要先对点进行排序(如按x或y坐标),然后分解为子问题。合并步骤往往需要利用几何性质减少计算量(如最近点对问题中只需检查每个点周围的6个点)。
4. 树与图问题模型
分治算法可以解决多种树和图相关问题:
树的遍历与分解
通过寻找树的重心分解树,解决路径查询等问题
矩阵链乘法
寻找矩阵相乘的最优顺序,分治结合动态规划的解法
快速傅里叶变换(FFT)
高效计算多项式乘法,基于分治思想的算法
图的连通分量
分治策略可用于并行计算图的连通分量
核心技巧
树和图问题的分治解法通常需要找到合适的分解点(如树的重心),使分解后的子问题规模显著减小。对于图问题,分治策略常与其他图算法(如并查集)结合使用。
分治算法练习题
逆序对计数
问题描述:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
提示:
- 基于归并排序的分治思想
- 在合并两个有序子数组时计数逆序对
- 时间复杂度O(n log n)
寻找峰值元素
问题描述:峰值元素是指其值严格大于左右相邻值的元素。给你一个整数数组nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。你可以假设nums[-1] = nums[n] = -∞。
提示:
- 使用二分查找的分治思想
- 比较中间元素与其右侧元素,决定搜索方向
- 时间复杂度O(log n)
不同的二叉搜索树
问题描述:给你一个整数n,求恰由n个节点组成且节点值从1到n互不相同的二叉搜索树有多少种?返回满足题意的二叉搜索树的种数。
提示:
- 以每个数字为根节点,递归计算左右子树的组合数
- 左子树的节点数为i-1,右子树的节点数为n-i
- 可以使用动态规划优化分治的重复计算
Pow(x, n)
问题描述:实现pow(x, n),即计算x的n次幂函数。
提示:
- 使用分治思想,将指数分解为n/2和n-n/2
- 处理特殊情况:n为0、负数
- 时间复杂度O(log n)
数组中的第K个最大元素
问题描述:在未排序的数组中找到第k个最大的元素。请注意,你需要找的是数组排序后的第k个最大的元素,而不是第k个不同的元素。
提示:
- 基于快速排序的分治思想(快速选择算法)
- 选择基准元素,将数组划分为两部分
- 根据基准元素的位置决定继续在左半部分还是右半部分查找
- 平均时间复杂度O(n),最坏情况O(n²)