常用排序算法总结

常用排序算法

排序算法非常的多,在学习数据结构和算法时肯定都会学习到关于排序的算法,虽然现在高级语言都自带内置的排序函数,但是理解一些常见排序算法的思想和适用场合也是非常重要的,这也是面试时的经典题目,所以准备找工作的我打算将这些算法复习和整理一遍,所有算法代码采用Python编写。(数组下标从0开始)

冒泡排序

冒泡排序应该是第一个学习的排序算法吧,在学习C语言时就会学到,虽然效率比较低,但是在某些不需要全部排序的情况下也是很有效的,比如只需找出前3大的元素时。

算法描述

  1. 从第一个记录开始,相邻记录两两比较,若前一个记录大于后一个记录,则交换

  2. 第一趟将序列中最大的记录放到了最后一个位置

  3. n个记录比较n-1趟

代码

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

优化冒泡代码

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

算法分析

冒泡排序是稳定的,时间复杂度很容易就看出是O(n^2),所以是一个比较慢的排序算法。优化的冒泡排序是设置一个标识符,当flag为真时说明没有发生交换,则后面都为有序的,可以直接跳出循环。

选择排序

基本思想:序列大小为N,则共进行N-1趟排序,第一趟选出最小的与第一个元素交换,第二趟在剩余的无序序列中选出最小的与第二个元素交换,一直进行N-1趟。

代码

def select_sort(lists):
    length, i = len(lists), 0
    while i < length-1:
        min_temp = i
        for j in range(i+1, length):
            if lists[j] < lists[min_temp]:
                min_temp = j
        lists[i], lists[min_temp] = lists[min_temp], lists[i]
        i += 1

算法分析

时间复杂度O(n^2),算法是稳定的。选择排序与冒泡排序有点相似,每一轮排序结束时最大的元素将被放置到序列的一端,区别在于选择排序只交换一次,冒泡排序可能交换很多次。

快速排序

快速排序之所以叫快速排序,那当然是因为他快了~

基本思想:任取待排序对象序列中的某个对象v(枢轴,基准,支点),按照该对象的关键字大小,将整个序列划分为左右两个子序列:

  1. 左侧子序列中所有对象的关键字都小于或等于对象v的关键字;
  2. 右侧子序列中所有对象的关键字都大于或等于对象v的关键字;
  3. 对象v则排在这两个子序列中间(也是它最终的位置)

算法步骤:首先选取一个基准元素(pivot),基准元素可以任意选择,将基准元素与第一个元素交换。令i指向第一个元素,j指向最后一个元素,首先从最后一个元素开始向前遍历序列,遇到比pivot小的元素时,将j指向的值赋值给i指向的位置;然后i加1,从i开始向后遍历,直到遇到比pivot大的元素,赋值给j指向的位置;然后开始移动j....算法一直重复直到i==j,将pivot的值赋值给j指向的位置。到这一步就将序列分成左右两部分,左边部分小于等于pivot,右边部分大于等于pivot,递归执行上述步骤直到左右序列长度都为1

讲解示例

有数组如下:

0 1 2 3 4 5 6 7
30 2 87 25 49 50 22 12

令pivot = lists[0] = 30, i = 0, j = 7

j=7时,lists[7]=12 < pivot,lists[i]=lists[j]

得到如下数组: [12, 2, 87, 25, 49, 50, 22, 12]

i++, i=1时,lists[1]=2 < pivot, i++

i=2时, lists[2]=87 > pivot, lists[j]=lists[i]

得到如下数组:[12, 2, 87, 25, 49, 50, 22, 87]

j--, j=6, lists[6]=22 < pivot, lists[i]=lists[j]

得到如下数组:[12, 2, 22, 25, 49, 50, 22, 87]

i++, i=3时,lists[i]=25 < pivot, i++

i=4时, lists[i]=49 > pivot, lists[j]=lists[i]

得到如下数组: [12, 2, 22, 25, 49, 50, 49, 87]

j--, j=5时,lists[j]=50 > pivot, j--

j=4时, j == i,lists[j]=pivot

得到如下数组:[12, 2, 22, 25, 30, 50, 49, 87]

代码

def quick_sort(lists, left, right):
    if left < right:
        i, j, pivot = left, right, lists[left]
        while i < j:
            while i < j:
                if lists[j] < pivot:
                    lists[i] = lists[j]
                    break
                j -= 1
            while i < j:
                if lists[i] > pivot:
                    lists[j] = lists[i]
                    break
                i += 1
        lists[j] = pivot
        quick_sort(lists, left, j-1)
        quick_sort(lists, j+1, right)

算法分析

