聊聊几个简单的排序算法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhchs2012/article/details/79978775

前言

排序是算法的入门知识,其经典思想可以用在许多算法中,在实际应用中是相当常见的一类。记得在本科的数据结构课上就有讲过几个经典的排序算法,现在来好好地回顾下。
在回顾之前,了解一个概念,这个概念也是我刚刚了解的。(手动扶额-。-)
排序算法稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
这个稳定的概念有什么用呢?不管,先记着再说啦。

一、冒泡排序(BubbleSort)

两个相邻的数比较大小,较大的数下沉,较小的冒起来。
过程为:

  1. 比较相邻的两个数据,如果第二个数小,就交换位置;
  2. 从前往后两两比较,一直到比较最后面两个数据。最终最大的数被交换到末尾位置,这样第一个最大数的位置就排好了;
  3. 继续重复上述过程,依次将第2、3…n-1个最大的数排好位置。

当然也可以反着来,从后面往前比较,先排好最小的数到数列到开头的位置。


python代码实现:

def bubble_sort(list):
    l = len(list)
    for j in range(len(list)-1):   #单纯地设定循环次数,标识趟次
        for i in range(l-1):
            if list[i] > list[i+1]:
                list[i],list[i+1] = list[i+1],list[i]
            else:
                pass
        l = l - 1   #减少比较次数,最后n位的最大数已经排好,不需要再进行比较了
    return list

搬运来的动图,特别直观:
这里写图片描述

1.1鸡尾酒排序

这是一种冒泡排序的改进算法,可以称之为双向冒泡排序:

  1. 先对数组从左往右进行升序的冒泡排序;
  2. 再对数组进行从右往左的降序冒泡排序;
  3. 循环往复,不断缩小没有排序的数组范围。
def cocktail_sort(list):
    l = len(list)
    start = 0
    end = l - 1
    flag = True   #标志上一轮循环是否有交换,若无,则表示排序已经完成,无需继续循环(冒泡排序优化点)
    while flag:
        flag = False
        for i in range(start,end,1):
            if list[i] > list[i+1]:
                list[i],list[i+1] = list[i+1],list[i]
                flag = True
            else:
                pass
            end = end - 1
        for i in range(end,start,-1):
            if list[i] < list[i-1]:
                list[i],list[i-1] = list[i-1],list[i]
                flag = True
            else:
                pass
            start = start + 1
    return list

再搬运一张鸡尾酒排序动图:
这里写图片描述

二、 选择排序(SelctionSort)

选择排序十分简单直观,步骤如下:

  1. 在序列中找到最小(大)元素,存放在序列的起始位置;
  2. 再从剩余的未排序序列种继续寻找最小(大)的元素,存放在已排序序列的后一位;
  3. 重复到第n-1次,完成排序。
def selection_sort(list):
    l = len(list)
    for i in range(l-1):
        _index = list.index(min(list[i:l]))   #也可以再套一层循环,逐一比较出最小值
        list[i],list[_index] = list[_index],list[i]

感谢网上的动图:
这里写图片描述

三、插入排序(InsertionSort)

对于未排序数据,在已排序序列中从后向前扫描,找到相应位置插入。十分类似我们打扑克时抓牌的过程。

  1. 从第一个元素开始,单个元素当然是可以认为已排序;
  2. 取出下一个元素,与已排序序列从后向前扫描比较,直到找到已排序元素小于或等于新元素的位置;
  3. 将新元素插入到找到的位置后一个的位置;
  4. 继续取下一个元素重复步骤。
def insertion_sort(lists):
    l = len(lists)
    for i in range(1,l):   #大循环,开始依次抽取元素进行插入
        _tmp = lists[i]
        for j in list(range(i))[::-1]:
            if _tmp < lists[j]:   #比前一个小就继续前进,被比较元素向后移一位
                lists[j+1] = lists[j]
                if j == 0:   #比较到队首了,说明临时值是最小的
                    lists[j] = _tmp
            else:
                lists[j+1] = _tmp
                break
    return lists

继续搬运:
这里写图片描述

3.1希尔排序

插入排序对那些基本有序的序列排序效率高,但对于乱序的序列,移动次数非常多导致效率较低。所以就有了插入排序的改进版——希尔排序。希尔排序会优先比较距离较远的元素,又称之为缩小增量排序。

  1. 设定步长,按步长将原序列分为若干子序列,分别对子序列进行插入排序;
  2. 逐渐减小步长,重复步骤1。直至步长为1,此时序列基本有序,最后进行一次插入排序。

可以看出,希尔排序可以在一开始就对距离较远的元素进行换位、排序。避免了插入排序中,一个元素在往前比较过程中大量元素被逐一移动的过程。希尔排序的关键就是步长的选择,看到一种说法说步长用质数是个不错的选择;也有一说用序列[1,4,13,40,121,364…]后一元素是前一元素的3倍+1;当然还有更加简单粗暴的len/2,然后依次除以2直至1.
希尔排序动图展示(这里展示的步长分别为5、2、1):
这里写图片描述
代码略,本质上就是按照步长进行多次插入排序。

四、快速排序(Quicksort)

快速排序简称快排,这个排序算法就厉害了,听说面试官特别喜欢考,所以重点来了,同志们。

  1. 从序列中取出一个值作为基准;
  2. 把所有比基准值小的摆放在基准的左边,比基准大的摆在基准的右边(相等的数任意一边)。这就完成了一次分区操作;
  3. 对左右两个子序列继续做这样的分区操作,直至每个区间只有一个数。这是一个递归过程。

失败的一次尝试:

