DataStructuresAlgorithms之01-SortingAlgorithm排序算法


@Author: liuyangly1
@Date : 2021-07-08 09:10:24
@Blog : https://blog.csdn.net/liuyang_1106
@Github: https://github.com/liuyangly1
@Email : 522927317@qq.com


数据结构和算法之排序算法-Python

1. 排序算法分类

比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。

  • 交换排序(冒泡排序,快速排序)
  • 插入排序(简单插入排序,希尔排序)
  • 选择排序(简单选择排序,堆排序)
  • 归并排序(二路归并排序,多路归并排序)

非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。

  • 计数排序
  • 桶排序和
  • 基数排序

2. 排序算法复杂度

  • 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
  • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
  • 稳定性:如果a原本在b前面,而a=b,排序之后a仍然在b的前面,则稳定,反之则不稳定。
排序方法时间复杂度(平均、最坏、最好)空间复杂度稳定性
冒泡排序 O ( n 2 ) , O ( n 2 ) , O ( n ) O(n^2), O(n^2), O(n) O(n2),O(n2),O(n) O ( 1 ) O(1) O(1)稳定
快速排序 O ( n l o g 2 n ) , O ( n 2 ) , O ( n l o g 2 n ) O(nlog_2n), O(n^2), O(nlog_2n) O(nlog2n),O(n2),O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)不稳定
插入排序 O ( n 2 ) , O ( n 2 ) , O ( n ) O(n^2), O(n^2), O(n) O(n2),O(n2),O(n) O ( 1 ) O(1) O(1)稳定
希尔排序 O ( n 1.3 ) , O ( n 2 ) , O ( n ) O(n^{1.3}), O(n^2), O(n) O(n1.3),O(n2),O(n) O ( 1 ) O(1) O(1)不稳定
选择排序 O ( n 2 ) , O ( n 2 ) , O ( n 2 ) O(n^2), O(n^2), O(n^2) O(n2),O(n2),O(n2) O ( 1 ) O(1) O(1)不稳定
堆排序 O ( n l o g 2 n ) , O ( n l o g 2 n ) , O ( n l o g 2 n ) O(nlog_2n), O(nlog_2n), O(nlog_2n) O(nlog2n),O(nlog2n),O(nlog2n) O ( 1 ) O(1) O(1)不稳定
归并排序 O ( n l o g 2 n ) , O ( n l o g 2 n ) , O ( n l o g 2 n ) O(nlog_2n), O(nlog_2n), O(nlog_2n) O(nlog2n),O(nlog2n),O(nlog2n) O ( n ) O(n) O(n)稳定
计数排序 O ( n + k ) , O ( n + k ) , O ( n + k ) O(n+k), O(n+k), O(n+k) O(n+k),O(n+k),O(n+k) O ( n + k ) O(n+k) O(n+k)稳定
桶排序 O ( n + k ) , O ( n 2 ) , O ( n ) O(n+k), O(n^2), O(n) O(n+k),O(n2),O(n) O ( n + k ) O(n+k) O(n+k)稳定
基数排序 O ( n ∗ k ) , O ( n ∗ k ) , O ( n ∗ k ) O(n*k), O(n*k), O(n*k) O(nk),O(nk),O(nk) O ( n + k ) O(n+k) O(n+k)稳定

3. 比较排序算法

3.1 冒泡排序

原理:冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

平均时间复杂度、空间复杂度和稳定性 O ( n 2 ) , O ( 1 ) , 稳 定 O(n^2), O(1), 稳定 O(n2),O(1),

实现

  1. 从i开始,依次选择前n-i个值;
  2. 两两相邻取值并比较交换;
  3. 最后选择最大的值放在n-i-1上;
