排序(一)比较排序

全文所说排序默认升序

  • 插入排序
    • 直接插入
    • 二分插入
    • 希尔排序
  • 交换排序
    • 冒泡排序
    • 快速排序
  • 选择排序
    • 简单选择排序
    • 堆排序
  • 归并排序

一、插入排序
选择一个元素插入到有序序列中去,使序列仍保持有序

1.直接插入排序:从序列中选择一个元素 p ,依次和它前面的每个元素 f 比较,如果 p < f,就把 f 向后移动,直到最终 p >= f,则将p的值放在当前 f 的后面

func SInsertSort(arr []int) {
    for i:=1;i<len(arr);i++ {
        p := arr[i]
        j := i-1
        for j>=0 && p < arr[j] {
            arr[j+1] = arr[j]
            j--
        }
        arr[j+1] = p
    }
}

最坏时间复杂度:元素arr[i]要和前面的每一个元素比较,比较次数为 i*i。
所以 T_w(n) =1 + 2 +... + n-1 = O(n^2) = O(n^2)
平均时间复杂度:因为arr[i]的平均比较次数为 \frac{1+i}{2}
所以 T_e(n)\frac{1}{2}T_w(n) = O(n^2)
空间复杂度:S(n) = O(1)
稳定性:稳定(原来相等的元素相对位置不会边)

2.二分插入排序:
利用二分查找确定应该插入的位置,可以较少比较次数,但是元素移动次数不会减少

func BInsertSort(arr []int) {
    for i := 1; i < len(arr); i++ {
        p := arr[i]
        left := 0
        right := i - 1

        for right >= left {
            mid := (left + right) / 2
            if p < arr[mid] {
                right = mid - 1
            } else {
                left = mid + 1
            }
        }

        for j := i - 1; j >= left; j-- { //left就是p应该放置的位置
            arr[j+1] = arr[j]
        }
        arr[left] = p
    }

    fmt.Println("二分插入", arr)
}

left就是p最终放置位置的原因:
right >= left 最后一次为true时,只能是right-left == 0;或者right-left=1;分析这两种情况,可以发现left都应该是最终放置位置。

因为只是减少了一些比较的次数,移动次数还是不变,所以耗时是要少一些,但复杂度肯定仍旧是O(n^2)
最坏时间复杂度T_w(n) = = O(n^2)
平均时间复杂度T_e(n) = O(n^2)
空间复杂度:S(n) = O(1)
稳定性:稳定(原来相等的元素相对位置不会边)

3.希尔排序:二分和直插的移动过程,都是每次移动一步,比如第9个元素,确定其最终该放置在arr[2],那就要依次移动[2,8]的所有元素,要移动7次。希尔排序的出发点是希望降低移动次数。希尔排序是设计想法是多次排序,刚开始每次移动一大步,使其离最终位置比较接近。

过程:确定一组降序的增量序列,比如9,5,3,1,依次用来对原序列进行排序。比如用9排序,将如下位置的元素排序
[0] [9] [18] [27]... 排序,假如为abcd,排序后变成[0]=c, [9]=a, [18]=b, [27]=d
[1] [10] [19] [28]... 排序
[2] [11] [20] [29]... 排序
....

用增量5对新序列再次排序
[0] [5] [10] [15]...
[1] [6] [11] [16]...
[2] [7] [12] [17]...

微信图片_20220822182227.jpg

5排序就是把afk三个之间交换位置,bgl三个之间交换位置...

