Go:实现经典排序算法

经典排序算法

  1. 排序算法在时间复杂度上分为三个档次:O(n),O(nlgn),O(n^2)
  2. 排序算法的稳定性。如果待排序的列表中存在相同排序值的元素,在排序前后相同排序值的元素排序后相对位置不变。
  3. 是否原地排序。也就是说算法是否需要额外空间。

这里的例子都是递减的排序,按时间复杂度分为了三个类别

1. O(n^2)

1.1 冒泡排序

冒泡排序原理
冒泡算法解析
冒泡比较简单,每次选出子序列的最大值,放置最前端。

最优时间复杂度 最坏时间复杂度 时间复杂度 空间复杂度 稳定排序 原地排序
O(n) O(n^2) O(n^2) O(1)
package main

func bubbleSort(nums []int) {
    for i := 0; i < len(nums); i++ {
        for j := i + 1; j < len(nums); j++ {
            if nums[i] < nums[j] {
                nums[i], nums[j] = nums[j], nums[i]
            }
        }
    }
}

1.2 插入排序

插入排序原理
插入排序解析
插入排序, 类似拿扑克的方式。0~i-1 区间保持有序,循环遍历将第i个元素插入有序区间。

最优时间复杂度 最坏时间复杂度 时间复杂度 空间复杂度 稳定排序 原地排序
O(n) O(n^2) O(n^2) O(1)
package main

func insertSort(nums []int) {
    for i := 1; i < len(nums); i++ {
        key := nums[i]
        // 保证 0 ~ i-1 有序
        j := i - 1
        for j >= 0 && nums[j] < key {
            nums[j + 1] = nums[j]
            j--
        } 
        // 填坑 插入位置
        nums[j + 1] = key
    }
}

1.3 选择排序

选择排序原理
选择排序解析
选择排序跟插入排序的相似点在于也是要区分两个区间。选择排序是交换元素而不是移动。

最优时间复杂度 最坏时间复杂度 时间复杂度 空间复杂度 稳定排序 原地排序
O(n^2) O(n^2) O(n^2) O(1)
package main

func selectSort(nums []int) {
    // start 为无序起始位置 max 为区间最大值的位置
    start, max := 0, 0
    for i := 0; i < len(nums); i++ {
        // 找出区间最大值 max
        for j := i; j < len(nums); j++ {
            if nums[j] > nums[max] {
                max = j
            }
        }
        // 筛出区间最大元素放入左边
        if nums[max] > nums[start] {
            nums[max], nums[start] = nums[start], nums[max]
        }
        max = start + 1
        start++
    }
}

1.4 希尔排序

希尔排序原理
希尔排序解析
改良版本的插入排序,把步长 step 替换为1,发现和插入排序一摸一样。

最优时间复杂度 最坏时间复杂度 时间复杂度 空间复杂度 稳定排序 原地排序
O(n) O(n^2) O(nlgn) O(1)
package main

func shellSort(nums []int) {
    // step 为步长 每次对半分 ps: 按维基百科介绍有比较好的 step 公式,这里取一个比较简单的规则
    step := len(nums) >> 1
    for step > 0 {
        // 步长内插入排序 注意是从后到前
        for i := step; i < len(nums); i++ {
            // 每列最后一个元素
            key := nums[i]
            j := i - step
            // 按步长
            for j >= step - 1 && nums[j] < key {
                nums[j + step] = nums[j]
                j -= step
            }
            nums[j + step] = key
        }
        step = step >> 1
    }
}

2. O(nlgn)

2.1 快速排序

快速排序原理
快速排序解析
快速排序运用的也是分治的思想。挑选基准值,然后把小于基准值的放左边,大于的放右边。最后迭代到1的时候就是排序结束。

最优时间复杂度 最坏时间复杂度 平均时间复杂度 空间复杂度 稳定排序 原地排序
O(nlgn) O(n^2) O(nlgn) O(lgn)
package main

func partition(nums []int, p, q int) int {
    // 基准值的选择 有更优方式 这里简化
    // partition 的方式实现的有点巧妙,可以想象成如何用O(n) 的算法把序列按给定数字 分成大于和小于的两份。
    // 类似于选择排序 所以是不稳定的 采用双指针思想 i 记录分割点,j 遍历交换。
    base := nums[q]
    // i 记录按大小划分的位置
    i := p - 1
    for j := p; j < q-1; j++ {
        if nums[j] < base {
            i++
            nums[j], nums[i] = nums[i], nums[j]
        }
    }
    nums[i+1], nums[q] = nums[q], nums[i+1]
    return i + 1
}

