八种常用排序算法(python)

常见排序方法的时间与空间复杂度
在这里插入图片描述
平均情况时间复杂度
最坏和最好情况是极端情况,发生的概率并不大。为了更有效的表示平均情况下的时间复杂度,引入另一个概念:平均情况时间复杂度,全称叫加权平均时间复杂度或者期望时间复杂度。(引入各自情况发生的概率再具体分析)
多数情况下,我们不需要区分最好、最坏、平均情况时间复杂度。只有同一块代码在不同情况下时间复杂度有量级差距,我们才会区分3种情况,为的是更有效的描述代码的时间复杂度。
均摊时间复杂度
应用场景:均摊时间复杂度和摊还分析应用场景较为特殊,对一个数据进行连续操作,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度较高。而这组操作其存在前后连贯的时序关系。
计算:我们将这一组操作放在一起分析,将高复杂度均摊到其余低复杂度上,所以一般均摊时间复杂度就等于最好情况时间复杂度。
举例 : 有一个长度为n的数组,如果数组没满,就往里插入一个数,如果数组满了,就遍历求和.那么绝大多数情况下都是O(1),只有最后一次是O(n),均摊以后就是O(1)。

// array 表示一个长度为 n 的数组
// 代码中的 array.length 就等于 n
int[] array = new int[n];
int count = 0;

void insert(int val) {
  if (count == array.length) {
     int sum = 0;
     for (int i = 0; i < array.length; ++i) {
        sum = sum + array[i];
     }
     array[0] = sum;
     count = 1;
  }

  array[count] = val;
  ++count;
}

一、插入排序
通过构建有序序列,对于未排序的序列,在已排序的序列中从后向前扫描,找到相应位置插入。
1)从第一个元素开始,该元素可以被认为已经被排序。
2)取出下一个元素,在已经排序的元素序列中从后向前扫描。
3)如果该元素(已排序)大于新元素,将该元素移到下一位置。
4)重复步骤3),直到找到已排序的元素小于或者等于新元素的位置。
5)将新元素插入到该位置后。
6)重复步骤2)~5)。

def InsertSort(myList):
    #获取列表长度
    length = len(myList)

    for i in range(1,length):
        #设置当前值前一个元素的标识
        j = i - 1
        temp = myList[i]
        #继续往前寻找,如果有比临时变量大的数字,则后移一位,直到找到比临时变量小的元素或者达到列表第一个元素
        while j>=0 and myList[j] > temp:
            myList[j+1] = myList[j]
            j = j-1
            #将临时变量赋值给合适位置
        myList[j+1] = temp


myList = [49,38,65,97,76,13,27,49]
InsertSort(myList)
print(myList)

二、冒泡排序
1)比较相邻的元素,如果第一个比第二个大,就交换他们两个。
2)对每一对相邻元素做同样的工作,从开始第一对到结尾最后一对。这步做完后,最后的元素会是最大的元素。
3)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

def bubble_sort(lists):
    # 冒泡排序
    count = len(lists)
    for i in range(0, count - 1):
        for j in range(0, count - 1 - i):
            if lists[j] > lists[j + 1]:#if语句的判断是包含在时间复杂度的计算中。
                a = lists[j]
                lists[j] = lists[j+1]
                lists[j+1] = a
    return lists

myList = [49,38,65,97,76,13,27,49]
bubble_sort(myList)
print(myList)

优化算法(在最优算法下冒泡排序的最优时间复杂度为O(n))

def bubble_sort(items):
    for i in range(len(items) - 1):
        flag = False
        for j in range(len(items) - 1 - i):
            if items[j] > items[j + 1]:
                items[j], items[j + 1] = items[j + 1], items[j]
                flag = True
        if not flag:
            break
    return items

三、选择排序
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

import sys 
def select_sort(lists):
	for i in range(len(lists)):    
	    min_idx = i 
	    for j in range(i+1, len(lists)): 
	        if lists[min_idx] > lists[j]: #if语句的判断是包含在时间复杂度的计算中。
	            min_idx = j 
	                
	    lists[i], lists[min_idx] = lists[min_idx], lists[i] 
    return lists

