【算法学习】分治法应用—线性时间选择

问题描述

给定线性序集中n个元素和一个整数k,1≤k≤n,要求找出这n个元素中第k小的元素。

解决策略

假设都使用 数组array[n] 存储这个线性序列

策略一: 先排序

算法思想

直接使用排序算法对这个线性序列进行排序,那么第k小的元素就是array[k-1]

时间复杂度

时间复杂度最好的排序算法,如「快速排序」,也需要 O ( n l o g n ) O(nlogn) O(nlogn),排序后查找只需要 O ( 1 ) O(1) O(1),故最终的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

策略二:线性时间选择

算法思想

使用的是“线性时间选择算法”(Quickselect),这个算法基于快速排序的分区思想,能在 O(n) 平均时间复杂度内找到任意第k小元素。
通过「快速排序」的学习,可以发现

划分函数partition() 每进行一轮划分之后,结果都能使 基准元素(x = a[p] 放到当前序列中的正确位置
所以,可以根据这个确定位置的次序与k的大小关系,往下查找第k个数:

  • if 确定位置 <= k: 在左区间 继续查找
  • if 确定位置 > k : 在右区间继续查找
流程
  • 使用partition函数进行分区,实现类似快速排序的分割
  • 利用select函数递归选择基于分区的信息来选择所需的第k个元素
核心算法描述
template<class Type>
Type Select(Type a[],int p,int r,int k)
{
      if (p==r) return a[p];
      int i=Partition(a,p,r);
      j=i-p+1;	// 计算新基准点的在现区间的次序
      if (k<=j) return Select(a,p,i,k);
      else return Select(a,i+1,r,k-j);
}
完整代码
#include <iostream>

using namespace std;

const int MAX = 100001; // 最大范围
int a[MAX];

// 根据基准点划分区间
int partition(int a[], int p, int r){
    int i = p, j = r + 1;
    int x = a[p]; // 基准元素
    while (true) {
    while (a[++i] < x && i < r) {} // 从左向右找第一个大于等于 x 的元素, 注意边界
    while (a[--j] > x && j > l) {} // 从右向左找第一个小于等于 x 的元素, 注意边界
        if (i >= j) break; // 左右指针汇合
        swap(a[i], a[j]);
    }
    a[p] = a[j]; // 将分元素放到正确的位置
    a[j] = x; // 将基准元素放到正确位置
    return j; // 返回基准位置
}

int select(int a[], int p, int r, int k){
    if (p == r) return a[p]; // 最小问题规模:只有一个元素
    
    int i = partition(a, p, r); // 调用划分函数进行划分
    int j = i - p + 1; // 计算新基准点的在现区间的次序
    
    // 根据划分结果进行查找
    if (j == k)
        return a[i];
    else if (j > k)
        return select(a, p, i - 1, k);
    else
        return select(a, i + 1, r, k - j);
}

int main(){
    int n, k;
    cin >> n >> k;
    
    for(int i = 0; i < n; ++i){ 
        cin >> a[i];
    }
    
    // 求第k个数
    int res = select(a, 0, n - 1, k);
    
    cout << res << endl;
    
    return 0;
}
时间复杂度

平均情况下为 O ( n ) O(n) O(n),最坏情况下可能达到 O ( n 2 ) O(n^2) O(n2)
可以通过随机选择基准值或加入中位数的中位数划分策略来改进。

优化

通过 随机选择基准值进行优化

随机选择基准值
// 随机选择基准值
int randomPartition(int a[], int l, int r)
{
  int i = l + rand() % (r - l + 1); // 随机选择一个元素作为基准值
  int temp = a[i];                  // 将分界点元素与最前一个元素交换
  a[i] = a[l];
  a[l] = temp;
  return partition(a, l, r); // 调用划分函数
}
优化后的完整代码
#include <iostream>

using namespace std;

const int MAXN = 100; // 设置最大 n 的范围
int a[MAXN];          // 数组

// 划分函数
int partition(int a[], int l, int r)
{
  // 选择基准元素
  int i = l, j = r + 1;
  int x = a[l];

  // 划分区间
  while (true)
  {
    while (a[++i] < x && i < r) // 从左向右找第一个大于等于 x 的元素, 注意边界
      ;
    while (a[--j] > x && j > l) // 从右向左找第一个小于等于 x 的元素, 注意边界
      ;
    if (i >= j) // 如果两个指针相遇,则退出循环
      break;
    swap(a[i], a[j]); // 交换元素
  }
  // 此时 j 指向分界点元素
  a[l] = a[j]; // 将分界点元素放到正确的位置
  a[j] = x;    // 将基准元素放到最终位置
  return j;    // 返回分界点位置
}

// 随机选择基准值
int randomPartition(int a[], int l, int r)
{
  int i = l + rand() % (r - l + 1); // 随机选择一个元素作为分界点
  int temp = a[i];                  // 将分界点元素与最前一个元素交换
  a[i] = a[l];
  a[l] = temp;
  return partition(a, l, r); // 调用划分函数
}

// 计算第 k 小的元素
int randomSelect(int a[], int l, int r, int k)
{
  if (l == r)
    return a[l];
  int i = randomPartition(a, l, r); // 随机选择分界点
  int j = i - l + 1;                // 计算新基准点的在现区间的次序

  // 根据划分结果进行查找
  if (j == k)
    return a[i];
  else if (j > k)
    return randomSelect(a, l, i - 1, k);
  else
    return randomSelect(a, i + 1, r, k - j);
}

// 主函数
int main(){
    int n, k;
    cin >> n >> k;
    
    for(int i = 0; i < n; ++i){ 
        cin >> a[i];
    }
    
    // 求第k个数
    int res = select(a, 0, n - 1, k);
    
    cout << res << endl;
    
    return 0;

⚠️值得注意的

  1. partition函数中的数组越界问题

添加ij越界检查

  • && i < r检查左指针i是否越右界
  • && j > p检查右指针是否越左界
  1. select函数中关系选择正确的区间

递归选择条件分析
划分操作:
partition 方法在 a[i] 处划分数组,使得 a [ p . . . i − 1 ] < = a [ i ] < a [ i + 1... r ] a[p...i-1] <= a[i] < a[i+1...r] a[p...i1]<=a[i]<a[i+1...r]
变量 j 的意义:
j = i - p + 1 表示 a[i] 在当前子数组(从 p 到 r)中的第几小的位置(位置是相对于1开始)。
递归部分的问题:
条件 j >= k:
这种条件会导致当 j == k 时进入错误的区间选择。
j >= k 中,当 j == k 时已经找到所需元素 a[i],因此直接返回 a[i] 才是正确的选择。
右区间调用:
else 部分继续在右区间进行,减少k后递归,这部分通常是正确的。

修正建议:
根据 j 和 k 的关系选择正确的区间
j == k 时,返回 a[i]
j > k 时,递归在左区间 [p, i-1]
j < k 时,递归在右区间 [i+1, r],但此时需要调整 kk - j

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Huazzi_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值