重拾算法之路——线性时间选择

***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

第一章:分治与递归

线性时间选择

算法描述:

给定线性序集中n个元素和一个整数k,1 ≤ k ≤ n,要求找出这n个元素中第k小的元素。即如果将这n个元素依其线性序排列时,排在第k个位置的元素即为要找的元素。当k=1时,就是找最小元素,k=n时,就是找最大元素,当k=(n+1)/2时,就是找中位数。

算法分析:

在某些特殊情况下,很容易设计出解选择问题的线性时间算法。例如,找n个元素的最小元素和最大元素显然可以在O(n)
时间内完成。如果 k ≤ n/log(n) ,通过堆排序算法可以在 O(n+klog(n)) = O(n)时间内找出第k小元素,当k≥n-n/log(n)时也一样。

一般的选择问题,特别是中位数的选择问题似乎比找最小元素要难。但事实上,从渐进阶的意义上看,它们是一样的。一般选择问题也可以在O(n)时间内得到解决。

这次讨论 一般选择问题的一个分治算法 RandomizedSelect。这个算法是模仿 快速排序算法 设计出来的。基本思想也是 对输入数组 进行递归划分。

☆ 注意:本算法与快速排序算法 不同之处: 本算法 只对 划分出的
子数组之一 进行递归处理。 ☆

算法程序:

<span style="font-family:Comic Sans MS;">#include <iostream>
#include <ctime>
#include <cstdlib>

using namespace std;

template <class Type>
void Swap(Type& x,Type& y)
{
    Type temp = x;
    x = y;
    y = temp;
}

int Random(int l, int r)
{
    srand((unsigned)time(NULL));
    int rd = rand()%(r-l+1)+l;
    return rd;
}

// Partition函数
template <class Type>
int Partition(Type a[],int l,int r)
{
    int i = l,j = r + 1;
    Type temp = a[l];

    while(true)
    {
        while( a[++i] < temp && i < r );
        while( a[--j] > temp );
        if( i >= j )   break;
        Swap(a[i],a[j]);
    }
    a[l] = a[j];
    a[j] = temp;
    return j;
}

template<class Type>
int RandomizedPartition(Type a[],int l,int r)
{
    int tem = Random(l,r);
    Swap(a[tem],a[l]);
    return Partition(a,l,r);
}

template <class Type>
Type RandomizedSelect(Type a[],int l,int r,int k)
{
    if(l == r)  return a[l];
    int tem,j;
    tem = RandomizedPartition(a,l,r);
    j = tem - l + 1;
    if(k <= j)  return RandomizedSelect(a,l,tem,k);
    else    return RandomizedSelect(a,tem+1,r,k-j);
}</span>

这里面的函数:

——Swap   交换两个元素的值

——Random 取两个区间内的随机数

——Partition 在快排中也有见过,就是以第一个数为标准,小于它的在它左面,大于它的在它右面

——RandomizedPartition
加了随机数的Partition,这次就不一定以第一个数为标准了,在范围内随机一个数为标准

——RandomizedSelect
求l到r,第k小的元素

算法讲解:

在上述算法程序中,执行完函数RandomizedPartition后,数组a[l:r]会被分为两个部分:a[l:tem] 和 a[tem+1:r],但左面部分的每一个元素都不大于 右面部分的任何一个元素。

接着计算左面部分元素个数j,如果 k ≤ j ,则a数组中第k小的元素坐落在子数组a[l:tem]中。反之坐落在a[tem+1:r]中。所以只需要对一个子数组进行扩展。

在最坏的情况下,算法RandomizedSelect需要 Ω(n^2)的计算时间。尽管这样,该算法的平均性能很好,因为随机划分数RandomizedPartition使用了随机数产生器Random,在这个条件下可以证明,算法RandomizedSelect可以在O(n)平均时间内找出n个输入元素中的第k小元素。

算法优化:

接下来讨论一个可以在最坏情况下用O(n)时间就完成选择任务的算法Select。

如果能在线性时间内找到一个划分基准,使得按照这个基准所划分出的两个子数组长度都至少为元数组长度的 m 倍(0 < m < 1),那么在最坏情况下用O(n)时间就可以完成选择任务。

