golang 写个快速排序

追风骚年 · · 469 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

快速排序是大多数语言内置 sort 函数的默认实现方式,简单可分为两路排序和三路排序,我在相关资料中,发现两路排序也有多种实现方式。

有些语言 sort 函数会包含 快速 希尔 插入 多种形式。

排序描述

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

排序实现

两路快排

两路快排的逻辑

严蔚敏版

这个名词来自于 b 站的评论,这个快排思路很容易理解,非常适合入门

func quickSortV1(arr []int, low, hight int) {
    // 当 low = hight  时跳出
    if low >= hight {
        return
    }

    left, right := low, hight
    pivot := arr[left] // 为了简单起见,直接取左边的第一个数

    for left < right {
        // 先从右边开始迭代

        // 右边的数如果比pivot大,那么就应该将他放在右边,继续向左滑动,遇到一个比他小的为止
        for left < right && arr[right] >= pivot {
            right--
        }

        // 小数移动到左边
        if left < right {
            arr[left] = arr[right]
        }

        // 左边的数如果比pivot小,那么就应该将他放在左边,继续向右滑动,遇到一个比他大的为止
        for left < right && arr[left] < pivot {
            left++
        }

        // 大数移动到又边
        if left < right {
            arr[right] = arr[left]
        }

        // left == right ,pivot 即是最终位置
        if left <= right {
            arr[left] = pivot
        }
    }

    //因为 pivot 的最终位置已锁定

    // 继续排序左边部分
    quickSortV1(arr, low, right-1)
    // 继续排序右边部分
    quickSortV1(arr, right+1, hight)
}

优化版2

func quickSortV2(arr []int, low, hight int) {
    if low < hight {
        left, right := low, hight
        pivot := arr[(low+hight)/2] // 这里的经验值取的是中间数,经过 Benchmark 测试,确实比较优秀

        for left <= right {
            // 从左边开始迭代

            // 左边的数如果比 pivot 小,那么就应该将他放在左边,继续向右滑动,遇到一个比他大的为止
            for arr[left] < pivot {
                left++
            }

            // 右边的数如果比 pivot 大,那么就应该将他放在右边,继续向左滑动,遇到一个比他小的为止
            for arr[right] > pivot {
                right--
            }

            // 这里进行一次交换,将上面碰到的大数和小数交换一次
            //left 继续右走,right 继续左走 注意这里还不一定相遇,去继续执行上面的逻辑
            if left <= right {
                arr[left], arr[right] = arr[right], arr[left]
                left++
                right--
            }
        }

        // 【 xxx[xxxxx]xxxxxx】
        // [ xxx【xxxxx】xxxxxx]
        // [] => left,right
        // 【】 => low,hight
        // 这里其实挺费解? 但是如果 right 在 low 左侧,那么我们排序的访问就不是 low到 hight 这一段切片
        // 丧失了排序一段 quickSort 中参数的意义
        // 如果想不通也可以去掉,让递归到下一层调用栈中弹出
        if low < right {
            quickSortV2(arr, low, right)
        }

        // 同理
        if hight > left {
            quickSortV2(arr, left, hight)
        }
    }
}

大体上和第一个版本差不多,但是函数更加简洁了,

优化版3

func quickSortV3(arr []int, left, right int) {
    if left >= right {
        return
    }
    cur, lo := left+1, left
    for cur <= right {
        if arr[cur] <= arr[left] {
            arr[lo+1], arr[cur] = arr[cur], arr[lo+1]
            lo++
        }
        cur++
    }
    arr[left], arr[lo] = arr[lo], arr[left]
    quickSortV3(arr, left, lo-1)
    quickSortV3(arr, lo+1, right)
}

这个版本是所有快速排序中,看起来比较难以理解,只有一个指针,从左到右滑动,设计非常巧妙。

pivot 经验值对结果的影响

func BenchmarkQuickSortV2(b *testing.B) {
    arr := rand.Perm(math.MaxInt16)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        quickSortV2(arr, 0, len(arr)-1)
    }
    // pivot center: BenchmarkQuickSortV2-8         2529        427417 ns/op           0 B/op          0 allocs/op
    // pivot left:  BenchmarkQuickSortV2-8           100     211982204 ns/op           0 B/op          0 allocs/op
    // pivot right: BenchmarkQuickSortV2-8           100     258063634 ns/op           0 B/op          0 allocs/op
    // pivot random: BenchmarkQuickSortV2-8          913       1303080 ns/op           0 B/op          0 allocs/op

}

这边测试了四种情况,中间值最优。

三路快排

func quickSort3(arr []int, left, right int) {

    if left >= right {
        return
    }

    pivot := arr[left]
    lo, gt, cur := left, right+1, left+1

    for cur < gt {
        if arr[cur] < pivot {
            arr[cur], arr[lo+1] = arr[lo+1], arr[cur]
            lo++
            cur++
        } else if arr[cur] > pivot {
            arr[cur], arr[gt-1] = arr[gt-1], arr[cur]
            gt--
        } else {
            cur++
        }
    }

    arr[left], arr[lo] = arr[lo], arr[left]
    quickSort3(arr, left, lo-1)
    quickSort3(arr, gt, right)
}

用了3个指针,表示小于,等于,大于三个部分,从而减少相等的数在其中来回交换。

总结

由此可见快速排序是一种不稳定的排序,对于数据本身是有要求,对于 pivot 如何取也是有要求,属于经验取值了,如果对于源数据是逆序的情形,快排会退化成冒泡。

参考文档


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:追风骚年

查看原文:golang 写个快速排序

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

469 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传