八大排序算法详解(二)

学习算法:八大排序算法详解(二)

接上一篇博文学习算法:八大排序算法详解(一)

5.直接选择排序

    直接选择排序与冒泡排序相似,在每一趟循环中,选择一个最小的或最大的值放到队首或者队尾,所不同的是,直接选择排序是申明一个标志位,这个标志位指向选择出来的最值,当一趟走完之后,在将标志位的值与队首的值想交换,而冒泡排序是比较一次就进行一次交换。但是冒泡排序是稳定的排序算法,而直接选择排序是不稳定的排序算法,这一点很好理解。

5.2 用python实现代码如下:
def select_sort(list):
    count = len(list)
    for i in range(0,count):
        mnist = i
        for j in range(i+1,count):
            if(list[j]<list[mnist]):
                mnist = j
        list[i],list[mnist] = list[mnist],list[i]
    return list

if __name__=="__main__":  
    list1 = [5,4,6,1,3,2,8,9,7,10] 
    list2 = select_sort(list1)
    print list2
5.3 复杂度分析
时间复杂度:

    在直接选择排序中,对于每一个元素的归位,都必须在所有元素之间进行一次比较,因此最好的情况与最坏的情况下,比较次数没有区别,第一个元素要比较n-1次,第二个元素要比较n-2次…因此累加求和可知,比较次数为n(n-1)/2。而在最好的情况下,所有的元素都不进行交换操作,而最坏的情况下要进行n-1次交换,因此,直接选择排序的时间复杂度均为O( n2 )。

空间复杂度:

    直接选择排序中,只用到了一个标志位空间和进行交换时用到一个交换空间,为常数量,因此其空间复杂度为O(1)。

6.归并排序

6.1 基本概念:

    归并排序,顾名思义,其操作核心为归并操作,对于两个有序的序列,将其进行归并操作,归并到一个序列中,得到完全有序的序列,该算法是采用分治法的一个典型的应用,通常我们使用的为二路归并,相应的还有三路归并..n路归并。
    其基本做法很好理解,对于两个已经有序的序列a,b(假设均为升序排列),分别给两个序列i和j的标志位,以及一个存储排序结果的数组c,如果a[i]

6.2 用python实现代码如下:
def merge_sort(left,right):
    result = []
    i=0
    j=0
    while(i<len(left) and j<len(right)):
        if(left[i]<right[j]):
            result.append(left[i])
            i=i+1
        else:
            result.append(right[j])
            j=j+1
    result=result +left[i:]
    result = result +right[j:]
    return result

def merge(list):
    count = len(list)
    if count<=1:
        return list
    num = count/2
    left = merge(list[:num])
    right = merge(list[num:])
    return merge_sort(left,right)

if __name__=="__main__":  
    list1 = [5,4,6,1,3,2,8,9,7,10] 
    list2 = merge(list1)
    print list2
6.3 复杂度分析
时间复杂度:

    因为在我们的实现代码中,每次划分序列的时候,都是从中间开始划分,最好的情况和最坏的情况其实要进行相同次数的操作,因此,这里最好情况和最坏情况的时间复杂度相同。
    再看具体的操作,假设将一个长度为n的序列进行排序需要的时间为T[n],首先考虑第一次划分,将一个n的序列划分为两个n/2的序列,两个序列分别排序,在合并,合并两个n/2的序列为一个n的序列需要的时间复杂度为O(n),因为只要进行线性的n次比较然后插入,而将两个n/2的序列进行排序需要的时间为2T(n/2),因此,
        T[n] = 2T[n/2]+O(n),
    每一次都进行n/2的划分的情况下,一共要进行logn次的划分,则将公式中的T(n/2)一次一次的二分划分,直到变为T[1],可以算出:
        T(n) = n+nlogn
    因此,算法的时间复杂度为O(nlogn)。

空间复杂度:

    归并排序算法计算空间复杂度时,同样分为两部分,一部分是在归并计算的时候,使用了一个长度为n的临时数组保存结果,另一部分是在进行递归调用的时候,保存每次的中间节点的入栈操作,为logn次,因此使用的空间为(n+logn)次,后面的递归一次减少。因此空间复杂度为O(n)。

7.基数排序

