Go入门:sort包

简介: 版权声明:本文为博主原创文章,未经博主允许不得转载。https://blog.csdn.net/weixin_46618592/article/details/128916888?spm=1001.2014.3001.5501

前言

切片是Go语言中引入的用于在大多数场合替代数组的语法元素。切片是长度可变的同类型元素序列,它不支持存储不同类型的元素。
有序列的地方就有排序的需求。在各种排序算法都已经成熟的今天,我们完全可以针对特定元素类型的切片手写排序函数/方法,但多数情况下不推荐这么做,因为Go标准库内置了sort包可以很好地帮助我们实现原生类型元素切片以及自定义类型元素切片的排序任务。

一、sort包简介

Go的sort包用来排序,二分查找等操作。

二、sort包内排序原理实现

type Interface interface {
    // Len是集合中元素的个数。
    Len() int
    // Less是排序条件(索引i与j的元素对比排序)
    Less(i, j int) bool
    // Swap交换索引i和j的元素。
    Swap(i, j int)
}

// Sort按Less方法确定的升序对数据进行排序。
func Sort(data Interface) {
    n := data.Len()
    if n <= 1 {
        return
    }
    limit := bits.Len(uint(n))
    pdqsort(data, 0, n, limit)
}

入口的 Sort 函数调用的 pdqsort 并不完全是快排。

pdqsort实质为一种混合排序算法,在不同情况下切换到不同的排序机制,该实现灵感来自C++和RUST的实现,是对C++标准库算法introsort的一种改进,其理想情况下的时间复杂度为 O(n),最坏情况下的时间复杂度为 O(n* logn),不需要额外的空间。

pdqsort算法的改进在于对常见的情况做了特殊优化,其主要的思想是不断判定目前的序列情况,然后使用不同的方式和路径达到最优解;其实现就是对下面三种情况的不断循环:

  • 短序列情况:对于长度在 [0, MAX_INSERTION] 的输入,使用 insertion sort (插入排序)来进行排序后直接返回,这里的 MAX_INSERTION 我们选定为 12。
  • 最坏情况,如果发现改进的 quicksort 效果不佳(limit == 0),则后续排序都使用 heap sort 来保证最坏情况时间复杂度为 O(n*logn)。
  • 正常情况,对于其他输入,使用改进的 quicksort 来排序
func pdqsort(data Interface, a, b, limit int) {
    const maxInsertion = 12

    var (
        wasBalanced    = true // whether the last partitioning was reasonably balanced
        wasPartitioned = true // whether the slice was already partitioned
    )

    for {
        length := b - a

        if length <= maxInsertion {
            insertionSort(data, a, b)
            return
        }

        // Fall back to heapsort if too many bad choices were made.
        if limit == 0 {
            heapSort(data, a, b)
            return
        }

        // If the last partitioning was imbalanced, we need to breaking patterns.
        if !wasBalanced {
            breakPatterns(data, a, b)
            limit--
        }

        pivot, hint := choosePivot(data, a, b)
        if hint == decreasingHint {
            reverseRange(data, a, b)
            // The chosen pivot was pivot-a elements after the start of the array.
            // After reversing it is pivot-a elements before the end of the array.
            // The idea came from Rust's implementation.
            pivot = (b - 1) - (pivot - a)
            hint = increasingHint
        }

        // The slice is likely already sorted.
        if wasBalanced && wasPartitioned && hint == increasingHint {
            if partialInsertionSort(data, a, b) {
                return
            }
        }

        // Probably the slice contains many duplicate elements, partition the slice into
        // elements equal to and elements greater than the pivot.
        if a > 0 && !data.Less(a-1, pivot) {
            mid := partitionEqual(data, a, b, pivot)
            a = mid
            continue
        }

        mid, alreadyPartitioned := partition(data, a, b, pivot)
        wasPartitioned = alreadyPartitioned

        leftLen, rightLen := mid-a, b-mid
        balanceThreshold := length / 8
        if leftLen < rightLen {
            wasBalanced = leftLen >= balanceThreshold
            pdqsort(data, a, mid, limit)
            a = mid + 1
        } else {
            wasBalanced = rightLen >= balanceThreshold
            pdqsort(data, mid+1, b, limit)
            b = mid
        }
    }
}

为了更方便理解应用排序函数Sort,我们需要让被排序的切片类型实现 sort.Interface接口,以整型切片排序为例:

// IntSlice将Interface的方法附加到[]int,按递减顺序排序。
type IntSlice []int

func (x IntSlice) Len() int           { return len(x) }
func (x IntSlice) Less(i, j int) bool { return x[i] > x[j] }
func (x IntSlice) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }

