2

冒泡排序

两层循环
外层循环:遍历数组中待排序部分
内层循环:当前元素和它后面的元素比较大小,如果逆序就交换位置;下一个元素和它后面的元素比较大小,如果逆序就交换位置...直到结束

内层循环的排序过程,就是一个冒泡的过程,很形象,当内层排序结束后,最后一个元素就是已经排好序的元素,就像气泡冒出水面了。

数组:[5,1,3,4,2]

外层遍历,遍历数组待排序部分:
当前待排序部分是整个数组:
[【5,1,3,4,2】]
    内层遍历,当前元素和后面的元素比较大小,如果逆序就交换位置
    [(5,1),3,4,2] => [(1,5),3,4,2]
    [1,(5,3),4,2] => [1,(3,5),4,2]
    [1,3,(5,4),2] => [1,3,(4,5),2]
    [1,3,4,(5,2)] => [1,3,4,(2,5)]
5已排好序,5之前的部分是待排序部分

当前待排序部分:
[【1,3,4,2】,/5/]
    内层遍历,当前元素和后面的元素比较大小,如果逆序就交换位置
    [(1,3),4,2,/5/] => [(1,3),4,2,/5/]
    [1,(3,4),2,/5/] => [1,(3,4),2,/5/]
    [1,3,(4,2),/5/] => [1,3,(2,4),/5/]
4已排好序,4之前的部分是待排序部分

...以此类推

时间复杂度:

最坏情况:
冒泡排序一共要进行(n-1)次循环,每一次循环都要进行当前n-1次比较
所以一共的比较次数是: (n-1) + (n-2) + (n-3) + … + 1 = n*(n-1)/2;
所以冒泡排序的时间复杂度是 O(n2)

最好情况:待排序数组本身就是正序的,一轮扫描即可完成排序,此时时间复杂度仅为比较时的时间复杂度,为O(n)

平均情况:O(n2)

空间复杂度:
就是在交换元素时那个临时变量所占的内存空间,最优的空间复杂度就是开始元素顺序已经排好了,则空间复杂度为0,最差的空间复杂度就是开始元素逆序排序了,则空间复杂度为O(n),平均的空间复杂度为O(1)

def bubble_sort(nums):
    n = len(nums)
    
    for i in range(n):
        # 外层每结束一次循环,就有1个元素已排序
        # i=0 没有已排序元素;i=1 1个已排序元素;i=2 2个已排序元素...
        # 所以内层循环次数 = 元素个数-i
        # 同时内层循环最后一次遍历,只需要遍历到倒数第二个元素,和倒数第一元素比较即可,所以内层循环次数 = 元素个数-i-1
        for j in range(n-i-1):
            if nums[j] > nums[j+1]:
                nums[j],nums[j+1]=nums[j+1],nums[j]
    return nums

快速排序

快速排序有种二分法+双指针+递归的感觉
选择一个基准值,设置两个指针left和right,left负责小于基准值的部分,right负责大于基准值的部分,将数组分成小于该值、大于等于该值两部分
再将这两部分当成一个新的数组,重复上面的步骤
直到最终两部分只有1个元素为止

具体流程:

数组= [4,3,5,7,6,1,2,4]

设置一个基准值,一般选择数组第一个元素即可
基准值:4
设置两个指针,left在头,right在尾

基准值:4
[4(l),3,5,7,6,1,2,4(r)]
因为left指向的元素就是基准值本身,所以我们先去比较right指向的元素
right也指向4,>=基准值,属于right的范围,不做改变,right向前移动

基准值:4
[4(l),3,5,7,6,1,2(r),4]
right指向2,<基准值,属于left的范围,将2赋值给left指针,left向后移动

基准值:4
[2,3(l),5,7,6,1,2(r),4]
left指向3,<基准值,属于left的范围,不做改变,left向后移动

基准值:4
[2,3,5(l),7,6,1,2(r),4]
left指向5,>=基准值,属于right的范围,将5赋值给right指针,right向前移动

基准值:4
[2,3,5(l),7,6,1(r),5,4]
right指向1,<基准值,属于left的范围,将1赋值给left指针,left向后移动

基准值:4
[2,3,1,7(l),6,1(r),5,4]
left指向7,>=基准值,属于right的范围,将7赋值给right指针,right向前移动


基准值:4
[2,3,1,7(l),6(r),7,5,4]
right指向6,>=基准值,属于right的范围,不做改变,right向前移动

基准值:4
[2,3,1,7(l,r),6,7,5,4]
此时left和right相遇,将该值改为基准值

基准值:4
[2,3,1,4(l,r),6,7,5,4]
此时指针的左半部分[2,3,1]都是小于基准值4的
指针的右半部分[6,7,5,4]都是大于等于基准值4的
这两个部分重新选额各自的基准值,重复上述快排步骤,直到递归结束
最终就将整个数组完成排序

