基于python的排序算法实现(直接和折半插入排序、希尔排序、冒泡排序、快速排序、简单选择排序、堆排序、归并排序)

最近因为找实习的需要,对常见的几大类排序算法做了基于python的实现,特地拿出来和大家分享,也欢迎看到的朋友如果发现里面有bug的话积极提出,共同进步


首先给一张各种常见排序算法的性能比较的表,如下图:

在这里插入图片描述

1. 插入排序

核心思想:每次将一个待排序的记录,按其关键字的大小插入到前面已经排好序的子序列中,直到全部记录插入完毕。每次插入,都能得到一个局部有序的子序列。

不同插入排序方法的区别:从局部有序序列中选择待排序记录的插入位置的方法不同

  1. 直接插入排序:线性查找插入位置
  2. 折半插入排序:折半查找的方式查找插入位置,显然这种排序方式只能用在能用下标对元素进行索引的数据结构中,如数组

希尔排序的思想和以上两种插入排序算法都不一样:希尔排序先取一个小余 n n n的步长 d 1 d_{1} d1,将表中的全部记录分为 d 1 d_{1} d1组,分别对每个组进行直接插入排序;然后取第二个步长 d 2 < d 1 d_{2}<d_{1} d2<d1,重复上述过程对每个组中的数据进行直接插入排序,直到 d t = 1 d_{t}=1 dt=1,即所有记录都放在一个组中。希尔排序的好处在于,当步长逐渐缩小,数据的有序性也逐渐增加,而直接插入排序的运行时间和数据有序性是直接相关的,数据基本有序时,数据的移动次数会变少很多,可以较好的完成排序过程。

希尔提出的方法是:
d 1 = n / 2 d_{1}=n/2 d1=n/2
d i + 1 = ⌊ d i / 2 ⌋ d_{i+1}=\lfloor d_{i}/2 \rfloor di+1=di/2

1.1 直接插入排序

空间复杂度为 O ( 1 ) , O(1), O(1)时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)

def straight_insertion_sort(nums, incremental=True):

    nums.insert(0, 0)  # 0位置不存元素,用作哨兵
    nums_len = len(nums)

    if incremental == True:

        # 升序排列
        for i in range(2, nums_len):

            if nums[i] < nums[i-1]: # 如果需要插入
            
                nums[0] = nums[i]  # 在0位置插入“哨兵”
                j = i-1
                while nums[0] < nums[j]: # 找到插入的位置,并在比较的过程中每次都将元素后移,当当前元素小于等于待插入元素跳出
                    nums[j+1] = nums[j] # 每次将当前元素后移
                    j -= 1
                nums[j+1] = nums[0]  # 将待插入元素放入

        print("排序后的升序序列为:", nums[1:])

    else:

        # 降序排列
        for i in range(2, nums_len):

            if nums[i] > nums[i-1]: # 如果需要插入
            
                nums[0] = nums[i]  # 在0位置插入“哨兵”
                j = i-1
                while nums[0] > nums[j]: # 找到插入的位置,并在比较的过程中每次都将元素后移,当当前元素大于等于待插入元素跳出。因为有哨兵的存在,肯定不会越界
                    nums[j+1] = nums[j] # 每次将当前元素后移
                    j -= 1
                nums[j+1] = nums[0]  # 将待插入元素放入

        print("排序后的降序序列为:", nums[1:])

if __name__ == "__main__":

    nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]
    
    straight_insertion_sort(nums, True)
    

1.2 折半插入排序

空间复杂度为 O ( 1 ) O(1) O(1),时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)



