数据结构与算法之排序算法

学习笔记

排序算法思维导图,来自:十大经典排序算法
在这里插入图片描述

排序算法(英语:Sorting algorithm)是一种能将一串数据依照特定顺序进行排列的一种算法。


排序算法的稳定性

稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序

也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。

当相等的元素是无法分辨的,比如像是整数,稳定性并不是一个问题。然而,假设以下的数对将要以他们的第一个数字来排序。

(4, 1)  (3, 1)  (3, 7)5, 6

在这个状况下,有可能产生两种不同的结果,一个是让相等键值的纪录维持相对的次序,而另外一个则没有:

(3, 1)  (3, 7)  (4, 1)  (5, 6)  (维持次序)
(3, 7)  (3, 1)  (4, 1)  (5, 6)  (次序被改变)

不稳定排序算法可能会在相等的键值中改变纪录的相对次序,但是稳定排序算法从来不会如此。

其他基本术语说明:

引用:十大经典排序算法
在这里插入图片描述

1 冒泡排序

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

重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。

遍历数列的工作是重复的进行直到没有在需要的交换,也就是说该数列已经排序完成,这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

冒泡排序算法的运作如下:

  • 比较相邻的元素。如果第一个比第二个大(升序),就交换它们两个。
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  • 针对所有的元素重复以上的步骤,除了最后一个。
  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

冒泡排序的分析

在这里插入图片描述
冒泡排序的动图演示

在这里插入图片描述

时间复杂度

  • 最优时间复杂度: O ( n ) O(n) O(n) (表示遍历一次发现没有任何可以交换的元素,排序结束。)
  • 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 稳定性:稳定

冒泡排序的实现

def bubble_sort(alist):
    for j in range(len(alist)-1,0,-1):
        # j表示每次遍历需要比较的次数,是逐渐减小的
        for i in range(j):
            if alist[i] > alist[i+1]:
                alist[i], alist[i+1] = alist[i+1], alist[i]

li = [54,26,93,17,77,31,44,55,20]
print(li)
bubble_sort(li)
print(li)
'''
[54, 26, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 26, 31, 44, 54, 55, 77, 93]
'''   

冒泡复杂度的优化

# 如果没有交换过就直接退出
# 优化冒泡算法
def bubble_sort(alist):
    """冒泡排序"""
    for j in range(len(alist)-1): 
        count = 0
        for i in range(len(alist)-j-1):
            if alist[i] > alist[i+1]:
                alist[i], alist[i+1] = alist[i+1], alist[i]
                count += 1
        if 0 == count:
            return alist

2 选择排序

选择排序(Selection sort)的工作原理是首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。

选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。

选择排序分析

在这里插入图片描述
选择排序动图演示

红色表示当前最小值,黄色表示已排序序列,蓝色表示当前位置。

在这里插入图片描述

在这里插入图片描述

时间复杂度

  • 最优时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 稳定性:不稳定(考虑升序每次选择最大的情况)

选择排序的实现

def selection_sort(alist):
    n = len(alist)
    # 需要进行n-1次选择操作
    for i in range(n-1):
        # 记录最小位置
        min_index = i
        # 从i+1位置到末尾选择出最小数据
        for j in range(i+1, n):
            if alist[j] < alist[min_index]:
                min_index = j
        # 如果选择出的数据不在正确位置,进行交换
        if min_index != i:
            alist[i], alist[min_index] = alist[min_index], alist[i]

alist = [54,226,93,17,77,31,44,55,20]
print(alist)
selection_sort(alist)
print(alist)

'''
[54, 226, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 31, 44, 54, 55, 77, 93, 226]
'''

3 插入排序

插入排序(Insertion Sort)是一种简单直观的排序算法。

它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入

插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

插入排序分析

在这里插入图片描述
图片来源于捎的饭捎的饭

在这里插入图片描述


例如现在拿到一副牌

Input: {5, 3, 8, 4, 2, 6, 1, 7}

  • 首先拿起第一张牌, 手上有 {5}

  • 拿起第二张牌 3,把 3 与 有序{5}从后往前比较大小 ,insert 到手上的牌 {5},得到 {3 ,5}

  • 拿起第三张牌 8,把 8 与 有序{3,5}从后往前比较大小, 此处 insert 最后, 得到 {3 ,5,8}

  • 拿起第四张牌 4,把 4 与 有序{3,5,8}从后往前比较大小,此处 insert 到 3,5 之间, 得到 {3,4,5,8}

以此类推。


插入排序动图演示

在这里插入图片描述
在这里插入图片描述

