十大排序算法

本文所有排序算法均使用python

第一部分:O(N^2)的排序算法

学过的人应该都知道,排序算法最好的应该是O(N log N)之类的排序算法,但是O(N^2)的算法是这些算法的基础,主要用途在于:

  • 因为简单,易于实现,所以它是简单情境的首选;
  • 复杂的排序算法思想是从简单的排序算法思想中衍生出来的,比如说希尔排序;
  • 而作为子过程,又可以用来改进更加复杂的算法

1.冒泡排序(Bubble Sort)

1.1 冒泡排序原理

冒泡排序是最简单的排序算法了,它的想法也很简单,如果是从小到达排序的话,就是每次比较两个元素,看他们的顺序有没有错,如果错了就换过来。
这里写图片描述

1.2 冒泡排序算法步骤

A:比较两个元素,如果第一个比第二个大,则两个元素交换位置
B:对每一对相邻的元素做同样的工作,从第一对到最后一对。这一次执行完,最后一个元素是最大的元素。
C:除了最后一个元素之外,对其余元素做同样的操作
D:重复以上A,B,C三步,直到每个元素都不需要交换位置为止

1.3 算法实现

def bubbleSort(arr):
    for i in range(1,len(arr)):
        for j in range(0,len(arr) - i):
            if arr[j] > arr[j+1]:
                arr[j] , arr[j+1] = arr[j+1] , arr[j]
    return arr

1.4 算法效率

冒泡排序在所有数据都是正序的时候,最快(都排好序了,目的已达到)。当所有数据都排反序的时候最慢(用for循环反序输出就OK了)。

2.选择排序

2.1 选择排序原理

第一遍循环找到最小的元素放到第一位,第二次循环找到一个第二小元素,放到第二位,以此完成排序。在每次循环中,先把第一个元素标记为最小元素,然后将它与其后面的元素比较,发现比它小的,就将其标记为本次循环的最小元素。
这里写图片描述
无论什么数据进入,时间复杂度都是 O(n2),

2.2 算法步骤

A:先从未排序序列中找到最小(大)元素放到起始位置
B:再从剩余未排序序列中继续寻找最小(大)元素放到已排序末位
C:重复以上两步,直到排序完成

2.3 算法实现

def selectSort(arr):
#关键点:找到[i,n]中的最小值
    for i in range(len(arr)-1):
        for j in range( i+1 , len(arr)):
            if arr[j] < arr[i]:
                arr[i] ,arr[j] = arr[j],arr[i]
    return arr

3.插入排序

3.1 插入排序原理

只要打过扑克牌的人应该都能非常好的理解插入排序。它的原理是通过构建有序序列,对与未排序序列,从后向前扫描,逐个比较之后,插入到相应的位置上。
这里写图片描述

3.2 算法步骤

A:将第一待排序序列的第一个元素看成有序序列,将第二个元素到最后一个元素当成未排序序列
B:从头到尾扫描未排序序列,将其插到已排序序列的的适当位置(如果两个元素相等,则插入到已排序序列元素的后面)。

3.3 算法实现

def insertionSort(arr):
    for i in range(len(arr)):
        preindex = i-1
        current = arr[i]
        while preindex >=0 and arr[preindex]>current:
            arr[preindex +1] = arr[preindex]
            preindex -= 1
        arr[preindex + 1] = current
    return arr

难点:

  • 往前遍历

优点之一就是第二层循环在找到合适的插入位置之后可以提前终止,同时直接使用赋值操作,不像c++中使用swap来交换位置。如此得出的效果要更好,特别是当数组为近乎有效的数组时,插入排序的效率与NlogN级别的排序算法相差无几。而且当数组是完全有序的数组时,插入排序算法的性能可以达到O(n),因为第二层循环可以直接终止,而只进行第一层循环,因此插入排序的思想在接下来的算法能帮助我们改进算法。

4.希尔排序

4.1算法思想

希尔排序也称递减增量排序算法,是插入排序的改进版本。希尔排序是根据插入排序的以下两点性质而提出的改进的:
插入排序对已经排好序的序列效率高,可以达到线性排序的效率;
但插入排序是低效的,因为每次只能将数据移动一位;

