排序算法(四)归并排序的实现(python)

1 归并排序

1945年由约翰·冯·诺伊曼(John von Neumann)首次提出。
在这里插入图片描述

1.1 执行流程

(1) 不断地将当前序列平均分割成2个子序列,直到不能再分割(序列中只剩1个元素)。
(2)不断地将2个子序列合并成一个有序序列,直到最终只剩下1个有序序列。
在这里插入图片描述

1.2 分割divide的实现

归并排序的思想是,如果要对一个数组进行排序,则先将数组分割为两部分,将两部分数组排序完后,将他们合并在一起成为一个新的有序数组,而对分割形成的数组,依旧采用这个思路进行排序,显然这是一个递归的过程。而归并排序的基本结构也十分好写,只需先分割对子序列排序,然后再合并即可(关键在于合并的实现),当然如果数组中只存在一个数据,就不用再排序了,直接返回就好了。

array=np.random.randint(0,10000,20000,dtype=int)
#这里start和end索引采用左闭右开
def merge_sort(start,end):
    if end-start<2:#当数组中只有一个元素,则不用再排序
        return
    mid=(end+start)>>1#取中心
    merge_sort(start,mid)
    merge_sort(mid,end)
    merge(start,mid,end)#见下

1.3 合并merge的实现

在这里插入图片描述
对于两个排好序的数组,我们如何将其合并为一个有序的大数组?方法也很简单,以递增数组为例,最小的元素一定是两个数组最左侧元素之一,我们只需进行比较,就能知道哪个是最小值。

在这里插入图片描述
得到最小元素后,我们将其放置到索引为0的位置,并更新左右未排序元素起始位置的索引。

在这里插入图片描述

下面,继续获取最小元素,放置到插入的索引中,并更新索引。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
此时左侧数组已经全部排完了,此时只需要将右侧未排序数组依次填入即可。
在这里插入图片描述
可以看到通过这样的方法,就可以将两个数组合并了,但还存在这一个问题,我们的合并实际上是在同一个数组上进行的,而不是像现在这样在另一个数组中完成了排序。如果我们选择直接将比较得到的最小值放置到原数组上,那可能会对左侧数组的数据进行覆盖,造成一些数据的丢失,为此我们可以将左侧数据复制出来,再在原数组上排序,如下图所示。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
左侧的数据排完后,可以发现,右侧已经不用排了。
在这里插入图片描述
由此,合并操作结束。

1.3.1 代码实现

通过以上过程的演示,我们知道合并过程中需要一个辅助数组,用来存储两个合并数组中那个左侧的数组,而每一对需要合并的数组都需要这样一个数组,其大小也不尽相同,如果每次为合并操作分配刚好满足其大小的数组,等合并结束再进行释放,显然回有些浪费(内存的分配与释放较耗时间),为此我们设置一个较大的全局数组来满足所有合并要求(大小为数组的二分之一)。
而在左右数组合并的过程中,也伴随着进行比较的元素的索引变化,在此我们用ls(left start)、le(left end)表示左侧进行比较的元素索引与左侧比较结束时的索引( [ls,le)左闭右开),由于左侧数组将被复制到新数组中,那么ls初始应该为0,而le为mid-start;
在这里插入图片描述

再用rs(right start)、re(right end)表示左侧进行比较的元素索引与左侧比较结束时的索引( [rs,re)左闭右开)。显然一开始rs=mid,re=end。

在这里插入图片描述

最后是插入当前最小元素的位置我们用ai表示,且一开始ai=start。

在这里插入图片描述
完成索引初始化后,我们就可以开始插入了。显然当左侧元素都插完后,整个数组也已经有序了,因此我们可将ls<le作为循环进行的条件,而在循环中,我们需要对ls与rs指向的元素进行比较,而由循环条件知,ls一定是合法的,而rs则不一定(可能已经走到了re),为此进行比较前还需要对re的大小进行判断,此外,为了保证归并排序的稳定性(两个相等的元素,排序后之前在前的元素依旧在前),我们只让右侧小于左侧元素时才将右侧元素插入至新数组左侧(等于时左侧元素插入),这样就确保了归并排序的稳定性。而当数组插入至数组前端后,我们就需要进行索引更新,右侧数组插入时,修改ai加1,rs加1;左侧数组插入时,ai加1,ls加1。

array=np.random.randint(0,10000,20000,dtype=int)
length=len(array)
left_array=np.zeros(length>>1)#设置辅助数组,大小为数组长度的一般

def merge_sort(start,end):
    if end-start<2:
        return
    mid=(end+start)>>1
    merge_sort(start,mid)
    merge_sort(mid,end)
    merge(start,mid,end)

def merge(start,mid,end):
    global array
    global left_array
    for i in range(mid-start):#复制左侧数组
        left_array[i]=array[i+start]
    ls,le=0,mid-start
    ai,ae=start,end
    rs,re=mid,end
    while ls<le:
        if rs<re and left_array[ls]>array[rs]:
            array[ai]=array[rs]
            ai+=1
            rs+=1
        else:
            array[ai]=left_array[ls]
            ai+=1
            ls+=1

封装成一个类:

class MergeSort():
    def merge_sort(self, array):
        self.array=array
        n=len(array)
        #self.left_array=[0]*(n>>1)#辅助数组采用1/2大小
        self.left_array=np.zeros(n>>1)
        self.inner_sort(0,n)

    def inner_sort(self,begin,end):#区间左闭右开
        if end-begin<2:
            return
        mid=(begin+end)>>1
        self.inner_sort(begin,mid)
        self.inner_sort(mid,end)
        self.merge(begin,mid,end)

    def merge(self,begin,mid,end):
        for i in range(mid-begin):#复制左侧数组
            self.left_array[i]=self.array[i+begin]
        ls,le=0,mid-begin#ls为left_array中的索引
        rs,re=mid,end
        ai=begin
        while ls<le:
            if rs<re and self.array[rs]<self.left_array[ls]:
                self.array[ai]=self.array[rs]
                ai+=1
                rs+=1
            else:
                self.array[ai]=self.left_array[ls]
                ai+=1
                ls+=1

1.4 复杂度

1.4.1 时间复杂度

通过下图,我们可以直观的感受到归并排序的时间复杂度是O(nlogn)。

在这里插入图片描述
我们也可以通过数学推导得到归并排序的时间复杂度。

def merge_sort(start,end):
    if end-start<2:
        return
    mid=(end+start)>>1
    merge_sort(start,mid)
    merge_sort(mid,end)
    merge(start,mid,end)

我们假设归并排序的时间复杂度为T(n)(当输入是长度为n的数组时),而由归并排序代码结构我们可以得到T(n)=2*T(n/2)+n,其中merge操作的时间复杂度为O(n),而当输入只有一个数时,T(1)=1。通过如下推到,即可得到T(n)=O(nlogn)。

令 S n = T n /n
S 1 = O(1)
S n = S n/2 + O(1) = S n/4 + O(2) = S n/8 + O(3) = S n/2k + O(k) = S 1 + O(logn) = O(logn)
T n = n ∗ S n = O(nlogn)

由于归并排序总是平均分割子序列,所以最好、最坏、平均时间复杂度都是 O(nlogn) ,属于稳定排序。

1.4.2 空间复杂度

在这里插入图片描述
由上图我们可以看出递归堆栈的深度为O(logn),而辅助数组的大小为O(n),由O(logn)+O(n)=O(n),我们可以得到归并排序的空间复杂度为O(n)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值