快排扩展,Partition函数的应用(一)

  快排作为平均速度最快的一种内部排序,基础思想是基于分治的。关于分治算法设计思想总结,我在讲解算法设计思路这篇文章讲到过。快速排序中一个非常核心的地方就是partition函数的作用,这个函数支撑了快排的分的过程。我觉得这个函数重要,不单是因为其支撑了快排,而且在很多算法设计的过程中也都可以使用的上。
  关于这个扩展,我打算分两次文章来写,我会着重的讲到问题的转换,这算是一个非常重要的算法求解能力。以下仍然从题目开始。

题目描述

  输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分。

题目分析

  简单粗暴的方式,可能就是类似于排序的方式,把偶数移动到后面,这种暴力法太过浪费了,这么高的复杂度排个序都够了,但是题目中显然没有要求排序,这样就太不合适,我们思考其他更低复杂度的算法。
  乍一看这个题目好像和partition函数的关系不大,但是仔细分析就能够发现联系。 partition函数是根据与pivot的大小关系,把数据分为两部分,而本题呢,是根据数据的奇偶性把数据分为两部分。再看题目中奇数位于数组的前半部分,奇数位于数组的后半部分,竟然与partition函数中比pivot小的放在前半部分,大的放在后半部分不谋而和。

python代码

  partition函数是判断大小进行前移或者后移,这里就改变判断条件,根据奇偶性前移或者后移即可。因为快排要多次调用partition函数,故而将其作为子函数调用,而这里只需要一趟移动即可,直接一个函数就够了。

def partition(array):
    start, end=0,len(array)-1

    while start<end:
        while start<end and not array[end]&1:
            end-=1
        array[start], array[end] = array[end], array[start]
        while start<end and array[start]&1:
            start+=1
        array[start],array[end]=array[end],array[start]
    return array

扩展分析

  我们提到过根据××把数据分为两部分,我们上面已经提到了两种情况,同时可能还有其他情况,我们可以总结升华一下,对于所有的判断条件,无非结果都是得到一个布尔值,真或假。那我们可以把所有的情况都抽象出来,而把判断条件作为一个函数,这个函数就是单独的返回布尔值。
  这样的分析就能使我们的代码进行解耦,这样就能处理任何情况下对数组的划分。

python代码

def isOdd(number):
    # 这里返回的int类型可以转换为bool类型
    return number&1

def partition(array,condition):
    # condition参数是一个函数
    start, end=0,len(array)-1

    while start<end:
        while start<end and not condition(array[end]):
            end-=1
        array[start], array[end] = array[end], array[start]
        while start<end and condition(array[start]):
            start+=1
        array[start],array[end]=array[end],array[start]
    return array

  如果题目要求,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变,那上面的方式可就不行了,因为我们都知道 快排是一个不稳定的排序,主要原因就是因为partition函数这里会改变相等元素的相对位置,因为大小判断变味了奇偶判断,那么值相等就对应了奇偶性相等 。所以partition函数也就会改变同奇偶性数据之间的相对位置。
  题目一变这种方法就失效了,需要寻求其他方法。

牺牲空间换时间,时间O(n),空间O(n)的算法

  在算法设计中,常会遇到这种抉择的过程,而 伴随计算机发展的一个重要思想就是权衡(TradeOff)。不能在两个维度上都得到最好的解的时候,就需要权衡。所以我们不得已牺牲空间换时间,要想快速解决这个问题,就得付出空间的代价。
  这种思路也很简单,就直接重新开辟一块空间,然后对数组扫描两次,第一次把奇数复制出去,第二次把偶数复制出去,放在奇数的后面。扫描两次,时间复杂度是O(n),整个数组复制了一次空间复杂度是O(n)。其他优化不能做到复杂度级别的降低,不予讲解。
  这种方法的代码很好写,就不过多强调。但是这种方法的前提是可以使用额外空间。如果不能使用,我们就得另谋他法。

基于排序的方法

  我们之前说过既然可以借鉴排序的思想,那我们就借鉴到底,我们把奇数全都当做0,偶数全都当做1,这样移动的结果就相当于进行了一趟排序,题目要求不改变元素的相对位置,这就意味着要求排序是稳定的。所以不稳定的排序直接排除(快排、堆排序、选择排序、希尔排序)。现在剩下稳定的排序(归并排序、冒泡排序、插入排序、计数排序),以及建立在其他排序基础上的稳定排序(桶排序、基数排序)。建立在其他排序基础之上的直接排序了,因为问题简单,用这两个排序降低不了复杂度。
  基本上可以确定的剩下这几个稳定的基础排序,排序本身的复杂度就是解决这个问题的复杂度。

排序 时间复杂度 空间复杂度 描述
计数排序 O ( n ) O(n) O ( n ) O(n) 需要占用额外空间
归并排序 O ( n l g n ) O(nlgn) O ( n ) O(n) 各种性质都比不上计数排序
插入排序 O ( n 2 ) O(n^2) O ( 1 ) O(1) 慢但是不占用额外空间,只有偶数才需要后移
冒泡排序 O ( n 2 ) O(n^2) O ( 1 ) O(1) 时间复杂度高但是不占用额外空间

  这里的思想很简单明了了,代码我就略去,可见各个解法都有缺陷。

总结

  通过这一小节的讲解,希望大家明白的是,问题之间的相互转换。各种已经掌握的算法是很好的算法设计基础,可能在不经意之间对问题的转换就能让我们通过已有算法来很好地解决当下的问题。这就要求我们第一要注重平时的基积累,不要仅仅停留在算法本身上,而应该看到算法所能带来的演变。第二需要我们能够发现问题之间的联系,把看似不想关的问题建立起联系,然后轻松求解,即所谓的举一反三。
  下一篇文章还会继续对partition函数进行讲解,下面是文章链接
快排扩展,Partition函数的应用(二)

发布了36 篇原创文章 · 获赞 4 · 访问量 47万+

猜你喜欢

转载自blog.csdn.net/m0_38065572/article/details/104341323