时间复杂度

  • 最优时间复杂度: O ( n ) O(n) O(n) (升序排列,序列已经处于升序状态)
  • 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 稳定性:稳定

插入排序的实现

(1)时间复杂度为 O ( n 2 ) O(n^2) O(n2)

def insert_sort(alist):
	for i in range(1,len(alist)):
		for j in range(i,0,-1):
			if alist[j] < alist[j-1]:
				alist[j], alist[j-1] = alist[j-1], alist[j]
	return alist

alist = [54,226,93,17,77,31,44,55,20]
print(alist)
insert_sort(alist)
print(alist)

'''
[54, 226, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 31, 44, 54, 55, 77, 93, 226]
'''

(2)时间复杂度为 O ( n ) O(n) O(n)

def insert_sort(alist):
    #插入排序
    for i in range(1, len(alist)):
        while i >0:
            if alist[i] < alist[i-1]:
                alist[i], alist[i-1] = alist[i-1], alist[i]
                i -= 1
            else:
                break
    return alist

alist = [54,226,93,17,77,31,44,55,20]
print(alist)
insert_sort(alist)
print(alist)

'''
[54, 226, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 31, 44, 54, 55, 77, 93, 226]
'''

4 希尔排序

希尔排序(Shell Sort)是插入排序的一种,也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。

希尔排序是非稳定排序算法。

希尔排序的基本思想是:

把记录按步长 gap 分组,对每组记录采用直接插入排序方法进行排序。
随着步长逐渐减小,所分成的组包含的记录越来越多,当步长的值减小到 1 时,整个数据合成为一组,构成一组有序记录,则完成排序。

希尔排序的分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(1)
在这里插入图片描述
(2)
在这里插入图片描述

希尔排序动图演示
在这里插入图片描述

在这里插入图片描述
图片来自:漫画:什么是希尔排序?


时间复杂度

  • 最优时间复杂度:根据步长序列的不同而不同
  • 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 稳定性:不稳定

希尔排序的实现

def shell_sort(alist):
    n = len(alist)
    # 初始步长
    gap = n // 2
    # 当 gap = 1 时为简单插入排序
    while gap >=1:
        # 按步长进行插入排序
        for i in range(gap, n):
            # j = gap、gap+1、gap+2、...、n-1
            j = i  
            # 插入排序
            while j>=gap and alist[j-gap] > alist[j]:
                alist[j-gap], alist[j] = alist[j], alist[j-gap]
                j -= gap
        # 得到新的步长
        gap = gap // 2

alist = [54,26,93,17,77,31,44,55,20]
print(alist)
shell_sort(alist)
print(alist)

'''
[54, 26, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 26, 31, 44, 54, 55, 77, 93]
'''

5 快速排序

快速排序,又称划分交换排序,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

步骤为:

  • 从数列中挑出一个元素,称为"基准"(pivot),
  • 重新排序数列,所有元素比基准值小的摆放在基准前面所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

快速排序的分析

在这里插入图片描述
图片来自:https://cuijiahua.com/blog/2017/12/algorithm_4.html

快速排序动图演示

  1. 把第一个元素定为基准 key=6
  2. 两个指针,分别为 low ,初始指向头 6 ;high ,初始指向尾 1
  3. 首先,比较 high 指向的元素和 key 大小,如果 high 指向的元素大,指针向右移动,如果 high 指向的元素小,将其元素赋给 low ,low 向左一步。这里 1<6 ,把 1 赋给 low
  4. 只要赋值一次,就交换指针移动权。
  5. 接下来比较 low 指向的元素和 key 大小,如果 low 指向的元素小,指针向左移动,如果 low 指向的元素大,将其元素赋给 high ,high 向右一步。这里 3<6 ,low 向左一步,7>6,把 7 赋给 high ,high 向右一步
  6. 当两个指针重合,即为key的位置
  7. 此时key都小于右边,都大于左边
    在这里插入图片描述

时间复杂度

  • 最优时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
  • 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 稳定性:不稳定

快速排序的实现

def quick_sort(alist, start, end):
    """快速排序"""

    # 递归的退出条件
    if start >= end:
        return

    # 设定起始元素为要寻找位置的基准元素
    mid = alist[start]

    # low为序列左边的由左向右移动的游标
    low = start

    # high为序列右边的由右向左移动的游标
    high = end

    while low < high:
        # 如果low与high未重合,high指向的元素不比基准元素小,则high向左移动
        while low < high and alist[high] >= mid:
            high -= 1
        # 将high指向的元素放到low的位置上
        alist[low] = alist[high]

        # 如果low与high未重合,low指向的元素比基准元素小,则low向右移动
        while low < high and alist[low] < mid:
            low += 1
        # 将low指向的元素放到high的位置上
        alist[high] = alist[low]

    # 退出循环后,low与high重合,此时所指位置为基准元素的正确位置
    # 将基准元素放到该位置
    alist[low] = mid

    # 对基准元素左边的子序列进行快速排序
    quick_sort(alist, start, low-1)

    # 对基准元素右边的子序列进行快速排序
    quick_sort(alist, low+1, end)


