Python 排序算法

插入排序

原理:

  • 如果列表有多个元素,从列表的第二个元素开始,让它和前一个元素对比
  • 如果这个元素比前一个元素小,就和前一个元素互换位置,并继续和前面的元素对比。如果当前元素比前面的元素大,就不做任何操作,转而处理后面的元素。

譬如:

  1. 现有列表:[0, 2, 10, 3, 38, 6]
  2. 第一次排序,将索引 1 的 2 和前面的元素 0 对比。 2 > 0,因此不做任何操作,处理后续的数据:
  3. 处理索引 2 的数据 10,10 大于前一个元素: 2,因此不做处理,执行下一步
  4. 索引 3 的数据 3 < 10, 因此互换位置后得到:0, 2, 3, 10, 38, 6 , 3 继续和前面的 2 对比, 3>2,不做处理,处理后续数据:
  5. 索引 4 的数据 38 > 10, 不做处理,处理后续数据:
  6. 索引 5 的数据 6 < 38, 互换位置:0, 2, 3, 10, 6, 38 后继续和前面的 10 对比,6 < 10, 继续互换位置:0, 2, 3, 6, 10, 38, 6 继续和前面的 3 对比,6 > 3,因此不做处理
def insert_sort(ls):
    length = len(ls)
    for i in range(length):
        if i+1 < length:  # i+1 代表后一个元素的索引,它不能超出列表的索引范围
            while i >= 0 and ls[i + 1] < ls[i]:
                """
                i >= 0 为了防止超出索引范围,因为 i 的值会动态变化:i -= 1
                ls[i + 1] < ls[i] 是对比前后两个元素的大小,后面的元素较小的话,就互换两者的位置
                """
                ls[i], ls[i + 1] = ls[i + 1], ls[i]
                i -= 1
    return ls


ls = insert_sort([0, 2, 10, 3, 38, 6, 11])
print(ls)

希尔排序

希尔排序是更高级些的插入排序。它的做法是先初始化一个间隔 gap = len(list)//2 ,将第 i 个元素和 i+gap 个元素进行对比,如果后者较小就互换位置(这里其实就是插入排序的算法)。之后缩小间隔为 gap = gap//2 ,继续进行插入排序... 直到间隔为1(间隔最开始通常取值为列表长度的一半,以后每次递减一半,直到变成 1 为止)

它的原理就是比较相隔较远距离的两个数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。

譬如针对一个长度为 6 的列表,我们设置初始间隔为 3,因此索引为 0, 31, 42, 5 构成三个分组。针对每个分组的列表,执行插入排序。然后将间隔设置为之前间隔的一半,再次对列表进行分组排序... 直到间隔为 1 .

示例:

  1. 现有列表:[0, 29, 1, 2, 9, 3],设置初始间隔为列表长度的一半: gap = 3
  2. 对列表中,每隔 gap 个元素构成一个分组:索引 0, 31, 42, 5 三个分组
  3. 针对每个分组,进行插入排序
  4. 设置新的 gap = gap//2 ,此时 gap = 1,按照这个 gap ,重新分组并对它进行插入排序,这样就完成了最终的排序(gap 为 1)。

代码:

def shellSort(arr):
    length = len(arr)
    gap = length // 2

    while gap:
        # 下面是插入排序的代码
        for i in range(length):
            if i + gap < length:
                while i >= 0 and arr[i + gap] < arr[i]:  # 普通的插入排序每次是和后一个元素对比,这里是和后面第 gap 个元素对比
                    arr[i + gap], arr[i] = arr[i], arr[i + gap]
                    i -= gap
        gap = gap // 2
    return arr


ls = shellSort([0, 29, 1, 2, 9, 3])
print(ls)

快速排序

对于一个待排序的列表,先选一个基准值,让列表中的元素和这个基准值对比,分成两部分:大于这个值的部分、小于这个值的部分;再分别对这两个部分进行找基准值的操作,最后将这个最小部分、基准值、最大部分合并。

def quicksort(array):
    if len(array)<2:  # 长度小于2,不用排序
        return array
    else:
        pivot = array[0]  # 基准点
        small = [i for i in array[1:] if i <= pivot]  # 找到小于基准点的部分
        big = [i for i in array[1:] if i > pivot]  # 大于基准点的部分
        return quicksort(small) + [pivot] + quicksort(big)  # 合并
    
print(quicksort([10,2,6,12,39,10]))

冒泡排序

冒泡排序的逻辑很简单:对n个数,每次只比较2个数。1和2比较,谁大谁放在后面,2再和3比较,3和4比较...n-1和n比较,这样到最后,最大的数就被放在了最后一个位置。继续循环,1和2,2和3,n-2和n-1(第一次循环最大数已经放在最后一个位置了,不用再比了)...

def bubble(array):
    n = len(array)
    for i in range(n): # 第一层循环,只是为了循环次数,每循环一次,只能将一个最大数放到后面
        for j in range(n-i-1): # 第二层循环,查找最大数,一直将它挪放到后面
            if array[j] > array[j+1]:
                array[j],array[j+1] = array[j+1],array[j]
    return array

print(bubble([5,2,3,9,1,0]))

选择排序

选择排序和冒泡有些像,冒泡是每次挪动一个数,选择是每次比较一个数。

对于n个数,

loop1:将第一个数和后面全部的数比较,找到最小数,放到第一位

loop2:将第二个数和后面所有的数比较,找到最小数,放到第二位

....

所以,选择排序每次只挪动一次数,而不像冒泡每两个数比较一次就挪动一次。

def selectSort(arr):
    for i in range(len(arr)):
        min = i # 记录最小值的索引
        for j in range(i+1,len(arr)):
            if arr[j] < arr[min]:
                min = j
        if i != min: # 如果当前索引的值不是最小值
            arr[i],arr[min] = arr[min],arr[i]
    return arr
