在一个由n个元素组成的集合中,第 i 个顺序统计量就是该集合中第 i 小的元素。要查询一个集合中第 i 小的元素,能够先对集合排序,然后取目标元素,这样能够再O(nlgn)的时间内解决。可是这样的先排序的算法时间复杂度比較高,实际上,这是能够在O(n)时间内完毕的。
9.1、查找n个元素的集合中的最大值和最小值均须要O(n)的时间,由于须要至少n-1次比較。假设须要同一时候找出最大值和最小值,能够这样做,每两个元素做一次比較,将较大者与最大值比較,较小者与最小值比較,这样就是两个元素须要三次比較,共须要3n/2次比較,所以时间复杂度也是O(n)。
习题 9.1-1
先两两比較,较小的组成新的数组再两两比較,不断如此,从而形成一个倒立的树,终于找出最小的元素,由于最上一层是n/2次比較,这又能够看做一个满二叉树,所以总的比較次数为n/2+n/2-1=n-1,找出一个最小的元素须要n-1次比較,而n个元素中的第2小元素一定跟最小的元素比較过,在这个倒立书中,共同拥有lgn个元素跟最小元素比較过,所以找到第2小元素须要lgn-1次比較,共所以须要n+lgn-2次比較。
习题 9.1-2
先两两比較n/2次得到较大值组n/2个元素,和较小值组n/2个元素,然后较大值组找出最大值须要 n/2-1 次比較,较小值组找出最小值须要 n/2-1 次比較,所以找出最大值和最小值,最坏情况下须要3n/2-2次比較。
9.2、要在线性时间内找到n个元素的集合的第 i 个顺序统计量,其要点就是使用类似高速排序的划分来不断缩小查找的空间,在高速排序中,对集合元素进行一次划分之后,就知道划分主元所在的顺序位置(即主元是属于第几小的元素),然后依据我们的目标元素的位置,从而选择划分主元的左右区间的一个作为下一个的划分区间,不断划分,直到划分主元即为目标元素为止。因此,能够使用高速排序中的随机化划分方法,利用随机化方法找到划分主元,这样就能够保证在平均情况下,每次的划分区间缩小一半,从而在O(n)的时间内找到第 i 个顺序统计量,因此这样的随机化方法是在期望时间为O(n)的时间内完毕,最坏情况依旧是O(n^2)。
9.3、要想在最坏情况下时间复杂度依旧为O(n),就必须保证每次划分都尽可能的平衡,至少不要出现主元的一边为空的情况,因此最坏情况线性时间的选择算法就是每次都选择划分区间中元素的中位数作为划分主元。在选择划分区间的中位数的时候,非常巧妙的使用了递归的查找方法,否则时间复杂度就会变为O(n^2)。之所以算法可以在O(n)时间内找到第 i 小的元素,就是由于没有使用排序,一旦使用排序,时间复杂度就变为了O(nlgn)。
/* * 算法导论 第九章 中位数和顺序统计学 * 线性时间选择元素 */ #include <iostream> #include <ctime> using namespace std; int minimum(int *arr, int len); int randomizedSelect(int *arr, int p, int r, int i); int randomizedPartition(int *arr, int p, int r); void exchange(int arr[], int i, int j); int partition(int arr[], int p, int r); int select(int *arr, int p, int r, int i); int partitionWithPivot(int *arr, int p, int r, int pivot); void printArray(int arr[], int len, char *str); int getMedian(int *arr, int p, int r); void randomizedQuickSort(int *arr, int p, int r); int main() { int len = 15; int *arr = new int[len]; srand(time(NULL)); for (int i=0; i<len; i++) { arr[i] = rand() % 100; } printArray(arr, len, "原数组"); cout<<"最小元素为:"<<minimum(arr, len)<<endl; int i = 3; int elem = randomizedSelect(arr, 0, len-1, i); randomizedQuickSort(arr, 0, len-1); printArray(arr, len, "排序后的数组"); cout<<"第 "<<i<<" 小的元素为:"<<elem<<endl<<endl; for (int i=0; i<len; i++) { arr[i] = rand() % 100; } printArray(arr, len, "原数组"); i = 10; elem = select(arr, 0, len-1, i); randomizedQuickSort(arr, 0, len-1); printArray(arr, len, "排序后的数组"); cout<<"第 "<<i<<" 小的元素为:"<<elem<<endl; delete[] arr; return 0; } /* * 求数组中的最小值 * 时间复杂度为O(n) */ int minimum(int *arr, int len) { int min = arr[0]; for (int i=1; i<len; i++) { if (arr[i] < min) min = arr[i]; } return min; } /* * 随机化选择算法 * 选择arr[p..r]中第 i 小的元素 * 使用高速排序中的随机化划分方法,平均情况下每次将选择空间缩小一半 * 所以在平均情况下,时间复杂度为O(n) */ int randomizedSelect(int *arr, int p, int r, int i) { if (p == r) { return arr[p]; } int q = randomizedPartition(arr, p, r); int k = q - p + 1; if (k == i) { return arr[q]; } else if (i < k) { return randomizedSelect(arr, p, q-1, i); } else { return randomizedSelect(arr, q+1, r, i-k); } } /* * 随机化高速排序 * 期望时间复杂度为O(nlgn) */ void randomizedQuickSort(int *arr, int p, int r) { if (p < r) { int q = randomizedPartition(arr, p, r); randomizedQuickSort(arr, p, q-1); randomizedQuickSort(arr, q+1, r); } } /* * 随机化划分 */ int randomizedPartition(int *arr, int p, int r) { srand(time(NULL)); int i = p + rand() % (r-p+1); exchange(arr, i, r); return partition(arr, p, r); } void exchange(int arr[], int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } int partition(int arr[], int p, int r) { int pivot = arr[r]; int i = p - 1;//i前面的(包含i)的元素都是不大于pivot的,i后面的都是大于pivot的元素 int j;//j后面的(包含j)都是还没有划分的 for (j=p; j<=r-1; j++) { if (arr[j] <= pivot) { i++; exchange(arr, i, j); } } i++; exchange(arr, i, r); return i; } /* * 最坏情况下线性时间选择算法 * 此算法依旧是建立在高速排序的划分算法基础之上的 * 可是与randomizedSelect算法的不同指之处,就是此算法的本质 * 是保证了每次划分选择的划分主元一定是一个较好的主元,算法先对数组5个一组进行分组 * 然后选择每组的中位数,再递归的选择各组中位数中的中位数 * 从而找出划分区间的中位数作为数组的划分主元,以此保证划分的平衡性 * 选择中位数的时候必须使用递归调用的方法才干减少时间复杂度 * 从而保证在最坏情况下都得到一个好的划分 * 最坏情况下时间复杂度为O(n) */ int select(int *arr, int p, int r, int i) { if (p == r) { return arr[p]; } int len = r-p+1; int medianCnt = 1; if (len > 5) medianCnt = len%5 > 0 ? len/5+1 : len/5; int *medians = new int[medianCnt]; //使用插入排序找出每组的中位数 for (int j=0, k=p; j<medianCnt; j++) { if (j == medianCnt-1) { medians[j] = getMedian(arr, k, r); } else { medians[j] = getMedian(arr, k, k+4); k += 5; } } //递归调用select线性时间函数自身选择中位数组中的中位数 int pivot = select(medians, 0, medianCnt-1, (medianCnt+1)/2); delete[] medians; int q = partitionWithPivot(arr, p, r, pivot); int k = q-p+1; if (i == k) { return pivot; } else if (i < k) { return select(arr, p, q-1, i); } else { return select(arr, q+1, r, i-k); } } /* * 依据指定的划分主元pivot来划分数组 * 并返回主元的顺序位置 */ int partitionWithPivot(int *arr, int p, int r, int pivot) { int i = p - 1; int j = p; for (; j<=r; j++) { //此处用<较为准确 if (arr[j] < pivot) { i++; exchange(arr, i, j); } } for (int j=i+1; j<=r; j++) { if(arr[j] == pivot) { exchange(arr, i+1, j); break; } } return i+1; } /* * 利用插入排序选择中位数 */ int getMedian(int *arr, int p, int r) { //插入排序 for (int i=p+1; i<=r; i++) { int key = arr[i]; int j = i - 1; while (j >= p && arr[j] > key) { arr[j+1] = arr[j]; j--; } arr[j+1] = key; } return arr[(p+r)/2]; } void printArray(int arr[], int len, char *str) { cout << str << endl; for (int i=0; i<len; i++) { cout << arr[i] << " "; } cout << endl; }