希尔排序的思想是:先将待排序序列分割成若干个子序列,然后将每个子序列用插入排序进行排序,待子序列中的记录“基本有序”时,再对所有子序列进行一次直接插入排序。

4.2 算法步骤

4.3 算法实现

import math
def shellSort(arr):
    gap = 1
    while(gap < len(arr)/3):
        gap = gap*3 + 1
    while gap>0:
        for i in range(gap,len(arr)):
            temp = arr[i]
            j = i-gap
            while j>=0 and arr[j] > temp:
                arr[j+gap] = arr[j]
                j -= gap
            arr[j + gap] = temp
        gap = math.floor(gap/3)
    return arr

第二部分:NlogN算法

5.归并排序(merge sort)

5.1 算法思想

归并算法采用的是分而治之(divide and conquer),他建立在归并操作上,归并操作是指将两个已经排序的序列合成一个序列,他是归并算法的基石。

这里写图片描述
因此,在把两个已经排序的数组放到一起时,需要额外的申请空间。在实际的应用中,我们优先考虑的是时间复杂度。那具体应该怎么做呢?
这里写图片描述
我们的做法就是确定三个索引在数组内进行追踪,蓝色箭头 表示在归并的过程中我们要跟踪的位置,两个红色的箭头表示两个待归并的数组当前的头元素。

这里写图片描述
归并排序的特点之一就是这三个箭头。

5.2 算法步骤

A:申请空间,使其大小为两个已经排序的序列之和,用来存放合并后的序列
B:设定两个指针,最初位置为两个已经排序序列的起始位置
C:选择两个指针代表的元素,并比较两个元素,选择相对较小的元素放到合并空间,并将指针移动下一个位置
D:重复步骤C,直到某一指针指向序列尾
E:将另一序列的所有元素直接复制到合并序列尾

5.3 算法实现

def mergeSort(arr):
    if(len(arr)<2):
        return arr
    middle = int(len(arr)/2)
    left,right = arr[:middle],arr[middle:]
    return merge(mergeSort(left),mergeSort(right))

def merge(left,right):
    result = []
    while left and right:
        if left[0]<=right[0]:
        #为了不用具体的索引,我们直接把它pop出来
            result.append(left.pop(0))
        else:
            result.append(right.pop(0))
        while left:
            result.append(left.pop(0))
        while right:
            result.append(right.pop(0))
    return result
lst = [111,333,555,666,77,777,1,11,4,6,7,9]
mergeSort(lst)

结果:[1, 77, 111, 333, 555, 666, 777, 4, 11, 6, 7, 9]

6.快速排序

6.1 快速排序原理

快速排序的原理就是对于一个数组,从前找一个比首元素大的元素,从后找一个比最后一个元素小的元素,然后将两个元素进行交换。
这里写图片描述
如图所示,快速排序选择第一个元素放到第四的位置上,然后将所有元素分为两个个部分,第一部分小于4,第二部分大于4.然后再对这两部分再重新选择像4这样的标定点,使用快速排序的思想排序,知道整个数组全部有序为止。这个将数组拆分的操作称为partition.

6.2 算法步骤

  • 从数组中选出一个元素作为标定点v,定义对应的三个索引,v为l,小于v的最后一个元素为j,当前考虑的元素为i,如图所示
    这里写图片描述
  • partition操作:以标定点将数组拆分成小于v和大于v的,
    • 对于当前的元素e,索引为i,将e与v比较后,
    • 如果它大于v,直接将索引移到下一个元素即可,i+1
    • 如果它小于v,将i和j+1对应的元素互换位置,j+1
  • 递归得把小于基准值的子数组和大于基准值的子数组排序

6.3 python实现

第一版实现与优化:
def quickSort(arr,left = None,right = None):
    left = 0 if not isinstance(left,(int,float)) else left
    right = len(arr) - 1 if not isinstance(right,(int,float)) else right
    if left < right:
        if(right - left < 16): #优化1,当数组列表足够小是采用插入排序
            insertionSortForQS(arr,left,right)
        else:
            p = partition(arr,left,right)
            quickSort(arr,left,p -1 )
            quickSort(arr, p + 1,right)
    return arr

def partition(arr,left,right):
    v = arr[left]
    j = left + 1
    i = j
    while i <= right:
        if arr[i] < v:
            swap(arr,j , i)
            j += 1
        i += 1
    swap(arr,j - 1,left)
    return j - 1

