排序算法总结(基于Python)

排序算法总结(基于Python)

比较排序

(基于比较的排序)给定一个包含n个对象的待排序序列{a_1},{a_2},...,{a_n}。假如已知如何比较任意两个对象的大小关系,以及如何对这一序列排序。对于自定义对象,比较规则应当满足两个性质:传递性全序性

冒泡排序

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
特点:调整相邻两个对象的位置,每进行一次内循环,将最大值调整到最后。
时间复杂度:{O}\left( {{{n}^2}} \right)

def bubble_sort(arr):
    n = len(arr)

    # 遍历所有数组元素
    for i in range(n):
        for j in range(0, n - i - 1):
            # 比较大小
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]

梳排序

梳排序是改良自冒泡排序,其要旨在于消除乌龟,亦即在阵列尾部的小数值,这些数值是造成冒泡排序缓慢的主因。相对地,兔子,亦即在阵列前端的大数值,不影响冒泡排序的效能。

冒泡排序总是比较相邻的值。所以一次交换中只能删除一个逆序计数。而梳式排序通过使用大于1的间隔来改进冒泡排序。间隔从一个较大的值开始,并在每次迭代中缩小1.3倍,直到达到值1。因此,梳式排序在一次交换中删除了多个逆序计数,比冒泡排序性能上更好。

收缩系数的设定影响着梳排序的效率。根据经验,收缩系数是1.3 (通过在超过20万个随机列表上测试梳排序)。

虽然平均情况下,它比冒泡排序性能更好,但在最坏的情况下仍然是{O}\left( {{{n}^2}} \right)

def comb_sort(arr):
    the_len = len(arr)
    if the_len < 2:  # 0和1
        return arr
    else:
        i = int(the_len / 1.3)
        while i >= 1:
            for j in range(the_len):
                if i + j >= the_len:
                    i = int(i / 1.3)
                    break
                else:
                    if arr[j] >= arr[j + i]:
                        arr[j], arr[j + i] = arr[j + i], arr[j]
        return arr

堆排序

堆排序(Heap Sort)是利用堆这种数据结构所设计的一种排序算法。堆的结构是一棵完全二叉树的结构,并且满足堆积的性质:每个节点(叶节点除外)的值都大于等于(或都小于等于)它的子节点。
堆排序先按从上到下、从左到右的顺序将待排序列表中的元素构造成一棵完全二叉树,然后对完全二叉树进行调整,使其满足堆积的性质:每个节点(叶节点除外)的值都大于等于(或都小于等于)它的子节点。构建出堆后,将堆顶与堆尾进行交换,然后将堆尾从堆中取出来,取出来的数据就是最大(或最小)的数据。重复构建堆并将堆顶和堆尾进行交换,取出堆尾的数据,直到堆中的数据全部被取出,列表排序完成。总的时间复杂度:{{O}}\left( {n + k\log n} \right)

堆结构分为大顶堆小顶堆

  • 大顶堆:每个节点(叶节点除外)的值都大于等于其子节点的值,根节点的值是所有节点中最大的,所以叫大顶堆,在堆排序算法中用于升序排列。
    -小顶堆:每个节点(叶节点除外)的值都小于等于其子节点的值,根节点的值是所有节点中最小的,所以叫小顶堆,在堆排序算法中用于降序排列。

堆排序的原理如下:

  1. 将待排序列表中的数据按从上到下、从左到右的顺序构造成一棵完全二叉树。
  2. 将完全二叉树中每个节点(叶节点除外)的值与其子节点(子节点有一个或两个)中较大的值进行比较,如果节点的值小于子节点的值,则交换他们的位置(大顶堆,小顶堆反之)。
  3. 将节点与子节点进行交换后,要继续比较子节点与孙节点的值,直到不需要交换或子节点是叶节点时停止。比较完所有的非叶节点后,即可构建出堆结构。
  4. 将数据构造成堆结构后,将堆顶与堆尾交换,然后将堆尾从堆中取出来,添加到已排序序列中,完成一轮堆排序,堆中的数据个数减1。
  5. 重复步骤2,3,4,直到堆中的数据全部被取出,列表排序完成。
def heap_sort(array):
    first = len(array) // 2 - 1
    for start in range(first, -1, -1):
        # 从下到上,从右到左对每个非叶节点进行调整,循环构建成大顶堆
        big_heap(array, start, len(array) - 1)
    for end in range(len(array) - 1, 0, -1):
        # 交换堆顶和堆尾的数据
        array[0], array[end] = array[end], array[0]
        # 重新调整完全二叉树,构造成大顶堆
        big_heap(array, 0, end - 1)
    return array


