今天在看《算法:C语言实现》时,在快速排序那一章最后一节讲述了利用快速排序的思想,快速排序每次划分后在枢轴的左边的元素都比枢轴小,在枢轴右边的数都比枢轴大(或相等),而划分后枢轴本身就放在了(有序时)它自身应该在的位置,在每次划分后判断枢轴下标和k的大小就可以快速找出数列中第k小的数了。
看完之后,我想既然利用快速排序的思想可以很快的找到第k小的数,那么能不能利用计数排序的思想来查找第k小的数呢,仔细一想,完全可以!计数排序是利用一个计数数组C来记录待排序数组中各个不同数值出现的次数,然后通过一次遍历计数数组C,利用C[i] += C[i-1]就可以得到小于等于数值i的元素的个数,然后按照C[i]就可以把待排序数组中值为i的数放到它应该在的位置上。那怎么利用这个计数数组C呢?在对计数数组进行遍历后(执行C[i] += C[i-1]),C[i]代表待排序数组中值小于等于i的元素个数,而C[i-1]代表值小于等于i-1的元素个数,如果 C[i-1]
< k并且k <= C[i],那么待排序数组中第k小的数必定就是值为i的数!查找就可以结束了。所以只需要找到第一个满足条件k <= C[i]的i即可。
在说具体的算法之前先约定第k小的数为在数列有序时从0开始的第k-1个数,也就是k>0。
下面先从最简单的算法讲起:
一、先排序整个数列然后取第k-1个数
这个方法简单粗暴,先把数列按从小到大的方式排列,就可以轻松得到第k-1个数了,但是,这个方法让整个数列都有序了,做了太多的无用功。
二、利用选择排序
第一个方法简单直接,但是由于我们只需要得到的是整个数列的第k小的数,所以,我们可以用选择排序的思想:选择排序每一次遍历数列都从剩余N-i个数中选取一个最小的数和前面的第i个值进行交换,直到i等于N-1时整个数列就是有序的了。对于选取第k小的数的问题,我们只需要进行k趟选择排序就可以得到第k小的数了。
代码如下:
//利用选择排序的思想,找k趟就能找到第k小的元素 void selectSortSearch(int array[], int size, int k) { int minIndex; int i = 0; int j = 0; for (i = 0; i < k; ++i) { minIndex = i; for (j = i + 1; j < size; ++j) { if (array[j] < array[minIndex]) { minIndex = j; } } swap(&array[i], &array[minIndex]); } printf("find: %d\n", array[k - 1]); } void swap(int *value1, int *value2) { int temp = *value1; *value1 = *value2; *value2 = temp; }
这种方法使用于k值比较小的情况,在k值较小的时这种方法的效率和选择算法以及基于计数排序思想的查找的效率差不多,但是一旦k的值比较大时这种方的效率就降低了。
三、选择算法
算法描述:
利用快速排序的的划分方法重排数组a[l],....,a[r],返回一个整数i,满足a[l],.....,a[i-1]都小雨a[i],a[i+1],....,a[r]都大于或等于a[i],如果k等于i,那么我们的工作就完成了。否则,如果k小于i,那么继续对左边的子序列进行处理;如果i大于k,那么dai继续对右边的子序列进行处理。
代码如下:
//利用递归的快速排序的思想进行查找第k小的元素 void recQuickSearch(int array[], int low, int high, int k) { int index; if (high < low) {//注意这里不能用<= return ; } index = findPivotIndex(array, low, high); if (index == k - 1) { printf("find: %d\n", array[index]); return ; } if (index > k - 1) { recQuickSearch(array, low, index - 1, k); } if (index < k - 1) { recQuickSearch(array, index + 1, high, k); } } int findPivotIndex(int array[], int low, int high) { int pivot = array[low]; while (low < high) { while (low < high && array[high] >= pivot) { --high; } array[low] = array[high]; while (low < high && array[low] <= pivot) { ++low; } array[high] = array[low]; } array[low] = pivot; return low; }
以上是递归形式的选择算法,下面是非递归形式的选择算法:
//利用非递归的快速排序的思想查找第k小的元素 void quickSearch(int array[], int low, int high, int k) { int index = 0; while (high >= low) { index = findPivotIndex(array, low, high); if (index == k - 1) { printf("find: %d\n", array[index]); break ; } if (index > k - 1) { high = index - 1; } if (index < k - 1) { low = index + 1; } } }
四、利用计数排序的思想
算法描述:
在对计数数组进行遍历后(执行C[i] += C[i-1]),C[i]代表待排序数组中值小于等于i的元素个数,而C[i-1]代表值小于等于i-1的元素个数,如果 C[i-1] < k并且k <= C[i],那么待排序数组中第k小的数必定就是值为i的数!查找就可以结束了。所以只需要找到第一个满足条件k
<= C[i]的i即可。
代码如下:
//利用计数排序的思想查找第k小的元素 void countSearch(int array[], int size, int k) { assert(array != NULL && size > 0); //计数数组,用于统计数组array中各个不同数出现的次数 //由于数组array中的数属于0...RANDMAX-1之间,所以countArray的大小要够容纳RANDMAX个int型的值 int *countArray = (int *) calloc(RANDMAX, sizeof(int)); //统计数组array中各个不同数出现的次数,循环结束后countArray[i]表示数值i在array中出现的次数 int index = 0; for (index = 0; index < size; ++index) { countArray[array[index]]++; } //有可能countArray[0]就已经比k大了 if (countArray[0] >= k) { printf("find: 0\n"); } else { //统计数值比index小的数的个数,循环结束之后countArray[i]表示array中小于等于数值i的元素个数 for (index = 1; index < RANDMAX; ++index) { countArray[index] += countArray[index - 1]; //当第一次满足条件时就代表第k小的数在小于等于index的元素并且大于index-1的之间 if (countArray[index] >= k) { printf("find: %d\n", index); break ; } } } free(countArray); }
利用计数排序思想查找第k小的数虽然运行速度很快,但是它也有一个和计数排序一样的缺点,就是空间复杂度太高了,不适合用于在元素值的范围很大的数列中寻找第k小的数。还有,不适合于数列中含有负数的情况。
五、利用堆排序思想(一)
算法描述:
对待查找数组进行堆排序,只需进行k趟即可找出第k小的元素了。
//利用小顶堆,维护一个初始大小为size的堆,k次堆排就可得到第k小的元素 void heapSearch1(int array[], int size, int k) { assert(array != NULL && size > 0 && k < size); int *heapPointer = array - 1; //自底向上调整数组,使其成为一个堆 int index = 0; for (index = size / 2; index > 0; --index) { fixDown1(heapPointer, index, size); } //交换堆顶元素和最后一个元素并调整堆结构 //执行k次就可以得到第k小的元素了 for (index = 0; index < k; ++index) { swap(&heapPointer[1], &heapPointer[size--]); fixDown1(heapPointer, 1, size); } printf("find: %d\n", heapPointer[size + 1]); } //从下标为index的节点开始向下调整,使树变成堆有序的(小顶堆) void fixDown1(int array[], int index, int size) { int i = index; int j = 2 * index; while (2 * i <= size) {//当下标为i的节点有孩子时 j = 2 * i;//让j指向左孩子 //当i有右孩子并且右孩子比左孩子小时,让j指向右孩子 if (j + 1 <= size && array[j + 1] < array[j]) { ++j; } //如果待调整元素的值小于较大孩子时,调整结束退出循环 if (array[i] <= array[j]) { break; } //否则交换待调整元素和其较大子节点 swap(&array[i], &array[j]); i = j;//让i指向调整后的待调整节点 } }
构造一个大小为N的堆所用的比较次数少于2N次,移去k个最小元素所用的比较次数少于2k*lgN次,总共需要2N + 2k*lgN次比较。
六、利用堆排序思想(二)
上一种思想是利用堆排序进行k趟排序就可以得到第k小的数了,但是这中方法需要维护大小为N的数组,而我们需要的只是第k小的元素,那么,我们可以使用一个大小为k的大顶堆来维护最小的k个数,然后用待查找数组中剩下的元素array[j]和堆顶元素进行比较,如果比堆顶元素小则把堆顶元素值置为array[j],然后进行堆调整,遍历整个数组之后堆顶元素就是第k小的元素了。
代码如下:
//维护一个大小为k的大顶堆,当待查找数组中的元素array[i]比堆顶元素小的时候,把堆顶元素替换为array[i], //然后调整堆结构,使其保持大顶堆的性质,这样遍历完整个待查找数组后堆顶元素就是第k小的元素 //堆排序,利用大顶堆,从小到大排序 void heapSearch2(int array[], int size, int k) { int heapSize = k; int *heap = (int *) calloc(heapSize + 1, sizeof(int)); int i = 0; for (i = 0; i < heapSize; ++i) { heap[i + 1] = array[i]; } //自底向上调整数组heap,使其成为一个大顶堆 for (i = heapSize / 2; i > 0; --i) { fixDown2(heap, i, heapSize); } int j = 0; for (j = k; j < size; ++j) { if (heap[1] > array[j]) { heap[1] = array[j]; fixDown2(heap, 1, heapSize); } } printf("find: %d\n", heap[1]); } //从下标为index的节点开始向下调整,使树变成堆有序的 void fixDown2(int array[], int index, int size) { int i = index; int j = 2 * index; while (2 * i <= size) {//当下标为i的节点有孩子时 j = 2 * i;//让j指向左孩子 //当i有右孩子并且右孩子比左孩子大时,让j指向右孩子 if (j + 1 <= size && array[j] < array[j + 1]) { ++j; } //如果待调整元素的值大于较大孩子时,调整结束退出循环 if (array[i] > array[j]) { break; } //否则交换待调整元素和其较大子节点 swap(&array[i], &array[j]); i = j;//让i指向调整后的待调整节点 } }
构造大小为k的堆需要2k次比较,然后用待查找数组剩下的N-k个元素和堆顶元素进行比较,比较次数为N-k,若小于堆顶元素,就置堆顶元素值为该值,接着对堆进行调整以维持大顶堆的性质,每次至多需要2lgk次比较,也就是2(N-k)*lgk次比较,所以总的比较次数为2k + (N - k) + 2(N - k)*lgk = N + k + 2(N - k)*lgk次比较。这种方法使用的空间和k成正比,所以在k较小且N很大时,对于找出N个元素中的第k小的元素有很高的时间效率,对于随即关键字以及其他情况,这种方法中堆操作的上界lgk在k相对N较小是可能为O(1)!