八大排序算法详解(一)

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

    相信很多人在学习算法的过程中,首先接触到的就是所谓的排序算法和查找算法,本文介绍了算法基础中的8种排序算法,权当学习笔记。文章中引用到了一些其他的博文中的内容,在写的过程中,将尽量标明引用出处,若有漏掉的地方,敬请谅解。同时,如果文章中有些地方讲解有误,也请多多包含。通常我们所说的8种排序算法分别为插入排序,希尔排序,冒泡排序,快速排序,直接选择排序,堆排序,归并排序,基数排序。

1.插入排序

1.1 相关概念

    插入排序简单理解就是将一个数组分为两部分,假设前面一部分的数组是已经排好序的数组,而后面一部分是待排序的数组,在一层循环中,当前指针指向的值作为key值,而当前指针前面是已经排好序的数组,从当前指针往前遍历,如果是升序排列,则找到第一个比当前key值小的元素,将key值插入到其后面,后面的元素在遍历过程中依次向后移动,因此在排序的过程中其实是将key值取出来,而数组中的大小是增加了一个存储空间的。
伪代码可简单表示为:

INSERTION-SORT(A)
for i =2 to A.length
    key = A[i]
    //insert A[i] into the sorted sequence A[1,2,,,i-1].
    j = i-1
    while j>0 and A[j]>key
        A[j+1] = A[j]
        j = j-1
    A[j+1] = key
1.2 用python实现代码如下:
def insert_sort(lists):
    #降序排列
    count=len(lists)
    for j in range(1,count):
        key = lists[j]
        i = j-1
        while i>-1 and lists[i]<key:
            lists[i+1] = lists[i]
            i=i-1
        lists[i+1] = key
    return lists   
if __name__=="__main__":
    list1 = [12,5,8,7,6,4]
    list2 = insert_sort(list1)
    print list2
1.3 复杂度分析
时间复杂度:

    在最坏的情况下,数组完全逆序,从第二个元素开始,第二个元素要移动1次,第三个元素移动2次,第四个元素移动3次…到第n个元素要移动n-1次,因此,求和为 n2/2 ,因此最坏情况下的时间复杂度为O( n2 )。
    在最好的情况下,数组是有序的,每次考察一个元素都要检查一次,因此最好的情况下的时间复杂度为O(n)。

空间复杂度:

    插入排序只需要两个用来遍历数组的常数i、j,然后需要一个用于存放key值的临时空间,所以空间复杂度是常量。则空间复杂度为O(1)

2. 冒泡排序

2.1 相关概念

    冒泡排序,顾名思义,排序过程中,数组内的元素就像气泡一样,相邻的元素相互比较(以升序排序为例),如果右边的元素比左边的小,则两两之间交换,一次循环中最大的元素像气泡一样向上移动。最原始的冒泡排序算法并没有在代码中加标志位,一种改进的冒泡排序算法是在代码中加上标志位,记录单次循环是否进行了元素交换,如果在一次循环中,没有元素发生交换,则说明排序成功,直接跳出。

2.2 用python实现代码如下:
def bubble_sort(list):
    count = len(list)
    state = 1
    for i in range(0,count):
    #从0开始到count-1,依次确定每一位上的数,按照从小到大排列,每次找到最小的一个数放到i位
        if(state ==1):
            state=0
            for j in range(i+1,count):
                if(list[i]>list[j]):
                    list[i],list[j] = list[j],list[i]  #第i位放较小的数    
                    state = 1
         else:
            return list
    return list

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

    这里进行的复杂度分析并不考虑改进后的情况,分析在不设置标志位的情况下的时空复杂度。
在最好的情况下,数组有序,此时第一个元素比较n-1次,第二个元素比较n-2次…累加求和,得到结果为(n(n-1)/2)则此时的时间复杂度为O( n2 )。
    在最坏的情况下,数组逆序,第一个元素要进行n-1次比较,比较之后进行一次元素交换的工作,因此,相比较于最好的情况,只是增加了一道工序,得到结果为(2n(n-1)/2),因此最坏的情况下时间复杂度同样为O( n2 )。
    在考虑标志位的情况下,最好时,经过一轮的比较,程序跳出循环,因此时间复杂度为O(n),最坏的情况下则要考虑到标志位的赋值操作,每一次交换都要进行一次赋值,得到结果为(3n(n-1)/2),代价增大,但实际上其时间复杂度任然保持在O( n2 )。

空间复杂度:

    冒泡排序只需要两个用来遍历数组的常数i、j,然后需要一个用于交换的临时存储空间tmp,再加一个判断是否需要继续执行算法的变量flag,所以空间复杂度是常量。则空间复杂度为O(1)

3. 希尔排序

3.1 相关概念

    希尔排序(Shell Sort)按照操作方式来算,其实是插入排序的一种。也称缩小增量排序,但是在某些情况下它比插入排序更加高效,希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;循环一次,增量按比减小,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
    插入排序是稳定的排序,而希尔排序是不稳定的,假设有数组[5, 21 , 22 ,1,3],其中 21 表示第一个2,则当增量为2 的时候,5, 22 ,3被分为一组,另外两个元素被分为一组,经过一次排序后,数组变为[ 22 ,1,3, 21 ,5],由此可见,希尔排序是不稳定的。
    以数组[5,4,6,1,3,2,8,9,7,10]为例,令step=2,即增量一次变为原来的1/2,数组长度为10,第一次增量为10/2 = 5,则将数组分成了5组,[5,2],[4,8],,,[3,10],在每一个小组内进行插入排序,排序后为[2,5,4,8,6,9,1,7,3,10]。下一步增量变为5/2 = 2,此时将数组分为了两组[2,4,6,1,3]和[5,8,9,7,10],再分别插入排序。

