十大基础实用算法之寻找最小(最大)的k个数-线性查找算法

例如:输入1,2,3,4,5,6,7,8这8个数字,则最小的4个数字为1,2,3,4。

思路1:最容易想到的方法:先对这个序列从小到大排序,然后输出前面的最小的k个数即可。如果选择快速排序法来进行排序,则时间复杂度:O(n*logn)

注:针对不同问题我们应该给出不同的思路,如果在应用中这个问题的规模不大,或者求解前k个元素的频率很高,或者k是不固定的。那么我们花费较多的时间对问题排序,在以后是使用中可以线性时间找到问题的解,总体来说,那么思路一的解法是最优的。

思路2:在思路1的基础上更进一步想想,题目并没有要求要查找的k个数,甚至后n-k个数是有序的,既然如此,咱们又何必对所有的n个数都进行排序列?如此,我们能想打的一个方法是:遍历n个数,先把最先遍历到得k个数存入大小为k的数组之中,对这k个数,利用选择或交换排序,找到k个数中的最大数kmax(kmax设为k个元素的数组中最大元素),用时O(k)(你应该知道,插入或选择排序查找操作需要O(k)的时间),后再继续遍历后n-k个数,x与kmax比较:如果x<kmax,则x代替kmax,并再次重新找出k个元素的数组中最大元素kmax‘;如果x>kmax,则不更新数组。这样,每次更新或不更新数组的所用的时间为O(k)或O(0),整趟下来,总的时间复杂度平均下来为:n*O(k)=O(n*k)

思路3:与思路2方法类似,只是用容量为k的最大堆取代思路2中数组的作用(从数组中找最大数需要O(k)次查找,而从更新一个堆使之成为最大堆只需要O(logk)次操作)。具体做法如下:用容量为k的最大堆存储最先遍历到的k个数,并假设它们即是最小的k个数,建堆费时O(k)后,有k1<k2<…<kmax(kmax设为大顶堆中最大元素)。继续遍历数列,每次遍历一个元素x,与堆顶元素比较,x<kmax,更新堆(用时logk),否则不更新堆。这样下来,总费时O(k+(n-k)*logk)=O(n*logk)。

思路4:按编程之美中给出的描述,类似快速排序的划分方法,N个数存储在数组S中,再从数组中随机选取一个数X(随机选取枢纽元,可做到线性期望时间O(N)的复杂度),把数组划分为Sa和Sb俩部分,Sa<=X<=Sb,如果要查找的k个元素小于Sa的元素个数,则返回Sa中较小的k个元素,否则返回Sa中所有元素+Sb中小的k-|Sa|个元素。像上述过程一样,这个运用类似快速排序的partition的快速选择SELECT算法寻找最小的k个元素,在最坏情况下亦能做到O(N)的复杂度。oh,太酷了,有没有!

思路5:仍然用到数据结构:堆。具体做法为:针对整个数组序列建最小堆,建堆所用时间为O(n),然后取堆中的前k个数,总的时间复杂度即为:O(n+k*logn)。

思路6:与上述思路5类似,不同的是在对元素数组原地建最小堆O(n)后,然后提取K次,但是每次提取时,换到顶部的元素只需要下移顶多k次就足够了,下移次数逐次减少(而上述思路5每次提取都需要logn,所以提取k次,思路5需要k*logn。而本思路只需要K^2)。此种方法的复杂度为O(n+k^2)。此方法可能不太直观,一个更直观的理解是:每次取出堆顶元素后,最小堆的性质被破坏了,我们需要调整最小堆使之满足最小堆的性质。由于我们只需要求取前k个数,我们无需将整个堆都完整的调整好,只需保证堆的最上面k个数是最小的就可以,即第一趟调整保持第0层到第k层是最小堆,第二趟调整保持第0层到第k-1层是最小堆…,依次类推。

几个简单的提取前k个元素,竟然可以有这么多的思路来实现,复杂度逐渐降低,感觉是多么酷的一件事情啊。

下面给出思路三和思路四的参考代码(这些代码都凝结了快速排序和堆排序的思想,所以说之前的算法实用,主要是思想):