def big_heap(array, start, end):
    root = start
    # 左孩子的索引
    child = root * 2 + 1
    while child <= end:
        # 节点有右子节点,并且右子节点的值大于左子节点,则将child变为右子节点的索引
        if child + 1 <= end and array[child] < array[child + 1]:
            child += 1
        if array[root] < array[child]:
            # 交换节点与子节点中较大者的值
            array[root], array[child] = array[child], array[root]
            # 交换值后,如果存在孙节点,则将root设置为子节点,继续与孙节点进行比较
            root = child
            child = root * 2 + 1
        else:
            break

归并排序

归并排序(Merge Sort)是一种非常高效的排序方式,它用了分治的思想,基本排序思想是:先将整个序列两两分开,然后每组中的两个元素排好序。接着就是组与组和合并,只需将两组所有的元素遍历一遍,即可按顺序合并。以此类推,最终所有组合并为一组时,整个数列完成排序。
时间复杂度:{O}\left( {n\log n} \right)

def merge(s1,s2,s):
    i = j = 0
    while i+j<len(s):
        # j==len(s2)时说明s2走完了,或者s1没走完并且s1中该位置是最小的
        if j==len(s2) or (i<len(s1) and s1[i]<s2[j]):
            s[i+j] = s1[i]
            i += 1
        else:
            s[i+j] = s2[j]
            j += 1

def merge_sort(s):
    n = len(s)
    # 剩一个或没有直接返回
    if n < 2:
        return
    # 拆分
    mid = n // 2
    s1 = s[0:mid]
    s2 = s[mid:n]
    # 子序列递归调用排序
    merge_sort(s1)
    merge_sort(s2)
    # 合并
    merge(s1,s2,s)

快速排序

