数据结构与算法:排序算法的稳定性以及各性能比较python实现

招聘笔试中经常会考到排序算法,在此做一个总结。

一、算法概念

1.排序算法的稳定性

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

1.简单选择排序

一趟简单排序的操作为:通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1<=i<=n)个记录交换之。

  • 时间复杂度:O(n^{2}),n(n-1)/2 = 1/2(n^2 + n)
  • 空间复杂度:O(1)
  • 稳定性:不稳定
#选择排序
def selection_sort(list1):
    print('selection_sort:')
    list2 = list1.copy()
    n = len(list2)
    for i in range(0, n - 1):
        min_ = i
        print(list2)
        for j in range(i + 1, n):
            if list2[j] < list2[min_]:
                min_ = j
        list2[i],list2[min_] = list2[min_],list2[i]
    print(list2,'\n')

2.冒泡排序

它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素已经排序完成。

  • 时间复杂度:O(n^{2}),n(n-1)/2 = 1/2(n^2 + n)
  • 空间复杂度:O(1)
  • 稳定性:稳定
#冒泡排序
def bubble_sort(list1):
    print('bubble_sort:')
    list2 = list1.copy() 
    n = len(list2)
    for i in range(0, n - 1):
        print(list2)
        for j in range(0, n - i - 1 ):
            if list2[j] > list2[j+1]:
                list2[j],list2[j+1] = list2[j+1],list2[j]
    print(list2,'\n')

3.插入排序 

将序列的第一个元素当做已经排序好的序列,然后从后面的第二个元素开始,逐个元素进行插入,直到整个序列有序为止。第i趟操作为:在含有i-1个记录的有序子序列r[1...i-1]中插入一个记录r[i]后,变成含有i个记录的有序子序列r[1..i]。

  • 时间复杂度:O(n^{2})
  • 空间复杂度:O(1)
  • 稳定性:稳定
#插入排序
def insertion_sort(list1):
    print('insertion_sort:')
    list2 = list1.copy()
    n = len(list2)
    for i in range(1,n):
        print(list2)
        value = list2[i]
        pos = i
        while pos > 0 and value< list2[pos - 1]:
            list2[pos] = list2[pos - 1]
            pos -= 1
        list2[pos] = value
    print(list2)

4.希尔排序

希尔排序(有时称为“递减递增排序”缩小增量排序 Diminishing Increment Sort)通过将原始列表分解为多个较小的子列表来改进插入排序,每个子列表使用插入排序进行排序。 选择这些子列表的方式是希尔排序的关键。不是将列表拆分为连续项的子列表,希尔排序使用增量i(有时称为 gap),通过选择 i 个项的所有项来创建子列表。

下图1中显示增量为3得到的子列表。个子列表可以通过插入排序进行排序。完成这些排序后,我们得到如图2所示的列表。虽然这个列表没有完全排序,但发生了很有趣的事情。 通过排序子列表,我们已将项目移动到更接近他们实际所属的位置。

5.10.å¸å°æåº.figure6

图1-2 

图3展示了使用增量为 1 的插入排序; 换句话说,标准插入排序。注意,通过执行之前的子列表排序,我们减少了将列表置于其最终顺序所需的移位操作的总数。对于这种情况,我们只需要四次移位完成该过程。

5.10.å¸å°æåº.figure8

图3 

  • 时间复杂度:O(n^{3/2}) 当n在某个特定范围内,希尔排序所需的比较和移动次数约为n^{1.3},当n\rightarrow \infty时,可以减少到n(log_{2}n)^{2}
  • 空间复杂度:O(1)
  • 稳定性:不稳定
#希尔排序
def shell_sort(list1, gap):
    print('shell_sort')
    list2 = list1.copy()
    n = len(list2)
    sub_n = n//gap
    for i in range(gap):
#         print(i, list2)       
        for j in range(1, sub_n):
            pos = j 
            value = list2[pos * gap + i]
#             print(value)
            while pos > 0 and value < list2[(pos - 1) * gap + i]:
#                 print(list2)
#                 print(pos, value, list2[(pos - 1) * gap + i])
                list2[pos*gap+i] = list2[(pos - 1) * gap + i]
                pos -= 1 
            list2[pos*gap+i] = value
            print('move:', list2)
    insertion_sort(list2)

5.堆排序

详细原理介绍请看相关博文:

https://blog.csdn.net/qq_18888869/article/details/88886270

6.归并排序

首先介绍一下分治法(Divide and Conquer)