思路三:

#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#define PARENT(i) (i)/2
#define LEFT(i) 2*(i)+1
#define RIGHT(i) 2*(i+1)

void swap(int *a,int *b)
{
    *a=*a^*b;
    *b=*a^*b;
    *a=*a^*b;
}
void max_heapify(int *arr,int index,int len)//建立大顶堆的过程,求前k个最小,要健最大堆
{
    int l=LEFT(index);//所有操作类似于堆排序
    int r=RIGHT(index);
    int largest;
    if(l<len && arr[l]>arr[index])
        largest=l;
    else
        largest=index;
    if(r<len && arr[r]>arr[largest])
        largest=r;
    if(largest != index){//将最大元素提升,并递归
        swap(&arr[largest],&arr[index]);
        max_heapify(arr,largest,len);//递归
    }
}

void build_maxheap(int *arr,int len)//开始建立大顶堆是必须的
{
    int i;
    if(arr==NULL || len<=1)
        return;
    for(i=len/2+1;i>=0;--i)
        max_heapify(arr,i,len);
}

void k_min(int *arr,int len,int k)
{
    int i;
    build_maxheap(arr,k);
    for (i = k;i < len;i++)
    {
        if (arr[i] < arr[0])//就是这一个地方跟堆排序不一样,这里只是交换比堆顶大的元素。
        {
            arr[0] = arr[i];
            max_heapify(arr,0,k);
        }
    }
}
/*
void heap_sort(int *arr,int len)//这是堆排序的主函数
{
    int i;
    if(arr==NULL || len<=1)
        return;
    build_maxheap(arr,len);

    for(i=len-1;i>=1;--i){
        swap(&arr[0],&arr[i]);
        max_heapify(arr,0,--len);
    }
}
*/

int main()
{
    int arr[10]={91,8,6,82,15,18,7,46,29,12};
    int i;
    k_min(arr,10,4);
    for(i=0;i<10;++i)//只需要输出前k个元素即可。
        printf("%d ",arr[i]);
    system("pause");
}

思路四的实现:

Kbig(S, k):
     if(k <= 0):
          return []     // 返回空数组
     if(length S <= k):
          return S
     (Sa, Sb) = Partition(S)
     return Kbig(Sa, k).Append(Kbig(Sb, k – length Sa)  

Partition(S):
     Sa = []            // 初始化为空数组
     Sb = []        // 初始化为空数组
     Swap(s[1], S[Random()%length S])   // 随机选择一个数作为分组标准,以
                        // 避免特殊数据下的算法退化,也可
                       // 以通过对整个数据进行洗牌预处理
                        // 实现这个目的
     p = S[1]
     for i in [2: length S]:
         S[i] > p ? Sa.Append(S[i]) : Sb.Append(S[i])
                            // 将p加入较小的组,可以避免分组失败,也使分组
                           // 更均匀,提高效率
     length Sa < length Sb ? Sa.Append(p) : Sb.Append(p)
     return (Sa, Sb)  

下面是代码实现:

#include <stdio.h>
    #include <stdlib.h>
     void swap(int *a,int *b)
    {   *a=*a^*b;
        *b=*a^*b;
        *a=*a^*b;
    }
    /*
    为了简单起见,这里只是单纯的选取第一个元素作为枢纽元素。这样选取枢纽,就难避免使得算法容易退化。
    */
    int k_big(int arr[],int low,int high,int k)
    {
        int pivot  = arr[low];//pivot的选择可以更优
        int high_tmp = high;
        int low_tmp = low;
        while(low < high){
            //从右向左查找,直到找到第一个小于枢纽元素为止
            while (low < high && arr[high] >= pivot)
            {
                --high;
            }
            arr[low] = arr[high];
            //从左向右查找,直到找到第一个大于枢纽元素为止
            while (low < high && arr[low] <= pivot)
            {
                ++low;
            }
            arr[high] = arr[low];
        }
        arr[low] = pivot;//这里low == high

        if (low == k - 1)//正好取到了第k个值
        {
            return arr[low];
        }else if(low > k - 1)//前k个值在low前面的子数组中
        {
            return k_big(arr,low_tmp,low-1,k);
        }else//前low个数值已经是前k个数值,后k-low个在后半部分中
        {
            return k_big(arr,low+1,high_tmp,k);
        }
    }
    int main()
    {
        int arr[10]={-91,0,6,82,15,18,7,46,-29,12};
        int i;
        k_big(arr,0,9,4);
        for(i=0;i<10;++i)
            printf("%d ",arr[i]);
        system("pause");
    }

下面是《算法设计与分析》书中给出的一种思路,源码如下:

//QuickSelect 将第k小的元素放在 a[k-1]
void QuickSelect( int a[], int k, int left, int right )
{
    int i, j;
    int pivot;

    if( left + cutoff <= right )
    {
        pivot = median3( a, left, right );
        //取三数中值作为枢纽元,可以很大程度上避免最坏情况
        i = left; j = right - 1;
        for( ; ; )
        {
            while( a[ ++i ] < pivot ){ }
            while( a[ --j ] > pivot ){ }
            if( i < j )
                swap( &a[ i ], &a[ j ] );
            else
                break;
        }
        //重置枢纽元
        swap( &a[ i ], &a[ right - 1 ] );  

        if( k <= i )
            QuickSelect( a, k, left, i - 1 );
        else if( k > i + 1 )
            QuickSelect( a, k, i + 1, right );
    }
    else
        InsertSort( a + left, right - left + 1 );
}

这个快速选择SELECT算法,类似快速排序的划分方法。N个数存储在数组S中,再从数组中选取“中位数的中位数”作为枢纽元X,把数组划分为Sa和Sb俩部分,Sa<=X<=Sb,如果要查找的k个元素小于Sa的元素个数,则返回Sa中较小的k个元素,否则返回Sa中所有元素+Sb中小的k-|Sa|个元素,这种解法在平均情况下能做到  O(N)  的复杂度。

下面是算法导论中给出的一种方法:

《算法导论》介绍了一个随机选取主元的选择算法RANDOMIZED-SELECT。它每次都是随机选取数列中的一个元素作为主元,在  O(n) 的时间内找到第k小的元素,然后遍历输出前面的k个小的元素。平均时间复杂度:  O(n+k)=O(n)  (当k比较小时)。

我们知道,快速排序是以固定的第一个或最后一个元素作为主元,每次递归划分都是不均等的,最后的平均时间复杂度为:  O(n*logn) 。而RANDOMIZED-SELECT与普通的快速排序不同,它每次递归都是随机选择序列,从第一个到最后一个元素中任一一个作为主元。

下面是RANDOMIZED-SELECT(A, p, r)完整伪码:

PARTITION(A, p, r)         //partition过程 p为第一个数,r为最后一个数
  x ← A[r]               //以最后一个元素作为主元
  i ← p - 1
  for j ← p to r - 1
       do if A[j] ≤ x
             then i ← i + 1
                  exchange A[i] <-> A[j]
  exchange A[i + 1] <-> A[r]
  return i + 1

RANDOMIZED-PARTITION(A, p, r)      //随机快排的partition过程
  i ← RANDOM(p, r)                                 //i  随机取p到r中个一个值
  exchange A[r] <-> A[i]                         //以随机的 i作为主元
  return PARTITION(A, p, r)            //调用上述原来的partition过程

RANDOMIZED-SELECT(A, p, r, i)       //以线性时间做选择,目的是返回数组A[p..r]中的第i 小的元素
  if p = r          //p=r,序列中只有一个元素
      then return A[p]
  q ← RANDOMIZED-PARTITION(A, p, r)   //随机选取的元素q作为主元
  k ← q - p + 1             //k表示子数组 A[p…q]内的元素个数,处于划分低区的元素个数加上一个主元元素
  if i == k                        //检查要查找的i 等于子数组中A[p....q]中的元素个数k
      then return A[q]        //则直接返回A[q]
  else if i < k
      then return RANDOMIZED-SELECT(A, p, q - 1, i)
          //得到的k 大于要查找的i 的大小,则递归到低区间A[p,q-1]中去查找
  else return RANDOMIZED-SELECT(A, q + 1, r, i - k)
          //得到的k 小于要查找的i 的大小,则递归到高区间A[q+1,r]中去查找。  

也就是线性查找算法:

算法步骤:

1. 将n个元素每5个一组,分成n/5(上界)组。

2. 取出每一组的中位数,任意排序方法,比如插入排序。

3. 递归的调用selection算法查找上一步中所有中位数的中位数,设为x,偶数个中位数的情况下设定为选取中间小的一个。

4. 用x来分割数组,设小于等于x的个数为k,大于x的个数即为n-k。

5. 若i==k,返回x;若i<k,在小于x的元素中递归查找第i小的元素;若i>k,在大于x的元素中递归查找第i-k小的元素。

终止条件:n=1时,返回的即是i小元素。

十大基础实用算法之寻找最小(最大)的k个数-线性查找算法

时间: 2024-10-06 01:18:58

十大基础实用算法之寻找最小(最大)的k个数-线性查找算法的相关文章

十大基础实用算法之迪杰斯特拉算法、最小生成树和搜索算法

迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个节点到其他节点的最短路径. 它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止. 基本思想 通过Dijkstra计算图G中的最短路径时,需要指定起点s(即从顶点s开始计算). 此外,引进两个集合S和U.S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),而U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离). 初始时,S中只有起点s:U中是除s之外的顶点,并且U中顶点的路径是"起点s