def binary_insertion_sort(nums, incremental=True):

    nums.insert(0, 0)  # 0位置不存元素,用作哨兵
    nums_len = len(nums)

    if incremental == True:

        # 升序排列
        for i in range(2, nums_len):

            if nums[i] < nums[i-1]: # 如果需要插入
            
                nums[0] = nums[i]  # 在0位置插入“哨兵”

                # 在局部有序序列中折半查找寻找插入位置
                low = 1
                high = i - 1
                while low <= high:
                    mid = (low + high) // 2
                    if nums[mid] <= nums[0]:  # 当mid值小于等于待插入元素,low=mid+1
                        # 这里尤为要注意的就是相等时,应该是low = mid + 1,可以保证循环的结束条件是low = high
                        low = mid + 1
                    else:
                        high = mid - 1
                
                for j in range(i-1, high, -1):
                    nums[j+1] = nums[j]

                nums[high+1] = nums[0]
                

                
        print("排序后的升序序列为:", nums[1:])

    else:

        # 降序排列
        for i in range(2, nums_len):

            if nums[i] > nums[i-1]: # 如果需要插入
            
                nums[0] = nums[i]  # 在0位置插入“哨兵”

                # 在局部有序序列中折半查找寻找插入位置
                low = 1
                high = i - 1
                while low <= high:
                    """"
                        这里注意low=high的时候还是得判断一下
                    
                    """
                    mid = (low + high) // 2
                    if nums[mid] >= nums[0]:  # 当mid值大于等于待插入元素,low=mid+1
                        # 这里尤为要注意的就是相等时,应该是low = mid + 1,可以保证循环的结束条件是low = high
                        low = mid + 1
                    else:
                        high = mid - 1
                
                for j in range(i-1, high, -1):
                    nums[j+1] = nums[j]
                nums[high+1] = nums[0]

            # print(nums[1:])
                

                
        print("排序后的降序序列为:", nums[1:])


if __name__ == "__main__":

    nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]
    
    binary_insertion_sort(nums, True)
    # binary_insertion_sort(nums, False)

1.3 希尔排序

空间复杂度为 O ( 1 ) O(1) O(1),最坏情况下的时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)

希尔提出的方法是:
d 1 = n / 2 d_{1}=n/2 d1=n/2
d i + 1 = ⌊ d i / 2 ⌋ d_{i+1}=\lfloor d_{i}/2 \rfloor di+1=di/2



def shell_sort(nums, incremental=True):

    nums.insert(0, 0)  # 0位置不存元素也不用做哨兵,只是用来暂存元素的,因为希尔排序中是分组排序的,不是每次都能遍历到0元素位置
    d_i = (len(nums)-1) // 2
    
    if incremental == True: 
  
        while d_i >= 1:  # 对于每一个步长

            for i in range(1, d_i+1): # 确定每一组的起始元素,每两个元素之间相差d_i
                # 每一个组中的元素下标为:i, i+d_i, i+2*d_i, i+3*d_i, ......

                for j in range(i+d_i, len(nums), d_i):  # 对每一组做直接选择排序

                    if nums[j] < nums[j-d_i]:
                        
                        nums[0] = nums[j]  # 暂存当前元素

                        k = j - d_i
                        #print("k={},len(nums)={},nums[k]={}, nums[0]={}".format(k, len(nums), nums[k], nums[0]))
                        while k > 0 and nums[k] > nums[0]:  # 这里因为没有哨兵,所以要注意防止越界
                            nums[k+d_i] = nums[k]
                            k -= d_i
                        nums[k+d_i] = nums[0]

            d_i = d_i // 2

            print("排序后的升序序列为:", nums[1:])

    else:

        while d_i >= 1:  # 对于每一个步长

            for i in range(1, d_i+1): # 确定每一组的起始元素,每两个元素之间相差d_i
                # 每一个组中的元素下标为:i, i+d_i, i+2*d_i, i+3*d_i, ......

                for j in range(i+d_i, len(nums), d_i):  # 对每一组做直接选择排序

                    if nums[j] > nums[j-d_i]:
                        
                        nums[0] = nums[j]  # 暂存当前元素

                        k = j - d_i
                        while k > 0 and nums[k] < nums[0]:
                            nums[k+d_i] = nums[k]
                            k -= d_i
                        nums[k+d_i] = nums[0]

            d_i = d_i // 2
        
        print("排序后的降序序列为:", nums[1:])
        
                

if __name__ == "__main__":

    nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]

    # shell_sort(nums, True)
    shell_sort(nums, False)
    


2. 交换排序

核心思想:根据序列中两个关键字的比较结果来对换关键字在序列中的位置,每轮交换结束后,都能确定一个元素的最终位置。