时间复杂度:

  • 最好情况:O(nlogn)
    最好情况时,每次基准值都在在数组中间并且刚好将数组平分,经过logn次划分,就分出了左右两部分
    每一次划分,都涉及到n个元素
    所以总共时间为O(n)*O(logn)=O(nlogn)
  • 最坏情况:O(n^2)
    最坏情况时,每次基准值都在数组开头,并且数组完全正序,那么就需要划分n次,才能分出左右两部分
    每一次划分,都涉及到n个元素
    所以总共时间为O(n)*O(n)=O(n^2)
  • 平均:O(nlogn)

空间复杂度:
最优的情况下空间复杂度为:O(logn);每一次都平分数组的情况
最差的情况下空间复杂度为:O( n );退化为冒泡排序的情况

def quickSort(nums,left,right):
    # 递归结束条件:子数组长度被分割到只有1个元素是,此时left和right指针指向同一个元素,left和right的值相等
    if left>=right:
        return
    
    # 递归执行快排操作
    
    # 先对传递进来的数组,执行选取基准值、排出较小、较大两部分
    mid = partition(nums,left,right)
    
    # 再分别对 较小部分、较大部分,再次递归执行快排
    quickSort(nums,left,mid-1)
    quickSort(nums,mid+1,right)
    
    return nums
    

def partition(nums,left,right):
    # 设置数组第一个元素作为基准值
    pivot = nums[left]
    
    # 遍历元素,移动left、right指针,
    while left < right:
        # 此处必须先判断right部分,因为第一个left是基准值,相当于一个空位
        # 如果先判断left部分的话,left先移动,那么基准值的那个空位就永远空着了
        # 在left和right相遇之前,right指向的元素>=基准值,就是正常的,只向前移动right指针
        while left < right and nums[right]>=pivot:
            right-=1
        # 否则,nums[right]<pivot,则将right的值赋值给left
        nums[left] = nums[right]
        
        # 在left和right相遇之前,left指向的元素<=基准值,就是正常的,只向后移动left指针
        while left < right and nums[left]<=pivot:
            left+=1
        # 否则,nums[left]>pivot,则将left的值赋值给right
        nums[right] = nums[left]
        
    # 最后,当left=right时,left和right相遇,排序结束
    # 将基准值放在该位置上
    nums[left] = pivot
    # 返回基准值的索引
    return left

选择排序

将数组分为已排序部分和待排序部分
多次遍历数组的待排序部分
每次遍历,选出待排序部分的最小值,它和待排序部分的第一个元素交换位置,变成已排序部分

数组:[5,1,3,4,2]

开始时,整个数组都是待排序部分
[【5,1,3,4,2】]

待排序部分最小值为1,和待排序部分第一个元素5交换位置,1变成已排序
[1,【5,3,4,2】]

待排序部分最小值为2,和待排序部分第一个元素5交换位置,2变成已排序
[1,2,【3,4,5】]

待排序部分最小值为3,和待排序部分第一个元素3交换位置,3变成已排序
[1,2,3,【4,5】]

待排序部分最小值为4,和待排序部分第一个元素4交换位置,4变成已排序
[1,2,3,4,【5】]

此时待排序部分只有一个元素5,不需要排序了,就完成了全部排序
[1,2,3,4,5]

时间复杂度:O(n^2)
第一次内循环比较N - 1次,然后是N-2次,N-3次,……,最后一次内循环比较1次。
共比较的次数是 (N - 1) + (N - 2) + ... + 1,求等差数列和,得 (N - 1 + 1)* N / 2 = N^2 / 2。
舍去最高项系数,其时间复杂度为 O(N^2)。

def selectionSort(nums):
    n = len(nums)
    
    for i in range(n):
        # 设置一个指针,保存待排序部分最小值的索引
        min_index = i
        # 遍历待排序部分,从i开始,i之前的是已排序部分
        for j in range(i,n):
            # 更新最小值的索引
            if nums[j]<nums[min_index]:
                min_index = j
        # 将待排序部分的第一个元素,和最小值元素,交换位置
        # 相当于将最小值放到已排序部分,待排序部分向后缩
        nums[i],nums[min_index] = nums[min_index],nums[i]
    return nums

插入排序

将数组分为已排序部分和待排序部分
开始时将数组的第一个元素当作已排序部分
遍历待排序部分的每个元素,与已排序部分的元素比较,插入到已排序部分中的正确位置上

数组:[5,1,3,4,2]

开始时,第一个元素5当作已排序部分,遍历待排序部分的元素
[/5/,(1),3,4,2]

待排序的1,小于已排序的5,将1放在5前面
[/1,5/,(3),4,2]

待排序的3,小于已排序的5,将3放在5前面
[/1,3,5/,(4),2]
待排序的3,大于已排序的1,将3放在1后面,即不改变位置
[/1,3,5/,(4),2]

待排序的4,小于已排序的5,将4放在5前面
[/1,3,4,5/,(2)]
待排序的4,大于已排序的3,将4放在3后面,即不改变位置
[/1,3,4,5/,(2)]

