前言:
在计算机科学中,找到数组中第k个最小元素是一个常见的问题。常规的做法是对数组进行排序,然后取第k个元素作为结果。然而,排序整个数组的时间复杂度为O(nlogn),并不是最优解。本文将介绍一种名为线性时间选择(Linear Time Select)的算法,它能够在O(n)的时间复杂度内解决这个问题。
算法原理:
线性时间选择算法通过利用"划分"和"中位数"的概念来进行众多元素的筛选和缩小范围,最终找到第k个最小的元素。
算法运行演示--->线性时间选择 例子演示
具体的算法步骤如下:
函数准备
1.findMedian():对数组排序,并返回数组的中位数(在此问题中每个数组长度<=5 所以此函数的时间复杂度是5log5 是常数)
// 寻找子数组的中位数 并对数组排序
int findMedian(vector<int>& nums, int start, int end) {
// 将子数组排序
sort(nums.begin() + start, nums.begin() + end );
// 返回中位数
return nums[(start + end) / 2];
}
2.partition():划分数组(将小于pivot的划分在左边大于的划分在右边),并返回pivot数对应的索引。
int partition(vector<int>& nums, int start, int end, int pivot) {
int left = start;
int right = end;
while (true) {
// 在左侧找到第一个大于等于pivot的数
while (nums[left] < pivot) {
left++;
}
// 在右侧找到第一个小于等于pivot的数
while (nums[right] > pivot) {
right--;
}
if (left >= right) { // 找到中间位置
return right;
}
// 交换左右两个数的位置
swap(nums[left], nums[right]);
left++;
right--;
}
}
3.线性时间选择函数linearTimeSelect(vector<int>& nums, int k, int start, int end) 引用传递,改变原数组
例子:23个数据,找第8个最小的数:
1.将数组划分为若干个长度为5的子数组,并对每个子数组进行排序,并将这些中位数组成一个新的数组,称为medians。
// 将数组划分为若干个长度为5的子数组,对子数组排序。并找到每个子数组的中位数放入 数组medians
vector<int> medians;
for (int i = start; i <= end; i += 5) {
int subEnd = i + 5 < end ? i + 5 : end;
int median = findMedian(nums, i, subEnd);
medians.push_back(median);
}
2.递归调用线性时间选择函数,求出medians数组的中位数,记为medianOfMedians,并且根据medianOfMedians将nums数组进行划分,即将小于medianOfMedians的元素放在medianOfMedians的左侧,大于medianOfMedians的元素放在右侧。
// 递归调用线性时间选择函数,查找新数组中的中位数,并划分成长度为5的子数组排序。
int medianOfMedians = linearTimeSelect(medians, medians.size() / 2, 0, medians.size() - 1);
// 划分数组,找到中位数的在num数组中的索引 并将数组按照medianOfMedians划分(前面值小于medianOfMedians后面值大于medianOfMedians)
int pivotIndex = partition(nums, start, end, medianOfMedians);
3.如果medianOfMedians的索引恰好等于k,则找到第k个最小元素,返nums[medianOfMedians]。
如果k小于medianOfMedians的索引,则在左侧继续递归查找第k个最小元素。
如果k大于medianOfMedians的索引,则在右侧继续递归查找第k个最小元素。
// 根据中位数的索引与第k个最小元素的位置关系,递归地对其中一个部分进行查找
if (k == pivotIndex) { // 找到第k个最小元素
return nums[pivotIndex];
} else if (k < pivotIndex) { // 第k个最小元素在左侧
return linearTimeSelect(nums, k, start, pivotIndex - 1);
} else { // 第k个最小元素在右侧
return linearTimeSelect(nums, k, pivotIndex + 1, end);
}
完整代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 寻找子数组的中位数 并对数组排序
int findMedian(vector<int>& nums, int start, int end) {
// 将子数组排序
sort(nums.begin() + start, nums.begin() + end );
// 返回中位数
return nums[(start + end) / 2];
}
// 划分数组,返回pivot数大小的索引并对数组进行排序
int partition(vector<int>& nums, int start, int end, int pivot) {
int left = start;
int right = end;
while (true) {
// 在左侧找到第一个大于等于pivot的数
while (nums[left] < pivot) {
left++;
}
// 在右侧找到第一个小于等于pivot的数
while (nums[right] > pivot) {
right--;
}
if (left >= right) { // 找到中间位置
return right;
}
// 交换左右两个数的位置
swap(nums[left], nums[right]);
left++;
right--;
}
}
// 线性时间选择函数
int linearTimeSelect(vector<int>& nums, int k, int start, int end) {
// 如果只有一个元素,直接返回
if (start == end) {
return nums[start];
}
// 将数组划分为若干个长度为5的子数组,对子数组排序。并找到每个子数组的中位数放入 数组medians
vector<int> medians;
for (int i = start; i <= end; i += 5) {
int subEnd = i + 5 < end ? i + 5 : end;
int median = findMedian(nums, i, subEnd);
medians.push_back(median);
}
// 递归调用线性时间选择函数,查找新数组中的中位数,并划分成长度为5的子数组排序。
int medianOfMedians = linearTimeSelect(medians, medians.size() / 2, 0, medians.size() - 1);
// 划分数组,找到中位数的在num数组中的索引 并将数组按照medianOfMedians划分(前面值小于medianOfMedians后面值大于medianOfMedians)
int pivotIndex = partition(nums, start, end, medianOfMedians);
// 根据中位数的索引与第k个最小元素的位置关系,递归地对其中一个部分进行查找
if (k == pivotIndex) { // 找到第k个最小元素
return nums[pivotIndex];
} else if (k < pivotIndex) { // 第k个最小元素在左侧
return linearTimeSelect(nums, k, start, pivotIndex - 1);
} else { // 第k个最小元素在右侧
return linearTimeSelect(nums, k, pivotIndex + 1, end);
}
}
int main() {
vector<int> nums = {3, 1, 7, 5, 9, 2};
int k = 5;
int result = linearTimeSelect(nums, k - 1, 0, nums.size() - 1); // 注意将k减1,因为索引从0开始
cout << "第" << k << "小的元素是:" << result << endl;
return 0;
}
时间复杂度:
线性时间选择算法的时间复杂度为O(n),其中n是数组的长度。具体来说,算法通过划分和中位数的概念,将数组不断缩小范围,直到找到第k个最小元素。
在每一次递归调用线性时间选择函数时,都会对子数组进行划分和排序操作。对于每个子数组,我们需要线性时间(O(n))来找到其中位数,并对其进行排序。
假设每次划分后,我们能够将数组划分为规模减少了常数倍的子问题。那么,递归调用的次数可以表示为T(n) = T(n/5) + O(n),其中T(n/5)表示对规模为n/5的子问题递归调用线性时间选择函数的时间复杂度。
根据主定理(Master Theorem),可以得知这样的递归关系的时间复杂度为O(n)。
因此,整个线性时间选择算法的时间复杂度为O(n)。无论数组的规模有多大,算法都能在线性时间内找到第k个最小元素,这是该算法的优势之一。