def swap(arr,i,j):
    arr[i], arr[j] = arr[j], arr[i]


def insertionSortForQS(arr,first,last):
    #专门为辅助快速排序设计的插入排序
    for i in range(first+1,last+1):
        curr = arr[i]
        index = i
        while index > first and arr[index-1]>curr:
            arr[index]=arr[index-1]
            index=index-1
        arr[index]=curr
    return arr
第二版优化:解决近乎有序数组使得算法达到O(n^2)的问题

快速排序的数组是完全有序的数组,因为我们每次选择的都是最左边的元素作为标定点,这样来切分会达到O(n^2)的时间复杂度,为了解决这个问题,我们可以随机的取标定点。

此时快速排序的时间复杂度的期望值为nlogn.

def partition(arr,left,right):
    pivot = randint(left,right) #取随机标定点
    swap(arr,left,pivot)
    v = arr[left]
    j = left + 1
    i = j
    while i <= right:
        if arr[i] < v:
            swap(arr,j , i)
            j += 1
        i += 1
    swap(arr,j - 1,left)
    return j - 1
第三版优化:针对有大量重复元素的列表:实现双路快速排序

由前一版的代码可以看到我们已经默认把元素划分成了<v和>=v这种情况,但如果列表中的元素有大量重复值时,快速排序算法就会出现如下的情况:
这里写图片描述
两个部分依然有很大的可能出现极度不平衡的情况,因此,我们可以换一个写partition的思路,使用双路快速排序:
这里写图片描述
对于<v的部分从前往后遍历,如果小于v,索引直接+1,对于>v的部分,从后往前遍历,如果大于v,索引-1,如果不满足两种情况,把i,j两个位置的元素交换,这个步骤相当于把=v的元素分配到两边,使得生成的树不这么倾斜。

def partition2Ways(arr,left,right):
    pivot = randint(left,right)
    swap(arr,left,pivot)
    v = arr[left]
    i = left + 1
    j = right
    done = False
    while not done:
        while arr[i] < v and i <= right:
            i += 1
        while arr[j] > v and j >= left:
            j -= 1
        if i > j:
            done = True
        else:
            swap(arr,i,j)
            i += 1
            j -= 1
    swap(arr,left,j)
    #当循环结束的时候,i位于第一个大于v的位置,j位置最后一个小于v的元素,故把第一个标定点元素和最后一个小于v的元素(j对应的位置)互换位置。最后返回j
    return j
第四版优化:针对有大量重复元素的列表:实现三路快速排序

事实上,如果存在大量的重复元素可以直接使用三路快速排序更好,图示为程序运行的中间状态:
这里写图片描述
多定义两个索引,lt指向小于v的最后一个元素,gt指向大于v的第一个元素。
- 如果当前元素arr[i]=v,直接i+= 1
- 如果arr[i] < v,把arr[i]和arr[lt+1]互换,同时lt+=1,i += 1
- 如果arr[i]> v,把arr[gt-1]和arr[i]互换,同时i += 1
- 最后lt停止在等于v的第一个元素,gt停止在大于v的第一个元素

#实现三路快速排序
def quickSort3Ways(arr,left = None,right = None):
    left = 0 if not isinstance(left,(int,float)) else left
    right = len(arr) - 1 if not isinstance(right,(int,float)) else right
    if left < right:
        if(right - left <16): #优化1:当列表足够小时,采用插入排序
            insertionSortForQS(arr,left,right)
            return
        pivot = randint(left, right)
        swap(arr,left,pivot)
        v = arr[left]
        #为了保证初始的区间时空的,做如下设定
        lt = left #arr[lt+1,...lt] < v
        gt = right + 1 #arr[gt,...,r] > v
        i = left + 1 #arr[lt+1,...r) == v
        while i < gt:
            if arr[i] < v:
                swap(arr,lt + 1, i)
                i += 1
                lt += 1
            elif arr[i] > v:
                swap(arr,gt-1, i)
                gt -= 1
            else:
                i += 1
        swap(arr,left,lt)
        quickSort3Ways(arr,left,lt - 1) #swap之后没有对lt进行-=1操作,所以这里使用lt-1
        quickSort3Ways(arr, gt, right)
    return arr
  • 13
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值