常用算法——排序算法

    所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。

一、排序分类

    排序(Sorting) 是计算机程序设计中的一种重要操作,它的功能是将一个数据元素(或记录)的任意序列,重新排列成一个关键字有序的序列。

    排序就是把集合中的元素按照一定的次序排序在一起。一般来说有升序排列和降序排列2种排序,在算法中有十种基本排序:

    (1)冒泡排序;(2)选择排序;(3)插入排序;(4)希尔排序;(5)归并排序;(6)快速排序;(7)基数排序;(8)堆排序;(9)计数排序;(10)桶排序。

表1 十大排序算法性质汇总表

*希尔排序的分析是一个复杂的问题,因为它的时间是所取“增量”序列的函数,这涉及一些数学上尚未解决的难题。因此,到目前为止尚未有人求得一种最好的增量序列,但大量的研究已得出一些局部的结论,但结论并不一致。如有研究者利用大量的实验统计资料得出希尔排序的平均时间复杂度为O(n^1.25)到O(1.6*n^1.25)。

十种常见排序算法一般分为以下几种:

(1)非线性时间比较类排序:交换类排序(快速排序和冒泡排序)、插入类排序(简单插入排序和希尔排序)、选择类排序(简单选择排序和堆排序)、归并排序(二路归并排序和多路归并排序);

(2)线性时间非比较类排序:计数排序、基数排序和桶排序。

总结:(1)在比较类排序中,归并排序号称最快,其次是快速排序和堆排序,两者不相伯仲,但是有一点需要注意,数据初始排序状态对堆排序不会产生太大的影响,而快速排序却恰恰相反。

(2)线性时间非比较类排序一般要优于非线性时间比较类排序,但前者对待排序元素的要求较为严格,比如计数排序要求待排序数的最大值不能太大,桶排序要求元素按照hash分桶后桶内元素的数量要均匀。线性时间非比较类排序的典型特点是以空间换时间。

图1 十种常见排序算法分类图

二、常用排序算法

(一) 选择排序

    原理:从所有记录中选出最小的一个数据元素与第一个位置的数据元素交换;然后在剩下的数据元素当中再找最小的与第二个位置的数据元素交换,循环到只剩下最后一个数据元素为止。

或者

    从所有记录中选出最大的一个数据元素与最后位置的数据元素交换;然后在剩下的数据元素当中再找最大的与倒数第二个位置的数据元素交换,循环到只剩下最后一个数据元素为止。

降序则将上述的最小改为最大、最大改为最小。

    所以选择排序有两种编程方法:

方法一

    程序用到双重循环,外循环控制轮次数,内循环用于找最小值。

    以5个数为例,共需要前述的步骤重复4个轮次。

原始数据:[ 90, 68, 31, 65, 87 ]

第1轮次:[ 31, 68, 90, 65, 87 ]  # 将所有n个数中最小值与第1个数交换

第2轮次:[ 31, 65, 90, 68, 87 ]  # 将剩下的n-1个数中最小值与第2个数交换

第3轮次:[ 31, 65, 68, 90, 87 ]  # 将剩下的3个数中最小值与倒数第3个数交换

第4轮次:[ 31, 65, 68, 87, 90 ]  # 将剩下的2个数中最小值与倒数第2个数交换

    剩下的最后1个数就是最大数,因此需进行n-1轮比较。

    例1 用选择排序方法一将列表mlst中的元素按升序排列。

mlst=[80, 58, 73, 90, 31, 92, 39, 24, 14, 79, 46, 61, 31, 61, 93, 62, 11, 52, 34, 17]
for l in range(len(mlst)-1):
    minidx = l
    for i in range(l, len(mlst)):
        if mlst[i] < mlst[minidx]:
            minidx = i
    mlst[minidx], mlst[l] = mlst[l], mlst[minidx]
print(mlst)

方法二

    程序用到双重循环,外循环控制轮次数,内循环用于找最大值。

    仍以5个数为例,共需要前述的步骤重复4个轮次。

原始数据:[ 90, 68, 31, 65, 87 ]

第1轮次:[ 87, 68, 31, 65, 90 ]        # 将所有n个数中最大值与最后1个数交换

第2轮次:[ 65, 68, 31, 87, 90 ]        # 将剩下的n-1个数中最大值与倒数第2个数交换

第3轮次:[ 65, 31, 68, 87, 90 ]        # 将剩下的3个数中最大值与第3个数交换

第4轮次:[ 31, 65, 68, 87, 90 ]        # 将剩下的2个数中最大值与第2个数交换

剩下的最后1个数就是最小数,因此也需进行n-1轮比较。

例2 用选择排序方法二将列表mlst中的元素按升序排列。

mlst=[80, 58, 73, 90, 31, 92, 39, 24, 14, 79, 46, 61, 31, 61, 93, 62, 11, 52, 34, 17]
for r in range(len(mlst)-1, 1, -1):
    maxidx = 0
    for i in range(r+1):
        if mlst[i] > mlst[maxidx]:
            maxidx = i
    mlst[maxidx], mlst[r] = mlst[r], mlst[maxidx]
print(mlst)

    时间复杂度:最坏、最好和平均复杂度均为O(n2),因此,选择排序也是常见排序算法中性能最差的排序算法。

   选择排序的比较次数与文件的初始状态没有关系,在第i趟排序中选出最小排序码的记录,需要做n-i次比较,因此

    总的比较次数是:

(二) 冒泡排序

    冒泡排序是一种简单的排序算法。

    原理:它重复地走访过要排序的元素,依次比较两个相邻的元素,如果顺序错误就把他们交换过来。重复进行走访元素的工作,直到没有相邻元素需要交换,也就是说该元素列已经排序完成。

    冒泡排序算法(升序):

第1轮:在数据集中依次比较相邻的2个数的大小,如果前面的数大,后面的数小,则交换;n个数需要比较n-1次,其结果是将最大的数交换到最后位置(下标n-1)。

第2轮:从剩下的未排序数据中(n-1个)重复上述步骤,n-1个数需要比较n-2次,其结果是将次大的数交换到倒数第2个位置(下标n-2)。

……

第n-1轮:比较剩下2个数,如果前面的大,后面的小,则交换。

结束。交换次数nlogn

    如果是降序排序的话,则只要将上述算法中的交换条件改成前面的数小,后面的数大即可。

    以5个数为例,共需要前述的步骤重复4个轮次,每个轮次中需要比较相邻2个数n-i次(i为轮次数)。

原始数据:    [ 60, 56, 45, 31, 28 ]

第1轮次:   [ 56, 45, 31, 28, 60 ]        # 将n个数中的最大数沉底(倒数第1个)

第2轮次:   [ 45, 31, 28, 56, 60 ]        # 将剩下n-1个数中的最大数沉次底(倒数第2个)

第3轮次:   [ 31, 28, 45, 56, 60 ]        # 将剩下3个数中的最大数沉到第3个

第4轮次:   [ 28, 31, 45, 56, 60 ]        # 将最后2个数中的最大数沉到第2个

    剩下的第1个数就是最小数。

    在比较交换过程中,大的数逐步往后移动,相对来讲小的数逐步向前移动;如同水中的气泡慢慢向上升,故得名。

    程序用到双重循环,外循环控制轮次数,内循环控制相邻2个数的比较(交换)。

    例3 用冒泡排序法将列表mlst中的元素按升序排列。

    编程方法一:第一重循环倒序

a=[80, 58, 73, 90, 31, 92, 39, 24, 14, 79, 46, 61, 31, 61, 93, 62, 11, 52, 34, 17]
for r in range(len(a), 0, -1):
    print(r)
    for i in range(1,r):
        if a[i-1]>a[i]:
            a[i-1], a[i] = a[i], a[i-1]
    print(a)

    编程方法二:编程方法一:第一重循环顺序

a=[80, 58, 73, 90, 31, 92, 39, 24, 14, 79, 46, 61, 31, 61, 93, 62, 11, 52, 34, 17]
for i in range(len(a)-1):                # 只需要经过len(a)-1轮,排序即结束,i代表每一轮比较
    for j in range(1, len(a)-i):         #这里面的i就是当前所在的轮数,j表示每一轮要遍历的元素
        if a[j-1] > a[j]:                # 假设当j=1时,这里是第一个元素和第二个比较
            a[j-1], a[j] = a[j], a[j-1]  # Python常用的交换变量写法
print(a)

    时间复杂度:若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数C 和记录移动次数M均达到最小值:

    Cmin=n-1, Mmin=0。

    所以,冒泡排序最好的时间复杂度为O(n)。

    若初始文件是反序的,需要进行n-1轮排序。每轮排序要进行n-i次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:

    冒泡排序的最坏时间复杂度为O(n²)。

    综上,因此冒泡排序总的平均时间复杂度为O(n²)。

(三) 插入排序

    算法描述:一般来说,插入排序都采用 in-place在数组上实现:

  1. 第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后;
  6. 重复步骤2~5。

    例4 用插入排序法将列表mlst中的元素按升序排列。

a=[80, 58, 73, 90, 31, 92, 39, 24, 14, 79, 46, 61, 31, 61, 93, 62, 11, 52, 34, 17]
for i in range(1, len(a)):
    key = a[i]
    j = i-1
    while j >=0 and key < a[j]:
        a[j+1] = a[j]
        j -= 1
    a[j+1] = key
print(a)

(四) 快速排序

     快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为较小和较大的2个子序列,然后递归地排序两个子序列。

    步骤为:

  1. 挑选基准值:从数列中挑出一个元素,称为"基准"(pivot);
  2. 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成;
  3. 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。
  4. 递归到最底部的判断条件是数列的大小是0或1,此时该数列显然已经有序。
  5. 选取基准值有数种具体方法,此选取方法对排序的时间性能有决定性影响。

    例5 用快速排序法将列表mlst中的元素按升序排列。

a=[80, 58, 73, 90, 31, 92, 39, 24, 14, 79, 46, 61, 31, 61, 93, 62, 11, 52, 34, 17]
n = len(a)
def partition(a, low, high):
    i = low-1             # 最小元素索引
    pivot = a[high]    
    for j in range(low, high):
        # 当前元素小于或等于 pivot
        if   a[j] <= pivot:
            i += 1
            a[i],a[j] = a[j],a[i]
    a[i+1],a[high] = a[high],a[i+1]
    return i+1
# a[] --> 排序数组
# low   --> 起始索引
# high  --> 结束索引
 
# 快速排序函数
def quickSort(a,low,high):
    if low < high:
        pi = partition(a,low,high)
        quickSort(a, low, pi-1)
        quickSort(a, pi+1, high)

quickSort(a, 0, n-1)
print(a)

猜你喜欢

转载自blog.csdn.net/hz_zhangrl/article/details/129511598