最坏情况为线性时间的选择算法

求给定输入中第k大的数的算法。

这是一个常见面试题,通常的解法也很明显,使用类似快排的思想。

每趟运行,把数组的值分成两部分,一部分比pivot大,一部分比pivot小,因为我们知道pivot在数组中的位置,所以比较k和pivot的位置就知道第k大的值在哪个范围,我们不断的进行recursion, 直到pivot就是第k大的值。

这个算法的时间预期是O(n)。这里需要注意的是讲的仅限于它的预期,对于这个算法,其在最差情况下,时间复杂度则为n的平法。

参阅快速排序的无敌对手一文,我们是可以构建出一个这样的序列的。最简单的情况,每趟快排的时候我们以第一个为主元,那么对于一个已经排序好的序列,我们要找最大的数,最后的时间花费就退化成了n的平方。

《算法导论》9.3章给出了一个最差情况也为线性O(n)的算法。

Step 1:把数组划分为若干个子数组,每个子数组里包含5个数,因为会有无法整除的可能,所以最后一个子数组会小于5.

Step 2:用插入排序把这5个数排序,然后找出中位数,也就是第3个。

Step 3:把获得的中位数又排序(这个地方错误,不是排序,应该递归调用SELECT),找出中位数的中位数x(如果有偶数个中位数,为了方便,约定x是较小的中位数)。

Step 4:把原来的数组使用类似快排的方法,分成两个部分。让k比划分的低区中的元素数目多1,因此x是第k小元素,并且有n-k个元素在划分的高区.

Step 5:如果i =k,返回x。如果 i < k, 则在低区递归调用来找出第i小的元素.如果i> k,则在高区递归查找第i- k小的元素.

整个过程中,第1,2,4步所需时间为O(n), 注意第2步的复杂度不为O(n^2),第3步的复杂度为 T(n/5),第五步的复杂度为 T(7n/10)。

注意这里第2步虽然我们使用的是插入排序,但是待排的序列长度为常数5,所以对一组的排序时间花费为O(1),对于n/5个组,其时间预期是O(n/5),即O(n)。

时间预期为:

T(n) <= T( n/5 ) + T(7n/10+6) + O(n)

(书中通过数学方法最后推得时间预期是O(n)。因为需要较多的数学准备知识,这里不继续介绍。)

在这章的习题中,基于这个算法,要求证明原先Step 1中划分为每组3个和7个的情况的复杂度。7个的情况证明结果和5是一样的。但是对于3的情况,其结果最后可以证明出复杂度并非O(n)。

尝试证明关键步骤如下:

对于划分为3个元素的情况,可以得到递推式(过程略):

T(n) <= T( n/3 ) + T(2n/3+4) + O(n)

假设存在某个适当大的常数c,使得T(n)<=cn(为什么这样可查阅《算法导论》第一章),用an替代O(n)(因为O(n)代表的这部分的时间花费是线性的,那么必然存在一个常数a,使得an为这部分时间花费)用cn代换掉式中的T(n)那么有:

T(n)<= c(n/3) + c(2n/3+4) + an <= cn/3 + c + 2cn/3 + 4c + O(n)= cn + 5c + an

根据假设,T(n)的最大值是cn,那么又有:

cn + 5c + an <= cn

5c + an <=0

显然又 a, n > 0,那么欲使等式成立,必有c<=0。与我们假设的矛盾。所以我们的假设不成立。

因此,当我们尝试用3划分的时候,该算法的无法在线性复杂度内运行。

