分治法
分治法是一种算法设计策略,它将一个问题分割成许多小问题进行解决,最终将所有小问题的解合并为原问题的解。具体而言,分治法将原问题递归地分为多个规模相同或相似的子问题,并对这些子问题进行解决,最后再合并得到原问题的解。这种方法常用于高效解决复杂的问题,比如排序、搜索、图形问题等。
找第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)。
二、根据题目提示:
- 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后所在的数组下标。
- find函数
分析一下:
在数组中寻找第k小的数与partition函数有什么联系呢?思考一下:一次partition划分,返回了划分元素应该在的数组下标,那是不是就已经可以知道这个划分元素属于第几小了吗?举个例子:
4 9 7 3 2
上述数组进行1次partition划分,数组变化过程:
9 4 7 3 2, 3 4 7 9 2, 3 7 4 9 2, 3 2 4 9 7
返回了元素4所在的下标:2,即说明了元素4在上述数组中为第2+1 = 3 小;
那么问题来了,我的数组很长,你一次partition操作只能知道这次划分结果的1个元素在这个数组中的相对位置,如何在一个庞大的数组中,进行许多次partition操作来确定这个数组中第K小的数呢?
所以我们需要编写一个find函数。
思考一下目前需要解决的问题,就是find函数需要做的事。
待解决:
- 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));
}
}
总结
- 比较用排序的方法与分治的方法:
排序需要对整个数组元素进行操作,分治则不需要对所有元素进行操作。即,排序进行的partition操作远多于分治法进行的partition操作。
- 难点:
思考到分治法,将大数组划分为一个个区间进行查找;
在书写find函数时,思考到如何判定该数是否为第k小,以及在右侧区间查找时考虑到k的变化。