交换思想的排序算法主要包括两种

  1. 冒泡排序:假设待排序表长为 n n n,从后往前(或从前往后)两两比较相邻元素的值,若为逆序,则交换它们,直到序列比较完,我们成为一趟冒泡。一趟冒泡能确定一个元素的最终位置,所以一个序列排序需要 n n n趟冒泡。
  2. 快速排序:每次根据pivot值对序列进行二分,左边的值小于pivot,右边的值大于pivot,每趟交换结束后,pivot的最终位置是确定的。

2.1 冒泡排序

空间复杂度为 O ( 1 ) O(1) O(1)

最好的时间复杂度为 O ( n ) O(n) O(n),最差的时间复杂度为 O ( n 2 ) O(n^{2}) O(n2),平均时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)


def bubble_sort(nums, incremental=True):

    if incremental == True:

        for i in range(len(nums)):  # n趟冒泡

            flag = 0  # 判断本轮冒泡是否发生了交换,如果没有发生交换,说明整个序列有序,可以直接跳出,冒泡排序是不需要设置哨兵的,因为肯定不会越界
            for j in range(len(nums)-1, i, -1):
                if nums[j] < nums[j-1]:
                    tmp = nums[j]
                    nums[j] = nums[j-1]
                    nums[j-1] = tmp
                    flag = 1
            if flag == 0:
                break

        print("排序后的升序序列为:", nums)
    
    else:
        
        for i in range(len(nums)):  # n趟冒泡

            flag = 0  # 判断本轮冒泡是否发生了交换,如果没有发生交换,说明整个序列有序,可以直接跳出,冒泡排序是不需要设置哨兵的,因为肯定不会越界
            for j in range(len(nums)-1, i, -1):
                if nums[j] > nums[j-1]:
                    tmp = nums[j]
                    nums[j] = nums[j-1]
                    nums[j-1] = tmp
                    flag = 1
            if flag == 0:
                break

        print("排序后的降序序列为:", nums)


if __name__ == "__main__":

    nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]

    # bubble_sort(nums, True)
    bubble_sort(nums, False)



2.2 快速排序

最好情况下栈的深度为 O ( n ) O(n) O(n),最差情况下栈的深度为 O ( l o g 2 n ) O(log_{2}n) O(log2n),平均情况下的时间复杂度为 O ( l o g 2 n ) O(log_{2}n) O(log2n)

最坏情况下每次取的pivot为最右边或者最左边的值,时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)。最好的情况下,每次pivot去的都是中间值,时间复杂度为 O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n)


# flag = 0

def partition(nums, low, high):

    if low > high:  # 如果递归结束条件写在这,递归结束条件一定是low > high
        return

    tmp_low = low
    tmp_high = high
    
    #print("1---{}---{}".format(low, high))
        
    pivot = nums[low]

    while low < high:

        # 先从右边找出第一个小于pivot的值
        while low < high and nums[high] >= pivot:
            high -= 1
        
        nums[low] = nums[high] # 将high位置的值赋给low位置的值

        # 再从左边找到第一个大于pivot的值
        while low < high and nums[low] < pivot:
            low += 1

        nums[high] = nums[low] # 将low位置的值赋给high位置的值

    nums[low] = pivot

    # 确定了pivot的位置后,分别对左右两部分进行相同操作,每次都能确定一个pivot的最终位置
    # print("tmp_low={}, tmp_high={}, low={}".format(tmp_low, tmp_high, low))
    partition(nums, tmp_low, low-1)
    partition(nums, low+1, tmp_high)


def quick_sort(nums, incremental=True):

    if incremental == True:
        #print("2---", len(nums))
        partition(nums, 0, len(nums)-1)

        print(nums)


if __name__ == "__main__":

    nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]

    quick_sort(nums)




3. 选择排序

核心思想:每一趟(比如第 i i i趟)在后面 n − i + 1 n-i+1 ni+1个待排序的元素中选择出最小的元素,作为有序子序列的第 i i i个元素,直到第 n − 1 n-1 n1趟做完,待排序元素只剩下一个,就不再选了。