快速排序在实际应用中有最好的运行速度,平均时间复杂度为O(nlogn),但是最坏的情况下位O(n^2)。至于为什么快速排序在时间复杂度为O(nlogn)的一些排序算法中速度最快,感兴趣的可以自己去查查资料。此外影响快速排序效率的关键就是Pivot的选择,常见的有取第一个,取最后一个,取前中后的中间数,pivot如果能正好将序列左右等分,那效率就是最高的。

插入排序

插入排序有多种变形算法,我介绍一下直接插入排序、折半插入排序和希尔排序,前两种算法只在插入的时候有些区别,总体思想是一致的。

插入排序的思想

  1. 一个记录是有序的
  2. 从第二个记录开始,将每个记录插入到已排好序的序列中
  3. 一直进行到第n个记录

直接插入排序

直接插入排序第n次循环将下标为n的元素A插入合适的位置,将A依次与前一个元素B比较,若A>=B,则A不动,进入下一次循环;若A<B,则交换AB,重复上述操作。

代码

def direct_insert_sort(lists):
    for i in range(1, len(lists)):
        if lists[i] >= lists[i-1]:
            continue
        lists[i], lists[i-1] = lists[i-1], lists[i]
        for j in range(i-1, 0, -1):
            if lists[j] >= lists[j-1]:
                break
            lists[j], lists[j-1] = lists[j-1], lists[j]
    

算法分析

  1. 直接插入排序是稳定排序算法
  2. 时间复杂度:最好情况O(1),最坏O(n2),平均O(n2)
  3. 空间复杂度O(1)

折半插入排序

在插入排序中,第n次循环时,下标0到n-1的元素是有序的,可以用二分查找找到合适位置并插入第n个元素。

代码

def binary_insert_sort(lists):
    for i in range(1, len(lists)):
        if lists[i] >= lists[i-1]:
            continue
        # 折半查找
        low, high = 0, i-1
        while low <= high:
            mid = (high + low) // 2
            if lists[mid] > lists[i]:
                high = mid - 1
            else:
                low = mid + 1
        for j in range(i, low, -1):
            lists[j], lists[j-1] = lists[j-1], lists[j]

算法分析

为什么用low下标作为最终位置呢?可以看while循环的条件是low<=high,且是唯一跳出循环的条件,low与high只以1为增量,所以退出循环时low=high+1,但最后一次有效循环时,low与high与mid是相等的,如果lists[mid] > lists[i],则这个元素应该在mid前面,也就是low前面,如果lists[mid] <= lists[i],则这个元素应该在mid后面,即low=mid+1这个位置。

希尔排序

希尔排序与直接插入算法大致过程是一样的,希尔排序需要进行多趟排序,先进行宏观排序,再进行微观排序。宏观调整指跳跃式的插入排序

大致过程:

  1. 将数组分为若干个子序列进行插入排序

例如:将N个记录分成d个子序列
{R[1], R[1+d], R[1+2d] ....}
{R[2], R[2+d], R[2+2d] ....}
....
{R[d], R[d+d], R[d+2d] ....}

  1. 其中d为增量,他的值在排序过程中从大到小逐渐减少,最后一次排序变为1

代码

def shell_insert_sort(lists, dlta):
    # para dlta: 增量d的序列,保证最后一个为1
    for k in dlta:
        shell_insert(lists, k)


def shell_insert(lists, step):
    for i in range(step):
        for j in range(i+step, len(lists), step):
            if lists[j] >= lists[j-step]:
                continue
            lists[j], lists[j-step] = lists[j-step], lists[j]
            for j in range(j-step, i+step-1, -step):
                if lists[j] >= lists[j-step]:
                    break
                lists[j], lists[j-step] = lists[j-step], lists[j]

算法分析

希尔排序是不稳定的排序方法,即元素的相对顺序会改变。但是希尔排序的平均时间复杂度要比直接插入排序快一些,在O(n1.3)与O(n1.5)之间(来自教材)。希尔排序的优势在于减少了元素交换的次数,因为他可以以相对快的速度将大的数转移到数组的后面部分,取决于增量d的序列,因此增量d的序列的选择是算法效率好坏的关键。

归并排序

基本思想:(1)将N个记录看成n个长度为1的有序子表(2)将两两相邻的有序子表进行归并,若子表个数为奇数,最后一个子表进入下一次归并(3)重复步骤(2),直到归并成一个长度为N的有序表

代码

def merge_sort(lists):
    step, length = 1, len(lists)
    extend = [0 for i in range(length)]
    while step < length*2:
        start = 0
        while start < length:
            low, mid = start, min(start+step, length)
            high, temp = min(start+step+step, length), low
            start1, end1 = low, mid
            start2, end2 = mid, high
            while start1 < end1 and start2 < end2:
                if lists[start1] < lists[start2]:
                    extend[temp] = lists[start1]
                    temp += 1
                    start1 += 1
                else:
                    extend[temp] = lists[start2]
                    temp += 1
                    start2 += 1
            while start1 < end1:
                extend[temp] = lists[start1]
                temp += 1
                start1 += 1
            while start2 < end2:
                extend[temp] = lists[start2]
                temp += 1
                start2 += 1
            start += 2 * step
        lists, extend = extend, lists
        step += step