arr = selectSort([1,5,2,8,9,3])

归并排序

针对一个拥有多个元素的列表,将列表拆分成多个子列表(下文的例子中会拆分成只有一个元素的列表们),然后对这些列表进行合并。

算法步骤:

  1. 创建一个序列,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  4. 重复步骤 3 直到某一指针达到序列尾;
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。
import random


def merge_sort(ls):
    """归并排序。原理是将一个列表分割成多个部分,针对每个部分进行排序,然后再对已经排序完成的各个部分进行合并。

    :param ls: 待排序的列表
    :return:
    """

    def merge(ls1: list, ls2: list):
        """合并两个各自已经排好序的列表

        :param ls1: 已经排好序的列表
        :param ls2: 已经排好序的列表
        :return:
        """
        result = []
        # 因为两个列表已经排好序了,因此我们可以直接对比两个列表的元素,将较小的放入新列表中。
        while ls1 and ls2:
            if ls1[0] == ls2[0]:
                result.append(ls1.pop(0))
                result.append(ls2.pop(0))
            elif ls1[0] < ls2[0]:
                result.append(ls1.pop(0))
            else:
                result.append(ls2.pop(0))
        # 如果某个列表还有剩余的内容,则直接放到列表的后面(因为它已经排好序了)
        if ls1:
            result.extend(ls1)
        elif ls2:
            result.extend(ls2)
        return result

    length = len(ls)
    if length < 2:  # 返回的列表只有一个元素。只有一个元素的列表,可以看作是排好序的列表。
        return ls
    ls1, ls2 = ls[:length//2], ls[length//2:]
    return merge(merge_sort(ls1), merge_sort(ls2))


ls = [random.randint(0, 1000) for i in range(20)]
ls = merge_sort(ls)
print(ls)

堆排序

针对列表:arr = [0,9,2,1,7,3,6,5] 我们可以将其元素从上到下、从左往右排列,看作如下结构的堆:

       0
      / \
     9    2
   /  \   / \
  1   7  3   6
 /
5

堆末尾的几个节点:5, 6, 3, 7 ,它们没有子节点,因此这几个节点可以被称为 叶子节点。公式 len(arr) // 2 - 1 可以获取最后一个非叶子节点的索引,即上面例子中节点 1 的索引。

我们可以对堆按照某个格式排列,得到两种类型的堆:

大顶堆:每个节点的值,都比其子节点大(常用大顶堆来进行升序排序)
针对大顶堆,它满足:arr[i] >= arr[2i+1] and arr[i] >= arr[2i+2];  arr[2i+1] 是其左子节点,arr[2i+2] 是其右子节点
      10
     /  \
   9      8
  / \    / \
 7   6  5   4

       
小顶堆:每个节点的值,都比其子节点小(常用小顶堆来进行降序排序)
针对小顶堆,它满足:arr[i] <= arr[2i+1] and arr[i] <= arr[2i+2]
       1
     /   \
   2       3
  / \     / \
 4   5   6   7

将一个列表按照得到一个大顶堆后,便可以对其进行排序了:

  • 将堆的第一个元素和最后一个元素位置互换,这样最大值便放到列表的末尾了。
  • 重新对前 len(arr)-1 个元素进行排列,再次将其成为一个大顶堆,将堆的第一个元素和倒数第二个元素进行互换,这样第二大的值便放到了列表的倒数第二个位置。
  • 重复上述步骤
import random


def heap_sort(ls):
    """堆排序

    :param ls:
    :return:
    """
    def shift_node(ls, index, length):
        """对 ls 列表 index 索引的节点,分析它的子节点,子节点的索引要 <= length,如果子节点比父节点大,就将最大的子节点和父节点互换位置。

        :param ls: 列表
        :param index: 要处理的父节点的索引
        :return:
        """
        left = 2 * index + 1  # 左子节点的索引
        right = 2 * index + 2  # 右子节点的索引
        bigger = index

        if left <= length and ls[bigger] < ls[left]:  # 判断左节点的索引是否在合法的范围,并判断两个元素,获取较大值的索引
            bigger = left
        if right <= length and ls[bigger] < ls[right]:
            bigger = right

        if bigger != index:
            ls[bigger], ls[index] = ls[index], ls[bigger]  # 子节点和父节点互换位置
            shift_node(ls, bigger, length)  # 对子节点递归处理

    def make_max_heap(ls):
        """
        创建大顶堆(大顶堆适合做升序排序,小顶堆适合降序排序)
        :param ls: 待排序的列表
        :return:
        """
        length = len(ls)
        idx = length // 2 - 1  # 这是最后一个非叶子节点的节点索引
        for i in range(idx, -1, -1):  # 从最后一个非叶子节点开始,依次处理倒数第二、三... 的非叶子节点,如果它们的子节点比父节点大,就将子节点和父节点互换。完成一轮 for 循环后,就得到一个 大顶堆
            shift_node(ls, i, length-1)

    make_max_heap(ls)  # 生成 大顶堆 的结构:列表的第一个元素是列表的最大值
    length = len(ls)
    for j in range(length - 1, -1, -1):
        ls[0], ls[j] = ls[j], ls[0]  # 将大顶堆的第一个元素和末尾元素互换,这样就将最大值放到了末尾
        shift_node(ls, 0, j - 1)  # 重新对 ls 的前 j-1 个元素调整节点,使其也变成一个 大顶堆,然后将大顶堆的第一个元素和列表倒数第二个元素互换
    return ls

ls = [random.randint(0, 100) for i in range(20)]
print(heap_sort(ls))

posted @ 2020-06-02 15:16  wztshine  阅读(212)  评论(0编辑  收藏  举报