数据结构和排序算法-O(n)-排序算法

199 阅读2分钟

前言

经典的线性排序算法有以下三种,这三种排序算法对数据集的要求比较严格,因此应用不广。

  • 桶排序
  • 计数排序
  • 基数排序

桶排序

定义

将要排序的数据分到几个有序的桶里,每个桶里的数据再单独排序。桶内排完序后,再把桶内的数据按照顺序依次取出,组成的序列就是有序的。

数据集要求

  1. 数据可以很容易地划分成n个桶
  2. 数据在各个桶之间的分布是均匀的

这两点就就决定了桶排序的应用范围不广。

实现

//todo change
const oneBucketCapacity = 10

func BucketSort(nums []int) {
    if len(nums) == 0 {
        return
    }
    min, max := findMinAndMax(nums)
    width := max - min
    //创建桶
    len := len(nums)
    bucketSize := width / oneBucketCapacity
    if width % oneBucketCapacity != 0 {
        bucketSize += 1
    }
    buckets := make([][]int, bucketSize+1)


    //数据分发到桶
    for i := 0; i < len; i++ {
        bucketIndex := (nums[i]-min)/oneBucketCapacity
        if (nums[i]-min) % oneBucketCapacity != 0 {
            bucketIndex += 1
        }
        buckets[bucketIndex] = append(buckets[bucketIndex], nums[i])
    }

    //桶内排序,依次回放
    index := 0
    for _, bucket := range buckets {
        sort.Ints(bucket)
        for _, val := range bucket {
            nums[index] = val
            index += 1
        }
    }
}

func findMinAndMax(nums []int) (int, int) {
    min, max := nums[0], nums[0]
    for i:=1; i<len(nums); i++ {
        if min > nums[i] {
            min = nums[i]
        }
        if max < nums[i] {
            max = nums[i]
        }
    }
    return min, max
}

时间复杂度

假设sort.Ints使用的是快速排序,n为数组长度,m为桶数量,并且数据分桶均匀,则桶中数据量为k=n/m,那么时间复杂度为:O(m* klogk) = O(m * n/m * log(n/m)) = O(nlog(n/m))

  • 当m=n时,此时性能最好,时间复杂度为O(n)
  • 当m=1时,此时性能最差,时间复杂度为O(nlogn)

空间复杂度

非原地排序:O(n)

稳定性

取决于桶内排序使用的算法,如果是快排或者选择排序,则是不稳定的,其他的就是稳定的。

应用场景

内存不够时的外部排序:比如内存100M,对10G的订单数据进行排序。

  1. 遍历订单文件,找到最大值最小值
  2. 评估分桶
  3. 再次遍历订单文件,入桶
  4. 如果某些桶文件 > 100 M, 继续进行分桶,回到2
  5. 所有桶文件都 <= 100M,依次加载到内存,使用快速排序,再写回到一个新文件
  6. 该新文件订单有序

计数排序

定义

当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。

可以说,计数排序是桶排序的一种特例。

数据集要求

  1. 数据范围不大:比如高考分数、年龄这种范围
  2. 元素类型必须是非负整数:因为要映射到数组下标(数组下标>=0)

实现

func CountSort(nums []int) {
    n := len(nums)
    //1. 确定桶范围0-max
    max := nums[0]
    for i:=0; i<n; i++ {
        if max < nums[i] {
            max = nums[i]
        }
    }
    //2. 初始化桶
    bucket := make([]int, max+1)

    //3. 计数
    for i:=0; i<n; i++ {
        bucket[nums[i]] += 1
    }
    
    //4. 排名
    for i:=0; i<len(bucket)-1; i++ {
        bucket[i+1] += bucket[i]
    }
    
    //5. 计数排序关键步骤
    tmp := make([]int, n)
    for i:=n-1; i>=0; i-- {
        index := bucket[nums[i]]-1
        tmp[index] = nums[i]
        bucket[nums[i]] -= 1
    }

    //6. 赋值
    for i:=0; i<n; i++ {
        nums[i] = tmp[i]
    }
}

时间复杂度

最好最坏平均都是:O(n)

空间复杂度

非原地排序:O(n)

稳定性

计数排序是稳定的。

应用场景

  • 高考排名
  • 对用户按年龄进行排名

基数排序

数据集要求

  1. 数据可以分割出独立的位进行比较
  2. 位之间有递进关系,如果高位数据已经比较出结果,低位无需再比较
  3. 每一位的数据范围不大,可以用线性算法来排序

如果使用到计数排序,那么多一条规矩:如果数据集是数字,必须是自然数

实现

func radixSort(nums []int) {
    n := len(nums)
    //1. 找到最大值
    max := nums[0]
    for i:=1; i<n; i++ {
        if max < nums[i] {
            max = nums[i]
        }
    }
    //2. 计算位数
    bl := getNumBitLength(max)
    //3. 按位比较
    for i:=0; i<bl; i++ {
        sortByBit(nums, i)
    }
}

//要求:时间复杂度O(n),且稳定
//非常适合使用计数排序
func sortByBit(nums []int, bitIndex int) {
    n := len(nums)
    bitNums := make([]int, n)
    maxBit := 0
    for i:=0; i<n; i++ {
        bitNums[i] = getBitNum(nums[i], bitIndex)
        if maxBit < bitNums[i] {
            maxBit = bitNums[i]
        }
    }
    bucket := make([]int, maxBit+1)
    for i:=0; i<n; i++ {
        bucket[bitNums[i]] += 1
    }
    for i:=0; i<maxBit; i++ {
        bucket[i+1] += bucket[i]
    }
    tmp := make([]int, n)
    for i:=n-1; i>=0; i-- {
        index := bucket[bitNums[i]]-1
        tmp[index] = nums[i]
        bucket[bitNums[i]] -= 1
    }
    for i:=0; i<n; i++ {
        nums[i] = tmp[i]
    }
}

func getNumBitLength(num int) {
    ans := 0
    for num > 0 {
        ans += 1
        num /= 10
    }
    return ans
}

时间复杂度

最好最坏平均都是:O(n*k) = O(n)

k 即最大那个数据的位长度,n为数据量大小

空间复杂度

非原地排序:O(n)

稳定性

取决于sortByBit使用的排序算法,由于使用的计数排序,所以是稳定的。

应用场景

  • 排序电话号码