算法分析

归并排序有两种方式,一种自上而下,一种自下而上,我的示例代码为自下而上,也称为2路归并。归并排序是一个稳定的排序方法,每趟归并耗费O(n)的时间,归并趟数为logn,所以时间复杂度为O(nlogn)。但是也使用了额外的存储空间,空间复杂度为O(n)。

堆排序

堆排序属于选择排序,出发点是利用前一次比较的结果,减少比较次数。

了解堆排序首先需要知道堆的定义,这里用到的堆是完全二叉树,分为小根堆和大根堆。其中最大堆满足如下条件

  1. 父结点的值大于等于儿子结点

  2. 左右子树都是一个二叉堆

因为待排序的一般是序列,用序列表示如下:

A[i] >= A[2i+1]
且 A[i] > A[2
i+2]

堆排序需要解决两个问题

  1. 由一个无序序列建成一个最大(小)堆

  2. 弹出堆顶元素,调整剩余元素成为新的堆

算法步骤(大根堆)

  1. 建立大根堆

  2. 输出堆顶元素,即lists[0]与最后一个未排序的元素交换,第一次为最后一个,第二次为倒数第二个......

  3. 交换后,未排序的元素不再满足大根堆的特性,重新建堆

  4. 重复2.3两步,知道排序完成

代码

def heap_sort(lists):
    length = len(lists)
    for i in range(length//2-1, -1, -1):
        max_heap_adjust(lists, i, length)
    for i in range(length-1, 0, -1):
        lists[0], lists[i] = lists[i], lists[0]
        max_heap_adjust(lists, 0, i)

def max_heap_adjust(lists, start, end):
    while True:
        imax, ileft, iright = start, 2*start+1, 2*start+2
        if ileft < end and lists[start] < lists[ileft]:
            imax = ileft
        if iright < end and lists[imax] < lists[iright]:
            imax = iright
        if imax == start:
            break
        lists[start], lists[imax] = lists[imax], lists[start]
        start = imax

算法分析

堆排序是不稳定的排序,时间复杂度为O(nlogn)。且最慢情况下也为O(nlogn)。

这个算法需要对二叉树的一些特性有了解,不然边界情况很容易糊涂了,我自己写代码的时候少了一次循环,改来改去没发现,总觉得是对的,浪费了挺久时间。

总结

有些排序代码一下子写出来还是比较难的,但是算法更重要的是理解吧,毕竟写的这些排序算法也不太用的到,而且同样的思想和步骤也能写出不一样的代码,也会影响排序的快慢,开头也提到过了,高级语言的库里一般都自带排序算法,且速度更快,所以理解透彻就行了。下面我简单的以我写的代码测一下三个O(nlogn)时间复杂度的算法和Python内置sort()的速度快慢。序列全部random产生,同一组测试为同一个序列。

10000个元素

<function merge_sort at 0x00000245EC6AA9D8> costs : 0.03701162338256836
<function quick_sort at 0x00000245EC6AAC80> costs : 0.025029897689819336
<function heap_sort at 0x00000245EC6AAAE8> costs : 0.05319356918334961
<function python_sort at 0x00000245EC6AAE18> costs : 0.002984762191772461

50000个元素

<function merge_sort at 0x000002523ECCA9D8> costs : 0.21823906898498535
<function quick_sort at 0x000002523ECCAC80> costs : 0.1542985439300537
<function heap_sort at 0x000002523ECCAAE8> costs : 0.3872966766357422
<function python_sort at 0x000002523ECCAE18> costs : 0.016024351119995117

100000个元素

<function merge_sort at 0x0000023023FBA9D8> costs : 0.4917008876800537
<function quick_sort at 0x0000023023FBAC80> costs : 0.3395521640777588
<function heap_sort at 0x0000023023FBAAE8> costs : 0.7686326503753662
<function python_sort at 0x0000023023FBAE18> costs : 0.03353142738342285

可以看出python自带的排序sort()方法是远远快于我写的排序算法的。。。。不过快排确实是NlogN级别算法中速度最快的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 最近在看数据结构,研究了一下常用的几种排序方法:1.冒泡排序,2.选择排序,3.插入排序,4.希尔排序,5.快速排...
    wo883721阅读 1,477评论 0 4
  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    蚁前阅读 5,164评论 0 52
  • 概述:排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    每天刷两次牙阅读 3,726评论 0 15
  • 定义 插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于...
    craneyuan阅读 597评论 0 2
  • 排序算法说明 (1)排序的定义:对一序列对象根据某个关键字进行排序; 输入:n个数:a1,a2,a3,…,an 输...
    code武阅读 648评论 0 0