def quick_sort(arr):
    lefti = 0
    righti = len(arr) - 1
    if righti == -1:
        return 0
    else:
        x = arr[0]   #x即为基准
        count = 0
        k = 0
        while lefti < righti and count < 2:
            for i in range(righti,lefti,-1):
                if righti - lefti == 1:
                    count += 1
                if arr[i] < x and count < 2:
                    arr[lefti] = arr[i]
                    righti = i
                    k = i
                    lefti += 1
                    break
            for j in range(lefti,righti,1):
                if righti - lefti == 1:
                    count += 1
                if arr[j] > x and count < 2:
                    arr[righti] = arr[j]
                    lefti = j
                    k = j
                    righti -= 1
                    break
        arr[k] = x
        quick_sort(arr[:k])
        quick_sort(arr[k+1:])

排序过程没问题,只是迭代时修改的不是原数组,而是新的被拆分的数组,导致只有第一层循环的元素交换被保留。无奈还是参照下别的代码学习一哈把,coding能力还是有待加强。
参照资料后的改进版:

def quick_sort(arr,left,right):
    if left >= right:
        return
    low = left
    high = right
    key = arr[low]   #取序列的第一个值为基准
    while left < right:
        while left < right and arr[right] >= key:   #外层循环里已经有left<right但内层循环里仍然需要,因为要保证left和rigth最终会合相等,而不能让left在自增过程中超过right
            right -= 1
        arr[left] = arr[right]
        while left < right and arr[left] <= key:
            left += 1
        arr[right] = arr[left]
    arr[left] = key   #此时left和right已经相等
    quick_sort(arr,low,left-1)
    quick_sort(arr,left+1,high)

大神秀技巧版(一行代码实现):

quick_sort = lambda array: array if len(array) <= 1 else quick_sort([item for item in array[1:] if item <= array[0]]) + [array[0]] + quick_sort([item for item in array[1:] if item > array[0]]) 

这个lambda真的很精妙,用两个列表生成式拼接出完整列表,所有小于等于arr[0]的放左边,所有大于arr[0]的放右边。劣势是占用了新的内存空间,常规版的快排是in-palce的,都是原地操作。
动图演示:
这里写图片描述

五、归并排序(MergeSort)

将两个有序序列合并的方法很简单,比较2个序列的第一个数,谁小就取谁,取出后删除对应序列中的这个数。继续比较直至所有元素都被取出。将两个有序序列合并的过程称为2-路归并。归并排序就基于此:

  1. 把待排序的序列一分为二,分出两个子序列;
  2. 继续将子序列分裂直至子序列中只有一个元素,一个元素自然就算排序完成;
  3. 一路分裂一路归并,最终获得完整序列。
def merge(arrX,arrY):   #合并排序算法
        i,j = 0,0
        arrN = []
        while i < len(arrX) and j < len(arrY):
            if arrX[i] < arrY[j]:
                arrN.append(arrX[i])
                i += 1
            else:
                arrN.append(arrY[j])
                j += 1
        if i == len(arrX):
            arrN.extend(arrY[j:])
        if j == len(arrY):
            arrN.extend(arrX[i:])
        return arrN
def merge_sort(arr):   #迭代过程
    l = len(arr)
    if l <= 1:
        return arr
    else:
        X = merge_sort(arr[:round(l/2)])
        Y = merge_sort(arr[round(l/2):])
        return merge(X,Y)

动图演示:
这里写图片描述

六、计数排序(CountingSort)

计数排序不是基于比较的排序算法,它依靠一个辅助数组来实现,将输入的数据转化为键存储在专门准备的数组空间中,计数排序要求输入的数据必须是正整数,且最好不要过大。计数排序是用来排序0到100之间的数字且重复项比较多的最好的算法。

  1. 找到待排序序列种的最大值(也可以找出最小值,建立中间数组时节省一定的空间);
  2. 统计序列中每个值为i的元素出现的次数,存入数组C的第i项;
  3. 对所有计数从低到高向上累加,数组C[i]中的值会变成所有小于等于i的元素个数;
  4. 从原序列反向填充目标数组:将每个元素i填入新数组的第C[i]项,每放一个元素C[i]减1.
def counting_sort(arr):
    m = max(arr)
    c = [0 for i in range(m+1)]
    for i in arr:
        c[i] += 1   #c[i]表示在原序列arr中值为i的元素有几个
    for j in range(1,m+1):
        c[j] = c[j] + c[j-1]   #c[j]表示在原arr序列中最后一个值为j的元素排第几位,或者表示小于等于j的元素个数
    res = [None for i in range(len(arr))]
    for r in range(len(arr)-1,-1,-1):
        res[c[arr[r]]-1] = arr[r]   #因为索引是从0开始的,需要-1
        c[arr[r]] -= 1  #放置好一个元素,就需要在c数组中去掉一个元素,最后c数组会变成全0的数组
    return res

依然是动图伺候:
这里写图片描述

后话

其实还是有一些排序算法没有涉及,比如桶排序、基数排序、堆排序。毕竟不是专业的程序员,就不继续深究了。文中展示的所有代码都是本人手写并运行验证通过的,可放心复制粘贴食用。
关于复杂度也可以一张图说明:
这里写图片描述


还记得开头讲过的稳定性么,开写这篇博文时并不明白其作用。实现了这几个排序算法后也自己琢磨明白了一点。稳定的好处是:从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。
也就是说,实际应用中我们遇到的排序不是简单地针对一个数组或者一个序列,而是有很多维度的。我们针对其中一个维度进行稳定排序,原先其他维度的先后顺序不会被改变。这才是稳定性的意义所在。

最后感谢来自他人博客的动图,转载声明:
来源于一像素的博客:https://www.cnblogs.com/onepixel/articles/7674659.html

猜你喜欢

转载自blog.csdn.net/zhchs2012/article/details/79978775