例如,m=9/10 ,算法递归调用所产生的子数组长度至少缩短1/10。所以,在最坏情况下,算法所需的时间为T(n) 满足递归式T(n) ≤ T(9n/10)+O(n)。由此可得T(n)=O(n)。

算法步骤如下:

<1> 将所有的数n个以每5个划分为一组共组,将不足5个的那组忽略,然后用任意一种排序算法,因为只对5个数进行排序,所以任取一种排序法就可以了。将每组中的元素排好序再分别取每组的中位数,得到个中位数。

<2> 取这个中位数的中位数,如果是偶数,就找它的2个中位数中较大的一个作为划分基准。

<3> 将全部的数划分为两个部分,小于基准的在左边,大于等于基准的放右边。在这种情况下找出的基准x至少比个元素大。因为在每一组中有2个元素小于本组的中位数,有个小于基准,中位数处于,即个中位数中又有个小于基准x。因此至少有个元素小于基准x。同理基准x也至少比个元素小。而当n≥75时≥n/4所以按此基准划分所得的2个子数组的长度都至少缩短1/4。

程序:

<span style="font-family:Comic Sans MS;">template <class Type>
void Swap(Type& x,Type& y)
{
    Type temp = x;
    x = y;
    y = temp;
}
int Random(int l, int r)
{
//    srand((unsigned)time(NULL));
    int rd = rand()%(r-l+1)+l;
    return rd;
}
template <class Type>
int Partition(Type a[],int l,int r,Type x)
{
    int i = l-1,j = r + 1;

    while(true)
    {
        while(a[++i]<x && i<r);
        while(a[--j]>x);
        if(i>=j)    break;
        Swap(a[i],a[j]);
    }
    return j;
}
template <class Type>
void BubbleSort(Type a[],int l ,int r )
{
    for( int j = l ; j < r-1 ; ++j )
        for( int i = l ; i < r-1-j ; ++i )
            if( a[i] > a[i+1] )
                Swap(a[i],a[i+1]);
}
template <class Type>
Type Select( Type a[],int l ,int r,int k )
{
    if( (r-l) < 75 )    {
        // 元素小于75个,直接用某个排序排一下就行(这里用的冒泡)
        BubbleSort(a,l,r);
        return a[l+k-1];
    }
    for( int i = 0 ; i <= (r-l-4)/5 ; ++i ) {
        //将元素每5个分成一组,分别排序,并将该组中位数与a[l+i]交换位置
        //使所有中位数都排列在数组最左侧,以便进一步查找中位数的中位数
        BubbleSort(a,l+5*i,l+5*i+4);
        Swap(a[l+5*i+2],a[l+i]);
    }
    // 找中位数的中位数
    Type x = Select(a,l,l+(r-l-4)/5,(r-l-4)/10);
    int i = Partition(a,l,r,x);
    int j = i-l+1;
    if(k<=j)
        return Select(a,l,i,k);
    else
        return Select(a,i+1,r,k-j);
}</span>

***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

时间: 2024-08-09 22:01:30

重拾算法之路——线性时间选择的相关文章

重拾算法之路——前言

话说,自从大二下学期从ACM集训队退出后, 对于算法, 真的放下了很多. 曾经说过,离开不是句号, 现在的情况,越来越趋近叹号了 o(╯□╰)o.. 其实,算法的用处,对于目前状态的我, 真的非常非常...不重要 ( ⊙ o ⊙ )!, 在做项目的时候,会经历这样的过程: ①算法NB论,但自己只是知道NB,用不上 ②算法无用论,做了一段时间项目,发现算法这东西,在我们项目中根本用不上,要么就是包装好的东西,直接调用过来,无须我们担心. ③算法还是很NB,刚开始接触的项目,只是初步应用,大多处于初

重拾算法之路——二分搜索

***************************************转载请注明出处:http://blog.csdn.net/lttree******************************************** 隶属于--递归与分治 描述: 给定 已排好序 的n个元素a[0;n-1],现在要在这n个元素中找出一特定元素x. 朴素法: 当然这是好听的说法,明白点叫最笨的方法,就是顺序搜索,逐个比较0到n-1中元素,直至找出元素x或搜索遍所有元素,确定x不在元素中,这个方法