func main() {
    sl := IntSlice([]int{24, 46, 81, 9, 67, 6, 5, 13})
    fmt.Println(sl) // [24, 46, 81, 9, 67, 6, 5, 13]
    
    // Sort按照less条件排序
    sort.Sort(sl)
    fmt.Println(sl) // [81 67 46 24 13 9 6 5]
}
使用 sort.Sort 函数的实现排序后,因为我们并没有重写接口的Sort方法,所以默认使用sort包里Sort函数,它使用的是 快速排序(quickSort)
我们知道快速排序是在所有数量级为(O(nlogn))的排序算法中其平均性能最好的算法,但在某些情况下其性能却并非最佳。
Go sort包中的quickSort函数也没有严格拘泥于仅使用快排算法,而是 以快速排序为主,并根据目标状况在特殊条件下选择了其他不同的排序算法,包括 堆排序(heapSort)、插入排序(insertionSort)等。

sort.Sort函数不保证排序是稳定的,要想使用稳定排序,需要使用sort.Stable函数。(保证排序的稳定性,相等元素的相对次序不变)

注:稳定排序:假定在待排序的序列中存在多个具有相同值的元素,若经过排序,这些元素的相对次序保持不变,即在原序列中,若r[i]=r[j]且r[i]在r[j]之前,在排序后的序列中,若r[i]仍在r[j]之前,则称这种排序算法是稳定的(stable);否则称为不稳定的。

三、sort包内置函数

如果我们直接使用sort.Sort函数对切片进行排序还是比较繁琐的,所以sort包提供了许多内置函数,比如:Ints、Float64s、Strings、Slice,、Sort、 SearchInts、SearchFloat64s、SearchStrings和Search等。

1、sort.Ints(x []int)

    ints := []int{1, 4, 3, 2}
    fmt.Printf("%v\n", ints) 
    sort.Ints(ints) //默认升序
    fmt.Printf("%v\n", ints) //[1 2 3 4] 
    sort.Sort(sort.Reverse(sort.IntSlice(ints))) //降序排序 
    fmt.Printf("%v\n", ints) //[4 3 2 1]
sort.Strings(x []string) sort.Float64s(x []float64)使用方法相同。

2、sort.Slice(x any, less func(i, j int) bool)

Slice函数有个好处,如果传入对象是切片,实现回调函数即可,如果传入对象是结构体,也可以自定义排序规则。

  • 传入对象是切片,实现回调函数
    slices := []int{1, 1, 4, 5, 1, 4}
    sort.Slice(slices, func(i, j int) bool {
        return slices[i] < slices[j]
    })
    fmt.Printf("%v\n", slices)//[1 1 1 4 4 5]
  • 传入对象是结构体,可以自定义排序规则
    type stu struct {
        name string
        age  int
    }

    stus := []stu{{"h", 20}, {"a", 23}, {"h", 21}}
    sort.Slice(stus, func(i, j int) bool {
        if stus[i].name == stus[j].name {
            return stus[i].age > stus[j].age // 年龄逆序
        }
        return stus[i].name < stus[j].name // 名字正序
    })
    fmt.Printf("%v\n", stus) //[{a 23} {h 21} {h 20}]

3、sort.SearchInts(a []int, x int) int

作用:用来二分查找对应值的索引值,索引值从0开始。

    arr := []int{1, 2, 3, 4, 5, 6, 7}
    idx := sort.SearchInts(arr, 4)
    fmt.Printf("%v\n", idx) // 3
sort.SearchFloat64s(a []float64, x float64) int sort.SearchStrings(a []string, x string) int 功能同上。

4、sort.Search(n int, f func(int) bool) int

作用:自定义的二分查找,需要自己实现查找条件

arr := []int{1, 2, 3, 4, 5, 6, 7}
    idx := sort.Search(len(arr), func(i int) bool {
        return arr[i] > 4
    })
    fmt.Printf("%v\n", idx) //4
