查找数列中第K小的元素(C语言版)

今天在看《算法: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)!

时间: 2024-11-05 20:45:20

查找数列中第K小的元素(C语言版)的相关文章

[LeetCode] Kth Smallest Element in a Sorted Matrix 有序矩阵中第K小的元素

Given a n x n matrix where each of the rows and columns are sorted in ascending order, find the kth smallest element in the matrix. Note that it is the kth smallest element in the sorted order, not the kth distinct element. Example: matrix = [ [ 1, 5

leetcode.矩阵.378有序矩阵中第K小的元素-Java

1. 具体题目 给定一个 n x n 矩阵,其中每行和每列元素均按升序排序,找到矩阵中第k小的元素.请注意,它是排序后的第k小元素,而不是第k个元素. 示例: matrix = [ [ 1,  5,  9], [10, 11, 13], [12, 13, 15] ],k = 8, 返回 13. 2. 思路分析 二分法:初始化 low 指针为矩阵中最小元素:matrix[0][0],high 指针为矩阵中最大元素:matrix[rows-1][cols-1],之后进行二分查找,迭代时每次计算矩阵中

378 Kth Smallest Element in a Sorted Matrix 有序矩阵中第K小的元素

给定一个 n x n 矩阵,其中每行和每列元素均按升序排序,找到矩阵中第k小的元素.请注意,它是排序后的第k小元素,而不是第k个元素.示例:matrix = [   [ 1,  5,  9],   [10, 11, 13],   [12, 13, 15]],k = 8,返回 13.说明:你可以假设 k 的值永远是有效的, 1 ≤ k ≤ n2 .详见:https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/des

查找数组中第i小的元素

查找并输出数组中第i小的元素,这样的题目我们可以先对数组进行排序,然后输出相对应的第i小的元素:还有另外一种方法,一种解决选择问题的分治算法,该算法是以快速排序算法为模型的,与快速排序一样,我们仍然将输入数组进行划分,但与快速排序不同的是,快速排序会递归处理划分的两边,而该选择方法select只处理划分的一边.这一差异会在性能分析中体现出来:快速排序的期望运行时间为O(nlog(n)),而select的选择期望运行时间是O(n). 1.对数组进行排序,然后输入第i小的元素,这里排序算法用的是插入

二叉树中第 K小的元素

给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素. 说明:你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数. 思路: 二叉搜索树因其有序,故采用中序遍历,可以得到第K小的元素. /** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode(int x) : val(x

leetcode378 有序矩阵中第k小的元素

排序后取数组第k个元素,遍历需要n^2的复杂度,查找插入logn,时间复杂度O(n^2logn).方法很笨,完全就是STL过于牛x运行通过的. class Solution { public: int kthSmallest(vector<vector<int>>& matrix, int k) { //O(n2logn) vector<int> arr; for(int j=0;j<matrix[0].size();j++){ arr.push_back

查找序列中第K小的数与快速排序

#include <algorithm>#include <iostream> int Partition(int X[], int left, int right){ int pivot = X[left]; int L = left; int R = right; while(L < R) { while(X[L] <= pivot && L < right) { ++L; } while(X[R] > pivot &&

挑战面试编程:查找数组中第k大的数

查找数组中第k大的数 问题: 查找出一给定数组中第k大的数.例如[3,2,7,1,8,9,6,5,4],第1大的数是9,第2大的数是8-- 思路: 1. 直接从大到小排序,排好序后,第k大的数就是arr[k-1]. 2. 只需找到第k大的数,不必把所有的数排好序.我们借助快速排序中partition过程,一般情况下,在把所有数都排好序前,就可以找到第k大的数.我们依据的逻辑是,经过一次partition后,数组被pivot分成左右两部分:S左.S右.当S左的元素个数|S左|等于k-1时,pivo

顺序统计:寻找序列中第k小的数

最直观的解法,排序之后取下标为k的值即可. 但是此处采取的方法为类似快速排序分块的方法,利用一个支点将序列分为两个子序列(支点左边的值小于支点的值,支点右边大于等于支点的值). 如果支点下标等于k,则支点就是查找的值,如果支点的下标大于k,则在左子序列里继续寻找,如果支点下标小于k,则继续在支点右子序列里面继续寻找第(k-支点下标)小的值. c#实现算法如下: public class FindSpecialOrderElement<T> where T : IComparable<T&