def bubble_sort(nums):
    r"""冒泡排序
    Description:
    	通过两两比较,然后选择最大的数值
    Args:
        nums (list): 乱序数组。
    Returns:
    	None (None): 返回为空。
    Note: 
        原地修改,数组内存空间不变。
    """
    length = len(nums)
    # 数组为空,或数量为1, 直接返回数组。
    if(length == 0 or length == 1):
        return nums
    
    for i in range(length):
        # 依次两两相邻取值并比较交换,每次确定一个极大值。
        # length-i-1表示搜索第i+1个极大值。
        for j in range(0, length-i-1):
            if (nums[j] > nums[j+1]):
                nums[j+1], nums[j] = nums[j], nums[j+1]
     return None


# 函数测试
if __name__ == '__main__':
    nums = [19, 15, 6, 5, 4, 3, 2, 1]
    bubble_sort(nums)
    print('nums: ', nums)

3.2 快速排序

原理:快速排序,顾名思义就是一种以效率快为特色的排序算法,快速排序(Quicksort)是对冒泡排序的一种改进。由英国计算机专家:托尼·霍尔(Tony Hoare)在1960年提出。从排序数组中找出一个数,可以随机取,也可以取固定位置,一般是取第一个或最后一个,称为基准数。然后将比基准小的排在左边,比基准大的放到右边;如何放置呢,就是和基准数进行交换,交换完左边都是比基准小的,右边都是比较基准大的,这样就将一个数组分成了两个子数组,然后再按照同样的方法把子数组再分成更小的子数组,直到不能分解(子数组只有一个值)为止。以此达到整个数据变成有序序列。

时间复杂度和空间复杂度:时间复杂度:快速排序涉及到递归调用,所以该算法的时间复杂度还需要从递归算法的复杂度开始说起。

递归算法的时间复杂度公式: T [ n ] = a T [ n / b ] + f ( n ) T[n]=aT[n/b]+f(n) T[n]=aT[n/b]+f(n)

最优时间复杂度:每一次取到的元素都刚好平分整个数组。

T [ n ] = 2 T [ n / 2 ] + f ( n ) T[n] = 2T[n/2]+f(n) T[n]=2T[n/2]+f(n),T[n/2]为平分后的子数组的时间复杂度,f[n] 为平分这个数组时所花的时间。

