常见的排序算法

排序算法可以从不同角度,按不同方式分类,下面是一些常见的排序算法:

  • 插入排序
  • 选择排序
  • 交换排序
  • 分配排序
  • 归并排序
  • 外部排序

记录结构,后面讨论排序算法是,使用的示例数据结构就是一个表,假定表中元素是下面定义的record类的对象 :

class record:
    def __init__(self, key, datum):
        self.key = key
        self.datum = datum

排序只关心record对象的key成分,但为了完成排序,经常需要把整个对象搬来搬去。

插入排序:顾名思义其基本操作方式是插入,不断把一个个元素插入一个序列中,最终得到排序序列。为此只需要维持好所构造序列的序列性质,最终就能得到结果。

  • 算法的实现:插入的过程中需要一个个地处理未排序元素,最简单的方法是按下标处理,处理了一个元素就能留下一个空位,如果该空位与已排序序列相连,就可以直接用作该序列的延伸位置。把这些考虑综合起来,就得到如下图所示的状态安排,其中在连续表的前段积累已排序序列,通过这个序列的不断生长,最终完成整个序列的排序工作。如图所示,连续表的右边一段是尚未处理的元素,每次考虑这段的最左元素,即途中i标识的元素。

    

  • 插入排序算法就是找到第i个元素在前段的插入位置,并维持其余元素的序对顺序,就得到record序列的插入排序算法
def insert_sort(lst):
    for i in range(1, len(lst)):
        x = lst[i]
        j = i
        while j>0 and lst[j-1].key > x.key:
            lst[j] = lst[j-1]  # 反序逐个后移元素,确定插入位置
            j -= 1
        lst[j] = x
  • 算法分析:
    • 空间复杂度:计算中只用了两个简单变量,用于辅助定位和完成序列元素的位置转移,因此算法的空间复杂度是O(1),与序列大小无关。
    • 时间复杂度:最坏情况下时间复杂度是O(n**2),最好情况是O(n),所以算法时间复杂度仍然是O(n**2)
    • 这个算法是是稳定的,因为在内层循环中检索插入位置的过程中,一旦发现前面元素与当前元素关键码相同,就不再移动元素了,这种做法保证不会交换位置,因此算法稳定。
  • 插入排序算法的变形:采用二分法检索插入位置

选择排序:选择合适记录,严格按递增方式选出记录(每次选最小元素),简单的顺序排放就能完成排序工作

  • 选择排序基本思想:
    • 维护需要考虑的所有记录中最小的i个记录的已排序序列
    • 每次从剩余未排序的记录中选取关键码最小的记录,将其放在已排序序列记录的后面,作为序列的第i+1个记录,使已排序序列增长
    • 以空序列作为排序工作的开始,做到尚未排序的序列里只剩一个元素是(它必然为最大),只需要直接将其放在已排序的记录之后,整个排序就完成了
  • 算法实现:最简单的选择方法是顺序扫描序列中的元素,记住遇到的最小元素,一次扫描完毕就找到了一个最小元素,反复扫描就能完成排序工作。另一方面,选出了一个元素,原来的序列中就出现了一个空位,可以把这些空位集中起来存放排序好的序列 。如图所示,在排序过程中的任何时刻,表的前段积累了一批递增的已经排好序的记录,而且他们都不大于任何 一个未排序记录,下一步从未排序中选出最小记录,将其存放在已排序记录的后面,这样只剩一个记录其关键码一定最大,工作即可完成。

    

def select_sort(lst):
    for i in range(len(lst)-1):
        k = i
        for j in range(i, len(lst)):
            if lst[j].key < lst[k].key:
                k = j
        if i != k:
            lst[i], lst[k] = lst[k], lst[i]
  • 算法分析
    • 空间复杂度:算法中只用到几个变量,空间复杂度是O(1)
    • 时间复杂度:因为有两次循环,所以时间复杂度是O(n^2)
    • 算法稳定性:由于存在可能造成关键码相同元素交换次序,所以不稳定
    • 效率:低于插入排序