func ShellSort(arr []int) {
    ds := []int{9, 5, 3, 1}

    for i := 0; i < len(ds); i++ {
        d := ds[i]

        for j := d; j < len(arr); j++ {
            p := arr[j]

            k := j - d
            for k >= 0 && p < arr[k] { // 对每个子序列进行直接插入排序,也可以改成二分或其他
                arr[k+d] = arr[k]
                k -= d
            }

            arr[k+d] = p
        }
    }

    fmt.Println("希尔排序", arr)
}
// 希尔+二分插入
func ShellSort(arr []int) {
    ds := []int{3, 1}

    for i := 0; i < len(ds); i++ {
        d := ds[i]

        for j := d; j < len(arr); j++ {
            p := arr[j]

            left := j % d
            right := j - d
            for right >= left {
                mid := ((right-left)/d/2)*d + left
                if p < arr[mid] {
                    right = mid - d
                } else {
                    left = mid + d
                }
            }

            for k := j - d; k >= left; k -= d {
                arr[k+d] = arr[k]
            }
            arr[left] = p
        }
    }

    fmt.Println("希尔排序", arr)

}

增量序列的选取要求:

  • d[i] \le \sqrt{arr.length}
  • 递减
  • 最后一个值必须是1

时间复杂度: 与增量序列的选取有关,分析难度较大,下面是几个序列的结论

image.png

空间复杂度:S(n) = O(1)
稳定性:不稳定。
比如 1,3,2,2' 增量序列选 2,1,得到 1 2' 2 3

二、交换排序
比较元素的大小,根据结果交换位置

1.冒泡排序
下降法:从开头向后扫描,将大的放到后面去
上升法:从末尾向前扫描,将小的放到前面去

func BubbleSort() {
    arr := []int{
        9, 5, 47, 11, 19, 6, 34, 31, 2, 10,
    }

    for sortedLen := 0; sortedLen < len(arr)-1; sortedLen++ {
        for j := len(arr) - 1; j >= 1+sortedLen; j-- {
            p := arr[j]
            if p < arr[j-1] {// 上升法,把小的换到前
                arr[j] = arr[j-1]
                arr[j-1] = p
            }
        }
    }

    fmt.Println("冒泡排序", arr)
}

缺点:该序列已经全部有序时,程序不能感知到。比如 1,2,5,3,第一次上升后就已经全部有序了。
改进:

func BubbleSort() {
    arr := []int{
        9, 5, 47, 11, 19, 6, 34, 31, 2, 10,
    }

    sortedLen := 0
    hasUnSorted := true

    for hasUnSorted {
        hasUnSorted = false
        for j := len(arr) - 1; j >= 1+sortedLen; j-- {
            p := arr[j]
            if p < arr[j-1] {
                arr[j] = arr[j-1]
                arr[j-1] = p
                hasUnSorted = true
            }
        }

        sortedLen++
    }

    fmt.Println("冒泡排序", arr)

}

该版本还有缺点,每次可能还会和已经有序的部分比较一下。比如4,5,6,9,8,7,第一轮从头到尾交换后,得到4,5,6,7,9,8, 第二轮比较就应该在7之后的位置停止

下面是一个改进,记录了上一轮冒泡将最小元素放在了x处,下一次上升的上限就应该是停止处之后(记作x+1)。

func BubbleSort() {
    arr := []int{
        9, 5, 47, 11, 19, 6, 34, 31, 2, 10,
    }

    i := 0
    for i <= len(arr)-2 {
        j := len(arr) - 2
        for j >= i {
            p := arr[j]
            if p > arr[j+1] {
                arr[j] = arr[j+1]
                arr[j+1] = p
            }
            j--
        }
        i = j + 2
    }

    fmt.Println("冒泡排序", arr)
}

最坏时间复杂度:任意两个元素之间都逆序,最多就有\frac{n(n-1)}{2}个逆序,每次交换能减少一个逆序。 T_w(n) = O(\frac{n(n-1)}{2})≈\frac{n^2}{2}

平均时间复杂度: 逆序平均为\frac{\frac{n(n-1)}{2}+0}{2},所以T_e(n) = \frac{1}{2}T_w(n)\frac{n^2}{4}
空间复杂度:S(n) = O(1)
稳定性:稳定(原来相等的元素相对位置不会边)

2.快速排序
快速排序,任意选择一个元素,将比它大的放在其后,比它小的放在它前面。然后再分别对前后两端再次执行此操作。

