排序【5.1】桶排序&&基数排序&&计数排序

1、桶排序(Bucket Sort)

(1)基本思想

桶排序的基本思想是将一个数据表分割成许多buckets,然后每个bucket各自排序,或用不同的排序算法,或者递归的使用bucket sort算法。也是典型的divide-and-conquer分而治之的策略。它是一个分布式的排序,介于MSD基数排序和LSD基数排序之间。

(2)基本流程

建立一堆buckets;
遍历原始数组,并将数据放入到各自的buckets当中;
对非空的buckets进行排序;
按照顺序遍历这些buckets并放回到原始数组中即可构成排序后的数组。

图示
在这里插入图片描述

(3)算法复杂度

桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。

对N个关键字进行桶排序的时间复杂度分为两个部分
(1) 循环计算每个关键字的桶映射函数,这个时间复杂度是 O ( N ) O(N)
(2) 利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为 O ( N i l o g N i ) ∑O(N_i*logN_i) 。其中 N i N_i 为第i个桶的数据量。

很显然,
第(2)部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(N*logN)了。因此,我们需要尽量做到下面两点:
(1) 映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。
(2) 尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。 当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。

对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:
O ( N ) + O ( M ( N M ) l o g ( N M ) ) = O ( N + N ( l o g N l o g M ) ) = O ( N + N l o g N N l o g M ) O(N)+O(M*({N\over M})*log({N\over M}))=O(N+N*(logN-logM))=O(N+N*logN-N*logM) 当N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。

总结:桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。当然桶排序的空间复杂度为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。

(4)算法实现(python3)
"""
桶排序:

基本思想:
将一个数据表分割成许多buckets,然后每个bucket各自排序,或用不同的排序算法,或者递归的使用bucket sort算法。
也是典型的divide-and-conquer分而治之的策略。它是一个分布式的排序。

时间复杂度:O(N)+O(M*(N/M)*log(N/M)) = O(N+N*(logN-logM)) = O(N+N*logN-N*logM)
"""
class node(object):
    def __init__(self, initdata):
        self.data = initdata
        self.next = None


def getBucketIndex(value):
    return value//interval

def printBuckets(bucket):
    cur = bucket
    while cur:
        print(cur.data, " ",end = "")
        cur = cur.next
    print("\n")

def bucketSort(data):
    bucket = []
    for i in range(n_bucket):
        bucket.append(None)

    for i in range(n_data):
        pos = getBucketIndex(data[i])      # O(N) + O(M)
        current = node(data[i])
        cur = bucket[pos]
        # 比较排序
        if cur == None or cur.data > current.data:      # O(N/Mlog(N/M))
            current.next = cur
            bucket[pos] = current
            bucket[pos] = current
        else:
            last = cur
            while cur != None and cur.data < current.data:
                last = cur
                cur = cur.next
            current.next = cur
            last.next = current

    for i in range(n_bucket):
        print("Bucket[", i,"] : ", end = "")
        printBuckets(bucket[i])

    result = []
    for i in range(n_bucket):
        cur = bucket[i]
        while cur is not None:
            result.append(cur.data)
            cur = cur.next

    return result

if __name__ == '__main__':
    data = [20, 40, 30, 10, 80, 50, 60, 90]
    n_data = len(data)        # data size
    n_bucket = 5              # bucket size
    interval = 20             # bucket range

    print("before sort:", data)

    data_order = bucketSort(data)

    print("after sort:", data_order)

输出结果:

before sort: [20, 40, 30, 10, 80, 50, 60, 90]
Bucket[ 0 ] : 10  

Bucket[ 1 ] : 20  30  

Bucket[ 2 ] : 40  50  

Bucket[ 3 ] : 60  

Bucket[ 4 ] : 80  90  

after sort: [10, 20, 30, 40, 50, 60, 80, 90]

2、基数排序(Radix Sort)

(1)基本思想:

基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。

具体做法是:
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

(2)排序过程:

通过基数排序对数组{53, 3, 542, 748, 14, 214, 154, 63, 616},它的示意图如下:
在这里插入图片描述
在上图中,首先将所有待比较树脂统一为统一位数长度,接着从最低位开始,依次进行排序。

  1. 按照个位数进行排序。
  2. 按照十位数进行排序。
  3. 按照百位数进行排序。

排序后,数列就变成了一个有序序列。

radix_sort(a, n)的作用是对数组a进行排序:

  1. 首先通过get_max(a)获取数组a中的最大值。获取最大值的目的是计算出数组a的最大指数。
  2. 获取到数组a中的最大指数之后,再从指数1开始,根据位数对数组a中的元素进行排序。排序的时候采用了桶排序。
  3. count_sort(a, n, exp)的作用是对数组a按照指数exp进行排序。
    下面简单介绍一下对数组{53, 3, 542, 748, 14, 214, 154, 63, 616}按个位数进行排序的流程。

(01) 个位的数值范围是[0,10)。因此,参见桶数组buckets[],将数组按照个位数值添加到桶中。
在这里插入图片描述
(02) 接着是根据桶数组buckets[]来进行排序。假设将排序后的数组存在output[]中;找出output[]和buckets[]之间的联系就可以对数据进行排序了。
在这里插入图片描述

(3)复杂度分析:

空间
空间采用顺序分配,显然不合适,由于每个口袋都有可能存放所有的待排序的整数,所以,额外空间的需求为10n,太大了。
采用链表分配是合理的,额外空间的需求为n,通常再增加指向每个口袋的首尾指针就可以了。在一般情况下,设每个关键字的取值范围为radix, 首尾指针共计2×radix个,总的空间为O(n+2×radix)。

时间
如果每个数共有2位,因此执行2次分配和收集就可以了。在一般情况下,每个结点有d位关键字,必须执行d次分配和收集操作。
• 每次分配的代价:O(n)
• 每次收集的代价:O(radix)
• 总的代价为:O(d×(n+radix))

(4)算法实现(python3)
"""
基数排序:

基本思想:
基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。

时间复杂度:O(d(n+r))
"""

def countSort(data, exp):
    data_order = [0] * len(data)
    # 初始化计数数组
    count_arr = [0] * ((max(data) - min(data)) + 1)
    # 统计i的次数
    for i in range(len(data)):      # O(n)
        count_arr[((data[i] - min(data))//exp)%10] += 1
    # 对所有的计数累加
    for i in range(len(count_arr)-1):      # O(r)  r表示基数,本例中为10
        count_arr[i+1] += count_arr[i]
    # 逆向遍历源数组(保证稳定性),根据计数数组中对应的值填充到先的数组中
    for i in range(len(data)-1, -1, -1):      # O(n)
        data_order[count_arr[((data[i] - min(data))//exp)%10]-1] = data[i]
        count_arr[((data[i] - min(data)) // exp) % 10] -= 1

    for i in range(len(data)):      # O(n)
        data[i] = data_order[i]

def getMax(data):
    max = data[0]
    for i in range(len(data)):
        if data[i] > max:
            max = data[i]
    return max

def radixSort(data):
    exp = 1
    max = getMax(data)

    while max/exp > 0:      # O(d)  d表示最大数的长度
        countSort(data, exp)
        exp *= 10


if __name__ == '__main__':
    data = [234, 48, 76, 10, 98, 1, 237, 227]
    print("before sort:", data)

    radixSort(data)

    print("after sort:", data)

运行结果:

before sort: [234, 48, 76, 10, 98, 1, 237, 227]
after sort: [1, 10, 48, 76, 98, 227, 234, 237]

3、计数排序(Counting Sort)

目前介绍的利用比较元素进行排序的方法对数据表长度为n的数据表进行排序时间复杂度不可能低于O(nlogn)。但是如果知道了一些数据表的信息,那么就可以实现更为独特的排序方式,甚至是可以达到线性时间的排序。

它是一个不需要比较的,类似于桶排序的线性时间排序算法。该算法是对已知数量范围的数组进行排序。其时间复杂度为O(n),适用于小范围集合的排序。计数排序是用来排序0到100之间的数字的最好的算法。

比如100万学生参加高考,我们想对这100万学生的数学成绩(假设分数为0到100)做个排序。

(1)基本思想:

当数据表长度为n,已知数据表中数据的范围有限,比如在范围0−k之间,而k又比n小许多,这样可以通过统计每一个范围点上的数据频次来实现计数排序。

(2)排序过程:

总体思路:
根据获得的数据表的范围,分割成不同的buckets,然后直接统计数据在buckets上的频次,逐个累加,确定元素排序后的位置下标,然后倒序遍历原数组,逐个找到元素位置构成收集后的数据表,倒序的目的是保证相同的数字保持原来相对位置不变,保证排序稳定性。
下面以示例来说明这个算法:
假设a={8,2,3,4,3,6,6,3,9}, max=10。此时,将数组a的所有数据都放到需要为0-9的桶中。如下图:
在这里插入图片描述
在将数据放到桶中之后,再通过一定的算法,将桶中的数据提出出来并转换成有序数组。就得到我们想要的结果了。

(3)算法的步骤:

1.找出待排序的数组中最大和最小的元素
2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项
3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

(4)时间复杂度

时间复杂度为O(n),且排序是稳定的。

(5)算法实现(python)
"""
计数排序:

基本思想:
当数据表长度为n,已知数据表中数据的范围有限,比如在范围0−k之间,而k又比n小许多,这样可以通过统计每一个范围点上的数据频次来实现计数排序。

算法复杂度:O(n)
"""

def countSort(data):
    data_order = [0] * len(data)
    # 初始化计数数组
    count_arr = [0] * ((max(data) - min(data)) + 1)
    # 统计i的次数
    for i in range(len(data)):
        count_arr[data[i] - min(data)] += 1
    # 对所有的计数累加
    for i in range(len(count_arr)-1):
        count_arr[i+1] += count_arr[i]
    # 逆向遍历源数组(保证稳定性),根据计数数组中对应的值填充到先的数组中
    for i in range(len(data)-1, -1, -1):
        data_order[count_arr[data[i] - min(data)]-1] = data[i]
        count_arr[data[i] - min(data)] -= 1
    return data_order


if __name__ == '__main__':
    data = [20, 40, 30, 10, 60, 50]
    print("before sort:", data)

    # num_order = insertSort(num_list, num_len)
    data_order = countSort(data)

    print("after sort:", data_order)

运行结果:

before sort: [20, 40, 30, 10, 60, 50]
after sort: [10, 20, 30, 40, 50, 60]

猜你喜欢

转载自blog.csdn.net/olizxq/article/details/82828705