交换排序:一个序列中的记录没排好序,那么其中一定有逆序存在,如果交换所发现的逆序记录对,得到的序列将更接近排序序列,通过不断减少序列中的逆序,最终可以得到排序序列,采用不同的确定逆序方法和交换方法,可以得到不同的交换排序序列。

  • 起泡排序:起泡排序是一种典型的(也是最简单的)通过交换元素消除逆序实现排序的方法,其中的基本操作是比较相邻记录,发现相邻的逆序对时就交换它们,通过反复比较和交换,最终完成整个序列的排序工作
  • 算法实现:
def bubble_sort(lst):
    for i in range(len(lst)):
     found = False
for j in range(1, len(lst)-1): if lst[j-1].key > lst[j].key: lst[j-1], lst[j] = lst[j], lst[j-1]
          found = True
   if not found:
   break
  • 算法分析:
    • 时间复杂度:平均时间复杂度是O(n^2)
    • 空间复杂度:O(1)
    • 效率:比较低

快速排序:快速排序是实践中平均速度最快的算法之一,算法中也采用了逆序和交换位置的方法,但算法中最基本的思想是划分,即按照某种标准把考虑的记录划分为“小记录 ”和“大记录”,并通过递归不断划分,最终得到一个排序的序列

  • 算法基本过程:
    • 选择一种标准,把被排序序列中的记录按照这种标准分为大小两组,显然,从整体的角度,这两组记录的顺序已定,较小一组的记录应该排在前面。
    • 采用同样方式,递归的分别划分得到的这两组记录,并继续递归的划分下去
    • 划分总是得到越来越小的分组,如此工作下去直到每个记录组中最多包含一个记录时,整个序列的排序完成
  • 算法实现:
def quick_sort(lst):
    qsort_rec(lst, 0, len(lst)-1)
def qsort_rec(lst, l, r):
    if l>=r:  # 分段无记录或只有一个记录
        return
    i = l
    j = r
    pivot = lst[i]  # lst[i] 是初始空位
    while i < j:  # 找到pivot的最终位置
        while i < j and lst[j].key >= pivot.key:  # 用j向左扫描找到小于pivot的记录
            j -= 1
        if i < j:
            lst[i] = lst[j]
            i += 1  # 小记录移到左边
        while i < j and lst[i].key <= pivot.key:  # 用i向右扫描找到大于pivot的记录
            i += 1  
        if i < j:
            lst[j] = lst[i]
            j -= 1  # 大记录移到右边
    lst[i] = pivot  # 将pivot存入其最终位置
    qsort_rec(lst, 1,i-1)  # 递归处理左半区域
    qsort_rec(lst, i+1, r)  # 递归处理右半区域
  • 另一种实现
def quick_sort(lst):
    def qsort(lst, begin, end):
        if begin >= end:
            return
        pivot = lst[begin].key
        i = begin
        for j in range(begin+1, end+1):
            if lst[j].key < pivot:
                i += 1
                lst[i], lst[j] = lst[j], lst[i]
        lst[begin], lst[i] = lst[i], lst[begin]
        qsort(lst, begin, i-1)
        qsort(lst, i+1, end)
    qsort(lst, 0, len(lst)-1)

 

  • 算法分析:
    • 时间复杂度:O(nlogn)
    • 空间复杂度:O(logn)
    • 稳定性:不稳定

归并排序:归并算法是一种典型的序列操作,其工作是把两个或更多有序序列合并为一个有序序列,基于归并 的思想也可以 实现排序,称为归并排序,其基本方法如下:

  • 初始时,把待排序序列中的n个记录看成n个有序子序列(因为一个记录的序列总是排好序的),每个子序列的长度均为1
  • 把当时序列组里的有序子序列两两归并,完成一边后序列组里的排序序列个数 减半,每个子序列长度加倍
  • 对加长的有序子序列重复上面操作,最终得到一个长度为n的有序序列

