数据结构(Python语言描述)- 排序算法

目录

1.基本排序算法

1.1 选择排序

1.2 冒泡排序

1.3 插入排序

2. 更快排序算法

2.1 快速排序

2.2 归并排序(合并排序)


1.基本排序算法

计算机科学家设计了很多巧妙的策略对列表的项进行排序。每个Python排序函数都是在整数的一个列表上进行操作的,并且都会使用一个swap函数来交换列表中的两项的位置。

def swap(lyst, i , j):
    """Exchange the items at positions i and j."""
    temp = lyst[i]
    lyst[i] = lyst[j]
    lyst[j] = temp

1.1 选择排序

排序算法可能最简单的策略就是搜索整个列表,找到最小项的位置。如果该位置不是列表的第1个位置,算法就会交换这两个位置的项。然后,算法回到第2个位置并且重复这个过程,如果必要的话,将最小项和第2个位置的项交换。当算法到达整个过程的最后一个位置,列表就是排序好了的。这个算法就叫做选择排序(Selection Sort)。因为每一次经过主循环的时候,都会选择一个移动的项。图1展示了对于5个项的一个列表进行选择排序,在每一次搜索和交换之后的状态。在每一轮都只是交换两项,这两项的后面有星号表示。

图1 记录选择排序中的数据
未排序的列表 第1轮之后 第2轮之后 第3轮之后 第4轮之后
5 1* 1* 1* 1*
3 3 2* 2* 2*
1 5 5 3* 3*
2 2 3 5 4*
4 4 4 4 5*

Python实现:

def selectionSort(lyst):
    i = 0
    while i < len(lyst) - 1: # 做n-1次搜索
        minIndex = i
        j = i + 1
        while j < len(lyst):
            if lyst[j] < lyst[minIndex]:
                minIndex = j
            j += 1
        if minIndex != i:
            swap(lyst, i, minIndex)
        i += 1
    return lyst

选择排序在各种情况下的复杂度为O(n^{2})

1.2 冒泡排序

冒泡排序(Bubble Sort)策略是从列表的开头处开始,并且比较一对数据项,直到移动到列表的末尾。每当成对的两项之间的顺序不正确的时候,算法就交换其位置。这个过程的效果就是将最大的项以冒泡的方式排到列表的末尾。然后,算法从列表开头到倒数第2个列表项重复这一过程,以此类推,直到该算法从列表的最后一项开始执行。此时,列表是已经排序好的。

图2展示了对5个项的一个列表进行冒泡排序的过程。这个过程把嵌套的循环执行了4次,将最大的项冒泡到了列表的末尾。再一次,只有交换的项用星号标记了出来。

图2 记录冒泡排序过程
未排序的列表 第1轮之后 第2轮之后 第3轮之后 第4轮之后
5 4* 4 4 4
4 5* 2* 2 2
2 2 5* 1* 1
1 1 1 5* 3*
3 3 3 3 5*

Python实现:

def bubbleSort(lyst):
    n = len(lyst)
    while n > 1:
        i = 1
        while i < n:
            if lyst[i] < lyst[i - 1]:
                swap(lyst, i , i-1)
            i += 1
        n -= 1
    return lyst

和选择排序一样,冒泡排序也有一个嵌套的循环。冒泡排序的复杂度是O(n^{2})。和选择排序一样,如果列表是已经排序好的,冒泡排序不会执行任何的交换。然而,在最坏情况下,冒泡排序交换工作超过线性方式。

可以对冒泡排序进行一个小的调整,将其在最好情况下的性能提高到线性阶。如果在通过主循环的时候,没有发生交换,那么,列表就是已经排序好的。这种情况可能发生在任何一个轮次,但是在最好的情况下,第1轮就会发生。可以使用一个布尔标志来记录交换动作的出现,并且当内部循环没有设置这个标志的时候,就从函数返回。下面是修改后的冒泡排序函数:

def bubbleSortWithTweak(lyst):
    n = len(lyst)
    while n > 1:
        swapped = False # 如果在通过主循环的时候,没有发生交换,那么,列表就已经是排好序的了。
        i = 1
        while i < n:
            if lyst[i] < lyst[i - 1]:
                swap(lyst, i , i-1)
                swapped = True
            i += 1
        if not swapped:
            return lyst
        n -= 1
    return lyst

注意,这一修改只是改进了最好情况下的行为。在平均情况下,冒泡排序的这个版本的复杂度仍然是O(n^{2})

1.3 插入排序

我们修改后的冒泡排序,对于已经排序好的列表来说,其性能比选择排序要好。但是如果列表中的项是没有顺序的,我们修改后的冒泡排序版本的性能仍然很糟糕。另一种算法叫作插入排序(Insertion Sort),它试图以一种不同的方式来对列表进行排序。其策略如下:

  • 在第i轮通过列表的时候(其中i的范围从1到n-1),第i个项应该插入到列表的前i个项之中的正确位置。
  • 在第i轮之后,前i个项应该是排好序的。
  • 这个过程类似于人们排列手中的扑克牌的顺序,也就是说,如果你按照顺序放好了前i-1张牌,抓取了第i张牌,并且将其与手中的这些牌进行比较,直到找到其合适的位置。
  • 和其他的排序算法一样,插入排序包含两个循环,外围的循环遍历从1到n-1的位置。对于这个循环的每一个位置i,我们都保存该项并且从位置i-1开始内部循环。对于这个循环中的每一个位置j,我们都将项移动到位置j+1,直到找到了给保存的项(第i项)的插入位置。

图3展示了对5个项的一个列表进行插入排序,以及在每一次通过外围循环之后的状态。在下一个轮次中插入的项用一个箭头←标记出来,在将这个项插入之后,用星号将其标记出来。

图3 记录插入排序中的数据
未排序的列表 第1轮之后 第2轮之后 第3轮之后 第4轮之后
2 2 1* 1 1
5 ← 5(没有插入) 2 2 2
1 1← 5 4* 3*
4 4 4← 5 4
3 3 3 3← 5

Python实现:

def insertionSort(lyst):
    i = 1
    while i < len(lyst):
        itemToInsert = lyst[i]
        j = i - 1
        while j >= 0:
            if itemToInsert < lyst[j]:
                lyst[j + 1] = lyst[j]
                j -= 1
            else:
                break
        lyst[j + 1] = itemToInsert
        i += 1

插入排序的最坏情况的复杂度是O(n^{2})。列表中排好序的项越多,插入排序的效果越好,在最好情况下,列表本来就是有序的。那么,插入排序的复杂度是线性阶的。然而,在平均情况下,插入排序的复杂度仍然是二次方阶的。

2. 更快排序算法

到目前为止,我们上面的3种排序算法都有O(n^{2})的运行时间。这些排序算法还有几种变体,其中的一些稍微快一些。但是,在最坏情况下和平均情况下,它们的性能还是O(n^{2})。然而,我们可以利用一些复杂度为O(nlog n)的更好地算法。这些更好的算法的秘诀就是,采用分而治之(divide-and-conquer)的策略。也就是说,每一个算法都找到了一种方法,将列表分解为更小的子列表。随后,这些子列表再递归地排序。理想情况下,如果这些子列表的复杂度为log(n),而重新排列每一个子列表中的数据所需的工作量为n,那么,这样的排序算法总的复杂度就是O(nlog n)

2.1 快速排序

快速排序(Quick Sort)所使用的策略之一如下:

  1. 选取一个关键字(key)作为枢轴,一般取整组记录的第一个数/最后一个,这里采用选取序列第一个数为枢轴,也是初始的坑位。
  2. 设置两个变量left = 0;right = N - 1;
  3. 从left一直向后走,直到找到一个大于key的值,然后将该数放入坑中,坑位变成了array[left]。
  4. right一直向前走,直到找到一个小于key的值,然后将该数放入坑中,坑位变成了array[right]。
  5. 重复3和4的步骤,直到left和right相遇,然后将key放入最后一个坑位。
     

Python实现:

def quicksort(lyst, left, right):
    if left >=  right:
        return lyst
    low = left
    high = right
    key = lyst[low]
    while left < right:
        while left < right and lyst[right] > key:
            right -= 1
        array[left] = lyst[right]

        while left < right and lyst[left] <= key:
            left += 1
        lyst[right] = lyst[left]

    array[right] = key
    quicksort(lyst, low, left - 1)
    quicksort(lyst, left + 1, high)
    return lyst

快速排序的最好情况的复杂度是O(nlog n)。在最坏的情况下的复杂度是O(n^{2})

2.2 归并排序(合并排序)

归并排序(Merge Sort)算法利用了递归、分而治之的策略来突破O(n^{2})的障碍。概述如下:

  • 计算一个列表的中间位置,并且递归地排序其左边和右边的子列表(分而治之)。
  • 将两个排好序的子列表重新合并成单个排好序的列表。
  • 当子列表不再能够划分的时候,停止这个过程。

有3个Python函数在这个顶层的设计策略中协作:

  • mergeSort——用户调用的函数。
  • mergeSortHelper——一个辅助函数,它隐藏了递归调用所需要的额外参数。
  • merge——实现合并过程的一个函数。

Python实现:

def mergeSort(lyst):
    # lyst          list being sorted
    # copyBuffer    temporary space needed during merge
    copyBuffer = Array(len(lyst))
    mergeSortHelper(lyst, copyBuffer, 0, len(lyst) - 1)

def mergeSortHelper(lyst, copyBuffer, low, high):
    # lyst          list being sorted
    # copyBuffer    temporary space needed during merge
    # low, high     bounds of sublist
    # middle        midpoint of sublist
    if low < high:
        middle = (low + high) // 2 #  返回商的整数部分(向下取整)
        mergeSortHelper(lyst, copyBuffer, low, middle)
        mergeSortHelper(lyst, copyBuffer, middle + 1, high)
        merge(lyst, copyBuffer, low, middle, high)

def merge(lyst, copyBuffer, low, middle, high):
    # lyst          list that is being sorted
    # copyBuffer    temporary space needed during merge process
    # low           beginning of first sorted sublist
    # middle        end of first sorted sublist
    # middle + 1    beginning of second sorted sublist
    # high          end of second sorted sublist

    # Initialize i1 and i2 to the first items in each sublist
    i1 = low
    i2 = middle + 1

    # Interleave items from the sublists into the
    # copyBuffer in such a way that order is maintained.
    for i in range(low, high + 1):
        if i1 > middle:
            copyBuffer[i] = lyst[i2] # First sublist exhausted
            i2 += 1
        elif i2 > high:
            copyBuffer[i] = lyst[i1] # Second sublist exhasuted
            i1 += 1
        elif lyst[i1] < lyst[i2]: # Item in first sublist <
            copyBuffer[i] = lyst[i1]
            i1 += 1
        else:
            copyBuffer[i] = lyst[i2] # Item in second sublist <
            i2 += 1

    for i in range(low, high + 1): # Copy sorted items back to propoer position in lyst
        lyst[i] = copyBuffer[i]

merge函数将两个排好序的子列表合并到一个大的排好序的子列表中。第1子列表在low和middle之间,第2子列表在middle+1到high之间。这个过程包含了3个步骤:

  1. 将索引指针设置为每个子列表的第1项。这分别是low和middle+1的位置。
  2. 从每个子列表的第1项开始,重复地比较各项。将较小的项从其子列表中复制到复制缓存中,并且继续处理子列表中的下一项。重复这个过程,直到两个子列表中的所有的项都已经复制过了。如果先到达了其中的一个子列表的末尾,通过另一个子列表复制剩余的项,从而结束这个步骤。
  3. 将copyBuffer中的low和high之间的部分,复制回lyst中对应的位置。

归并排序的运行时间由两条for语句主导,其中每一条都循环(high-low+1)次。结果,该函数的运行时间是O(high-low),在一个层的所有合并花费时间是O(n)。由于mergeSortHelper在每一层都是尽可能平均地分割子列表,层级数是O(log n),并且在所有情况下,该函数的最大运行时间是O(nlog n)。

你可能感兴趣的:(数据结构与算法)