这个算法的实现代码比较复杂。对于每组划分5个元素的情况, 实现代码如下(该代码输出的是第i大的元素,上面的解释是输出第i小的元素):

  1 #include <stdlib.h>
  2 #include <stdio.h>
  3 #define swap(a,b) (a)^=(b);(b)^=(a);(a)^=(b)
  4 #define MAX 1000
  5
  6 void sort(int* input, int size){
  7     printf ( "sort arry size = %d\n", size );
  8     int i,j;
  9     for(i = 0; i< size ; i++){
 10         for(j = 0; j<size-i-1;j++){
 11             if(input[j]<input[j+1]){
 12                 swap(input[j],input[j+1]);
 13             }
 14         }
 15     }
 16 }
 17 void output(int * input, int size){
 18     for(;size>0 && *input;size--,input++){
 19         printf("%d ", *input);
 20     }
 21     printf("\n");
 22
 23 }
 24
 25 int partion(int *input, int size, int key){
 26     printf ( "--------------Step4---------------\n" );
 27     printf("key = %d \n", input[key]);
 28     int *head, *tail;
 29     head = input;
 30     tail = head + size - 1;
 31     swap(*head, input[key]);
 32
 33     int *k = head;
 34     while(head<tail){
 35         while(*tail && *k >= *tail){
 36             tail--;
 37         }
 38         if(tail<=head) break;
 39         swap(*k,*tail);
 40         k = tail;
 41         while(*head && *k < *head)
 42             head++;
 43         if(head>=tail) break;
 44         swap(*k,*head);
 45         k = head;
 46     }
 47     output(input, size);
 48     printf ( "--------------Step4 done--------------\n" );
 49     return k-input+1;
 50 }
 51
 52 int kselect(int *input, int size, int k){
 53     printf ( "start element : %d \n", *input );
 54     if(size<=5){
 55         sort(input, size);
 56         return input[k-1];
 57     }
 58     int mid[MAX] = {0};
 59     int midvalue[MAX] = {0};
 60     int groups = size/5;
 61     int i;
 62
 63     printf ( "-----------------step 1, 2--------------\n" );
 64     for(i = 0; i<groups;i++){
 65         sort(input+i*5, (i*5+5 > size) ? (size-1):5);
 66         printf ( "sorted group %d:\n", i );
 67         output(input+i*5, 5);
 68         mid[i] = i*5 + 2;
 69         midvalue[i] = input[i*5 + 2];
 70     }
 71
 72     printf ( "-----------------step 1, 2 done--------------\n" );
 73
 74     printf ( "---------step3-------------\n" );
 75     sort(midvalue, groups);
 76     printf ( "---------step3 done-------\n" );
 77     int m = -1;
 78     for(i = 0; i<5;i++){
 79         if(input[mid[i]] == midvalue[groups/2]){
 80             m = partion(input, size, mid[i]);
 81         }
 82     }
 83     if(m == k){
 84         return input[m-1];
 85     }
 86     if(k<m){
 87         return kselect(input,m,k);
 88     }
 89     else{
 90         return kselect(input+m, size - m, k-m);
 91     }
 92     return 0xffff;
 93 }
 94
 95 int main(){
 96     int input[] = {1,3,2,10,5,11, 12, 8 ,6, 7};     /*输出第7大的元素.*/
 97     int r = kselect(input,sizeof(input)/sizeof(int), 7);
 98     printf("result %d \n", r);
 99     return 0;
100 }