待排序的2,小于已排序的5,将2放在5前面
[/1,3,4,2,5/]
待排序的2,小于已排序的4,将2放在4前面
[/1,3,2,4,5/]
待排序的2,小于已排序的3,将2放在3前面
[/1,2,3,4,5/]
待排序的2,大于已排序的1,将2放在1后面,即不改变位置
[/1,2,3,4,5/]

排序完毕

时间复杂度:

  • 最好情况:[1,2,3,4,5],遍历n-1次,移动0次,时间复杂度O(n)
  • 最差情况:[5,4,3,2,1],遍历n-1次,移动1+2+...+n-1次,时间复杂度O(n^2)
  • 平均:O(N^2)
def insertionSort(nums):
    n = len(nums)
    
    # 将数组第一个1元素当作已排序,之后的作为待排序
    # 遍历待排序部分
    for i in range(1,n):
        # 用一个变量保存当前待排序元素,为之后插入操作,腾出插入空间
        tmp = nums[i]
        # i之前是已排序部分,遍历已排序部分,从后向前遍历
        for j in range(i-1,0,-1):
            # 如果 当前已排序元素nums[j] 大于 待排序元素nums[i]
            # 则 已排序元素向后移,最后一个已排序元素后面就是待排序元素
            # 之前已经用变量保存了待排序元素,所以不用担心待排序元素会丢失
            if nums[j]>nums[i]:
                nums[j+1]=nums[j]
            # 如果 当前已排序元素nums[j] 小于 待排序元素nums[i]
            # 则j+1是空位,将待排序元素插入j+1位置
            else:
                nums[j+1] = nums[i]
                break
    return nums

归并排序

先将数组不断向下二分,一直分到子数组只有一个元素不可再分为止。

然后开始向上排序合并子数组,用两个指针指向两个子数组,比较指针位置元素的大小,较小的元素先添加到数组中,并且移动该指针继续比较。

数组:[8,4,5,7,1,3,6,2]

先向下递归二分数组,直到子数组不可再分
 [8,4,5,7]     [1,3,6,2]
[8,4] [5,7]   [1,3] [6,2]
[8][4][5][7]  [1][3][6][2]

开始向上递归,通过两个指针比较元素大小,排序合并每层子数组
[8][4][5][7] [1][3][6][2]
[4,8] [5,7]  [1,3] [2,6]
 [4,5,7,8]    [1,2,3,6]
    [1,2,3,4,5,6,7,8]

指针排序过程:
例如
[4,8] [5,7]

两个子序列开头设置指针
[4(i),8] [5(j),7]

比较指针元素大小,较小的放入结果中,并向前移动该指针
[ ,8(i)] [5(j),7]
res=[4]

重复上述步骤
[ ,8(i)] [,7(j)]
res=[4,5]

[ ,8(i)] [ , ]
res=[4,5,7]

[ ,] [ , ]
res=[4,5,7,8]

时间复杂度:O(nlogn)
一个长度为n的序列,它的排序时间就等于,它二分后的2个长度为n/2的子序列的排序时间,加上它们的合并时间。
假设长度为n的序列,排序时间为T(n),那么长度为n/2的子序列,排序时间就为T(n/2),两个n/2的子序列合并,需要操作n个元素,所以T(n)的合并时间为n
所以,T(n)=2*T(n/2)+n

以此类推,
长度为n/2的子序列,它的排序时间T(n/2)=2*T(n/4)+n/2
长度为n/4的子序列,它的排序时间T(n/4)=2*T(n/8)+n/4
...
将T(n/4)带入到T(n/2),再将T(n/2)带入到T(n)
得到每一层T(n)
T(n)=2*T(n/2)+n
T(n)=4*T(n/4)+2n
T(n)=8*T(n/8)+3n
...
最后一层的子序列长度为1,所以最后一层的排序时间是T(1),用T(1)表示T(n)就是
logn表示:长度为n的数组,通过二分logn次,能达到不可分
T(n)=n*T(1)+(logn)*n
因为T(1)=0,所以T(n)=nlogn

def mergeSort(nums):
    n = len(nums)
    # 递归二分数组结束条件
    # 当递归二分数组,分到不可分时,返回数组
    if n==1:
        return nums
    # 找到数组中间位置
    mid = n//2
    # 递归二分数组
    left = mergeSort(nums[:mid])
    right = mergeSort(nums[mid:])
    
    # 合并排序部分
    tmp = []
    # 当 左右数组 都不为空时,比较左右的第1个元素,较小的放入结果
    while left and right:
        if left[0]<right[0]:
            tmp.append(left.pop(0))
        else:
            tmp.append(right.pop(0))
    # 当 右数组为空时,左数组的元素从第1个开始,挨个放入结果
    while left:
        tmp.append(left.pop(0))
    # 当 左数组为空时,右数组的元素从第1个开始,挨个放入结果
    while right:
        tmp.append(right.pop(0))
    # 返回该层的合并排序结果
    return tmp

Ethan
140 声望11 粉丝

水平较低,只是记录,谨慎参阅


引用和评论

0 条评论