类推: n = n / ( 2 ( m − 1 ) ) , T [ n ] = 2 m T [ 1 ] + m n n=n/(2^{(m-1)}),T[n]=2^m T[1]+mn n=n/(2(m1))T[n]=2mT[1]+mn`

推导: T [ n / ( 2 m ) ] = T [ 1 ] = = = > > n = 2 m = = = = > > m = l o g n T[n/(2^m)]=T[1] ===>> n=2^m ====>> m=logn T[n/(2m)]=T[1]===>>n=2m====>>m=logn

结果: T [ n ] = 2 ( l o g n ) T [ 1 ] + n l o g n = n T [ 1 ] + n l o g n = n + n l o g n T[n]=2^{(logn)}T[1]+nlogn=nT[1]+nlogn=n+nlogn T[n]=2(logn)T[1]+nlogn=nT[1]+nlogn=n+nlogn

  • 复杂度: O ( n l o g n ) O(nlogn) O(nlogn)

  • 最差时间复杂度:每一次取到的元素就是数组中最小/最大,冒泡排序

    • T [ n ] = n ∗ ( n − 1 ) = n 2 + n T[n] = n * (n-1) = n^2 + n T[n]=n(n1)=n2+n
    • 复杂度: O ( n 2 ) O(n^2) O(n2)
  • 平均时间复杂度: O(nlogn)

  • 空间复杂度:

    • 原地排序: O ( 1 ) O(1) O(1)
    • 最优: O ( l o g n ) O(logn) O(logn)
    • 最差: O ( n ) O(n) O(n)

实现

  • 快速排序规则:(1)取出基准数privot,以该值为中心轴,该处就有一个空位;(2)然后找右边j小于基准数的值,放在privort位置上;(3)再找左边i大于基准数的值,放在j上;(4)多次迭代,最后小于privot都放在左边,大于privot的值都放在右边;(5)最后把privot的值放在中间位置i上;
  • 递归排序,如果依次比较[0, mid]和[mid+1, n;
def partition(nums, left, right):
    r"""快速排序规则
    Description:
    	选择开始位置作为基准点,然后把小于基准点的数放在左边,大于基准点的数放右边,最后放置基准
   	点在中心,返回中心位置。
    Args:
        nums (list): 乱序数组。
        left (int): 开始位置。
        right (int): 结束位置。
    Returns:
    	i (int): 基准点中心位置。
    Note: 
        原地修改,数组内存空间不变。
    """
    pivot = nums[left]#初始化一个待比较数据
    i,j = left, right
    while(i < j):
        while(i<j and nums[j]>=pivot): #从后往前查找,直到找到一个比pivot更小的数
            j-=1
        nums[i] = nums[j] #将更小的数放入左边
        while(i<j and nums[i]<=pivot): #从前往后找,直到找到一个比pivot更大的数
            i+=1
        nums[j] = nums[i] #将更大的数放入右边
    #循环结束,i与j相等
    nums[i] = pivot #待比较数据放入最终位置 
    return i #返回待比较数据最终位置

def quick_sort(nums, left, right):
    r"""快速排序
    Description:
    	以基准点位置排序,然后递归基准点前部分和基准点后部分。
    Args:
        nums (list): 乱序数组。
        left (int): 开始位置
        right (int): 结束位置
    Returns:
    	None (None): 返回为空。
    Note: 
        原地修改,数组内存空间不变。
    """
    if left < right:
        index = partition(nums, left, right)
        quicksort(nums, left, index-1)
        quicksort(nums, index+1, right)
    return None


# 函数测试
if __name__ == '__main__':
    nums = [19, 15]
    quick_sort(nums, 0, len(nums)-1)
    print('nums: ', nums)
  • 快速选择算法:进一步判断选择的中间位置是否满足条件,不满足按条件递归;
# TOP k

def partition(nums, left, right):
    r"""快速排序规则
    Description:
    	选择开始位置作为基准点,然后把小于基准点的数放在左边,大于基准点的数放右边,最后放置基准
   	点在中心,返回中心位置。
    Args:
        nums (list): 乱序数组。
        left (int): 开始位置。
        right (int): 结束位置。
    Returns:
    	i (int): 基准点中心位置。
    Note: 
        原地修改,数组内存空间不变。
    """
    pivot = nums[left]#初始化一个待比较数据
    i,j = left, right
    while(i < j):
        while(i<j and nums[j]>=pivot): #从后往前查找,直到找到一个比pivot更小的数
            j-=1
        nums[i] = nums[j] #将更小的数放入左边
        while(i<j and nums[i]<=pivot): #从前往后找,直到找到一个比pivot更大的数
            i+=1
        nums[j] = nums[i] #将更大的数放入右边
    #循环结束,i与j相等
    nums[i] = pivot #待比较数据放入最终位置 
    return i #返回待比较数据最终位置

def topk_split(nums, k, left, right):
    r"""快速选择
    Description:
    	以基准点位置排序,然后根据选择的大小,按条件进行递归排序。
    Args:
        nums (list): 乱序数组。
        k (int): 选择的索引值。
        left (int): 开始位置
        right (int): 结束位置
    Returns:
    	None (None): 返回为空。
    Note: 
        原地修改,数组内存空间不变。
    """
    #寻找到第k个数停止递归,使得nums数组中index左边是前k个小的数,index右边是后面n-k个大的数
    if (left<right):
        index = partition(nums, left, right)
        if index==k:
            return 
       	# k在index ~ right
        elif index < k:
            topk_split(nums, k, index+1, right)
       	# k在0 ~ index
        else:
            topk_split(nums, k, left, index-1)
    return None

            
# 函数测试
if __name__ == '__main__':
    nums = [19, 15, 7, 8, 5, 2, 4, 2, 1]
    
    k = 1
    # 最小
    topk_split(nums, k, 0, len(nums)-1)
    print(nums[k-1])
    
    # 最大
    topk_split(nums, len(nums)-k, 0, len(nums)-1) #把k换成len(nums)-k
    print(nums[len(nums)-k] )

3.3 插入排序

原理:插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到 O(1)}的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

时间复杂度和空间复杂度

  • 时间复杂度:
    • 最优复杂度:O(n), 序列是升序排列,需要进行的比较操作需n−1次即可;
    • 最坏复杂度:O(n²),序列是降序排列,需要进行的比较共有$\frac{1}{2}n(n−1)$次,因为第i个元素要比较i-1次,即0+1+2+…+n-1;
    • 平均时间复杂度:O(n²);
  • 空间复杂度:O(1);

实现

  1. 现在第i个元素;
  2. 依次比较i-1个有序元素,如果大于第i个元素,则往后移动,否则停止;
  3. 插入第i个元素;
def insertion_sort(nums):
    r"""插入排序
    Description:
    	在已排序序列中从后向前扫描,找到相应位置并插入。
    Args:
        nums (list): 乱序数组。
    Returns:
    	None (None): 返回为空。
    Note: 
        原地修改,数组内存空间不变。
    """

    for i in range(len(nums)):
        # 取出新元素
        temp = nums[i]

        # 在已经排序的元素序列中从后向前扫描
        j = i - 1
        # 直到找到已排序的元素小于或者等于新元素的位置
        while (j>=0 and temp < nums[j]):
            # 该元素(已排序)大于新元素,将该元素移到下一位置
            nums[j+1] = nums[j]
            j -= 1
        # 插入新元素
        nums[j+1] = temp

    return None


# 函数测试
if __name__ == '__main__':
    nums = [1, 3, 2]
    insertion_sort(nums)
    print('nums: ', nums)

优化:折半插入排序

  • 采用二分查找有序插入的位置;
  • 时间复杂度:最好O(nlog2n),最坏O(n²),平均O(n²);
def binary_insertion_sort(nums):
    r"""折半插入排序
    Description:
        采用二分查找有序插入的位置,再向后移动元素并插入新元素。
    Args:
        nums (list): 数组,每一个元素为整数。
    Returns:
        None (None): 返回为空。
    Note:
        原地修改,数组内存空间不变。
    """	
    for i in range(len(nums)):
        # 取出新元素。
        temp = nums[i]

        # 二分查找插入位置。
        low = 0
        high = i -1
        while(low<=high):
            mid = low + (high - low) // 2
            if (nums[mid] > temp):  
                high = mid - 1
            else:
                low = mid + 1
        # 向后后移。
        for j in range(i-1, high, -1):
            nums[j+1] = nums[j]
        # 插入新元素。
        nums[high+1] = temp

    return None


# 函数测试
if __name__ == '__main__':
    nums = [1, 3, 2, 7, 5, 6]
    half_insertion_sort(nums)
    print('nums: ', nums)

3.4 希尔排序

原理:希尔排序,也叫做递减增量排序算法。先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。

时间复杂度和空间复杂度 O ( n 1.3 ) , O ( n 2 ) , O ( n ) O(n^{1.3}),O(n^2), O(n) O(n1.3),O(n2),O(n)

实现

  1. 最外层循环不断地用数组长度自除以2得到增量increment,直到0为止(不包含0),比如N = 64,增量就是[32, 16, 8, 4, 2];
  2. 内层循环(两层)实现了插入排序,为了防止数组下标溢出,i从increment开始自增,直到N为止( 不包含N),A[i],A[i - increment], A[i - 2 * increment]…A[i - k * increment]进行排序;
def shell_sort(nums): 
    r"""希尔排序
    Description:
        先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的
    记录"基本有序"时,再对全体记录进行依次直接插入排序。
    Args:
        nums (list): 乱序数组。
    Returns:
        None (None): 返回为空。
    Note:
        原地修改,数组内存空间不变。
    """
    # 定义递减增量为长度一半。
    gap = int(len(nums)/2)
  
    while gap > 0: 
        
        # 按划分子序列插入排序。
        for i in range(gap, len(nums)): 
  
            temp = nums[i] 

            # 按递减增量gap,直到找到已排序的元素小于或者等于新元素的位置。
            j = i-gap
            while  j >= 0 and nums[j] >temp: 
                # 该元素(已排序)大于新元素,将该元素移到下一位置
                nums[j+gap] = nums[j] 

                j -= gap 
            # 插入新元素
            nums[j+gap] = temp 

        gap = int(gap/2)

    return None


# 函数测试
if __name__ == '__main__':
    nums = [1, 3, 2, 7, 5, 6]
    shell_sort(nums)
    print('nums: ', nums)

3.5 简单选择排序

原理:从无序数列中选取最小的元素和无序数列中的第一个元素交换,每轮都可以确定最小元素的位置。

时间复杂度和空间复杂度 O ( n 2 ) , O ( n 2 ) , O ( n 2 ) O(n^2),O(n^2), O(n^2) O(n2),O(n2),O(n2)

实现

  1. 循环取无序序列中的第i个元素;
  2. 循环和后面的元素[i+1, n]比较并交换;
def simple_select_sort(nums):
    r"""简单选择算法
    Description:
        依次选取最小的元素和数组中的第一个元素交换
    Args:
        nums (list): 乱序数组。
    Returns:
        None (None): 返回为空。
    Note: 
        原地修改,数组内存空间不变。
    """

    for i in range(len(nums)):
        for j in range(i+1, len(nums)):
            if nums[j] < nums[i]:
                nums[j], nums[i] = nums[i], nums[j]
	return None


if __name__ == '__main__':
    nums = [5, 2, 8, 4, 7, 4, 3, 9, 2, 0, 1, 16]
    simple_select_sort(nums)

    print('nums: ', nums)

3.6 堆排序

原理:堆的本质是一个完全二叉树,每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

  • 堆分为大根(顶)堆与小根(顶)堆,升序排序采用大根堆,降序排序采用小根堆。

  • 堆是完全二叉树,利用层序遍历(遍历方式还有前中后)映射到数组后,假设树或子树的根节点为arr[root],则其对应的子节点分别为arr[root*2+1],arr[root*2+2]。

时间复杂度和空间复杂度 O ( n l o g n ) , O ( n l o g n ) , O ( n l o g n ) O(nlogn),O(nlogn), O(nlogn) O(nlogn),O(nlogn),O(nlogn)

实现

  1. 构建二叉树;
  2. 转换成一个大顶堆,循环从后往前(倒序),每个根节点与左右节点比较,小于根节点交换,递归对左右节点作为根节点,构建大顶堆;
  3. 依次交换头节点和指定的n-i尾节点(倒序),再构建大顶堆;
def heapify(nums, n, i): 
    r"""构建大顶堆
    Description:
        倒序,依次比较根节点、左节点和右节点并交换,再递归得到最大的根节点。
    Args:
        nums (list): 乱序数组。
        n (int): 数组长度。
        i (int): 根节点位置。
    Returns:
        None (None): 返回为空。
    Note: 
        原地修改,数组内存空间不变。
    """
    largest = i  
    l = 2 * i + 1     # left = 2*i + 1 
    r = 2 * i + 2     # right = 2*i + 2 
  
    if l < n and nums[i] < nums[l]: 
        largest = l 
  
    if r < n and nums[largest] < nums[r]: 
        largest = r 
  
    if largest != i: 
        nums[i],nums[largest] = nums[largest],nums[i]  # 交换
        # 此时largest位置的数字(也就是最开始输入那个lis[i])处于待定状态,需要在它所有根部中确定其位置
        heapify(nums, n, largest) 
  
def heapSort(nums): 
    r"""堆排序
    Description:
    	在大顶堆的基础上,交换首尾节点,然后再次递归构建大顶堆。
    Args:
        nums (list): 乱序数组。
    Returns:
    	None (None): 返回为空。
    Note: 
        原地修改,数组内存空间不变。
    """
    n = len(nums) 
  
    # Build a maxheap. 
    for i in range(n, -1, -1): 
        # 先把堆调整好小根堆的状态,在全堆中逐个调整每个数字的位置,调整的方法是在它所有根部中确定其位置
        heapify(nums, n, i) 
  
    # 一个个交换元素
    for i in range(n-1, 0, -1): 
        arr[i], nums[0] = nums[0], arr[i]   # 交换
        # 把新上来的0号安排到合适的位置上去,其中i指的是要调整的堆的范围
        heapify(nums, i, 0)
    return None
        
if __name__ == "__main__":
    nums = [5, 2, 8, 4, 7, 4, 3, 9, 2, 0, 1, 16]
    heap_sort(nums)

    print(nums)

3.7 归并排序

原理:归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

时间复杂度和空间复杂度

  • 最优时间复杂度: O ( n l o n g n ) O(nlongn) O(nlongn)
  • 最坏时间复杂度 : O ( n l o n g n ) ; O(nlongn); O(nlongn)
  • 稳定性:稳定;

实现

  1. 把长度为n的输入序列分成两个长度为n/2的子序列;
  2. 对这两个子序列分别采用归并排序;
  3. 将两个排序好的子序列合并成一个最终的排序序列;
def merge_sort(nums):
    r"""归并算法
    Description:
        把数组序列等分,递归两个子序列,然后在合并两个有序数组。
    Args:
        nums (list): 数组,每一个元素为整数。
    Returns:
        nums (list): 排序后数组
    Note: 
        数组内存空间改变。
    """
    length = len(nums)
    # 剩一个或没有直接返回,不用排序
    if length < 2:
        return nums
    # 拆分,子序列递归调用排序
    mid = length // 2
    left = merge_sort(nums[0 : mid])
    right = merge_sort(nums[mid : length])
    # 合并
    return merge(left, right)

def merge(left, right):
    r"""合并两个有序数组
    Description:
        利用双指针,比较合并两个有序数组。
    Args:
        left (list): 有序数组1。
        right (list): 有序数组2。
    Returns:
        nums (list): 排序后数组。
    Note: 
        数组内存空间改变。
    """
    i = j = 0
    result = []
    while i < len(left) and j < len(right):
        if left[i] < right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    if i == len(left):
        for num in right[j:]:
            result.append(num)
    else:
        for num in left[i:]:
            result.append(num)
    return result


if __name__ == "__main__":
    nums = [5, 2, 8, 4, 7, 4, 3, 9, 2, 0, 1, 16]
    nums = merge_sort(nums)

    print(nums)

4 非比较排序算法

4.1 计数排序

原理:计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

时间复杂度和空间复杂度

  • 时间复杂度O(n);
  • 空间复杂度O(n);

实现

​ 假设n个输入元素中每一个都是介于0到k之间的整数,此处k为某个整数。当 k = O ( n ) k=O(n) k=O(n)时,计数排序的运行时间为 Θ ( n ) Θ(n) Θ(n)。对每一个数的元素x,确定出小于x的元素个数。有了这一信息就可以把x直接放到最终输出数组中的位置上。

​ 计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

def counting_sort(nums):
    r"""计数排序
    Description:
        基于hashmap统计的思想,统计每个数的大小,然后依次排列。
    Args:
        nums (list): 乱序数组。
    Returns:
        None (None): 返回为空。
    Note: 
        数组内存空间改变。
    """

    bucketLen = max(nums)+1
    bucket = [0]*bucketLen
    sortedIndex =0
    numsLen = len(nums)
    # 统计数
    for i in range(numsLen):
        if not bucket[nums[i]]:
            bucket[nums[i]]=0
        bucket[nums[i]] += 1

    for j in range(bucketLen):
        while bucket[j] > 0:
            nums[sortedIndex] = j
            sortedIndex += 1
            bucket[j] -= 1

    return None


if __name__ == '__main__':
    nums = [2, 5, 3, 0, 2, 3, 0, 7]
    counting_sort(nums)
    print(nums)

4.2 桶排序

原理:桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

时间复杂度和空间复杂度:桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

实现

  1. 桶初始化:设置桶大小,和桶数量;
  2. 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  3. 对每个不是空的桶进行排序(插入排序);
  4. 从不是空的桶里把排好序的数据拼接起来;
def bucket_sort(nums, bucket_size=5):
    r"""桶排序
    Description:
        计数排序升级版本,先分段,把数划分到每个段去,然后对每个段进行插入排序,最后把每个段拼接。
    Args:
        nums (list): 乱序数组。
    Returns:
        None (None): 返回为空。
    Note: 
        数组内存空间改变。
    """
    # 桶初始化:桶数量和桶大小
    minValue = min(nums)
    maxValue = max(nums)
    bucket_num = math.floor((maxValue - minValue)/ bucket_size) + 1
    bucket = [[] for _ in range(bucket_num)] 

    # 映射分配数据到桶
    for num in nums:
        bucket[math.floor((num - minValue)/ bucket_size)].append(num)
    
    index = 0
    for i in range(bucket_num):
        # 桶插入排序
        for j in range(len(bucket[i])):
            num = bucket[i][j]
            k = j - 1
            while k >= 0 and bucket[i][k] > num:
                bucket[i][k+1] = bucket[i][k]
                k -= 1
            bucket[i][k+1] = num

        # 桶合并
        for j in range(len(bucket[i])):
            nums[index] = bucket[i][j]
            index += 1

    return None
    

if __name__ == '__main__':
    nums = [i for i in range(10)]
    nums.append(0)
    random.shuffle(nums)
    # print(nums)
    bucket_sort(nums)
    print(nums)

4.3 基数排序

原理:基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

时间复杂度和空间复杂度 O ( n ∗ k ) , O ( n ∗ k ) , O ( n ∗ k ) O(n*k), O(n*k), O(n*k) O(nk),O(nk),O(nk)

实现

  1. 取得数组中的最大数,并取得位数;

  2. nums为原始数组,从最低位开始取每个位组成radix数组;

  3. 对radix进行计数排序(利用计数排序适用于小范围数的特点);

  4. 先排元素的最后一位,再排倒数第二位,直到所有位数都排完。这里并不能先排第一位,那样最后依然是无序;

def radix_sort(nums):    
	r"""基数排序
    Description:
        基于位数来说,个位、十位……等,先按位数进行划分,然后再对划分进行排序,最后按位数大小组合。
    Args:
        nums (list): 乱序数组。
    Returns:
        None (None): 返回为空。
    Note: 
        数组内存空间改变。
    """
	i = 0 # 记录当前正在排拿一位,最低位为1
	j = len(str(max(nums)))  # 记录最大值的位数
	while i < j:     
		bucket_list =[[]
		for _ in range(10)] #初始化桶数组   
			for k in nums:
				bucket_list[int(k / (10**i)) % 10].append(k) # 找到位置放入桶数组       
		# print(bucket_list)
		nums.clear()
		for bucket in bucket_list:   # 放回原序列   
			for num in bucket:
				nums.append(num)  
		i += 1
	return nums


if __name__ == '__main__':    
	nums = [i for i in range(10)]
	nums.append(20)
	nums.append(300)
	random.shuffle(nums)
	# print(nums)
	radix_sort(nums)
	print(nums)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值