这种归并方法称为简单的二路归并排序,其中每次操作都是把两个有序序列合并为一个有序序列,也可以考虑三路归并 或者跟多 路归并。

  • 算法实现:归并算法分三层实现
    • 最下层:实现表中相邻的一对有序序列的归并工作,将归并的结果存入 另一个顺序表里的相同位置。
    • 中间层:基于操作1(一对序列的归并操作),实现对整个表里顺序各对有序序列的归并,完成一边归并,对 各序列的归并结果存入另一顺序表里的同位置分段
    • 最高层:在两个顺序表之间往复执行操作2,完成一遍归并后交换两个表的地位,然后 在重复操作2的工作,直至整个表里 只有 一个有序序列时排序完成
  • 最下面一层函数merge,它完成了表中连续排放的两个有序序列的归并工作,lfrom:被归并的有序段,lto:归并结果
    def merge(lfrom, lto, low, mid, high):
        i, j, k = low, mid, low
        while i < mid and j < high:
            if lfrom[i].key <= lfrom[j].key:
                lto[k] = lfrom[i]
                i += 1
            else:
                lto[k] = lfrom[j]
                j += 1
            k += 1
        while i < mid:
            lto[k] = lfrom[i]
            i += 1
            k += 1
        while j < high:
            lto[k] = lfrom[j]
            j += 1
            k += 1

    函数的第一个循环处理两个分段都未归并元素的情况,其每次迭代取出两个分段中当时的最小记录(它一定是这两个分段之一的最小记录),把它移到lto表里的下一个位置,在循环体里还要正确更新几个下标变量,当某个分段不再有更多记录时本循环结束,后随的两个循环把另一分段中剩下的元素逐一复制到表lto中,这两个循环只有一个真正执行。

  • 函数merge_pass实现一对对分段的一遍归并,它需要知道表长度和分段长度,参数llen和slen分别表示这两个长度,第一个循环处理一对对长度都为slen的分段,随后的if语句处理表最后剩下 的两个或一个分段。
    def merge_pass(lfrom, lto, llen, slen):
        i = 0
        while i + 2*slen < llen:
            merge(lfrom, lto, i, i+slen, i + 2*slen)
            i += 2*slen
        if i + slen < llen:
            merge(lfrom, lto, i, i + slen, llen)
        else:
            for j in range(i, llen):
                lto[j] = lfrom[j]

     

  • 最后时主函数merge_sort,它先安排 一个同样长度的表,而后在两个表之间往复的做一遍遍归并,直至完成工作
    def merge_sort(lst):
        slen, llen = 1, len(lst)
        templst = [None]*llen
        while slen < llen:
            merge_pass(lst, templst, llen, slen)
            slen *= 2
            merge_pass(templst, lst, llen, slen)
            slen *= 2

    整个序列的实际排序完成时,得到的结果有可能正好放在templst里,这是再执行一次merge_pass(循环体里的第二个merge_pass调用)就能把结果复制会原来的表lst.

  • 算法分析
    • 时间复杂度:O(nlogn)
    • 空间复杂度:O(n)

算法比较

分配排序和基数排序

  • 为每个关键码值设定一个桶(即是能够容纳任一多个记录的容器,例如用一个连续表或链接表)
  • 排序是简单的根据关键码把记录放入相应桶中
  • 存入所有记录后,顺序收集各个桶里的记录,就得到排序的序列

算法实现

  • 需要排序的任然是record类型的顺序表,list
  • 记录中的关键码是十进制数字的元组,包含r个元素
  • 排序算法的参数是表lst和关键码码元素的长度r
    def radix_sort(lst, d):
        rlists = [[] for i in range(10)]
        llen = len(lst)
        for m in range(-1, -d-1, -1):
            for j in range(llen):
                rlists[lst[j]].append(lst[j])
            j = 0
            for i in range(10):
                tmp = rlists[i]
                for k in range(len(tmp)):
                    lst[j] = tmp[k]
                    j += 1
                rlists[i].clear()

     

python系统的list排序

python系统有一个内置的排序函数sort,可以对任何迭代对象排序,得到一个排序的表,另外,表list类的对象也有一个sort方法,其实两者共享一个排序算法,这是一种混成式排序算法,称为Timsort,可译为蒂姆排序。

蒂姆排序是一种基于归并技术的稳定排序算法,其中结合使用归并排序和插入排序技术,最坏的时间复杂度是O(nlogn),该算法具有适应性,在被排序的数组元素接近排好序的情况下,它的时间复杂度可能远小于O(nlogn),有可能达到线性时间。蒂姆排序算法再最坏情况下需要n/2工作空间,引起其工作空间复杂度是O(n),但另一方面,如果情况比较有利,它只需要很少零食存储空间。

 

posted @ 2020-08-05 01:24  你的莫  阅读(269)  评论(0)    收藏  举报