选择排序主要两种:

  1. 简单选择排序:假设待排序表为 L [ 1 , 2 , ⋯   , n ] L[1, 2, \cdots, n] L[1,2,,n],第 i i i趟排序从 L [ i , i + 1 , ⋯   , n ] L[i, i+1, \cdots, n] L[i,i+1,,n]中选择出最小的元素与 L [ i ] L[i] L[i]交换, L [ i ] L[i] L[i]中的元素最终位置是确定的。

  2. 堆排序:在排序过程中,将 L [ 1 , 2 , ⋯   , n ] L[1, 2, \cdots, n] L[1,2,,n]看做是一棵完全二叉树的顺序存储结构,利用完全二叉树的顺序存储结构中的双亲节点和孩子节点的下标关系,从中选择出关键字最大(或最小)的元素

3.1 简单选择排序

空间复杂度为 O ( 1 ) O(1) O(1)

最好和最坏的时间复杂度都是 O ( n 2 ) O(n^{2}) O(n2)



def simple_selection_sort(nums, incremental=True):

    if incremental == True:

        for i in range(len(nums)): # 每次确定一个位置的值
            
            min_index = i
            for j in range(i+1, len(nums)): # 从剩余的元素中得到其中的最小值
                if nums[j] < nums[min_index]:
                    min_index = j
            if min_index != i: 
                tmp = nums[i]
                nums[i] = nums[min_index]
                nums[min_index] = tmp

    else:

        for i in range(len(nums)):
            
            max_index = i
            for j in range(i+1, len(nums)):
                if nums[j] > nums[max_index]:
                    max_index = j
            if max_index != i:
                tmp = nums[i]
                nums[i] = nums[max_index]
                nums[max_index] = tmp


    print(nums) 





if __name__ == "__main__":

    nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]

    #simple_selection_sort(nums)
    simple_selection_sort(nums, False)


3.2 堆排序

空间复杂度为 O ( 1 ) O(1) O(1)

建堆的时间复杂度为 O ( n ) O(n) O(n),每次向下调整的时间复杂度为 O ( h ) O(h) O(h),最好、最坏和平均时间复杂度都为 O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n)

基于完全二叉树的顺序存储结构的大根堆和小根堆:

  1. 大根堆:双亲节点的值大于孩子节点的值,孩子节点的值的大小不限定
  2. 小根堆:双亲节点的值小于孩子节点的值,孩子节点的值的大小不限定

以小根堆为例:

堆的构建和维护主要涉及向上调整和向下调整两个过程

小根堆的初始堆构建过程:

  1. 从最后一个叶子节点的父节点开始,逐个节点向上调整,对每个节点:

    1.1 判断当前节点与孩子节点的大小,如果当前节点值最小,则不做向下调整,如果当前节点值不是最小,则将当前节点值和孩子节点中的最小值调换

    1.2 从上一步调换过后的孩子节点开始向下调整,返回1.1步递归直到结束

在构建了小根堆之后,我们每次取出堆顶元素,即为当前最小值,此时涉及到堆的删除操作,删除堆顶元素后,先将堆的最后一个元素填充到堆顶,再从堆顶开始做向下调整重新构建小根堆。注意此时不需要做向上调整,只需要做向下调整。

小根堆的插入操作:先将元素插入到堆的末端,再从堆的末端开始做向上调整。注意此时因为是插入到已经构建好的小根堆中,所以不需要向下调整,只需要做向上调整。

因为每次都会弹出堆顶元素,再将最末位的元素弹出填充到栈顶,所以我们可以将每次弹出的栈顶元素放到堆的末尾,以填充空出来的栈末尾的位置。在这种思路下,得到升序序列需要借助大根堆,得到降序序列需要借助小根堆

堆排序在取待排序序列的前n大或者前n小的元素时是非常有效的,尤其是在数据量较大(可能内存无法同时装下所有元素时)时,只需要维持一个大小为n的堆即可。

  1. 如果要得到前n大的元素,则维护一个小根堆,每次删除堆顶元素,然后加入一个新元素,直到所有元素都取完
  2. 如果要得到前n小的元素,则维护一个大根堆,每次删除堆顶元素,然后加入一个新元素,直到所有元素都取完

