欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

基础算法之排序算法总结(python版)

程序员文章站 2024-01-25 22:11:10
...

 常见排序算法包括有:冒泡,简单选择,直接插入,快排,归并排序,堆排序,希尔排序

目录

冒泡排序

思想:

复杂度分析:

 选择排序

思想:

复杂度分析

直接插入排序

思想:

复杂度分析

希尔排序

思想

复杂度分析

快速排序

思想:

复杂度分析

 快排优化

 归并排序

思想

复杂度分析

归并优化

堆排序

思想

复杂度分析

 总结


冒泡排序

思想:

冒泡排序应该是最耳熟能详的排序算法,它的基本思想是,比较相邻两个数字,将较大的交换至后面,每一轮比较结束,都会将当前比较序列的最大值甩到最后。

复杂度分析:

如果序列初始状态即为升序,那么只需要一轮比较,即基础算法之排序算法总结(python版)次。最坏的情况下是当初始状态为降序,那么需要比较基础算法之排序算法总结(python版)次比较,并且还要做同等次数的交换。因此总的时间复杂度为基础算法之排序算法总结(python版)

# 冒泡排序,相邻两数比较,每次排序最大值沉到最后
def bubblesort(nums):
    n = len(nums)
    changed = True
    for i in range(n):
        # 增加标志位,如果上一次排序没有交换,说明已经排序成功,提前结束
        if changed:
            changed = False
            # 每此排序之后最大值会沉到最后,不参与下一轮比较
            for j in range(0, n - 1 - i):
                if nums[j] > nums[j + 1]:
                    changed = True
                    # 将较大值甩到后面
                    nums[j], nums[j + 1] = nums[j + 1], nums[j]
    return nums

 选择排序

思想:

每次从待排序的数组中选择一个最小的,与数组第一个数字交换。

复杂度分析

与冒泡排序相比,选择排序并没有减少比较次数,并且它不像冒泡排序一样,可以判断是否已经排好序提前结束,所以比较次数永远都是基础算法之排序算法总结(python版)次。仅仅是减少了元素移动的次数,最好的时候不需要移动任何元素,最坏的时候也仅需要交换基础算法之排序算法总结(python版)次。因此总的时间复杂度为基础算法之排序算法总结(python版)。但是总体上选择排序性能是要略优于冒泡的。


# 选择排序,每次选择最小的与第一位交换
def selectsort(nums):
    n = len(nums)
    for i in range(n):
        minindex = i
        for j in range(i + 1, n):
            if nums[j] < nums[minindex]:
                minindex = j
        if minindex != i:
            nums[i], nums[minindex] = nums[minindex], nums[i]
    return nums


直接插入排序

思想:

假设已有部分排好序的数组,对每一个新数,在这个数组中找到它能够插入的合适位置,使数组仍保持有序。类似理扑克牌

复杂度分析

当数组初始有序时,只需要每个数字与它前一个数字比较一次,总共基础算法之排序算法总结(python版)次的比较。最坏当数组初始逆序时,每个数字需要与他前面基础算法之排序算法总结(python版)个数字比较,并且前基础算法之排序算法总结(python版)个数字都要后移一次。总的比较次数和移动次数都是基础算法之排序算法总结(python版)。因此总的时间复杂度为基础算法之排序算法总结(python版)

# 插入排序,第i个数跟前i-1个已排好序的数字比较,放在合适的位置
def insertsort(nums):
    n = len(nums)
    for i in range(1, n):
        if nums[i] < nums[i-1]:
            # 暂存需要插入的数据
            temp = nums[i]
            j = i - 1
            # 一边比较一边后移
            while j >= 0 and nums[j] > temp:
                nums[j + 1] = nums[j]
                j -= 1
            # 跳出循环时表示j>0 或者nums[j] <= temp,那么应该插在后一个位置
            nums[j + 1] = temp
    return nums

希尔排序

思想

希尔排序是在插入排序基础上进行做出的改进,之前说到插入排序是将一个数字插入有序数组,那么如果整个数组基本有序时,再对整个数组进行插入排序就会简单很多。

希尔排序的思想是将相距某个“增量”的记录组成一个子序列,在子序列内部进行插入排序,逐步减小增量,直到增量减为1。仔细观察代码可以发现,代码的6-13行部分其实与插入排序的4-14行基本相同,只不过将增量increment换为1。

复杂度分析

希尔排序的时间复杂度与增量的选择有关,目前最好的结果是基础算法之排序算法总结(python版)

# 希尔排序
def shellsort(nums):
    n = len(nums)
    increment = n / 2
    while increment >= 1:
        for i in range(increment, n):
            if nums[i] < nums[i - increment]:
                temp = nums[i]
                j = i - increment
                while j >= 0 and temp < nums[j]:
                    arr[j+increment] = arr[j]
                    j -= increment
                nums[j+increment] = temp
        increment /= 2