func QuickSort(arr []int, i int, j int) {
    v := arr[i]
    pv := i
    d := "end"

    for j > i {
        if d == "end" {
            if arr[j] < v {
                arr[i] = arr[j]
                pv = j
                i++
                d = "start"
            } else {
                j--
            }
        }

        if d == "start" {
            if arr[i] > v {
                arr[j] = arr[i]
                pv = i
                j--
                d = "end"
            } else {
                i++
            }

        }
    }

    arr[pv] = v

    if pv-1-i > 0 {
        QuickSort(arr, i, pv-1)
    }
    if j-pv-1 > 0 {
        QuickSort(arr, pv+1, j)
    }

}

arr := []int{
        9, 5, 47, 11, 19, 6, 34, 31, 2, 10,
    }
QuickSort(arr, 0, len(arr)-1)
fmt.Println(arr)

最好时间复杂度:O(nlogn)
最坏时间复杂度:O(n^2)
平均时间复杂度:O(nlogn)
空间复杂度:O(logn)

image.png

三、选择排序
每次选出最大或最小的放到后面或前面

1.简单选择排序

func SimpleSelectSort(arr []int)  {
    for i:=0;i<len(arr)-1;i++ {
        min := i 
        for j:=i+1;j<len(arr);j++ {
            if arr[j] < arr[min] {
                min = j
            }
        }

        v := arr[i]
        arr[i] = arr[min]
        arr[min] = v 
    }
}

时间复杂度T(n) = \frac{n(n-1)}{2}
空间复杂度:S(n) = O(1)
稳定性:稳定(原来相等的元素相对位置不会边)

2.堆排序
堆是一种特殊的完全二叉树,如果 父节点p\ge子节点c,称大根堆。父节点p\le子节点c,称小根堆。

堆排序就是将序列排成堆(利用顺序存储的话,只需要调整序列位置即可),这样根结点就是最大值(大根堆),然后将根结点和尾结点交换,这样就最大元素就放在了正确的位置上,然后再重新堆化前面的序列,再选出当前堆的最大,放到倒数第二个位置上...

举例说明:
原始序列[]int{9, 5, 47, 11, 19, 6, 34, 31, 2, 10,}待使用堆排序方法排序。
将其看作一棵顺序存储的完全二叉树

微信图片_20220824170807.jpg

从最后一个非叶结点开始向前遍扫描,遇到不符合堆序的就调整顺序。

  • arr[4]=19,符合堆序(大于它的所有子节点)
  • arr[3]=11,不符合堆序,arr[3] < arr[7],交换arr[3]和arr[7],得到arr[3]=31,arr[7]=11
  • arr[2]=47,符合堆序
  • arr[1]=5,不符合堆序,arr[1]<arr[3]=31,交换arr[1]和arr[3],得到arr[1]=31,arr[3]=5。此时arr[3]=5<arr[7]=11,再次不符合堆序,交换arr[3]和arr[7],得到arr[3]=11,arr[7]=5
  • arr[0]=9,不符合堆序,arr[0]<arr[2]...

这样就得到了堆化好的序列 []int{47,31,34,11,19,6,9,5,2,10,}

微信图片_20220824172148.jpg

完成了堆化之后,只需要重复 “找最大值(即当前堆根)” + “和堆尾元素交换位置” + “堆长度-1” + “重新堆化”,n-1遍即可。
第一步,交换arr[0]和arr[9],堆范围从arr[0]~arr[9]变成arr[0]~arr[8]
第二步,重新堆化,只需从根开始调整就行
重复...