四、希尔排序
利用插入排序的简单,克服插入排序每次只能交换相邻两个元素的缺点
排序思想:
1) 定义增量序列DM > DM-1 > … > D1 = 1
2)对每个DK进行“ DK ”间隔排序(k = M , M-1 , … 1)
原始希尔排序:
在这里插入图片描述
注:“DK”间隔有序的数列,在执行“DK-1”间隔排序后,仍然保持“DK”间隔有序的。增量元素不互质,小增量可能在后面的排序过程中不起作用。
Hibbard 增量序列 – DK = 2k - 1(保证了增量元素不互质) , 最坏情况下的时间复杂度为O(N3/2).

def shell_sort(alist):
    """希尔排序"""
    n = len(alist)
    gap = n // 2
    while gap >= 1:
        for j in range(gap, n):
            i = j
            while (i - gap) >= 0:
                if alist[i] < alist[i - gap]:
                    alist[i], alist[i - gap] = alist[i - gap], alist[i]
                    i -= gap
                else:
                    break
        gap //= 2


if __name__ == '__main__':
    alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print("原列表为:%s" % alist)
    shell_sort(alist)
    print("新列表为:%s" % alist)

五、堆排序
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
堆排序基本思路:
1)将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
2)将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
3)重新调整结构,使剩余元素满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
图解见链接:https://www.cnblogs.com/chengxiao/p/6129630.html

注:
a. 完全二叉树:对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。
b.堆的定义:
1)任意节点小于(或大于)它的所有后裔,最小元(或最大元)在堆的根上(堆序性)。
2)堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层尽可能地从左到右填入。
c.时间复杂度的推算:主要在初始化堆过程和每次选取最大数后重新建堆的过程。利用等比数列求和公式计算初始化建堆过程时间为O(n)。更改堆元素后重建堆时间为O(nlogn),所以总的时间复杂度为O(nlogn)。 因为堆排序是就地排序,空间复杂度为常数:O(1)。

#_*_coding:utf-8_*_
import time,random
def sift_down(arr, node, end):
    root = node
    #print(root,2*root+1,end)
    while True:
        # 从root开始对最大堆调整

        child = 2 * root +1  #left child
        if child  > end:
            #print('break',)
            break
        #print("v:",root,arr[root],child,arr[child])
        #print(arr)
        # 找出两个child中较大的一个
        if child + 1 <= end and arr[child] < arr[child + 1]: #如果左边小于右边
            child += 1 #设置右边为大

        if arr[root] < arr[child]:
            # 最大堆小于较大的child, 交换顺序
            tmp = arr[root]
            arr[root] = arr[child]
            arr[child]= tmp

            # 正在调整的节点设置为root
            #print("less1:", arr[root],arr[child],root,child)

            root = child #
            #[3, 4, 7, 8, 9, 11, 13, 15, 16, 21, 22, 29]
            #print("less2:", arr[root],arr[child],root,child)
        else:
            # 无需调整的时候, 退出
            break
    #print(arr)
    print('-------------')

def heap_sort(arr):
    # 从最后一个有子节点的孩子开始调整最大堆
    first = len(arr) // 2 -1
    #生成最大堆
    for i in range(first, -1, -1):
        sift_down(arr, i, len(arr) - 1)
    #[29, 22, 16, 9, 15, 21, 3, 13, 8, 7, 4, 11]
    print('--------end---',arr)
    # 将最大的放到堆的最后一个, 堆-1, 继续调整排序
    for end in range(len(arr) -1, 0, -1):
        arr[0], arr[end] = arr[end], arr[0]
        sift_down(arr, 0, end - 1)
        #print(arr)
def main():
    # [7, 95, 73, 65, 60, 77, 28, 62, 43]
    # [3, 1, 4, 9, 6, 7, 5, 8, 2, 10]
    #l = [3, 1, 4, 9, 6, 7, 5, 8, 2, 10]
    #l = [16,9,21,13,4,11,3,22,8,7,15,27,0]
    array = [16,9,21,13,4,11,3,22,8,7,15,29]
    #array = []
    #for i in range(2,5000):
    #    #print(i)
    #    array.append(random.randrange(1,i))

    print(array)
    start_t = time.time()
    heap_sort(array)
    end_t = time.time()
    print("cost:",end_t -start_t)
    print(array)
    #print(l)
    #heap_sort(l)
    #print(l)