快速排序

思想:

快排的基本思想是找到一个枢轴,经过一轮排序之后达到,比枢轴小的数都在枢轴的左边,比枢轴大的数都在枢轴的右边。再对左右两边的数组循环这个过程,以使整个序列有序。

下面以数组[6,1,5,8,3,7,4,9,2]做一次排序的演示(一般以第一个元素作为枢轴值):

基础算法之排序算法总结(python版)

 

复杂度分析

快排的时间性能取决于递归的深度,如果每次选择的枢轴可以实现将数组“对半分”,那么只需要递归基础算法之排序算法总结(python版)次,每一轮的partition需要扫描一次数组,也就是n次比较,那么最好情况下的时间复杂度为基础算法之排序算法总结(python版)。最坏的情况下,也就是当数组初始有序或者逆序,每次划分得到的两部分长分别是0,n-1,也就是说递归树是一个单支树,时间复杂度为基础算法之排序算法总结(python版)

而快排的空间消耗主要来自于递归造成的栈空间消耗,最好情况只需要基础算法之排序算法总结(python版)的空间,最快情况则需要基础算法之排序算法总结(python版)的空间。

def quicksort(nums, low, high):
    if low < high:
        pivot = patition(nums, low, high)
        quicksort(nums, low, pivot-1)
        quicksort(nums, pivot+1, high)

# 非递归实现
def quicksort(nums, low, high):
    para_stack = []
    if low < high:
        para_stack.append(low)
        para_stack.append(high)
        while len(para_stack) > 0:
            high = para_stack.pop()
            low = para_stack.pop()
            mid = partition(nums, low, high)
            if mid - 1 > low:
                para_stack.append(low)
                para_stack.append(mid - 1)
            if mid + 1 < high:
                para_stack.append(mid + 1)
                para_stack.append(high)

def patition(nums, low, high):
    # 以第一个元素作为枢轴值
    base = nums[low]
    while low < high:
        # high指针从后往前移,找到比枢轴值小的数字
        while low < high and nums[high] >= base:
            high -= 1
        # 将找到的数字复制至低位
        nums[low] = nums[high]
        # low指针从前往后移,找到比枢轴值大的数字
        while low < high and nums[low] <= base:
            low += 1
        # 将找到的数字复制至高位
        nums[low] = nums[low]
    # 跳出循环时,两指针重合,将枢轴值放入
    nums[low] = base
    return low

 快排优化

对于快排的优化主要是考虑如何减少递归深度。一种方法是优化枢轴的选择,通常情况下是固定选择待排序数组的第一个元素作为枢轴值,如果它刚好是数组中最大或者最小的数值,显然是很不合理的。因此考虑三数取中,也就是先选择三个数进行排序,选择中间数作为枢轴(一般是选择最左,最右,中间),这在概率上来说,极大减少取最大或者最小的数值作为枢轴值的可能。第二种方法是采用尾递归减少递归深度 ,修改代码如下,将其中一个递归改为迭代,减少堆栈深度。

def quicksort(nums, low, high):
    while low < high:
        pivot = patition(nums, low, high)
        quicksort(nums, low, pivot - 1)
        low = pivot + 1

 归并排序

思想

归并排序有两路归并和多路归并,这里只介绍两路归并。它的核心思想是将两个有序数组合并为一个有序数组。它的合并过程类似于一个倒置的完全二叉树。

基础算法之排序算法总结(python版)

复杂度分析

同样的,它的时间效率取决于递归深度,由于归并每次将数组从中间二分,递归树是完全二叉树,所以递归深度永远为基础算法之排序算法总结(python版),每次合并需要遍历一次数组,所以总的时间复杂度为基础算法之排序算法总结(python版)。归并排序需要一个辅助数组暂时存放合并后的结果,再加上递归所需要的堆栈空间,所以空间复杂度为基础算法之排序算法总结(python版)

# 归并排序
def mergesort(nums, low, high, helparr):
    if high > low:
        mid = (low+high) / 2
        # 将数组不断二分,直到每组只剩一个元素
        mergesort(nums, low, mid, helparr)
        mergesort(nums, mid+1, high, helparr)
        merge(nums, low, mid, high, helparr)


# 合并两个有序数组
def merge(num, low, mid, high, helparr):
    # 前半截数组的下标
    firstindex = low
    # 后半截数组的下标
    secondindex = mid+1
    # 辅助数组的下标
    helpindex = low
    while firstindex <= mid and secondindex <= high:
        # 将较小值复制到辅助数组中
        if num[firstindex] <= num[secondindex]:
            helparr[helpindex] = num[firstindex]
            firstindex += 1
        else:
            helparr[helpindex] = num[secondindex]
            secondindex += 1
        helpindex += 1
    # 将较长的那个数组剩余部分直接复制到辅助数组中
    if firstindex <= mid:
        helparr[helpindex:high + 1] = num[firstindex:mid + 1]
    if secondindex <= high:
        helparr[helpindex:high + 1] = num[secondindex:high + 1]
    # 将合并后的结果放回原数组
    num[low:high+1] = helparr[low:high+1]