3.2 用python 实现代码如下:
def shell_sort(list):
    count = len(list)
    step = 2 #每次令步长变为原来的一半
    group = count/step
    while group>0:#直到最终步长为0。  1/2 = 0。
        # 对于长度为10的数组,第一次有5组。第二次又2组,第三次1组。第四次不满足条件,跳出循环
        for i in range(0,group):
            #如果有5组。那么同组元素之间的间隔为5,j=i+5, j依次往上加,直到超出范围
            j = i+group #第一次初始化相加一下,相当于插入排序中从第二个值开始,插入排序的步长为1,这里步长为group
            while j<count:#在不超出的情况下
                k = j-group #k为j-group,在k不超出范围的情况下,比较key值与k值,若满足条件,交换,k依次递减,这样依次交换,把key的值放到属于它的位置
                key = list[j]
                while k>=0:
                    if(list[k]>key):
                        list[k+group] = list[k]
                        list[k] = key
                    k = k-group
                j=j+group
        group = group/step
    return list

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

    增量排序的时间复杂度依赖于所取增量序列的函数,但是到目前为止还没有一个最好的增量序列.有人在大量的实验后得出结论;当n在某个特定的范围后希尔排序的比较和移动次数减少至n^1.3 不管增量序列如何取值,都应该满足最后一个增量值为1.

空间复杂度:

    希尔排序其实是一种变相的插入排序,因此,其空间复杂度和插入排序一样,为O(1)。

4. 快速排序

4.1 相关概念

    关于快速排序,这里有一篇博客我觉得讲的很好,就直接拿来用了,相关连接坐在马桶上看算法:快速排序,实际上,在初始学习算法的过程中,我有很大一部分的相关知识都是从该作者那里学习的,此处表示感谢。
以下部分来自该博文(其实是很喜欢他文章里面的小图):
    假设我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数(不要被这个名词吓到了,就是一个用来参照的数,待会你就知道它用来做啥的了)。为了方便,就让第一个数6作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列:
3 1 2 5 4 6 9 7 10 8
    在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6。想一想,你有办法可以做到这点吗?
    方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即=10),指向数字。
                        这里写图片描述
    首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。
                        这里写图片描述
                        这里写图片描述
    现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下:
    6 1 2 5 9 3 4 7 10 8
                        这里写图片描述
                        这里写图片描述

    到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下:
6 1 2 5 4 3 9 7 10 8

    第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下:
3 1 2 5 4 6 9 7 10 8
                        这里写图片描述
                        这里写图片描述
                        这里写图片描述
    到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。
解释完毕。现在基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。

    快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)。其实快速排序是基于一种叫做“二分”的思想。

至此引用该博客完毕
    快速排序的基本思想想必该作者已经讲得很清楚了,再下面分别对左右进行递归调用即可完成排序。
    关于该作者文中多次提到的,为什么每次必须要从j哨兵开始移动,此处给出解释:
假设先从左边开始,当i停止移动的时候,此时i上面所对应的值应该是大于基数的,然后j开始移动,j没有找到比基数小的数停止,而是按照与i碰头的条件停止了,此时ij所指的是同一个大于基数的数,将基数与i想交换,此时就把大的数调整到左边了。相应的,如果从右边先开始,那么ij停止的时候,所指的数应该是一个比基数小的数,这时候将i与基数调换,就没有问题了。

4.2 用python实现代码如下:
def quick_sort(list,start,end):
    if start>=end:
        return list
    key = list[start]
    i = start
    j = end
    while i<j:
        while list[j]>=key and j>i:
            j=j-1
        while list[i]<=key and i<j:
            i=i+1
        list[i],list[j] = list[j],list[i] #在i<j的情况下,一次一次地交换i和j   
    list[start],list[i] = list[i],list[start]#最终i和j相等的时候,交换key值和i的值

    quick_sort(list,start,i-1)#调用递归
    quick_sort(list,i+1,end)#调用递归
    return list    

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

    关于快速排序算法的时间复杂度分析,需要一定的数学基本知识,参考博文:快速排序时间复杂度的证明
    在最好的情况下:Partition每次都划分得很均匀,如果排序n个关键字,其递归树的深度就为 [log2n]+1( [x] 表示不大于 x 的最大整数),即仅需递归 log2n 次,需要时间为T(n)的话,第一次Partiation应该是需要对整个数组扫描一遍,做n次比较。然后,获得的枢轴将数组一分为二,那么各自还需要T(n/2)的时间(注意是最好情况,所以平分两半)。于是不断地划分下去,就有了下面的不等式推断:
                        这里写图片描述
这说明,在最优的情况下,快速排序算法的时间复杂度为O(nlogn)。

    在最坏的情况下:当待排序的序列为正序或逆序排列时,且每次划分只得到一个比上一次划分少一个记录的子序列,注意另一个为空。如果递归树画出来,它就是一棵斜树。此时需要执行n‐1次递归调用,且第i次划分需要经过n‐i次关键字的比较才能找到第i个记录,也就是枢轴的位置,因此比较次数为
                        这里写图片描述
,最终其时间复杂度为O(n^2)。
    关于一般情况下的时间复杂度,额。。。还是看原文吧。快速排序的平均时间复杂度也是:O(nlogn)

空间复杂度:

    在快速排序中,如果不考虑递归,对于一次排序,即将一个数放到中间的位置,由于所使用的辅助空间为常量,则空间复杂度为O(1),则此时快速排序的空间主要考虑递归过程中的所消耗的空间,由于每一次递归需要一个量级为1的空间,而在最好的情况下,n长度的数组需要logn次的递归,因此,空间复杂度为O(logn)。
    在最坏的情况下,前面分析需要n-1次递归,则空间复杂度为O(n)。

关于后面的四种排序算法,请参考下一篇博文八大排序算法详解(二)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值