func helper(nums []int, p, r int){
    if p >= r {
        return
    }
    m := partition(nums, p, r)
    helper(nums, p, m-1)
    helper(nums, m+1, r)
}

func quickSort(nums []int){
    helper(nums, 0, len(nums) - 1)
}

2.2 归并排序

归并排序原理
归并排序解析
归并排序使用了分治的思想。可以用递归实现也可以用迭代实现。
分:分解待排序的 n 个元素的序列成各具 n/2 个元素的两个子序列
解决:排序子序列
合:合并两个已排序的子序列

最优时间复杂度 最坏时间复杂度 平均时间复杂度 空间复杂度 稳定排序 原地排序
O(nlgn) O(nlgn) O(nlgn) O(nlgn)
package main

func helper(nums []int, p, q int) {
    if q <= p {
        return 
    }
    r := (q + p) >> 1
    helper(nums, p, r)
    helper(nums, r+1, q)
    merge(nums, p, q, r)
}

func merge(nums []int, p, q, r int) {
    i, j := p, r+1 
    var res []int
    for i <= r && j <= q {
        if nums[i] > nums[j] {
            res = append(res, nums[i])
            i++
        } else {
            res = append(res, nums[j])
            j++
        }
    }
    for i <= r {
        res = append(res, nums[i])
        i++
    }
    for j <= q {
        res = append(res, nums[j])
        j++       
    }
    // 已排序区间
    for i := 0; i < q-p+1; i++ {
        nums[p+i] = res[i]
    }
}

func mergeSort(nums []int) {
    helper(nums, 0, len(nums)-1)
}

2.3 堆排序

堆排序原理
堆排序解析
主要需要了解堆这个数据结构,以及如何构建。由于我们的排序,都是降序,这里讨论小顶堆。

由于堆是完全二叉树,通常堆是通过一维数组来实现的。在数组起始位置为0的情形中:
父节点i的左子节点在位置 (2i+1)
父节点i的右子节点在位置 (2i+2)
子节点i的父节点在位置 floor(i/2)

大顶堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
创建最大堆(Build Max Heap):将堆中的所有数据重新排序
堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算

核心点在于堆化的实现(heapify)。

最优时间复杂度 最坏时间复杂度 平均时间复杂度 空间复杂度 稳定排序 原地排序
O(nlgn) O(nlgn) O(nlgn) O(1)
package main

// 自底向上 使每个点 满足堆条件
func siftUp(nums []int, i, end int) {
    smallest := i
    l := 2*i + 1
    r := 2*i + 2
    if end >= r && nums[i] > nums[r] {
        smallest = r
    }
    if end >= l && nums[smallest] > nums[l] {
        smallest = l
    }
    if i != smallest {
        nums[i], nums[smallest] = nums[smallest], nums[i]
        siftUp(nums, smallest, end)
    }
}

func heapify(nums []int) {
    base := len(nums) / 2 - 1
    // 从第一个非叶子节点开始 直到 root
    for i := base; i >= 0; i-- {
        siftUp(nums, i, len(nums) - 1)
    }
}

func heapSort(nums []int) {
    // 构建小顶堆 堆顶为最大值 0~i 依次取堆顶 即完成排序
    heapify(nums)
    for i := len(nums) - 1; i >= 0; i-- {
        // 删除元素 堆尾放回堆顶 重新构造 最小值放堆尾进行排序
        nums[i], nums[0] = nums[0], nums[i]
        siftUp(nums, 0, i-1)
    }
}

3. O(n) TODO

3.1 基数排序

基数排序原理
基数排序解析

3.2 计数排序

计数排序解析

3.3 桶排序

桶排序解析

总结

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

推荐阅读更多精彩内容

  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    蚁前阅读 5,155评论 0 52
  • 话不多数,先上两张图: 名词解释: n:数据规模k:“桶”的个数In-place:占用常数内存,不占用额外内存Ou...
    牛奶芝麻阅读 35,090评论 6 44
  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,636评论 0 13
  • 该系列文章主要是记录下自己暑假这段时间的学习笔记,暑期也在实习,抽空学了很多,每个方面的知识我都会另起一篇博客去记...
    Yanci516阅读 12,160评论 6 19
  • 羊年(2015年)端午节的头日,阿金在网上发出了含自己的亲人和老家岭下众兄弟弟媳们及所有网友端午节快乐的祝福。遗憾...
    镇南方良金阅读 135评论 0 1