线性时间选择
这里介绍算法中的线性选择算法,顺便说一下快排和快速选择算法。
小提示
线性时间选择是快速选择算法的升级版,而快速选择算法又是基于快速排序的
快速排序算法
先来了解快排:快排用了递归,步骤如下:
- 选择一个数作为基准(通常是第一个数)
- 将大于这个数的数放它右边,小于这个数的放左边
- 再对左右区间不断划分,直到每个区间里只有一个数
盗图:
代码:
package src.app;
public class TestJava{
public static void main(String[] args){
int[] arr = {
10,7,2,4,7,62,3,4,2,1,8,9,19};
sort(arr);
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
//快速排序总函数
public static void sort(int[] nums){
sort(nums, 0, nums.length - 1);
}
//快排核心
public static void sort(int[] nums, int low, int high){
if(low >= high) return;
//划分数组,比主元大的数在它的右边,比主元小的数在它的左边
//构建分界点索引p
int p = partition(nums, low, high);
//划分好了之后,索引为p的数就已经处于他应该处于的位置上了
//也就是,比nums[p]大的数都已经在nums[p]的右边
//比nums[p]小的数都已经在nums[p]的左边
sort(nums, low, p - 1);
sort(nums, p + 1, high);
}
public static int partition(int[] nums, int low, int high){
if(low >= high) return low;
int pivot = nums[low];
int i = low, j = high + 1;
while(true){
while(nums[++i] < pivot){
if(i == high) break;
}
while(nums[--j] > pivot){
if(j == low) break;
}
if(i >= j) break;
//很好理解,走到了这里,一定会有nums[i] > pivot 以及 nums[j] < pivot
//为了保证pivot左边全是比她小的数,右边全是比她大的数
//我们交换此时nums[i]和nums[j]的位置
// 保证 nums[lo..i] < pivot < nums[j..hi]
swap(nums, i, j);
}
// 将 pivot 值交换到正确的位置
swap(nums, j, low);
// 现在 nums[lo..j-1] < nums[j] < nums[j+1..hi]
return j;
}
public static void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
ps:java中Array库的sort()函数不仅采用了快速排序算法,也采用了归并排序算法
快速选择算法
快速选择也应用了快排的思想,用了分治,选取第K大的数,其思想是:每次划分之后判断第k个数在左右哪个部分,然后递归对应的部分。
第K大的数,转换成已为升序的数组里的索引就是nums.length - k,快速选择算法的时间复杂度大多情况下为o(n),但最坏情况下为o(n的平方),为了尽可能减少这样的情况发生,通常先将数组打乱。代码如下:
package src.app;
import java.util.Random;
public class TestJava{
public static void main(String[] args){
int[] arr = {
10,7,2,4,7,62,3,4,2,1,8,9,19};
sort(arr);
//查找第五大的数
int kth = findkthLargest(arr, 5);
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
System.out.println(kth);
}
//快速排序总函数
public static void sort(int[] nums){
sort(nums, 0, nums.length - 1);
}
//快排核心
public static void sort(int[] nums, int low, int high){
if(low >= high) return;
//划分数组,比主元大的数在它的右边,比主元小的数在它的左边
//构建分界点索引p
int p = partition(nums, low, high);
//划分好了之后,索引为p的数就已经处于他应该处于的位置上了
//也就是,比nums[p]大的数都已经在nums[p]的右边
//比nums[p]小的数都已经在nums[p]的左边
sort(nums, low, p - 1);
sort(nums, p + 1, high);
}
public static int partition(int[] nums, int low, int high){
if(low >= high) return low;
int pivot = nums[low];
int i = low, j = high + 1;
while(true){
while(nums[++i] < pivot){
if(i == high) break;
}
while(nums[--j] > pivot){
if(j == low) break;
}
if(i >= j) break;
//很好理解,走到了这里,一定会有nums[i] > pivot 以及 nums[j] < pivot
//为了保证pivot左边全是比她小的数,右边全是比她大的数
//我们交换此时nums[i]和nums[j]的位置
// 保证 nums[lo..i] < pivot < nums[j..hi]
swap(nums, i, j);
}
// 将 pivot 值交换到正确的位置
swap(nums, j, low);
// 现在 nums[lo..j-1] < nums[j] < nums[j+1..hi]
return j;
}
public static void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
public static int findkthLargest(int[] nums, int k ){
//为防止出现最坏情况导致时间复杂度为n平方的情况,先把数组打乱了再进行快速选择
shuffle(nums);
int low = 0, high = nums.length - 1;
//索引转换
k = nums.length - k;
while (low <= high) {
int p = partition(nums, low, high);
if(p < k){
//第k大的数在右边
low = p + 1;
}
else if(p > k){
//第k大的在左边
high = p - 1;
}
else{
//找到第k大的数
return nums[k];
}
}
return -1;
}
//用来打乱的函数
public static void shuffle(int[] nums){
int n = nums.length;
Random rand = new Random();
for (int i = 0; i < n; i++){
int r = i + rand.nextInt(n - i);
swap(nums, i, r);
}
}
}
快速选择算法与线性选择算法的关系
线性选择算法改变了快速选择算法的主元选取规则,采用中位数集合的中位数作为主元
算法的思路是:
- 首先把数组按5个数为一组进行分组,最后不足5个的忽略。对每组数进行排序(如插入排序)求取其中位数。
- 把上一步的所有中位数移到数组的前面,对这些中位数递归调用线性时间选择算法求得他们的中位数。
- 将上一步得到的中位数作为划分的主元进行整个数组的划分。
- 判断第k个数在划分结果的左边、右边还是恰好是划分结果本身,前两者递归处理,后者直接返回答案
package root;
public class T3_DAC_Liner_SelectN {
public static void swap(int a[], int i,int j){
int temp=a[j];
a[j] = a[i];
a[i] = temp;
}
//冒泡排序
public static void bubbleSort(int a[], int l, int r){
for(int i=l; i<r; i++)
{
for(int j=i+1; j<=r; j++)
{
if(a[j]<a[i])swap(a,i,j);
}
}
}
//递归寻找中位数的中位数
public static int FindMid(int a[], int l, int r){
if(l == r) return l;
int i = 0;
int n = 0;
for(i = l; i < r - 5; i += 5)
{
bubbleSort(a, i, i + 4);
n = i - l;
swap(a,l+n/5, i+2);
}
//处理剩余元素
int num = r - i + 1;
if(num > 0)
{
bubbleSort(a, i, i + num - 1);
n = i - l;
swap(a,l+n/5, i+num/2);
}
n /= 5;
if(n == l) return l;
return FindMid(a, l, l + n);
}
//进行划分过程
public static int Partion(int a[], int l, int r, int p){
swap(a,p, l);
int i = l;
int j = r;
int pivot = a[l];
while(i < j)
{
while(a[j] >= pivot && i < j)
j--;
a[i] = a[j];
while(a[i] <= pivot && i < j)
i++;
a[j] = a[i];
}
a[i] = pivot;
return i;
}
public static int Select(int a[], int l, int r, int k){
int p = FindMid(a, l, r); //寻找中位数的中位数
int i = Partion(a, l, r, p);
int m = i - l + 1;
if(m == k) return a[i];
if(m > k) return Select(a, l, i - 1, k);
return Select(a, i + 1, r, k - m);
}
public static void main(String[] args) {
int a[]= {
3,0,7,6,5,9,8,2,1,4,13,11,17,16,15,19,18,12,10,14,23,21,
27,26,25,29,28,22,20,24,33,31,37,36,35,39,38,32,30,34,43,41,47,46,45,49,
48,42,40,44,53,51,57,56,55,59,58,52,50,54,63,61,67,66,65,69,68,62,60,64,
73,71,77,76,75,79,78,72,70,74};
for(int i = 0; i < 80; i++){
System.out.println("第"+(i+1)+"小数为: "+Select(a, 0, 79, i+1));
}
}
}
遇到的问题:
1.java的静态方法(main函数)不能调用非静态的函数。
解决方法 将所有函数重新命名为静态的或者取得一个全局静态变量,将非静态的函数赋值给这个静态变量,再用main函数用上这个变量。
参考
C/C++实现快排算法
快排亲兄弟:快速选择算法详解
0006算法笔记——【分治法】线性时间选择
算法题04:分治法:求第K小元素(线性时间选择算法)