下面这个算法比较靠谱:

  1 #include <iostream>
  2 #include <time.h>
  3 using namespace std;
  4
  5 const int num_array = 13;
  6 const int num_med_array = num_array / 5 + 1;
  7 int array[num_array];
  8 int midian_array[num_med_array];
  9
 10 //冒泡排序(晚些时候将修正为插入排序)
 11 /*void insert_sort(int array[], int left, int loop_times, int compare_times)
 12 {
 13     for (int i = 0; i < loop_times; i++)
 14     {
 15         for (int j = 0; j < compare_times - i; j++)
 16         {
 17             if (array[left + j] > array[left + j + 1])
 18                 swap(array[left + j], array[left + j + 1]);
 19         }
 20     }
 21 }*/
 22
 23 /*
 24 //插入排序算法伪代码
 25 INSERTION-SORT(A)                              cost    times
 26 1  for j ← 2 to length[A]                      c1      n
 27 2       do key ← A[j]                          c2      n - 1
 28 3          Insert A[j] into the sorted sequence A[1 ‥ j - 1].     0...n - 1
 29 4          i ← j - 1                           c4      n - 1
 30 5          while i > 0 and A[i] > key           c5
 31 6             do A[i + 1] ← A[i]               c6
 32 7             i ← i - 1                        c7
 33 8          A[i + 1] ← key                      c8      n - 1
 34 */
 35 //已修正为插入排序,如下:
 36 void insert_sort(int array[], int left, int loop_times)
 37 {
 38     for (int j = left; j < left+loop_times; j++)
 39     {
 40         int key = array[j];
 41         int i = j-1;
 42         while ( i>left && array[i]>key )
 43         {
 44             array[i+1] = array[i];
 45             i--;
 46         }
 47         array[i+1] = key;
 48     }
 49 }
 50
 51 int find_median(int array[], int left, int right)
 52 {
 53     if (left == right)
 54         return array[left];
 55
 56     int index;
 57     for (index = left; index < right - 5; index += 5)
 58     {
 59         insert_sort(array, index, 4);
 60         int num = index - left;
 61         midian_array[num / 5] = array[index + 2];
 62     }
 63
 64     // 处理剩余元素
 65     int remain_num = right - index + 1;
 66     if (remain_num > 0)
 67     {
 68         insert_sort(array, index, remain_num - 1);
 69         int num = index - left;
 70         midian_array[num / 5] = array[index + remain_num / 2];
 71     }
 72
 73     int elem_aux_array = (right - left) / 5 - 1;
 74     if ((right - left) % 5 != 0)
 75         elem_aux_array++;
 76
 77     // 如果剩余一个元素返回,否则继续递归
 78     if (elem_aux_array == 0)
 79         return midian_array[0];
 80     else
 81         return find_median(midian_array, 0, elem_aux_array);
 82 }
 83
 84 // 寻找中位数的所在位置
 85 int find_index(int array[], int left, int right, int median)
 86 {
 87     for (int i = left; i <= right; i++)
 88     {
 89         if (array[i] == median)
 90             return i;
 91     }
 92     return -1;
 93 }
 94
 95 int q_select(int array[], int left, int right, int k)
 96 {
 97     // 寻找中位数的中位数
 98     int median = find_median(array, left, right);
 99
100     // 将中位数的中位数与最右元素交换
101     int index = find_index(array, left, right, median);
102     swap(array[index], array[right]);
103
104     int pivot = array[right];
105
106     // 申请两个移动指针并初始化
107     int i = left;
108     int j = right - 1;
109
110     // 根据枢纽元素的值对数组进行一次划分
111     while (true)
112     {
113         while(array[i] < pivot)
114             i++;
115         while(array[j] > pivot)
116             j--;
117         if (i < j)
118             swap(array[i], array[j]);
119         else
120             break;
121     }
122     swap(array[i], array[right]);
123
124     /* 对三种情况进行处理:(m = i - left + 1)
125     1、如果m=k,即返回的主元即为我们要找的第k小的元素,那么直接返回主元a[i]即可;
126     2、如果m>k,那么接下来要到低区间A[0....m-1]中寻找,丢掉高区间;
127     3、如果m<k,那么接下来要到高区间A[m+1...n-1]中寻找,丢掉低区间。
128     */
129     int m = i - left + 1;
130     if (m == k)
131         return array[i];
132     else if(m > k)
133         //上条语句相当于if( (i-left+1) >k),即if( (i-left) > k-1 ),于此就与2.2节里的代码实现一、二相对应起来了。
134         return q_select(array, left, i - 1, k);
135     else
136         return q_select(array, i + 1, right, k - m);
137 }
138
139 int main()
140 {
141     //srand(unsigned(time(NULL)));
142     //for (int j = 0; j < num_array; j++)
143     //array[j] = rand();
144
145     int array[num_array]={0,45,78,55,47,4,1,2,7,8,96,36,45};
146     // 寻找第k最小数
147     int k = 4;
148     int i = q_select(array, 0, num_array - 1, k);
149     cout << i << endl;
150
151     return 0;
152 }
时间: 2024-10-12 12:49:15

最坏情况为线性时间的选择算法的相关文章

第九章 中位数和顺序统计量 9.2 期望为线性时间的选择算法