目录
相关文章
|
Go 开发者
Go语言包的组织与导入 -《Go语言实战指南》
本章详细介绍了Go语言中的包(Package)概念及其使用方法。包是实现代码模块化、复用性和可维护性的核心单位,内容涵盖包的基本定义、命名规则、组织结构以及导入方式。通过示例说明了如何创建和调用包,并深入讲解了`go.mod`文件对包路径的管理。此外,还提供了多种导入技巧,如别名导入、匿名导入等,帮助开发者优化代码结构与可读性。最后以表格形式总结了关键点,便于快速回顾和应用。
539 61
|
人工智能 安全 算法
Go入门实战:并发模式的使用
本文详细探讨了Go语言的并发模式,包括Goroutine、Channel、Mutex和WaitGroup等核心概念。通过具体代码实例与详细解释,介绍了这些模式的原理及应用。同时分析了未来发展趋势与挑战,如更高效的并发控制、更好的并发安全及性能优化。Go语言凭借其优秀的并发性能,在现代编程中备受青睐。
519 33
|
10月前
|
Cloud Native 安全 Java
Go语言深度解析:从入门到精通的完整指南
🌟蒋星熠Jaxonic,Go语言探索者。深耕云计算、微服务与并发编程,以代码为笔,在二进制星河中书写极客诗篇。分享Go核心原理、性能优化与实战架构,助力开发者掌握云原生时代利器。#Go语言 #并发编程 #性能优化
723 43
Go语言深度解析:从入门到精通的完整指南
|
JSON 中间件 Go
Go语言实战指南 —— Go中的反射机制:reflect 包使用
Go语言中的反射机制通过`reflect`包实现,允许程序在运行时动态检查变量类型、获取或设置值、调用方法等。它适用于初中级开发者深入理解Go的动态能力,帮助构建通用工具、中间件和ORM系统等。
770 63
|
10月前
|
Java 编译器 Go
【Golang】(1)Go的运行流程步骤与包的概念
初次上手Go语言!先来了解它的运行流程吧! 在Go中对包的概念又有怎样不同的见解呢?
456 4
|
11月前
|
Cloud Native 安全 Java
Go语言深度解析:从入门到精通的完整指南
🌟 蒋星熠Jaxonic,执着的星际旅人,用Go语言编写代码诗篇。🚀 Go语言以简洁、高效、并发为核心,助力云计算与微服务革新。📚 本文详解Go语法、并发模型、性能优化与实战案例,助你掌握现代编程精髓。🌌 从goroutine到channel,从内存优化到高并发架构,全面解析Go的强大力量。🔧 实战构建高性能Web服务,展现Go在云原生时代的无限可能。✨ 附技术对比、最佳实践与生态全景,带你踏上Go语言的星辰征途。#Go语言 #并发编程 #云原生 #性能优化
|
Go 持续交付 开发者
Go语言包与模块(module)的基本使用-《Go语言实战指南》
本章深入讲解Go语言中的包(Package)和模块(Module)概念。包是代码组织的最小单位,每个`.go`文件属于一个包,通过`import`实现复用;主程序包需命名为`main`。模块是Go 1.11引入的依赖管理机制,支持自动版本管理和私有/远程仓库,无需依赖GOPATH。通过实际示例,如自定义包`mathutil`和第三方模块`gin`的引入,展示其使用方法。常用命令包括`go mod init`、`go mod tidy`等,帮助开发者高效管理项目依赖。最后总结,包负责功能划分,模块实现现代化依赖管理,提升团队协作效率。
535 15
|
缓存 监控 安全
告别缓存击穿!Go 语言中的防并发神器:singleflight 包深度解析
在高并发场景中,多个请求同时访问同一资源易导致缓存击穿、数据库压力过大。Go 语言提供的 `singleflight` 包可将相同 key 的请求合并,仅执行一次实际操作,其余请求共享结果,有效降低系统负载。本文详解其原理、实现及典型应用场景,并附示例代码,助你掌握高并发优化技巧。
808 0
|
存储 算法 数据可视化
【二叉树遍历入门:从中序遍历到层序与右视图】【LeetCode 热题100】94:二叉树的中序遍历、102:二叉树的层序遍历、199:二叉树的右视图(详细解析)(Go语言版)
本文详细解析了二叉树的三种经典遍历方式:中序遍历(94题)、层序遍历(102题)和右视图(199题)。通过递归与迭代实现中序遍历,深入理解深度优先搜索(DFS);借助队列完成层序遍历和右视图,掌握广度优先搜索(BFS)。文章对比DFS与BFS的思维方式,总结不同遍历的应用场景,为后续构造树结构奠定基础。
734 10
|
存储 Go
Go 语言入门指南:切片
Golang中的切片(Slice)是基于数组的动态序列,支持变长操作。它由指针、长度和容量三部分组成,底层引用一个连续的数组片段。切片提供灵活的增减元素功能,语法形式为`[]T`,其中T为元素类型。相比固定长度的数组,切片更常用,允许动态调整大小,并且多个切片可以共享同一底层数组。通过内置的`make`函数可创建指定长度和容量的切片。需要注意的是,切片不能直接比较,只能与`nil`比较,且空切片的长度为0。
503 3
Go 语言入门指南:切片

热门文章

最新文章