7.1 基本概念:

    与前面的排序方式不同,基数排序属于属于一种分配式排序,又称为“桶子法”排序,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序。这里博客基数排序中对基数排序进行了很详细的讲解,此处借用他的一些图进行讲解,
    假设有欲排数据序列如下所示:
    73 22 93 43 55 14 28 65 39 81
    首先根据个位数的数值,在遍历数据时将它们各自分配到编号0至9的桶(个位数值与桶号一一对应)中。注意,这里使用基数排序是由低位到高位进行排序,先从个位数开始,再到十位数,再到百位数…
分配结果(逻辑想象)如下图所示:
这里写图片描述
    分配结束后。接下来将所有桶中所盛数据按照桶号由小到大(桶中由顶至底)依次重新收集串起来,得到如下仍然无序的数据序列:
    81 22 73 93 43 14 55 65 28 39
    接着,再进行一次分配,这次根据十位数值来分配(原理同上),分配结果(逻辑想象)如下图所示:
这里写图片描述
    分配结束后。接下来再将所有桶中所盛的数据(原理同上)依次重新收集串接起来,得到如下的数据序列:
14 22 28 39 43 55 65 73 81 93
    观察可以看到,此时原无序数据序列已经排序完毕。如果排序的数据序列有三位数以上的数据,则重复进行以上的动作直至最高位数为止。
    当然,前面的排序是从低位到高位进行排序,同样也可以从高位到低位进行排序,基于两种不同的排序顺序,我们将基数排序分为LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由数值的最右边(低位)开始,而MSD则相反,由数值的最左边(高位)开始。读者可以自行思考其中的异同点。

7.2 用python实现代码如下:
import math
def radix_sort(list,radix = 10):
    k = int(math.ceil(math.log(max(list),radix))) 
    bucket = [[] for i in range(radix)]
    for i in range(1,k+2):#此处应该多循环一轮
        for j in list:
            bucket[j/(radix**(i-1))%(radix*i)].append(j)
        del list[:]
        for z in bucket:
            list=list+z
            del z[:]
    return list
if __name__=="__main__":  
    list1 = [5,4,6,1,3,2,8,9,7,10] 
    list2 = radix_sort(list1)
    print list2

    代码中的k是计算数组中最大的数具有多少位,例如若最大为999,则k为3,最大为80,则为2。bucket则是申明一个高度为10的二维数组,作为bucket,j/(radix**(i-1))%(radix*i)用于计算由低到高的j的第i位上的数值。

7.3 复杂度分析

    基数排序的时间复杂度和空间复杂度均与数组内的数的最大值有关,分配的时间复杂度为O(n),收集的的时间复杂度为O(radix),分配和收集共需要distance趟,所以基数排序的时间复杂度为O(d(n+r))
    总结就是时间复杂度为O(nlog(r)m),其中r为采用的基数,m为堆数。
    该算法的空间复杂度就是在分配元素时,使用的桶空间;所以空间复杂度为:O(10 × length)= O (length)。

8.堆排序

8.1 基本概念:

    堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。
    堆排序的过程中,首先根据初始数组去构造初始堆(构建一个完全二叉树,保证所有的父结点都比它的孩子结点数值大)。每次交换第一个和最后一个元素,输出最后一个元素(最大值),然后把剩下元素重新调整为大根堆。 当输出完最后一个元素后,这个数组已经是按照从小到大的顺序排列了。
    所以按照上述思想,堆排序的主要难点在于每次进行交换并输出最大值之后,怎样调整堆,使剩下的元素重新成为一个大根堆或者小根堆,关于这一点,在博文图解堆排序中用图形很好地解释了如何进行堆的调整,简单的说,就是遍历每一个具有孩子节点的节点,若它的孩子节点比自己大,则交换其值,再检查交换过的孩子节点看是否满足大根的要求,如果不满足,再调整孩子节点,直到整个树满足要求。
    此外,堆排序是一种不稳定的排序算法,比如序列[3,6,27,6],在第一次调整后,输出27,并将27与第二个6想交换,此时第二个6交换到了第一个6前面,此时再次输出的就是第二个6,顺序被打乱。

