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)。