快速排序(Quicksort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
时间复杂度:{O}\left( {n\log n} \right)
稳定性:不稳定

步骤为:

  1. 从数列中挑出一个元素,称为"基准"(pivot)。
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
    递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

基准点的选择对快速排序的性能有极大的影响。基准点若选成区间的最大/最小值会导致退化(遍历区间时pivot始终大于或小于其他所有元素,这会导致第6和7行的循环把区间遍历两遍);最好的情况应选择区间内元素的中位数(每次恰好将区间内元素分割成大于、小于基准点的两部分,只需遍历一遍),但这通常无法实现,会导致时间常数大幅增高。
区间最左侧区间中央区间最右侧取中位数做基准点(三数取中法),这种方法只需少量比较即可获得(不那么准确的)中位数,在序列已有序时获得良好的效果。
快速排序的分治算法在大多数情况下内存访问效率极高,虽然其他算法都是 {O}\left( {n\log n} \right),但是快排比其他的快。

def quick_sort(alist, start, end):
    if start >= end:  # 递归的退出条件
        return
    mid = alist[start]  # 设定起始的基准元素
    low = start  # low为序列左边在开始位置的由左向右移动的游标
    high = end  # high为序列右边末尾位置的由右向左移动的游标
    while low < high:
        # 如果low与high未重合,high(右边)指向的元素大于等于基准元素,则high向左移动
        while low < high and alist[high] >= mid:
            high -= 1
        alist[low] = alist[high]  # 走到此位置时high指向一个比基准元素小的元素,将high指向的元素放到low的位置上,此时high指向的位置空着,接下来移动low找到符合条件的元素放在此处
        # 如果low与high未重合,low指向的元素比基准元素小,则low向右移动
        while low < high and alist[low] < mid:
            low += 1
        alist[high] = alist[low]  # 此时low指向一个比基准元素大的元素,将low指向的元素放到high空着的位置上,此时low指向的位置空着,之后进行下一次循环,将high找到符合条件的元素填到此处

    # 退出循环后,low与high重合,此时所指位置为基准元素的正确位置,左边的元素都比基准元素小,右边的元素都比基准元素大
    alist[low] = mid  # 将基准元素放到该位置,
    # 对基准元素左边的子序列进行快速排序
    quick_sort(alist, start, low - 1)  # start :0  low -1 原基准元素靠左边一位
    # 对基准元素右边的子序列进行快速排序
    quick_sort(alist, low + 1, end)  # low+1 : 原基准元素靠右一位  end: 最后

内省排序

内省排序(Introsort)是快速排序和 堆排序 的结合,由 David Musser 于 1997 年发明。内省排序其实是对快速排序的一种优化,保证了最差时间复杂度为2\log n

内省排序将快速排序的**最大递归深度限制为2\log n **,超过限制时就转换为堆排序。这样既保留了快速排序内存访问的局部性,又可以防止快速排序在某些情况下性能退化为{n}^2

heap排序在平均时间复杂度是 {O}\left( {n\log n} \right),最坏情况也是 {O}\left( {n\log n} \right),看起来要比快排要快。但是实际上,快排是要比heap排序要快,第一个原因是:heap排序虽然和快排在平均情况下的时间复杂度是 {O}\left( {n\log n} \right),但是heap排序的时间常数要比快排的时间常数要大。第二个原因是:据统计,快排的最坏情况在是很少发生的。第三个原因是:快排能够比较好的吻合程序的空间局部性原理,因为它操作的基本都是相邻的元素(虚拟存储器的设计理论基础中就有程序的时间局部性和空间局部性),能够减少内存缺页中断的发生次数。

import random
from math import log2


def percolateDown(heap, idx, maxIdx=None, *, reverse=False):
    if maxIdx is None:
        maxIdx = len(heap) - 1
    while idx < maxIdx:
        largestIdx = idx
        if 2 * idx + 1 <= maxIdx and (reverse ^ (heap[2 * idx + 1] > heap[largestIdx])):
            largestIdx = 2 * idx + 1
        if 2 * idx + 2 <= maxIdx and (reverse ^ (heap[2 * idx + 2] > heap[largestIdx])):
            largestIdx = 2 * idx + 2

        if largestIdx != idx:
            heap[idx], heap[largestIdx] = heap[largestIdx], heap[idx]
            idx = largestIdx
        else:
            break


def heapify(heap, maxIdx=None, *, reverse=False):
    if maxIdx is None:
        maxIdx = len(heap) - 1
    for idx in range(maxIdx // 2, -1, -1):
        percolateDown(heap, idx, reverse=reverse)


def heapSort(heap, reverse=False):
    heapify(heap, reverse=reverse)
    for idx in range(len(heap) - 1, 0, -1):
        heap[0], heap[idx] = heap[idx], heap[0]
        percolateDown(heap, 0, idx - 1, reverse=reverse)
    return heap


def findPivot(begin, end):
    return random.randint(begin, end)


def partition(array, begin, end, *, reverse=False):
    pivotIdx = findPivot(begin, end)
    pivot = array[pivotIdx]

    array[end], array[pivotIdx] = array[pivotIdx], array[end]
    firstLarger = begin
    for idx in range(begin, end):
        if reverse ^ (array[idx] <= pivot):
            array[idx], array[firstLarger] = array[firstLarger], array[idx]
            firstLarger += 1

    array[end], array[firstLarger] = array[firstLarger], array[end]
    return firstLarger


def introSort(array, begin=0, end=None, depth=0, *, reverse=False):
    if end is None:
        end = len(array) - 1

    if depth < log2(len(array)):
        if begin < end:
            mid = partition(array, begin, end, reverse=reverse)
            introSort(array, begin, mid - 1, depth + 1, reverse=reverse)
            introSort(array, mid + 1, end, depth + 1, reverse=reverse)
    else:
        array[begin:end + 1] = heapSort(array[begin:end + 1], reverse=reverse)

Timesort

Timsort是一种混合、稳定高效的排序算法,源自合并排序和插入排序,旨在很好地处理多种真实数据。它由Tim Peters于2002年实施使用在Python编程语言中。该算法查找已经排序的数据的子序列,并使用该知识更有效地对其余部分进行排序。这是通过将已识别的子序列(称为运行)与现有运行合并直到满足某些条件来完成的。从版本2.3开始,Timsort一直是Python的标准排序算法。如今,Timsort 已是是 Python、 Java、 Android平台 和 GNU Octave 的默认排序算法。
针对现实中需要排序的数据分析看,大多数据通常是有部分已经排好序的数据块,Timsort 就利用了这一特点。Timsort 称这些已经排好序的数据块为 “run”,我们可以将其视为一个一个的“分区”。在排序时,Timsort迭代数据元素,将其放到不同的 run 里,同时针对这些 run ,按规则进行合并至只剩一个,则这个仅剩的 run 即为排好序的结果。
换句话说,就是分析待排序数据,根据其本身的特点,将排序好的(不管是顺序还是逆序)子序列的分为一个个run分区,当然,这个分区run也存在一定的约束,即根据序列会产生一个minrun,如果原始的run小于minrun的长度,用插入排序扩充run,直到达到条件,之后使用归并排序来合并多个run。

def binary_search(arr, left, right, value):
    if left >= right:
        if arr[left] <= value:
            return left + 1
        else:
            return left
    elif left < right:
        mid = (left + right) // 2
        if arr[mid] < value:
            return binary_search(arr, mid + 1, right, value)
        else:
            return binary_search(arr, left, mid - 1, value)


def insertion_sort(arr):
    length = len(arr)
    for index in range(1, length):
        value = arr[index]
        pos = binary_search(arr, 0, index - 1, value)
        arr = arr[:pos] + [value] + arr[pos:index] + arr[index + 1:]
    return arr


def merge(l1, l2):
    if not l1:
        return l2
    if not l2:
        return l1
    if l1[0] < l2[0]:
        return [l1[0]] + merge(l1[1:], l2)
    else:
        return [l2[0]] + merge(l1, l2[1:])


def timsort(arr):
    if not arr:  # 空列表
        return
    runs, sorted_runs = [], []
    new_run = [arr[0]]
    length = len(arr)
    # 划分run区,并存储到runs里,这里简单的按照升序划分,没有考虑降序的run
    for index in range(1, length):
        if arr[index] < arr[index - 1]:
            runs.append(new_run)
            new_run = [arr[index]]
        else:
            new_run.append(arr[index])
        if length - 1 == index:
            runs.append(new_run)
            break

    for run in runs:
        insertion_sort(run)

    # 合并runs
    sorted_arr = []
    for run in runs:
        sorted_arr = merge(sorted_arr, run)
    return sorted_arr

非比较排序

桶排序

桶排序(Bucket Sort)是计数排序的推广,其主要思想是:将待排序集合中处于同一个值域的元素存入同一个桶中,也就是根据元素值特性将集合拆分为多个区域,则拆分后形成的多个桶,从值域上看是处于有序状态的。对每个桶中元素进行排序,则所有桶中元素构成的集合是已排序的。
算法实现步骤:

  1. 根据待排序集合中最大元素和最小元素的差值范围和映射规则,确定申请的桶个数;
  2. 遍历排序序列,将每个元素放到对应的桶里去;
  3. 对不是空的桶进行排序;
  4. 按顺序访问桶,将桶中的元素依次放回到原序列中对应的位置,完成排序。
def bucket_sort(array):
    min_num, max_num = min(array), max(array)
    # 桶的大小
    bucket_num = (max_num - min_num) // 3 + 1
    # 桶数组
    buckets = [[] for _ in range(int(bucket_num))]
    # 向桶数组填数
    for num in array:
        buckets[int((num - min_num) // 3)].append(num)
    new_array = list()
    for i in buckets:
        for j in sorted(i):
            new_array.append(j)
    return new_array

基数排序

基数排序(Radix Sort)是一种非比较型整数排序算法,是桶排序的扩展。基本思想是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。按照低位先排序,分别放入10个队列中,然后采用先进先出的原则进行收集;再按照高位排序,然后再收集;依次类推,直到最高位,最终得到排好序的数列。对于数值偏小的一组序列,其速度是非常快的,时间复杂度达到了线性,而且思想也非常的巧妙。
算法实现步骤

  1. 取得数组中的最大数,并取得位数;
  2. 对数位较短的数前面补零;
  3. 分配,先从个位开始,根据位值(0-9)分别放到0~9号桶中;
  4. 收集,再将放置在0~9号桶中的数据按顺序放到数组中;
  5. 重复3~4过程,直到最高位,即可完成排序。
def radix_sort(arr):
    n = len(str(max(arr)))  # 记录最大值的位数
    for k in range(n):  # n轮排序
        # 每一轮生成10个列表
        bucket_list = [[] for i in range(10)]  # 因为每一位数字都是0~9,故建立10个桶
        for i in arr:
            # 按第k位放入到桶中
            bucket_list[i // (10 ** k) % 10].append(i)
        # 按当前桶的顺序重排列表
        arr = [j for i in bucket_list for j in i]
    return arr
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容