在一个由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; }