def adjust_up(nums, index):  # 小根堆的向上调整过程。向上调整过程在插入时用

    while index >= 1:

        if index // 2 >= 1:
            if nums[index//2] > nums[index]:
                tmp = nums[index]
                nums[index] = nums[index//2]
                nums[index//2] = tmp
            else:  # 如果父亲节点更小,则不需要再继续向上调整
                break

        index = index // 2


def adjust_down(nums, index, n):
    
    while True:  # 对每一个节点做向下调整
        min_index = index
        if index * 2 <= n: # 当前节点的左孩子节点存在,判断是否比父亲节点小
            if nums[index*2] < nums[min_index]:
                min_index = index * 2
        if index * 2 + 1 <= n:  # 当前节点的右孩子存在,判断是否比父亲节点小
            if nums[index*2+1] < nums[min_index]:
                min_index = index * 2 + 1
        
        if min_index != index: # 说明孩子节点中有比父亲节点小的,则交换两个的值,再继续从最小的孩子节点向下调整
            tmp = nums[min_index]
            nums[min_index] = nums[index]
            nums[index] = tmp
            index = min_index
        else:  # 如果两个孩子节点都没有比当前节点小的,则本次向下调整结束
            break


def build_min_heap(nums, last_leaf):

    node_index = last_leaf // 2
    while node_index >= 1:  # 从最后一个叶节点的父亲节点开始遍历这个节点之前的所有点

        adjust_down(nums, node_index, last_leaf)
        
        node_index -= 1  # 向上对另一个元素进行调整

    print("构建的初始小根堆为:", nums[1:])


def heap_sort(nums, incremental=True):

    # 注意list的索引是从0开始,为了更好的利用下标进行索引,在0元素位置插入一个元素,使得真正的待排序序列从1开始
    # 此时,当父节点下标为i时,它的两个孩子节点的下标为2*i和2*i+1
    # 当孩子节点的下标为i时,其父节点的下标为i//2
    nums.insert(0, 0)

    last_leaf = len(nums) - 1

    build_min_heap(nums, last_leaf)  # 构建初始小根堆

    # 每次弹出堆顶元素,然后用最后一个元素填充到堆顶,将弹出的堆顶元素存放到末尾元素位置,从堆顶做一次向下调整
    for _ in range(len(nums)-2):
        
        tmp = nums[1]
        nums[1] = nums[last_leaf]
        nums[last_leaf] = tmp 

        last_leaf -= 1
        adjust_down(nums, 1, last_leaf)
    
    print(nums[1:])
    


if __name__ == "__main__":

    nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]

    heap_sort(nums, False)


4. 归并排序

核心思想:"归并"的含义是将两个或两个以上的有序表合并成一个新的有序表。假定待排序表有 n n n个记录,则可以看做有 n n n个长度为1的子表,然后两两进行归并,得到 ⌈ n / 2 ⌉ \lceil n/2 \rceil n/2个长度为2的有序子表;继续归并,直到最后得到长度为 n n n的有序表。

空间复杂度为 O ( n ) O(n) O(n),因为只需要开一个全局的和待排序序列等长的辅助数组就行

时间复杂度为 O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n)




def merge(nums, low, mid, high):

    # 先把两个有序子序列放入辅助列表中
    tmp_1 = nums[low:mid+1]
    tmp_2 = nums[mid+1:high+1]

    i_1 = 0
    i_2 = 0
    i = low
    while i_1 < len(tmp_1) and i_2 < len(tmp_2):
        if tmp_1[i_1] < tmp_2[i_2]:
            nums[i] = tmp_1[i_1]
            i_1 +=1
            i += 1
        else:
            nums[i] = tmp_2[i_2]
            i_2 +=1
            i += 1            

    # 如果还有一个序列没有被加入
    if i_1 < len(tmp_1):
        nums[i: high+1] = tmp_1[i_1: ]
    elif i_2 < len(tmp_2):
        nums[i: high+1] = tmp_2[i_2: ]




def merge_sort(nums, low, high):

    if low < high:

        mid = (low + high) // 2

        merge_sort(nums, low, mid) # 对左侧子序列进行递归排序
        merge_sort(nums, mid+1, high)  # 对右侧子序列进行递归排序

        # 左右两个子序列都完成排序后,将这两个子序列合并
        merge(nums, low, mid, high)
    else:
        return



if __name__ == "__main__":

    nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]

    merge_sort(nums, 0, len(nums)-1)

    print(nums)


  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值