十大基础实用算法之动态规划

动态规划(Dynamic programming)是一种在数学.计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法. 动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法. 动态规划背后的基本思想非常简单.大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解. 通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将

十大基础实用算法之快速排序和堆排序

快速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序 n 个项目要Ο(n log n)次比较.在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见.事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来. 快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists). 算法步骤: 1 从数列中挑出一个元素,称为 "基准"(pi

十大基础实用算法之归并排序和二分查找

归并排序 归并排序是建立在归并操作上的一种有效的排序算法.该算法是采用分治法(Divide and Conquer)的一个非常典型的应用. 算法步骤: 1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列 2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置 3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置 4. 重复步骤3直到某一指针达到序列尾 5. 将另一序列剩下的所有元素直接复制到合并序列尾 用分治策略解决问题分为三步:分解

十大基础实用算法之深度优先搜索和广度优先搜索

深度优先搜索算法(Depth-First-Search),是搜索算法的一种.它沿着树的深度遍历树的节点,尽可能深的搜索树的分支.当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点.这一过程一直进行到已发现从源节点可达的所有节点为止.如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止.DFS属于盲目搜索. 深度优先搜索是图论中的经典算法,利用深度优先搜索算法可以产生目标图的相应拓扑排序表,利用拓扑排序表可以方便的解决很多相

程序员必须知道的10大基础实用算法及其讲解

程序员必须知道的10大基础实用算法及其讲解 原文出处: cricode 算法一:快速排序算法 快速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序 n 个项目要Ο(n log n)次比较.在最坏状况下则需要Ο(n2)次比 较,但这种状况并不常见.事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构 上很有效率地被实现出来. 快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子

程序员必知的10大基础实用算法

    算法一:快速排序算法 快速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序 n 个项目要Ο(n log n)次比较.在最坏状况下则需要Ο(n2) 次比较,但这种状况并不常见.事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的 架构上很有效率地被实现出来. 快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists). 算法步骤: 1 从数列中挑出一个元

《转》程序员必须知道的10大基础实用算法及其讲解

来源: Cricode  发布时间: 2014-06-19 08:27  阅读: 2018 次  推荐: 8   原文链接   [收藏] 算法一:快速排序算法 快速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序n个项目要Ο(nlogn)次比较.在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见.事实上,快速排序通常明显比其他Ο(nlogn)算法更快,因为它的内部循环(innerloop)可以在大部分的架构上很有效率地被实现出来. 快速排序使用分治法(Divideandconque

[转载]程序员必须知道的10大基础实用算法及其讲解

算法一:快速排序算法   快速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序 n 个项目要Ο(n log n)次比较.在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见.事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来. 快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists). 算法步骤: 1 从数列中挑出一个元素,称为