十大排序算法实现(python)

1.冒泡排序

1.描述

  • 重复重复地走访过要排序的数列,比较相邻元素的大小,把大的元素换到后面,最大元素先浮出来,再比较剩余需要排序数列,同样的方法找出最大元素,直到没有序列需要再排序

2.代码

def bubbleSort(arr):
    n = len(arr)
    # 遍历所有数组元素
    for i in range(n):
        # Last i elements are already in place
        for j in range(n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr
  
arr = [64, 34, 25, 12, 22, 11, 90]
print(bubbleSort(arr))

3.优化版本

  • 一次循环下来发现一次交换也没有,说明剩下的气泡已经按顺序排列了,就不需要再循环了
def bubbleSort(arr):
    n = len(arr)
    # 遍历所有数组元素
    for i in range(n):
        indicator = False  # 用于优化(没有交换时表示已经有序,结束循环)
        # Last i elements are already in place
        for j in range(0, n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                indicator = True
        if not indicator:  # 如果没有交换说明列表已经有序,结束循环
            break
    return arr
arr = [64, 34, 25, 12, 22, 11, 90]
print(bubbleSort(arr))#[11, 12, 22, 25, 34, 64, 90]

4.时间复杂度和稳定性分析

  1. 时间复杂度
  • 平均情况的的时间复杂度为\(O(n^2)\)

    • 外层循环排序执行 N - 1次。内层循环相邻元素比较最多的时候执行N次,最少的时候执行1次,平均执行 \(\frac{N+1}{2}\)次。所以循环体内的比较交换约执行 \(\frac{(N - 1)(N + 1)}{2} = \frac{N^2 - 1}{2}\),按照计算复杂度的原则,去掉常数,去掉最高项系数,其复杂度为\(O(N^2)\)
  • 最好的时间复杂度为\(O(n)\)

  • 文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数\(C\)和记录移动次数\(M\)均达到最小值:\(C~min~=n-1\), \(M~min~=0\)

  • 最坏的时间复杂度为\(O(N^2)\)

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

    img

    img

    冒泡排序的最坏时间复杂度为\(O(n^2)\)

  1. 算法稳定性(稳定)

    冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会再交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。

2.选择排序

1.描述

  • 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序序列中继续寻找最小(大)元素,通过将最小元素和未排序序列第一个元素交换位置,将未排序序列最小元素放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

2.代码

# selection_sort.py
def selection_sort(arr):
    count = len(arr)
    for i in range(count - 1):  # 交换 n-1 次
        #在未排序序列中找到最小元素
        min = i
        # 从剩余未排序元素中继续寻找最小元素
        for j in range(i+1, count):
            if arr[min] > arr[j]:
                min = j
        arr[min], arr[i] = arr[i], arr[min]  # 交换,将最小的放到本轮的最前面
    return arr

my_list = [6, 23, 2, 54, 12, 6, 8, 100]
print(selection_sort(my_list))#[2, 6, 6, 8, 12, 23, 54, 100]

3.优化

  • 从每次找出一个,优化为每次找出2个,找最小元素和最大元素两个,再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾,从剩余未排序元素中继续寻找最大元素,然后放到已排序序列的开头,因为每次都比较两个,所以每次左右都要少比较一个
# selection_sort.py
def selection_sort(arr):
    count = len(arr)
    for i in range(count-1):  # 交换 n-1 次
        #在未排序序列中找到最小元素
        min = i
        max=count-1
        count-=1#每次左右都要少比较一个末尾的
        # 从剩余未排序元素中继续寻找最小元素
        for j in range(i+1, count):
            if arr[min] > arr[j]:
                min = j
            if arr[max]<arr[j]:
                max=j
        arr[min], arr[i] = arr[i], arr[min]  # 交换,将最小的放到本轮的最前面
        arr[max],arr[count]=arr[count],arr[max]## 交换,将最大的放到本轮的最后面
    return arr

my_list = [6, 23, 2, 54, 12, 6, 8, 100]
print(selection_sort(my_list))#[2, 6, 6, 8, 12, 23, 54, 100]

4.时间复杂度和稳定性分析

  1. 时间复杂度
  • 平均情况的的时间复杂度为\(O(n^2)\)

    • 第一次内循环比较N - 1次,然后是N-2次,N-3次,……,最后一次内循环比较1次。
      共比较的次数是 (N - 1) + (N - 2) + ... + 1。所以循环体内的比较交换约执行 \(\frac{(N - 1+1)N}{2} = \frac{N^2}{2}\),按照计算复杂度的原则,去掉常数,去掉最高项系数,其复杂度为\(O(N^2)\)
    • 虽然选择排序和冒泡排序的时间复杂度一样,但实际上,选择排序进行的交换操作很少,最多会发生 N - 1次交换。而冒泡排序最坏的情况下要发生\(\frac{N^2}{2}\)交换操作。这个意义上讲,交换排序的性能略优于冒泡排序
  • 最好的最坏的时间复杂度都为\(O(N^2)\)

    • 文件的初始状态是正序的,或是逆序的,都需要对未排序序列比较每个元素找到最小值排到已排序序列后
  1. 算法稳定性(不稳定)
  • 在一趟选择中,如果当前元素比一个元素大,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,在前面的5变成了后面,所以选择排序不是一个稳定的排序算法。

3.插入排序

1.描述

  • 构建有序序列,对于未排序数据,排序过程中每次从无序表中取出第一个元素,在已排序序列中从后向前扫描,该元素比排序序列的当前元素大,则将排序序列的当前元素后移,将该元素依次往前比较,直到找到比它小或等于它的元素插入在其后面

2.代码

def insertion_sort(arr):
    # 第一层for表示循环插入的遍数
    for i in range(1,len(arr)):#默认第一个是已经排好序的
        # 设置当前需要插入的元素
        current=arr[i]
        j=i-1
        while j>=0 and current<arr[j]:
            # 当比较元素大于当前元素则把比较元素后移
            arr[j+1]=arr[j]
            # 往前选择下一个比较元素
            j-=1
        # 当比较元素小于当前元素,则将当前元素插入在 其后面
        arr[j+1]=current
    return arr

print(insertion_sort([12, 11, 13, 5, 6]))#[5, 6, 11, 12, 13]

3.优化

  • 插入排序的方法需要每次将待插入元素与已排序序列中的每个元素作比较,基于这点特性,可以采用二分查找法来减少比较操作的次数,待插入的值与已排序区域的中间值比较,不断缩小区域范围,直到left和right相遇,当left和right相遇的时候,待插入的位置其实就是left的位置,此时要将left到有序序列的最后一个元素都向后移动一个位置,才能插入元素。
def insertion_sort(arr):
    # 第一层for表示循环插入的遍数
    for i in range(1,len(arr)):
        # 设置当前需要插入的元素
        current=arr[i]
        left=0
        right=i-1

        #待插入的值与已排序区域的中间值比较,不断缩小区域范围,直到left和right相遇。
        while left<=right:
            mid=(right+left)//2
            if current>arr[mid]:
                left=mid+1
            else:
                right=mid-1

        # 当left和right相遇的时候,待插入的位置其实就是left的位置,此时要将left到有序序列的最后一个元素都向后移动一个位置,才能插入元素。
        for j in range(i-1,left-1,-1):
            arr[j+1]=arr[j]

        # 插入元素
        if left!=i:
            arr[left]=current
    return arr

print(insertion_sort([12, 11, 13, 5, 6]))#[5, 6, 11, 12, 13]

4.时间复杂度和稳定性分析

  1. 时间复杂度
  • 平均情况的的时间复杂度为\(O(n^2)\)

    • 最好和最坏的平均\(\frac{\frac{N^2-N}{2}+N-1}{2}=\frac{N^2}{4}+\frac{N}{2}-\frac{1}{2}\)
  • 最好的时间复杂度为\(O(n)\)

    • 文件的初始状态是正序的,只需要遍历需要排序的元素n-1
  • 最坏的时间复杂度为\(O(N^2)\)

    • 若初始文件是反序的,未排序元素与排序序列循环比较N - 1次,然后是N-2次,N-3次,……,最后一次内循环比较1次。共比较的次数是 (N - 1) + (N - 2) + ... + 1。所以循环体内的比较交换约执行 \(\frac{(N - 1+1)(N-1)}{2} = \frac{N^2-N}{2}\),按照计算复杂度的原则,去掉常数,去掉最高项系数,其复杂度为\(O(N^2)\)
  1. 算法稳定性(稳定)

    比较是从有序序列的末尾开始,也就是把待插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面。否则一直往前找直到找到它该插入的位置。如果遇见一个与插入元素相等的,那么把待插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序仍是排好序后的顺序,所以插入排序是稳定的。

4.快速排序

1.描述

  • 从数列中挑出一个元素,称为”基准",把一个序列(list)分为较小和较大的2个子序列,用两个指针,一个从右往左找比基准小的元素,放到基准前,一个从左往右找比基准小的元素,放到基准后,使得基准前序列元素都比基准后序列元素小,然后递归地排序两个子序列,以此达到整个数据变成有序序列

2.代码

def quick_sort(alist, start, end):
    """快速排序"""
    if start >= end:  # 递归的退出条件
        return
    mid = alist[start]  # 设定起始的基准元素
    low = start  # low为序列左边在开始位置的由左向右移动的游标
    high = end  # high为序列右边末尾位置的由右向左移动的游标
    while low < high:
        # 如果low与high未重合,high(右边)指向的元素大于等于基准元素,则high向左移动
        while low < high and alist[high] >= mid:
            high -= 1
        alist[low] = alist[high]  # 走到此位置时high指向一个比基准元素小的元素,将high指向的元素放到low的位置上,此时high指向的位置空着,接下来移动low找到符合条件的元素放在此处
        # 如果low与high未重合,low指向的元素比基准元素小,则low向右移动
        while low < high and alist[low] < mid:
            low += 1
        alist[high] = alist[low]  # 此时low指向一个比基准元素大的元素,将low指向的元素放到high空着的位置上,此时low指向的位置空着,之后进行下一次循环,将high找到符合条件的元素填到此处

    # 退出循环后,low与high重合,此时所指位置为基准元素的正确位置,左边的元素都比基准元素小,右边的元素都比基准元素大
    alist[low] = mid  # 将基准元素放到该位置,
    # 对基准元素左边的子序列进行快速排序
    quick_sort(alist, start, low - 1)  # start :0  low -1 原基准元素靠左边一位
    # 对基准元素右边的子序列进行快速排序
    quick_sort(alist, low + 1, end)  # low+1 : 原基准元素靠右一位  end: 最后

if __name__ == '__main__':
    alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    quick_sort(alist, 0, len(alist) - 1)
    print(alist)#[17, 20, 26, 31, 44, 54, 55, 77, 93]

3.优化

  1. 解决数组近乎有序的情况下算法复杂度会退化为O(n2)级别的问题可以通过随机选择待处理元素的方法来避免

  2. 三路快排,通过明确划分出相等元素,解决重复元素造成划分不平衡的问题,划分的三组为大于array[left],小于array[left],等于array[left]

    • 每轮递归中,对于当前元素i,如果小于目标,放到左边的lest_group,如果大于目标,放到右边的greater_group,如果等于目标,放到中间。之后对两边的大小分组继续递归,直到排序完成。
import random
def quick_sort(alist, start, end):
    """快速排序"""
    if start<end:
        random_index=random.randint(start,end)
        alist[start],alist[random_index]=alist[random_index],alist[start]
        pivot = alist[start]  # 设定起始的基准元素
        low = start  # low为序列左边在开始位置
        high = end+1  # high为序列右边末尾位置
        i=start+1 #当前元素
        while i < high:
            if alist[i]<pivot:
                #当前元素小于基准,当前元素换到序列左边
                alist[i],alist[low+1]=alist[low+1],alist[i]
                low+=1
                i+=1
            elif alist[i] > pivot:
                # 当前元素小于基准,当前元素换到序列右边
                alist[i], alist[high - 1] = alist[high - 1], alist[i]
                high-= 1
                #序列右边元素不确定比基准元素小还是大,当前坐标不前进,继续比较
            else:
                i+=1
        #遍历完low数组前都是比基准数小的,放置基准数
        alist[start],alist[low]=alist[low],alist[start]
        # 对基准元素左边的子序列进行快速排序
        quick_sort(alist, start, low - 1)  # start :0  low -1 原基准元素靠左边一位
        # 对基准元素右边的子序列进行快速排序
        quick_sort(alist, low, end)  # low+1 : 原基准元素靠右一位  end: 最后

if __name__ == '__main__':
    alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    quick_sort(alist, 0, len(alist) - 1)
    print(alist)#[17, 20, 26, 31, 44, 54, 55, 77, 93]

4.时间复杂度和稳定性分析

  1. 时间复杂度
  • 平均情况的的时间复杂度为\(O(nlog(n))\)

  • 最好的时间复杂度为\(O(nlog(n))\)

  • 快排本身 实际上这就是一个递归求解的多叉树,什么时候把所有叶子分清楚了就结束了,每一层具体的时间复杂度只可能等于n,时间复杂度完全取决于树高,树高则取决于你的左右子树每层的工作量,比基准数大和小的数相等时最快,树高为nlog(n)

  • 最坏的时间复杂度为\(O(N^2)\)

    • 只有比基准数大或者小的数,退化成链表,第一次遍历n个数找大小,再递归遍历n-1个数,n-2个数,约执行 \(\frac{(N - 1+1)N}{2} = \frac{N^2}{2}\)
  • 就地快速排序使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据;

    最优的情况下空间复杂度为:O(logn) ;每一次都平分数组的情况

    最差的情况下空间复杂度为:O( n ) ;退化为冒泡排序的情况

  1. 算法稳定性(不稳定)

    有相等的数在基准数后,且比基准数小,在后面的相等数会因为比基准数小先放到基准数前,相等数的前后顺序不一样了

5.堆排序

1.描述

  • 堆分为大顶堆和小顶堆,满足\(Key[i]>=Key[2i+1]且key>=key[2i+2]\)称为大顶堆,满足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]称为小顶堆
  • 构建大顶堆
    • 对于每一个节点而言,他的左孩子节点编号时2∗(𝑖𝑛𝑑𝑒𝑥+1),右孩子节点编号是\(2∗(𝑖𝑛𝑑𝑒𝑥+2)\)
    • 非叶子节点下标=节点个数//2-1 :即若root从0开始,则 从 0~节点个数//2-1 这个闭区间范围内都是非叶子节点,节点个数//2-1之后就是叶子节点了
  • 大顶堆的维护:自底向上的维护,对于叶子节点而言没有左孩子右孩子,因为大顶堆要求左孩子右孩子都小于父节点,所以不考虑叶子节点,直接从非叶子节点开始.
  • 递归将大顶堆堆顶最大值调到最后,减小size,重头开始调整堆结构,根元素是最大值,最大值逐渐到最后,完成排序

2.代码

#大根堆调整
def get_max_heap(heap, size, root):  # 在堆中做结构调整使得父节点的值大于子节点
    left = 2 * root + 1
    right = left + 1
    larger = root
    if left < size and heap[larger] < heap[left]:  # 最大值和左节点比较
        larger = left
    if right < size and heap[larger] < heap[right]:  # 最大值和右节点比较
        larger = right
    if larger != root:  # 如果做了堆调整则larger的值等于左节点或者右节点的,这个时候做对调值操作
        heap[larger], heap[root] = heap[root], heap[larger]
        get_max_heap(heap, size, larger)#递归子树

#建立大根堆
def build_heap(heap):
    # 构造一个堆,将堆中所有数据重新排序
    for index in range(len(heap) // 2 - 1, -1, -1):  # 从第一个非叶子节点开始
        get_max_heap(heap, len(heap), index)

#堆排序
def sort(heap):
    build_heap(heap)  # 获得一个大顶堆
    for index in range(len(heap) - 1, -1, -1):
        heap[0], heap[index] = heap[index], heap[0]  # 将最大值调到最后
        get_max_heap(heap, index, 0)  # size递减,保证最大值不会被重新排序
    return heap

print(sort([54, 26, 93, 17, 77, 31, 44, 55, 20],))#[17, 20, 26, 31, 44, 54, 55, 77, 93]

3.优化

4.时间复杂度和稳定性分析

  1. 时间复杂度
  • 平均情况的的时间复杂度为\(O(Nlog(N))\)

  • 最好的时间复杂度为\(O(Nlog(N))\)

    • 文件的初始状态是正序的,每次还是从根节点往下循环查找,所以复杂度是 O(nlogn)
  • 最坏的时间复杂度为\(O(Nlog(N))\)

    • 更改堆元素后重建堆时间:O(nlogn),循环 n -1 次,每次都是从根节点往下循环查找,所以每一次时间是树高logn,总时间:logn(n-1) = nlogn - logn ,所以复杂度是 O(nlogn)
  1. 算法稳定性(不稳定)

    在一个长为n 的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n /2-1, n/2-2, ...1这些个父节点选择元素时,就会破坏稳定性。有可能某层父节点交换把后面一个元素交换过去了,而同层父节点有一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。

6.希尔排序

1.描述

  • 先将原序列划分成若干个子序列,其中划分的依据为按照间隔gap的大小分开。至于gap的选法可以不一样,我们以gap初始值选为序列总长度的一半为例。在每个子序列之内,使用插入排序(前一个跟后一个相比,如果后一个值比前一个值小则调换两者之间的位置)。遍历未排序的子序列中的值,进行完第一轮排序之后,减小gap的大小,重复上述操作。由于间隔gap的值在不断减小,也称为减小增量排序,直到gap=1的时候,也就完成了整个序列的排序

2.代码

def shellsort(arr):
    n=len(arr)
    gap=n//2
    while gap>=1:
        for i in range(n):
            j=i
            while j>=gap and arr[j-gap]>arr[j]:
                arr[j-gap],arr[j]=arr[j],arr[j-gap]
                j-=gap
        gap//=2
    return arr
print(shellsort([54, 26, 93, 17, 77, 31, 44, 55, 20]))#[17, 20, 26, 31, 44, 54, 55, 77, 93]

def shellsort1(arr):
    n=len(arr)
    gap=n//2
    while gap>0:
        for i in range(gap,n):
            current=arr[i]
            j=i
            while j>=gap and arr[j-gap]>current:
                arr[j]=arr[j-gap]
                j-=gap
            arr[j]=current
        gap//=2
    return arr
print(shellsort1([54, 26, 93, 17, 77, 31, 44, 55, 20]))#[17, 20, 26, 31, 44, 54, 55, 77, 93]

3.优化

  • 没有需要交换的,说明剩下的已经按顺序排列了,就不需要再循环了。
def shellsort(arr):
    n=len(arr)
    gap=n//2
    while gap>=1:
        for i in range(n):
            j=i
            while j>=gap:
                if arr[j-gap]>arr[j]:
                    arr[j-gap],arr[j]=arr[j],arr[j-gap]
                    j-=gap
                else:
                    break
        gap//=2
    return arr
print(shellsort([54, 26, 93, 17, 77, 31, 44, 55, 20]))#[17, 20, 26, 31, 44, 54, 55, 77, 93]

4.时间复杂度和稳定性分析

  1. 时间复杂度
  • 平均情况的的时间复杂度为\(o(Nlog(N))\)

  • 最好的时间复杂度为\(O(n)\)

    • 文件的初始状态是正序的,需要进行的比较操作需(n-1)次。后移赋值操作为0次。即O(n)
  • 最坏的时间复杂度为\(O(N^2)\)

    • 若初始文件是反序的,依次分成n/2,n/4个组,然后把每个组内元素排序,未排序元素与排序序列循环比较N - 1次,然后是N-2次,N-3次,……,最后一次内循环比较1次。共比较的次数是 (N - 1) + (N - 2) + ... + 1。第一次比较次数,每组2个元素:1n/2,第二次比较次数,每组4个元素:最坏(1+2+3)n/4,第三次比较次数,每组8个元素:最坏(1+2+3+……+7)*n/8。累加求极限,得到算法复杂度小于 O(n^2)
  1. 算法稳定性(不稳定)

    由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以,Shell排序是不稳定的

7.归并排序

1.描述

  • 使用了递归分治的思想,先递归划分子问题,通过递归中间值划分成只有一个元素的子序列,然后比较子序列值的大小合并结果。把待排序列看成由两个有序的子序列,然后合并两个子序列,然后把子序列看成由两个有序序列。其实就是先两两合并,然后四四合并

2.代码

def merge(l, r):
    # 将传入的l,r序列按从小到大的顺序合并成一个列表ans
    i = j = 0
    ans = []
    while i < len(l) and j < len(r):
        # 左数组当前值比右数组当前值小,左数组前面的值都比右数组当前值小,无逆序对
        if l[i] <= r[j]:
            ans.append(l[i])
            i += 1
        # 左数组当前值比右数组当前值大,左数组之后的值都比右数组当前值大,都是逆序,逆序数为len(l)-i
        else:
            ans.append(r[j])
            j += 1
    if i != len(l):
        ans += l[i:]
    if j != len(r):
        ans += r[j:]
    return ans

#自顶向下排序,递归,
def mergersort(arr):
    n=len(arr)
    if n>1:
        mid_index=(n+1)//2
        l=mergersort(arr[:mid_index])
        r=mergersort(arr[mid_index:])
    else:
        return arr

    return merge(l,r)

arr= [64, 34, 25, 12, 22, 11, 90]
print(mergersort(arr))#[11, 12, 22, 25, 34, 64, 90]

3.优化

  • 不使用递归,先分再排(自顶向下,先递归分成两个小部分直至不能再分,再通过归并成两个升序子序列(先各一个元素,后各两个元素,然后四个元素)

    • 使用迭代,边分边排(自底向下,边用while迭代分成两个最小子序列,边通过排序归并,逐步增加序列大小,直到不需要归并)
    def merge(l, r):
        # 将传入的l,r序列按从小到大的顺序合并成一个列表ans
        i = j = 0
        ans = []
        while i < len(l) and j < len(r):
            # 左数组当前值比右数组当前值小,左数组前面的值都比右数组当前值小,无逆序对
            if l[i] <= r[j]:
                ans.append(l[i])
                i += 1
            # 左数组当前值比右数组当前值大,左数组之后的值都比右数组当前值大,都是逆序,逆序数为len(l)-i
            else:
                ans.append(r[j])
                j += 1
        if i != len(l):
            ans += l[i:]
        if j != len(r):
            ans += r[j:]
        return ans
    
    #自底向上排序,不递归,只迭代
    def mergersort(arr):
        n=len(arr)
        size=1
        l=[]
        r=[]
        res=[]
        while size<n:
            i=0
            while i<n:
                l=arr[i:i+size]
                r=arr[i+size:i+size+size]
                res[i:i+size+size]=merge(l,r)
                i=i+size+size
            size+=size
            arr=res
        return arr
    arr= [64, 34, 25, 12, 22, 11, 90]
    print(mergersort(arr))#[11, 12, 22, 25, 34, 64, 90]
    

4.时间复杂度和稳定性分析

  1. 时间复杂度
  • 平均情况的的时间复杂度为\(O(nlog_2n)\)

    • 归并排序递归左右子序列向上合并过程类似于树,所以时间为树高log(n),每次递归比较n个元素的大小
  • 最好的时间复杂度为\(O(nlog_2n)\)

    • 文件的初始状态是正序的,也比较比较n个元素的大小,所以时间复杂度也为\(O(nlog_2n)\)
  • 最坏的时间复杂度为\(O(nlog_2n)\)

    • 若初始文件是反序的,相同
  1. 算法稳定性(稳定)
    • 是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。

8.计数排序

1.描述

  • 找到待排序列表中的最大值 k,开辟一个长度为 k+1 的计数列表,计数列表中的值都为 0。
  • 走访待排序列表,将走访到的元素值与计数列表中索引 i 的值做对等,计算每个值的数量。
  • 创建一个新列表,遍历计数列表,依次在新列表中按值和值的数量排序。

2.代码

def countsort(arr):
    if len(arr)<2:
        return arr
    max_num=max(arr)
    count=[0]*(max_num+1)
    #把值和下标顺序做对等,计算数的个数
    for num in arr:
        count[num]+=1
    n_arr =[]
    #把数按下标排序排列相应数量
    for i in range(len(count)):
        for j in range(count[i]):
            n_arr.append(i)
    return n_arr

print(countsort([54, 26, 93, 17, 77, 31, 20]))#[17, 20, 26, 31, 54, 77, 93]

3.优化

  • 列表里面的元素值都比较大,再创建列表的时候,长度可以参考最大值-最小值+1这样的方式去处理
def countsort(arr):
    if len(arr)<2:
        return arr
    max_num=max(arr)
    min_num=min(arr)
    count=[0]*(max_num-min_num+1)
    #按下标顺序,计算数的个数
    for num in arr:
        count[num]+=1
    n_arr =[]
    #把数按数量,下标排序
    for i in range(len(count)):
        for j in range(count[i]):
            n_arr.append(i)
    return n_arr

print(countsort([54, 26, 93, 17, 77, 31, 20]))#[17, 20, 26, 31, 54, 77, 93]

4.时间复杂度和稳定性分析

  1. 时间复杂度
  • 平均情况的的时间复杂度为\(O(n+k)\)

  • 最好的时间复杂度为\(O(n+k)\)

    • 假如排序数组的元素个数为n,均匀分布在n个下标中,遍历数组,按下标顺序计数需要O(n), 遍历整数范围的序列需要O(k),遍历含有个数需要O(1),那么最终的时间复杂度为O(n+k).
  • 最坏的时间复杂度为\(O(n)\)

    • 所有的元素n个都分在一个下标,遍历数组,按下标顺序计数需要O(n), 遍历整数范围的序列需要O(k),遍历含有个数需要O(n),那么最终的时间复杂度为O(nk).
  1. 算法稳定性(稳定)

    计数排序不进行比较,后面相同的元素后放到下标中,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以是一种稳定排序算法。

9.桶排序

1.描述

  • 假设有一组长度为N的待排关键字序列K[1....n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列元素放到相应区间,接着对每个桶B中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]….B[M]中的全部内容即是一个有序序列。

2.代码

  • 对于在【0,1)区间的序列
def bucketsort(arr):
    n=len(arr)
    # 1.创建n个空桶
    n_list=[[] for _ in range(n)]
    # 2.把arr[i] 插入到bucket[n*array[i]]
    for data in arr:
        index=int(data*n)
        n_list[index].append(data)
    # 3.桶内排序
    for i in range(n):
        n_list[i].sort()
    # 4.产生新的排序后的列表
    index=0
    for i in range(n):
        for j in range(len(n_list[i])):
            arr[index]=n_list[i][j]
            index+=1

    return arr

print(bucketsort([0.897, 0.565, 0.656, 0.1234, 0.665, 0.3434]))#[0.1234, 0.3434, 0.565, 0.656, 0.665, 0.897]
  • 对于在[1,M]区间的序列
def bucketsort1(arr):
    n=len(arr)
    # 1.创建n个空桶
    n_list=[[] for _ in range(n)]
    # 2.把arr[i] 插入到bucket[n*array[i]]
    for data in arr:
        index=(data-min(arr))//(((max(arr)-min(arr))//n)+1)
        n_list[index].append(data)
    # 3.桶内排序
    for i in range(n):
        n_list[i].sort()
        print()
    # 4.产生新的排序后的列表
    index=0
    for i in range(n):
        for j in range(len(n_list[i])):
            arr[index]=n_list[i][j]
            index+=1
    return arr
print(bucketsort1([49,38,35,97,76,73,27,49]))#[27, 35, 38, 49, 49, 73, 76, 97]

3.优化

  • 对于在[1,M]区间的序列,在将元素分到不同桶之后,不对桶内元素进行排序,而是将其一个一个放回原数组。对原数组进行插入排序
def bucket_sort(lst):
    #生成n个桶
    buckets = [0] * ((max(lst) - min(lst))+1)
    for i in range(len(lst)):
        buckets[lst[i]-min(lst)] += 1
    res=[]
    for i in range(len(buckets)):
        for j in range(buckets[i]):
            res.append(i+min(lst))
    return res
print(bucket_sort([54, 26, 93, 17, 77, 31, 20]))#[17, 20, 26, 31, 54, 77, 93]

4.时间复杂度和稳定性分析

  1. 时间复杂度
  • 平均情况的的时间复杂度为\(O(n)\)

  • 最好的时间复杂度为\(O(n)\)

    • 假如排序数组的元素个数为n,均匀分布在个数为n的桶中,那么每个桶中的元素个数为k=n/m,因为每个桶快速排序的时间复杂度为klogk即n/mlog(n/m),那么m个桶一起就是nlog(n/m),假如桶的个数m跟元素个数n十分接近,那么最终的时间复杂度为O(n).
  • 最坏的时间复杂度为\(O(nlog(n))\)

    • 所有的元素n个都分在一个桶里呢?这种情况下时间复杂度就退化成O(nlogn)了
  • 桶排序中,需要创建M个桶的额外空间,以及N个元素的额外空间

    所以桶排序的空间复杂度为 O(N+M)

  1. 算法稳定性(稳定)

    桶排序中,假如升序排列,a已经在桶中,b插进来是永远都会a右边的(因为一般是从右到左,如果不小于当前元素,则插入改元素的右侧)

    所以桶排序是稳定的

    PS:当然了,如果采用元素插入后再分别进行桶内排序,并且桶内排序算法采用快速排序,那么就不是稳定的

10.基数排序

1.描述

  • 基数排序是一种非比较型整数排序算法,是桶排序的拓展,将所有待比较的数值统一为相同的数位长度,数位较端的数前面补0,按照低位先排序,将元素分别放到0-9十个队列中,#按低位顺序重新排列表,再依次按照高位收集,直至完毕。

2.代码

def radixsort(arr):
    n=len(str(max(arr)))
    for k in range(n):
        bucket_list=[[] for _ in range(10)]#每个数都有0-9,所以建立10个桶
        for i in arr:
            #按第k位大小依次放到桶中
            bucket_list[i//(10**k)%10].append(i)
        arr=[j for i in bucket_list for j in i]#按低位顺序重新排列表
    return arr
print(radixsort([54, 26, 93, 17, 77, 31, 20]))#[17, 20, 26, 31, 54, 77, 93]

3.优化

4.时间复杂度和稳定性分析

  1. 时间复杂度
  • 平均情况的的时间复杂度为\(O(d(n+10))\)
    • 设待排序列为n个记录,d个位数,每位的取值范围为0-9,则进行链式基数排序的时间复杂度为O(d(n+10)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(10),共进行d趟分配和收集
  • 最好的时间复杂度为\(O(n+10)\)
    • 每个数只有一位,只进行一趟
  • 最坏的时间复杂度为\(O(d(n+10))\)
  • 空间复杂度\(O(10d+n))\)每个位数都需要放置到取值范围内10d,重排序列需要n
  1. 算法稳定性(稳定)

    • 基数排序不进行比较,后面相同的元素后放到下标中,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以是一种稳定排序算法。
posted @ 2020-12-24 17:29  一路向暖  阅读(240)  评论(0编辑  收藏  举报