package chap09_Medians_and_Order_Statistics; import static org.junit.Assert.*; import java.util.Random; import org.junit.Test; public class SearchAlorithms { /** * 分割(快速排序中对数组的分割) * * @param n * @param start * @param end * @return */ protected static

期望为线性时间的选择算法

randomized_select函数的期望运行时间是Θ(n),这里假设输入数据都是互异的.它返回数组A[p, r]中第i小的元素.该函数最坏情况运行时间为Θ(n2),即使是找最小元素也是如此,以为在每次划分时可能极不走运地总是按余下的元素中最大的来进行划分,而划分操作需要Θ(n)时间.我们也将看到该算法有线性的期望运行时间,又因为它是随机化的,所以不存在一个特定的会导致其最坏情况发生的输入数据. 输入:数组A,整数i(i不大于数组的个数). 输出:数组A中第i小的元素. 期望运行时间:Θ(n)

期望为线性时间的选择算法RANDOM_SELECT

1 #include<iostream> 2 #include<ctime> 3 using namespace std; 4 void swap(int *a, int *b) 5 { 6 int *c = a; 7 a = b; 8 b = c; 9 } 10 int Partition(int *A, int p, int r)// 划分 11 { 12 int x = A[r]; 13 int i = p - 1; 14 for (int j = p; j < r;

平均期望为线性时间的选择算法

从一组无序数据中选择出第i小的元素,采用了快速排序的思想. #include <iostream> #include <algorithm> using namespace std; int partition(int a[], int l, int r){ int key = a[l]; int i=l; swap(a[l],a[r]); for(int j=l;j<r;j++) if(a[j]<key) swap(a[i++],a[j]); swap(a[i],a[

最坏情况为线性的选择算法

基本思想 主体上是在期望为线性的选择算法上进行改进,将其中的随机的划分元素改为取中位数,使划分更均匀,从而达到最坏时时间复杂度也为线性.需要注意的是实现时里面的索引很晕,别搞混了.我就是先写了个很乱,然后实在改不下去了,就重写了,总共我大概写了5,6个小时吧.(可能我太菜了) 图解 代码 伪代码 这里书中未给伪代码,仅给了整个算法的流程,但并不影响我们的实现 C代码 #include <stdio.h> #define N 50 void show(int *a, int p, int r);

算法导论9.2以期望线性时间做选择

#include <stdint.h> #include <time.h> #include <iostream> #ifdef __linux #include <stdio.h> #include <stdlib.h> #endif void swap(int64_t* A, uint64_t i, uint64_t j) { int64_t tmp = A[i]; A[i] = A[j]; A[j] = tmp; } int64_t par

算法导论学习之线性时间排序+排序算法稳定性终结

前面我们学习的几种排序算法都是基于比较的,对于任何输入数据他们都是适用的,其最坏的时间复杂度不会低于nlgn: 但对于一些比较特殊的输入数据,我们可以不采取比较的方法而是采用其它的方法对其进行排序,以达到线性的时间复杂度.下面就来介绍三种这样的算法:计数排序,基数排序,桶排序(因为这几种算法不常见,我只实现了计数排序,其它两种排序用伪代码表示). 一.计数排序 算法思想:给定n个位于0–k之间的数(k是一个不太大的整数),我们可以统计出每个数前面有多少个小于它的数,然后就可以直接确定这个数在数组

算法导论——lec 08 线性时间排序

之前我们介绍了几种O(nlgn)的排序算法:快速排序.合并排序和堆排序,本节我们介绍基于比较的排序算法的下界以及几个线性时间的排序算法--计数排序.基数排序.桶排序. 一. 比较排序算法的下界 1. 决策树模型:比较排序可以被抽象的视为决策树.一棵决策树是一棵满二叉树,表示某排序算法 作用于给定输入所做的所有比较. 排序算法的执行对应于遍历一条从树的根到叶结点的路径.在每个内节结点处要做比较. 要使排序算法能正确的工作,其必要条件是,n 个元素的n!种排列中的每一种都要作为决策树 的一个叶子而出

读书日记- 线性时间排序算法

在最坏情况下,任何比较排序算法都需要做O(nlgn)次比较. 然而,在指定的条件下,线性时间的排序算法可以使得排序在O(n)时间内完成. 计数排序 假设n个输入元素中的每一个都是0到k区间内的一个整数,其中k为某个整数.k=O(n)时,排序运行时间为O(n). 主要思想: 创建长度为k的数组C,将对应的输入数组A的值作为索引来统计k数组每个下标出现的次数. 代码如下: void Counting_Sort(int* intArr,int* outArr,int k,int len) { int*