重拾算法之路——递归与分治基础

***************************************转载请注明出处:http://blog.csdn.net/lttree******************************************** 这个周末家里有点事,回了趟家,就断了一些学习计划.. 抓紧补上! 第一个算法--递归与分治 都知道,分治算法基本思想是 将一个难以直接解决的问题,分割成一些规模小的相同问题,以便各个击破,分而治之, 这样,我们就可以将一个复杂的算法,类型不变,规模越来越小,最终

重拾算法之路——算法概述及NP完全性理论

***************************************转载请注明出处:http://blog.csdn.net/lttree******************************************** 我的这系列文章参考的书是: 王晓东 的 <计算机算法设计与分析>(第4版) 首先,当然是第一章节的概述,这些东西简要说一下概念: 1.算法与程序的区别? --算法 指 解决问题的一种方法 或 一个过程 (严格的讲,算法是由若干条指令组成的有穷序列) --程序

重拾算法(0)——目录

现在到了重拾基础算法,掌握算法思维的时候.暂定要学习的算法如下表. 1 算法 2 KMP 3 树 4 遍历二叉树 5 线索二叉树 6 霍夫曼树 7 图 8 深度优先搜索 9 广度优先搜索 10 最小生成树 11 最短路径 12 拓扑排序 13 关键路径 14 查找 15 线性表的查找 16 折半查找 17 树的查找 18 二叉排序树 19 平衡二叉树 20 B-树 21 B+树 22 散列表的查找 23 构造方法 24 处理冲突的方法 25 查找 26 排序 27 插入排序 28 直接插入排序

重拾算法(5)——最小生成树的两种算法及其对比测试

重拾算法(5)——最小生成树的两种算法及其对比测试 什么是最小生成树 求解最小生成树(Minimum Cost Spanning Tree,以下简写做MST)是图相关的算法中常见的一个,用于解决类似如下的问题: 假设要在N个城市之间建立通信联络网,那么连通N个城市只需N-1条线路.这时自然会考虑这样一个问题:如何在最节省经费的前提下建立这个通信网. 在任意两个城市间都可以设置一条线路,相应地都要付出一定的经济代价.N个城市之间最多可能设置N(N-1)/2条线路,那么如何在这些线路中选择N-1条,

重拾算法(2)——线索二叉树

重拾算法(2)——线索二叉树 上一篇我们实现了二叉树的递归和非递归遍历,并为其复用精心设计了遍历方法Traverse(TraverseOrder order, NodeWorker<T> worker);今天就在此基础上实现线索二叉树. 什么是线索二叉树 二叉树中容易找到结点的左右孩子信息,但该结点在某一序列中的直接前驱和直接后继只能在某种遍历过程中动态获得. 先依遍历规则把每个结点某一序列中对应的前驱和后继线索预存起来,这叫做"线索化". 意义:从任一结点出发都能快速找到

重拾算法(1)——优雅地非递归遍历二叉树及其它

重拾算法(1)——优雅地非递归遍历二叉树及其它 本文中非递归遍历二叉树的思想和代码都来自这里(http://jianshu.io/p/49c8cfd07410#).我认为其思想和代码都足够优雅动人了,于是稍作整理,得到如下的程序. 前中后序遍历二叉树 1 public class BinaryTreeNode<T> 2 { 3 public T Value { get;set; } 4 public BinaryTreeNode<T> Parent { get;set; } 5 p

初学算法-快速排序与线性时间选择(Deterministic Selection)的C++实现

快速排序算法其实只做了两件事:寻找分割点(pivot)和交换数据. 所谓寻找分割点,既找到一个预计会在中间位置附近的点,当然尽量越接近中点越好. 所谓交换数据,就是把比这个分割点小的数据,最终都放在分割点左边,比它大的都放在右边. 设要排序的数组是A[left]--A[right],首先任意选取一个数据(一般算法:使用随机数选取一个区间内的数. 文艺算法:取A[left].A[right]和A[rand()]的中值. 二笔算法:选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,