if __name__ == "__main__":
    main()

六、快速排序

  1. 快速排序采用了一种分治的策略,通常称其为分治法。分治法的基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。

  2. 利用分治法可将快速排序分为三步:
    1)在数据集之中,选择一个元素作为”基准”(pivot)。
    2)所有小于”基准”的元素,都移到”基准”的左边;所有大于”基准”的元素,都移到”基准”的右边。这个操作称为分区 (partition) 操作,分区操作结束后,基准元素所处的位置就是最终排序后它的位置。
    3)对”基准”左边和右边的两个子集,不断重复第一步和第二步,直到索引low,high之间的元素只剩下一个为止。

  3. 递归算法的时间复杂度求法:代入法(代入法首先要对这个问题的时间复杂度做出预测,然后将预测带入原来的递归方程,如果没有出现矛盾,则是可能的解,最后用数学归纳法证明),迭代法, 差分方程法。

  4. 递归算法的时间复杂度:
    a.在最优情况下,Partition每次都划分得很均匀,如果排序n个关键字,快速排序算法的时间复杂度为O(nlogn)。
    在这里插入图片描述

  5. 借助递归树求解递归算法的时间复杂度(归并排序,快速排序, 斐波那契数列, 全排列):https://www.jianshu.com/p/6fa5a8ddd65f

def quick_sort_standord(array,low,high):
    ''' realize from book "data struct" of author 严蔚敏
    '''
    if low < high:
        key_index = partion(array,low,high)
        quick_sort_standord(array,low,key_index)
        quick_sort_standord(array,key_index+1,high)

def partion(array,low,high):
    key = array[low]#选择一个元素作为”基准”
    while low < high:
        while low < high and array[high] >= key:
            high -= 1
        if low < high:
            array[low] = array[high]

        while low < high and array[low] < key:
            low += 1
        if low < high:
            array[high] = array[low]

    array[low] = key
    return low

if __name__ == '__main__':
    array2 = [9,3,2,1,4,6,7,0,5]

    print(array2)
    quick_sort_standord(array2,0,len(array2)-1)
    print(array2)

七、归并排序
a. 采用分治法
分割:递归地把当前序列平均分割成两半。
归并:在保持元素顺序的同时将上一步得到的子序列归并到一起。
b. 具体归并操作:递归法(Top-down)
1)申请空间s,该空间用来存放合并后的序列。
2)比较两个排序序列起始位置指向的数,将较小的元素删除(pop)并添加到s中(append)。
3)重复步骤2直到其中一序列为空。
4)将另一序列剩下的所有元素直接复制到合并序列尾。
c.利用递归树方便求得时间复杂度为O(nlogn)
https://zh.wikipedia.org/wiki/归并排序#Python

# Recursively implementation of Merge Sort
#归并
def merge(left, right):
    result = []
    while left and right:
        if left[0] <= right[0]:
            result.append(left.pop(0))
        else:
            result.append(right.pop(0))
    if left:
        result += left
    if right:
        result += right
    return result

#划分
def merge_sort(L):
    if len(L) <= 1:
        # When D&C to 1 element, just return it
        return L
    mid = len(L) // 2
    left = L[:mid]
    right = L[mid:]

    left = merge_sort(left)
    right = merge_sort(right)
    # conquer sub-problem recursively
    return merge(left, right)
    # return the answer of sub-problem


if __name__ == "__main__":
    test = [1, 4, 2, 3.6, -1, 0, 25, -34, 8, 9, 1, 0]
    print("original:", test)
    print("Sorted:", merge_sort(test))

八、桶排序
桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶bucket里,桶的数量是数组中的最大数,遍历数组,将元素值作为索引并将bucke对应的值加一,最后按索引顺序取出bucket中对应元素大于1的索引即得到最后的有序序列。

def bucket_sort(array):
    if not array:
        return False
    max_len = max(array)+1
    book = [0 for x in range(0,max_len)]
    for i in array:
        book[i] += 1
    return [i for i in range(0,max_len) for j in range(0,book[i])]

def main():
    array = [5,4,4,74,90,2]
    array = bucket_sort(array)
    print(array)
if __name__ == '__main__':
    main()

缺点:无法排负数,无法排小数,book所占的空间由输入数组的最大值确定
针对存在负数的情况