很多有用的算法结构上是递归的,为了解决一个特定问题,算法一次或多次递归调用其自身以解决若干子问题。这些算法遵循分值的思想。分治法在每层递归时有三个步骤:

  • 分解:分解原问题为若干子问题,这些子问题是原问题的规模最小的实例
  • 解决:解决这些子问题,递归的求解这些子问题。当子问题规模足够小,就可以直接求解
  • 合并:合并这些子问题的解成原问题的解

下面介绍归并排序如何利用分治法解决问题:

考虑我们排序这个数组:[10,23,51,18,4,31,13,5] ,我们递归地将数组进行分解 

 当数组被完全分隔成只有单个元素的数组时,我们需要把它们合并回去,每次两两合并成一个有序的序列。

 

python实现: 

  • 分解:将待排序的n个元素分成各包含n/2个元素的子序列
  • 解决:使用归并排序递归排序两个子序列
  • 合并:合并两个已经排序的子序列以产生已排序的答案
  • 时间复杂度:O(nlog(n)) 
  • 空间复杂度:O(n)
  • 稳定性:稳定
#归并排序
def merging_sort(list1, ):
    list2 = list1.copy()
    length_list2 = len(list2)
    if length_list2 <= 1:
        return list2
    else:
        mid = int(length_list2/2)
        left_list = merging_sort(list2[:mid])
        right_list = merging_sort(list2[mid:])
        merged_list = merge_sorted_list(left_list, right_list)
        
        return merged_list
    
    

def merge_sorted_list(left_list, right_list):
    left_n = len(left_list)
    right_n = len(right_list)
    mergedlist = list()
    i = j = 0
    while i < left_n and j < right_n:
        if left_list[i] < right_list[j]:
            mergedlist.append(left_list[i])
            i += 1
        else:
            mergedlist.append(right_list[j])
            j += 1
    if i < left_n:
        mergedlist.extend(left_list[i:])
    if j < right_n:
        mergedlist.extend(right_list[j:])
    
    return mergedlist

7.快速排序

快排(与归并排序一样)也是一种分而治之(divide and conquer)的策略。归并排序把数组递归成只有单个元素的数组,之后再不断两两 合并,最后得到一个有序数组。这里的递归基本条件就是只包含一个元素的数组,当数组只包含一个元素的时候,我们可以认为它本来就是有序的(当然空数组也不用排序)。

快排的工作过程其实比较简单,三步走:

  • 选择基准值 pivot 将数组分成两个子数组:小于基准值的元素和大于基准值的元素。这个过程称之为 partition

  • 对这两个子数组进行快速排序。

  • 合并结果

  • 时间复杂度:O(nlog(n)) 
  • 空间复杂度:O(logn)
  • 稳定性:不稳定

 

#快速排序
def quick_sort(list2, low, high):
    print('quick_sort:')
#     list2 = list1.copy()
    if len(list2[low:high+1]) <= 1:
        return list2
    else:
        list2, pivot = partition(list2, low, high)
        print(list2)
        quick_sort(list2, low, pivot-1 )
        quick_sort(list2, pivot+1, high)
    
    print(list2)
    
def partition(list2, low, high):
#     list2 = list1.copy()
    pivotkey = list2[low] 
    while low < high:
#         print('low, high: {},{}'.format(low, high))
        while low < high and pivotkey < list2[high]:
            high -= 1
#             print('high:',high)
        list2[low] = list2[high]
        while low < high and pivotkey > list2[low]:
            low += 1
#             print('low:', low)
        list2[high] = list2[low]
        
    list2[low] = pivotkey
    
    return list2, low

8.基数排序

排序方法平均时间复杂度最好时间复杂度最坏时间复杂度辅助空间稳定性备注
冒泡排序O(n2)O(n)O(n2)O(1)稳定n小时较好
简单选择排序O(n2)O(n2)O(n2)O(1)不稳定n小时较好
直接插入排序O(n2)O(n)O(n2)O(1)稳定大部分已排序时较好
希尔排序  O(nlogn)~O(n2)O(n1.3)O(n2)O(1)不稳定s是所选分组
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)不稳定n大时较好
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定n大时较好
快速排序O(nlogn)O(n)O(n2)O(nlogn)~O(n)不稳定n大时较好

github代码:https://github.com/makang101/python-data-structure 

参考:

problem-solving-with-algorithms-and-data-structure-using-python 中文版

数据结构(C语言版)严蔚敏

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值