归并优化

 归并算法的时间复杂度已经很高了,而且很稳定,但是递归产生的堆栈空间消耗还有优化空间。所以可以考虑把递归改为迭代。

 

堆排序

思想

在描述堆排序的思想之前,回顾一下完全二叉树的一些性质:

  1. 如果完全二叉树有n个节点,那么树的深度为基础算法之排序算法总结(python版),(基础算法之排序算法总结(python版)表示不大于x的整数)
  2. 对完全二叉树从根开始,按照 0,1,2,3基础算法之排序算法总结(python版)开始编号,那么编号为i的结点,其左右孩子分别是基础算法之排序算法总结(python版),其双亲结点是基础算法之排序算法总结(python版)

堆在完全二叉树的基础上增加一条性质:每个结点的值都大于或等于(小于或等于)其左右孩子的结点值,称为大顶堆(小顶堆)。如果能够将待排序数组构造成一个大顶堆,那么根结点就是当前二叉树最大的数。将根结点提出,重新构造剩余数组,就可以依次将当前最大数找出。这就是堆排序。

那么问题就只有一个:如何根据数组构造大顶堆。这里以{50,10,90,30,70,40,80,60,20}为例演示调整过程:

0. 将数组建立为初始二叉树

基础算法之排序算法总结(python版)

基础算法之排序算法总结(python版)

 

1. 由于叶子结点不需要调整,那么从以最后一个叶结点的双亲结点,也即是最后一个非叶结点,为根结点的子树{30,60,20}开始调整。7号结点值最大,那么30和60互换

基础算法之排序算法总结(python版)

2. 接下来是以倒数第二个非叶结点为根结点的子树{90,40,80},此时根结点已经是最大值,不需要调整

3. 接下来是以1号结点的子树,由于70最大,所以将70和10互换

基础算法之排序算法总结(python版)

4. 最后是根结点,将50和90交换

基础算法之排序算法总结(python版)

5. 这里需要注意,由于50下放之后,导致2号结点的子树不满足大顶堆条件,所以需要接着调整,将50和80交换

基础算法之排序算法总结(python版)

6. 大顶堆构造结束,此时数组最大数已经到达堆顶。将堆顶元素和最后一个叶结点交换

基础算法之排序算法总结(python版)

7。 由于交换之后的根结点不满足大根堆条件,接着调整(也就是重复步骤4-6),不一样的是,已经被选出来的90不需要再次参与堆的调整。

复杂度分析

堆排序的时间主要消耗在初始建堆和重建堆的反复筛选上。

因为我们是完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较和若有必要的互换,对于每个非终端结点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为基础算法之排序算法总结(python版)

在正式排序时,第i 次取堆顶记录重建堆需要用基础算法之排序算法总结(python版)的时间(完全二叉树的某个结点到根结点的距离为基础算法之排序算法总结(python版),并且需要取n-1 次堆顶记录,因此,重建堆的时间复杂度为基础算法之排序算法总结(python版)

# 堆排序
def heapsort(nums):
    n = len(nums)
    # 建立大顶堆
    for i in range(n / 2 - 1, -1, -1):
        heapAjust(nums, i, n - 1)
    for i in range(n - 1, -1, -1):
        # 堆顶元素为最大值,交换至数组末尾
        nums[0], nums[i] = nums[i], nums[0]
        # 调整剩下数组仍为大顶堆
        heapAjust(nums, 0, i - 1)


def heapAjust(nums, start, end):
    temp = nums[start]
    # 记录较大的那个孩子下标
    child = 2 * start + 1
    while child <= end:
        # 比较左右孩子,记录较大的那个
        if child + 1 <= end and nums[child] < nums[child+1]:
            # 如果右孩子比较大,下标往右移
            child += 1
        # 如果根已经比左右孩子都大了,直接退出
        if temp >= nums[child]:
            break
        # 如果根小于某个孩子,将较大值提到根位置
        nums[start] = nums[child]
        # nums[start], nums[child] = nums[child], nums[start]
        # 接着比较被降下去是否符合要求,此时的根下标为原来被换上去的那个孩子下标
        start = child
        # 孩子下标也要下降一层
        child = child * 2 + 1
    # 最后将一开始的根值放入合适的位置(如果前面是交换,这句就不要)
    nums[start] = temp

 总结

基础算法之排序算法总结(python版)基础算法之排序算法总结(python版)

从待排序记录的个数上来说,待排序的个数n 越小,采用简单排序方法越合适。

此对于数据量不是很大而记录的关键字信息最较大的排序要求,简单排序算法是占优的。另外,记录的关键字信息量大小对那四个改进算法影响不大。

相关标签: 基础算法