def bucket_sort(array):
    if not array:
        return False
    offset = min(array)
    max_len = max(array) - offset + 1
    book = [0 for x in range(0,max_len)]
    for i in array:
        book[i - offset] += 1
    return [i + offset for i in range(0,max_len) for j in range(0,book[i])]

def main():
    array = [5,4,4,-2,-9,74,90,2]
    array = bucket_sort(array)
    print(array)
if __name__ == '__main__':
    main()

针对存在小数与负数的情况

# 可对小数排序
def bucket_sort(array):
    if not array:
        return False
    # 保留两位小数
    accuracy = 100.
    offset = int(min(array) * accuracy)
    max_len = int(max(array) * accuracy - offset + 1)
    book = [0 for x in range(0,max_len)]
    for i in array:
        book[int(i * accuracy - offset)] += 1
    return [(i + offset) / accuracy for i in range(0,max_len) for j in range(0,book[i])]

def main():
    array = [5,4,4,-2,-9,74,90,2]
    array = bucket_sort(array)
    print(array)
if __name__ == '__main__':
    main()

sort(),sorted()
底层实现就是归并排序,速度比我们自己写的归并排序要快很多(10~20倍),所以说我们一般排序都尽量使用sorted和sort。最坏与平均情况下的时间复杂度为O(nlogn)。
注:

  1. 快排对越混乱的数据,排序效果越好,对一个基本有序的序列排序却更复杂(它要交换很多次才能排好)。因为这样会导致每次轴划分出的两个子序列,一个趋近于1的数量级,一个趋近于n数量级,那么递归快排就近似总是对n做排序,时间复杂度O(n²),而且非常不符合快排的思想。比较好的情况是每次递归大致平分成两个n/2数量级的子序列,时间复杂度O(nlogn)。
  2. 对基本有序的序列比较适合适用冒泡排序。
  3. 若n较小(如n≤50),可采用直接插入或直接选择排序。当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插入,应选直接选择排序为宜。
  4. 若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜。
  5. 若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
  6. 稳定性判断:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
  7. 有大量重复元素时使用三分快排:
    提出的算法是: 对于每次切分:从数组的左边到右边遍历一次,维护三个指针lt,gt,i,其中lt指针使得元素(arr[0]-arr[lt-1])的值均小于切分元素;gt指针使得元素(arr[gt+1]-arr[N-1])的值均大于切分元素;i指针使得元素(arr[lt]-arr[i-1])的值均等于切分元素,(arr[i]-arr[gt])的元素还没被扫描,切分算法执行到i>gt为止。
    每次切分之后,位于gt指针和lt指针之间的元素的位置都已经被排定,不需要再去移动了。之后将(lo,lt-1),(gt+1,hi)分别作为处理左子数组和右子数组的递归函数的参数传入,递归结束,整个算法也就结束。
    在这里插入图片描述
  8. 排序算法动态演示:https://www.cnblogs.com/onepixel/p/7674659.html
  9. TOP k的解法:
    a) 用堆排来解决Top K :先建立一个包含K个元素的大顶堆,然后遍历集合,如果集合的元素比堆顶元素小(说明它目前应该在K个最小之列),那就用该元素来替换堆顶元素,同时维护该堆的性质,那在遍历结束的时候,堆中包含的K个元素是不是就是我们要找的最小的K个元素。
    速记: 最小的K个用最大堆,最大的K个用最小堆。
    堆排时间复杂度为n*logK。
    速记: 堆排的时间复杂度是nlogn,这里相当于只对前Top K个元素建堆排序,想法不一定对,但一定有助于记忆。
    不会占用太多的内存空间(事实上,一次只读入一个数,内存只要求能容纳前K个数即可)。这也决定了它特别适合处理海量数据。
    b) 用快速排序来解决Top K:我们知道,分治函数会返回一个position,在position左边的数都比第position个数小,在position右边的数都比第position大。我们不妨不断调用分治函数,直到它输出的position = K-1,此时position前面的K个数(0到K-1)就是要找的前K个数。
    快排时间复杂度为n。
    既然要交换元素的位置,那么所有元素必须要读到内存空间中,所以它会占用比较大的空间,至少能容纳整个数组;数据越多,占用的空间必然越大,海量数据处理起来相对吃力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值