算法分析与设计--------分治法之找第k小的数

分治法

分治法是一种算法设计策略,它将一个问题分割成许多小问题进行解决,最终将所有小问题的解合并为原问题的解。具体而言,分治法将原问题递归地分为多个规模相同或相似的子问题,并对这些子问题进行解决,最后再合并得到原问题的解。这种方法常用于高效解决复杂的问题,比如排序、搜索、图形问题等。

找第K小的数

设计一个平均时间为O(n)的算法,在n(1<=n<=1000)个无序的整数中找出第k小的数。

提示:函数int partition(int a[],int left,int right)的功能是根据a[left]~~a[right]中的某个元素x(如a[left])对a[left]~~a[right]进行划分,划分后的x所在位置的左段全小于等于x,右段全大于等于x,同时利用x所在的位置还可以计算出x是这批数据按升非降序排列的第几个数。因此可以编制int find(int a[],int left,int right,int k)函数,通过调用partition函数获得划分点,判断划分点是否第k小,若不是,递归调用find函数继续在左段或右段查找。

输入格式:
输入有两行:

第一行是n和k,0<k<=n<=10000

第二行是n个整数

输出格式:
输出第k小的数

输入样例:
在这里给出一组输入。例如:

10 4
2 8 9 0 1 3 6 7 8 2

输出样例:
在这里给出相应的输出。例如:
2

分析:

一、在一个随机序列中寻找到第K小的数,显而易见第一种办法就是将这个序列进行从小到大排序,然后直接输出下标为K-1的数即可。
采用快速排序:

  • 平均时间复杂度:O(nlogn)
  • 平均空间复杂度:O(logn)
    所以这种方法不满足题目要求,题目要求平均时间为O(n)。

二、根据题目提示:

  1. partition函数
    回顾一下快排的partiton:
    int partition(int[] arr,int left, int right){…}
    return pivot;
   public static int partition(int[] arr, int left, int right) {
        // 取首元素为划分点pivot
        int pivot = arr[left], i = left, j = right;
        while(i < j) {
            // 从右向左找到第一个小于等于pivot的数
            while(i < j && arr[j] >= pivot) j--;
            if(i < j) arr[i++] = arr[j];
            // 从左向右找到第一个大于等于pivot的数
            while(i < j && arr[i] <= pivot) i++;
            if(i < j) arr[j--] = arr[i];
        }
        arr[i] = pivot;
        return i;
    }

即:传入一个数组、左边界、右边界;返回划分元素(默认为数组首元素)在一次partition后所在的数组下标。

  1. find函数

分析一下:

在数组中寻找第k小的数与partition函数有什么联系呢?思考一下:一次partition划分,返回了划分元素应该在的数组下标,那是不是就已经可以知道这个划分元素属于第几小了吗?举个例子:

4 9 7 3 2

上述数组进行1次partition划分,数组变化过程:

9 4 7 3 23 4 7 9 23 7 4 9 23 2 4 9 7

返回了元素4所在的下标:2,即说明了元素4在上述数组中为第2+1 = 3 小;

那么问题来了,我的数组很长,你一次partition操作只能知道这次划分结果的1个元素在这个数组中的相对位置,如何在一个庞大的数组中,进行许多次partition操作来确定这个数组中第K小的数呢?

所以我们需要编写一个find函数。

思考一下目前需要解决的问题,就是find函数需要做的事。

待解决:

  1. find操作中,需要对partition返回的下标进行判断是否为我们所寻找的K。那在右区间查找时,find传入的k应该发生对应的变化。

编写find函数

返回值:第k小的数,所以类型为 int;
参数:待查初始数组arr[ ]、数组左边界、数组右边界、第k小的k;

函数体:
先进行partition操作,判断返回值是否为第K小,是则返回该位置的数,否则进行判断,比k小:去右侧区间进行find操作,再判断;比k大:去左侧进行find操作。

   // 查找第k小的数
    public static int find(int[] a, int left, int right, int k) {
        // 划分后x所在的下标为pivot
        int pivot = partition(a,left,right);
        // 如果a[pivot]为第k小的数,直接返回a[pivot]
        if (pivot - left + 1 == k) return a[pivot];
        // 如果a[pivot]小于第k小的数,在右侧区间查找,此时k变化为k - (pivot - left + 1)
        else if (pivot - left +1 < k) return find(a, pivot+1, right, k - pivot + left -1);
        // 如果a[pivot]大于第k小的数,在左侧区间查找,此时k不变
        else return find(a, left, pivot - 1, k);
        
    }

完整题解

import java.util.*;

public class Main {
    // partition划分函数
   public static int partition(int[] arr, int left, int right) {
        // 取首元素为划分点pivot
        int pivot = arr[left], i = left, j = right;
        while(i < j) {
            // 从右向左找到第一个小于等于pivot的数
            while(i < j && arr[j] >= pivot) j--;
            if(i < j) arr[i++] = arr[j];
            // 从左向右找到第一个大于等于pivot的数
            while(i < j && arr[i] <= pivot) i++;
            if(i < j) arr[j--] = arr[i];
        }
        arr[i] = pivot;
        return i;
    }
    
   // 查找第k小的数
    public static int find(int[] a, int left, int right, int k) {
        // 划分后x所在的下标为pivot
        int pivot = partition(a,left,right);
        // 如果a[pivot]为第k小的数,直接返回a[pivot]
        if (pivot - left + 1 == k) return a[pivot];
        // 如果a[pivot]小于第k小的数,在右侧区间查找,此时k变化为k - (pivot - left + 1)
        else if (pivot - left +1 < k) return find(a, pivot+1, right, k - pivot + left -1);
        // 如果a[pivot]大于第k小的数,在左侧区间查找,此时k不变
        else return find(a, left, pivot - 1, k);
        
    }
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        int k = input.nextInt();
        int[] a = new int[n];
        for (int i = 0; i < n; i++) {
            a[i] = input.nextInt();
        }
        System.out.println(find(a, 0, n-1, k));
    }
}

总结

  1. 比较用排序的方法与分治的方法:

排序需要对整个数组元素进行操作,分治则不需要对所有元素进行操作。即,排序进行的partition操作远多于分治法进行的partition操作。

  1. 难点:

思考到分治法,将大数组划分为一个个区间进行查找;
在书写find函数时,思考到如何判定该数是否为第k小,以及在右侧区间查找时考虑到k的变化。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值