8.2 用python实现代码如下:
def adjast_heap(list,i,size):
    #注意每次调整的时候size的值是不一样的,所以要将size的值作为参数传递进来
    lchild = 2*i
    rchild = 2*i+1 
    maxnum = i
    if(i<size/2):
        if (lchild<size and list[lchild]>list[maxnum]):
            maxnum = lchild
        if (rchild <size and list[rchild]>list[maxnum]):
            maxnum = rchild
        if maxnum!=i:
            list[i],list[maxnum] = list[maxnum],list[i]
            adjast_heap(list, maxnum, size)
    return list

def heap_sort(list):
    temp = []
    size = len(list)
    while size>0:
        for i in range(size/2,-1,-1):
            adjast_heap(list, i, size)#调整堆,生成大根堆
        temp.append(list[0])#输出第一个节点
        list[0],list[size-1] = list[size-1],list[0]
        size = size - 1
    return temp

if __name__=="__main__":  
    list1 = [5,4,6,1,3,2,8,9,7,10] 
    list2 = heap_sort(list1)
    print list2

    在adjast_heap函数中,传递进了三个参数,首先是list数组,其次是i值,i表示要调整的那个节点,然后是此轮要调整的list的长度(注意,每一轮输出过后,list的长度减小一个)
    代码中比较i节点的值和它的左右节点,如果发生了调整,则进一步调整它的孩子节点,这样保证了整棵树是大根的状态,在heap_sort函数中,调用前面的adjast将函数,首先初始化一个大根堆,然后交换输出最大值,然后每一次将第一个元素进行调整,再次得到大根堆,直到依次输出所有的值。

8.3 复杂度分析
时间复杂度

    堆排序的时间复杂度,主要在初始化堆过程和每次选取最大数后重新建堆的过程,复杂度的博文堆排序中进行了详细的证明,此处进行引用;
    初始化建堆过程时间:O(n)
    首先要理解怎么计算这个堆化过程所消耗的时间,可以直接画图去理解;
    假设高度为k,则从倒数第二层右边的节点开始,这一层的节点都要执行子节点比较然后交换(如果顺序是对的就不用交换);倒数第三层呢,则会选择其子节点进行比较和交换,如果没交换就可以不用再执行下去了。如果交换了,那么又要选择一支子树进行比较和交换;
    那么总的时间计算为:s = 2^( i - 1 ) * ( k - i );其中 i 表示第几层,2^( i - 1) 表示该层上有多少个元素,( k - i) 表示子树上要比较的次数,如果在最差的条件下,就是比较次数后还要交换;因为这个是常数,所以提出来后可以忽略;
    S = 2^(k-2) * 1 + 2^(k-3)2…..+2(k-2)+2^(0)*(k-1) ===> 因为叶子层不用交换,所以i从 k-1 开始到 1;
    这个等式求解,我想高中已经会了:等式左右乘上2,然后和原来的等式相减,就变成了:
    S = 2^(k - 1) + 2^(k - 2) + 2^(k - 3) ….. + 2 - (k-1)
    除最后一项外,就是一个等比数列了,直接用求和公式:S = { a1[ 1- (q^n) ] } / (1-q);
S = 2^k -k -1;又因为k为完全二叉树的深度,所以 (2^k) <= n < (2^k -1 ),总之可以认为:k = logn (实际计算得到应该是 log(n+1) < k <= logn );
    综上所述得到:S = n - longn -1,所以时间复杂度为:O(n)
    更改堆元素后重建堆时间:O(nlogn)
    推算过程:
    1、循环 n -1 次,每次都是从根节点往下循环查找,所以每一次时间是 logn,总时间:logn(n-1) = nlogn - logn ;
    所以综上,堆排序的时间复杂度为:O(nlogn)

空间复杂度

    因为堆排序是就地排序,如果每次堆调整完之后就地输出的话,所用的额外空间为常数,因此空间复杂度为常数:O(1)

总结

    我们经常提到的排序算法大体分为这8类,在每一个排序算法中,还有其他的相关的改进的以及不同实现的算法,其中每一种算法相关的时空复杂度网上有很多总结出来的表格,每一种复杂度的推导其实都需要相关的数学功底支撑,此处放上一张表格,无法在数学上完全推导的可以记一下相关的结果。
排序算法时空复杂度

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值