1.3排序——归并:一些计算递归的方法

我们来看看归并排序(merge sort)。

def MergeSort(lists):
    if len(lists) <= 1:
        return lists
    num = int( len(lists) / 2 )
    left = MergeSort(lists[:num])
    right = MergeSort(lists[num:])
    return Merge(left, right)
def Merge(left,right):
    r, l=0, 0
    result=[]
    while l<len(left) and r<len(right):
        if left[l] <= right[r]:
            result.append(left[l])
            l += 1
        else:
            result.append(right[r])
            r += 1
    result += list(left[l:])
    result += list(right[r:])
    return result

其实归并的主体思想还是很简单的,就是把两个排好序的数组以某种方法放在一起,让他们继续有序。其实归并排序的有难度的地方是递归的过程,当然,在这个算法中递归并不太难,而这种思想有的时候出现的方式很奇妙。

按照CLRS的伪代码,我们可以得到其大致思路:把2个数组末尾放一个很大的数,前边正常比较,小的排列;当某个数组已经达到那个很大的数时,另一个数组的所有数(除了末尾的那个)都会比它小。运算次数的控制就交给向数组中放数的变量,它从最左到最右端,只要它达到了最大,就停止。

那我们来写一下做归并的Merge函数。

template<typename T>
void Merge( T A[ ], int Left, int Center, int Right )  //A为总的数组,其余3个变量为左,中,右
{
    n1 = Center - Left + 1;  //左数组的总数
    n2 = Right - Center;
    T *L = new T[ n1 + 1 ];
    T *R = new T[ n2 + 1 ];
    for ( int n = 0; n != n1; ++n )
        L[ n ] = A [ Left + n ];
    for ( int n = 0; n != n1; ++n)
        R[ n ] = A[ Center + n + 1 ];
    L[n1] = 500000;  //最后一个数设为很大
    R[n2] = 500000;
    int i = 0, j = 0, k = Left;
    while( k <= Right)  //k是控制总排序个数的变量,可以看出k的数量为Right-Left+1,就是本次的总数
        if ( L[ i ] <= R [ j ])
            A[ k++ ] = L[ i++ ];  //对于小的,让它进入数组的前边,表示已排序
        else
            A[ k++ ] = R[ j++ ];
    delete[ ]L;
    delete[ ]R;
}

有一个精妙的地方,就是A[ k++ ] = L[ i++ ];,我们只需要先赋值,再自加,这样写就只用一句话,很简洁。记得C++ primer 5e 里边有说,不需要的时候尽量采用前置自加/自减,因为i++这种后置形式实际上是做完自加后,返回原值,它保存了不必要的变量,同时有时还会引起误解,除非有特别的作用。而这里,就是很叼的“特别的作用”了!记得谭浩强的神题i+++++i吗?如果不想有这种效果,就最好不要使用有误解的写法。

既然说到这里了,我们就顺便复习一下c++的递增/递减运算符重载。我们需要区分一哈后置和前置递增。简单实现一个前置递增:

someClass& someClass::operator++()
{
    check(thisNum);  //假设有个数是thisNum
    ++thisNum;
    return *this;
}

那么前置呢,为了区分,我们给他一个参数(但不调用):

somClass someClass::opearator++(int)  //这里不使用引用仅仅是因为和内置版本保持一致,返回一个值
{
    somClass ret = *this;
    ++*this;
    return ret;
}

这段代码的驱动代码为:

void MergeSort( T *A[ ], int L, int R )
{
    if ( p < r )
        int center = ( L + R ) / 2;
        MergeSort( A, L, Center );
        MergeSort( A, Center+1,R );
        Merge( A, L, Center, R );
}

不过这个代码也有个问题,就是使用所谓的“哨兵”,在paper里,它可以是无限大∞,可是实现中如何保持它最大呢,如果是固定的类型,比如int,我们尚且可以用INT_MAX,可是泛型呢?怎么办?所以,我们得使用其他的度量方式,那就是数组长度。这一段代码我建议大家自己写写,因为有各种各样的+1,-1,很适合练习一下自己的“逻辑思维”。那么,我们写一下?
首先,我们先写个主程序:

template<typename T>
void MergeSort( T A[ ], int Left, int Right )
{
    int Length = Right - Left + 1;
    T *tmp = new T[ Length ];  //new一个和原始数组一样长的tmp
    if ( tmp != nullptr )
    {
        if (L < R)
        {
            int Center = ( L + R ) / 2;
            MergeSort( A, Left, Center );
            MergeSort( A, Center + 1, Right );
            Merge( A, tmp, Left, Center, Right );
        }
        delete[ ]tmp;
    }
}

然后,我们写出它的merge例程:

template<typename T>
void Merge( T A[], T tmp[], int Left, int Center, int Right ) 
{
    int i = Left, j = Center, k = Left;
    while ( i != Center + 1 && j != Right + 1 )  //直到有一方为0,则停止循环
        if ( A[ i ] <= A[ j ])
            tmp[ k++ ] = A[ i++ ];
        else
            tmp[ k++ ] = A[ j++ ];
    //将剩余的放入,哪个剩余放哪个
    while ( i != Center + 1 )
        tmp[ k++ ] = A[ i++ ];
    while ( j != Right + 1 )
        tmp[ k++ ] = A[ j++ ];
    for ( int i = Left; i != Right + 1; ++i )
        A[ i ] = tmp[ i ];
}

这段程序其实很好理解,它的过程分为2段:1)在2个数组都非空之前,让他们比较然后进入tmp;2)当一个子数组全部放入tmp之后,直接把另一个剩余的部分放入tmp就好了,那2个while循环其实只执行一个,但是你完全不需要做判断,因为另一个会因为不满足条件而立刻结束。

现在我们来看看归并排序的时间复杂度。

归并排序把一个问题划分为2个子问题,其中一个大小是不超过N/2的最大整数,一个是超过N/2的最小整数。而这种不影响大方面的问题可以忽略。于是:

T(n)=T(n/2)+T(n/2)+Θ(n)=2T(n/2)+Θ(n)=2T(n/2)+cn

如果画一棵递归树,就会发现:(我不想画啊,去看CLRS啦~~):
每一棵树有2个分支,每个节点承担n/2的规模大小。这相当于子问题每一层都减少n/2的规模,那么树的高度就是log2n。而每一层的代价是cn,所以总代价为cn*log2n+cn=Θ(nlgn)。

递归树有一个小问题,如果两个子问题不一样怎么办?简单的说,取b小的那条路作为最长路径。你想,b小,说明这个子问题规模大,那它肯定需要被分的更多一些才能到最小情况。

然而每一次都画递归树好像有点麻烦,那么我们再介绍一种有趣的方法,主方法。看上边的式子:T(n)=2T(n/2)+cn,我们可以把它概括为:T(n)=aT(n/b)+f(n),a代表每一次产生几个子问题,1/b则是每次问题的规模。我们将不证明主定理,只使用,在主观上,它有3种条件下的使用:
1.若f(n)<nlogba,则T(n)=Θ(nlogba)。很好理解,就是一个大,那么它当然占用的时间是更多的。
2.若f(n)=nlogba,则T(n)=Θ(nlogbalgn)
3.若f(n)>nlogba,则T(n)=Θ(f(n));

不过,这不是很对的表述,因为不仅仅要<或者>,是要在多项式意义上<或>,也就是说那些<或>必须要<或>一个量:nε,ε是一个常数。简单的判断方法是做除法,若f/nlogba商是一个必然渐进小于nε的数(如lgn),那么它们不满足主方法条件,还是老老实实用递归树吧~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容