func HeapSort() {
    arr := []int{
        9, 5, 47, 11, 19, 6, 34, 31, 2, 10,
    }

    for i := (len(arr) - 2) / 2; i >= 0; i-- {
        // (len(arr) - 2)/2是最后一个结点的父节点,也就是最后一个非叶结点
        heapfy(arr, i, len(arr)-1)
    }

    for j := 0; j < len(arr)-1; j++ {
        max := arr[0]
        arr[0] = arr[len(arr)-1-j]
        arr[len(arr)-1-j] = max
        heapfy(arr, 0, len(arr)-1-j-1)
    }

    fmt.Println("堆排序", arr)
}
func heapfy(arr []int, root int, end int) {
    leftSon := 2*root + 1
    if leftSon > end {
        return
    }

    rightSon := 2*root + 2
    max := leftSon
    v := arr[root]

    if rightSon <= end && arr[rightSon] > arr[leftSon] {
        max = rightSon
    }

    if v < arr[max] {
        arr[root] = arr[max]
        arr[max] = v
        if max*2+1 <= end {
            heapfy(arr, max, end)
        }
    }
}

完全二叉树的顺序存储,通常从数组位置1开始存储,这样比较方便计算父子结点位置。如果一个结点在数组中的index是i,那其左子结点是2i,右子是2i+1,父结点是i/2(向下取整)。如果要从0开始存储的话,每个元素的index都会减少1,i变成i-1,设i-1=k,2i变成2i-1=2k+1,... 可以推出新的关系是 原结点k,左子节点2k+1, 右子结点2k+2 父结点 (k-1)/2 (下整)

平均时间复杂度:O(nlogn)
最坏时间复杂度:O(nlogn)
空间复杂度:O(1)
稳定性:不稳定

归并排序
归并排序是利用分治思想,欲排序abcd,先将ab,cd分别排序,然后再合并两个有序表。

func MergeSort(arr []int) {
    if len(arr) < 2 {
        return
    }
    if len(arr) == 2 {
        v := arr[0]
        if v > arr[1] {
            arr[0] = arr[1]
            arr[1] = arr[0]
        }
        return
    }
    mid := len(arr) / 2
    MergeSort(arr[:mid])
    MergeSort(arr[mid:])
    merge(arr)
}

func merge(arr []int) {
    mid := len(arr) / 2
    i := 0
    j := mid
    k := 0

    tempArr := make([]int, len(arr))
    for i < mid && j < len(arr) {
        if arr[i] <= arr[j] {
            tempArr[k] = arr[i]
            i++
            k++
        } else {
            tempArr[k] = arr[j]
            j++
            k++
        }
    }
    for i < mid {
        tempArr[k] = arr[i]
        i++
        k++
    }
    for j < len(arr) {
        tempArr[k] = arr[j]
        j++
        k++
    }
    for i2, v := range tempArr {
        arr[i2] = v
    }
}

非递归版本如下:

func MergeSort(arr []int) {
    segLen := 1
    for segLen < len(arr) {
        for i := 0; i < len(arr); i += 2 * segLen {
            merge(arr, i, i+segLen)
        }
        segLen *= 2
    }
    fmt.Println("归并排序", arr)
}

func merge(arr []int, start1 int, start2 int) {
    if start2 > len(arr)-1 {
        return
    }

    segLen := start2 - start1
    end2 := start2 + segLen - 1
    if end2 > len(arr)-1 {
        end2 = len(arr) - 1
    }

    p1 := start1
    p2 := start2
    i := 0
    tempArr := make([]int, end2-start1+1)
    for p1 < start2 && p2 <= end2 {
        if arr[p1] <= arr[p2] {
            tempArr[i] = arr[p1]
            i++
            p1++
        } else {
            tempArr[i] = arr[p2]
            i++
            p2++
        }
    }
    for p1 < start2 {
        tempArr[i] = arr[p1]
        i++
        p1++
    }
    for p2 <= end2 {
        tempArr[i] = arr[p2]
        i++
        p2++
    }
    for j := 0; j < len(tempArr); j++ {
        arr[start1+j] = tempArr[j]
    }
}

时间复杂度:O(nlogn)
空间复杂度:O(n)
稳定性:稳定

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

推荐阅读更多精彩内容