alist = [54,26,93,17,77,31,44,55,20]
print(alist)
quick_sort(alist,0,len(alist)-1)
print(alist)

'''
[54, 26, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 26, 31, 44, 54, 55, 77, 93]
'''

6 归并排序

归并排序是采用分治法的一个非常典型的应用。

归并排序的思想就是先递归分解数组,再合并数组。

将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。

归并排序的分析

引用图片:归并排序
在这里插入图片描述
引用图片:图解排序算法(四)之归并排序

治的详细过程

在这里插入图片描述
在这里插入图片描述

归并排序动图演示
(1)
在这里插入图片描述
(2)
在这里插入图片描述

时间复杂度

  • 最优时间复杂度:O(nlogn)
  • 最坏时间复杂度:O(nlogn)
  • 稳定性:稳定

快速排序的实现

递归操作

def merge_sort(alist):
    if len(alist) <= 1:
        return alist
    # 二分分解
    num = len(alist)//2
    # left(right)采用归并排序后形成的有序的新的列表
    left = merge_sort(alist[:num])
    right = merge_sort(alist[num:])
    # 合并
    # 将两个有序的子序列合并成一个整体
    return merge(left,right)

def merge(left, right):
    '''合并操作,将两个有序数组left[]right[]合并成一个大的有序数组'''
    #left与right的下标指针
    l, r = 0, 0
    result = []
    while l<len(left) and r<len(right):
        if left[l] < right[r]:
            result.append(left[l])
            l += 1
        else:
            result.append(right[r])
            r += 1
    result += left[l:]
    result += right[r:]
    return result

alist = [54,26,93,17,77,31,44,55,20]
print(alist)
sorted_alist = merge_sort(alist)
print(sorted_alist)

'''
[54, 26, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 26, 31, 44, 54, 55, 77, 93]
'''

7 堆排序

堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序

堆是具有以下性质的完全二叉树:

  • 每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;
  • 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

来自:图解排序算法(三)之堆排序
在这里插入图片描述
堆排序的基本思想是:

(1)将带排序的序列构造成一个大顶堆,根据大顶堆的性质,当前堆的根节点(堆顶)就是序列中最大的元素;
(2)将堆顶元素和最后一个元素交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值,然后将剩下的节点重新构造成一个大顶堆;
(3)重复步骤(2),如此反复,从第一次构建大顶堆开始,每一次构建,我们都能获得一个序列的最大值,然后把它放到大顶堆的尾部。
(4)最后,就得到一个有序的序列了。

时间复杂度

  • 最优时间复杂度:O(nlogn)
  • 最坏时间复杂度:O(nlogn)
  • 稳定性:不稳定

堆排序动图演示

(1)

在这里插入图片描述

(2)
在这里插入图片描述

8 计数排序

基本步骤

1.找出待排序的数组中最大和最小的元素;
2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1.

在这里插入图片描述

参考代码:计数排序

def count_sort1(array):
    length = len(array)
    res = [None] * length
    # 首次循环遍历, 每个列表的数都统计
    for index in range(length):
        # p 表示 a[i] 大于列表其他数 的次数
        p = 0
        # q 表示 等于 a[i] 的次数
        q = 0
        # 二次循环遍历, 列表中的每个数都和首次循环的数比较
        for two_index in range(length):
            if array[index] > array[two_index]:
                p += 1
            elif array[index] == array[two_index]:
                q += 1
        for k in range(p, p + q):  # q表示相等的次数,就表示, 从p开始索引后, 连续q次,都是同样的数
            res[k] = array[index]
    return res

dest = [5, 2, 7, 4, 8, 1, 6, 3]
result = count_sort1(dest)
print('最后的结果是:', result)

'''
最后的结果是: [1, 2, 3, 4, 5, 6, 7, 8]
'''

9 桶排序

划分多个范围相同的区间,每个子区间自排序,最后合并。

引用图片:【排序】图解桶排序
在这里插入图片描述

10 基数排序

在这里插入图片描述

常见排序算法效率比较

在这里插入图片描述

至此

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值