第八章 线性时间排序

摘要:

  本章先回顾了前面介绍的合并排序、堆排序和快速排序的特点及运行运行时间。合并排序和堆排序在最坏情况下达到O(nlgn),而快速排序最坏情况下达到O(n^2),平均情况下达到O(nlgn),因此合并排序和堆排序是渐进最优的。这些排序在执行过程中各元素的次序基于输入元素间的比较,称这种算法为比较排序。接下来介绍了用决策树的概念及如何用决策树确定排序算法时间的下界,最后讨论三种线性时间运行的算法:计数排序、基数排序和桶排序。这些算法在执行过程中不需要比较元素来确定排序的顺序,这些算法都是稳定的。

1、决策树模型

  在比较排序算法中,用比较操作来确定输入序列<a1,a2,......,a3>的元素间次序。决策树是一棵完全二叉树,比较排序可以被抽象视为决策树,表示某排序算法作用域给定输入所做的比较。在决策树中,节点表示为i:j,其中1≤i,j≤n,n是待排序元素个数,叶子节点是排序的结果。节点的左子树满足ai≤aj,右子树满足ai>aj。排序算法正确工作的必要条件是:n个元素的n!中排列中的每一种都要作为决策树的一个叶子而出现。举例说明,先有序列A<3,2,1>,对其进行有小到达进行插入排序,排序的决策树如下图所示:

在决策树中,从跟到任意一个可达叶子节点之间最长路径的长度,表示对应的排序算法中最坏情况下的比较次数。

定理:对于一个比较排序算法在最坏情况下,都需要做Ω(nlgn)此比较。

推论:堆排序和合并排序都是渐进最优的比较排序算法。

2、计数排序

  计数排序假设n个输入元素中的每一个都介于0和k之间的整数,k为n个数中最大的元素。当k=O(n)时,计数排序的运行时间为θ(n)。计数排序的基本思想是:对n个输入元素中每一个元素x,统计出小于等于x的元素个数,根据x的个数可以确定x在输出数组中的最终位置。此过程需要引入两个辅助存放空间,存放结果的B[1...n],用于确定每个元素个数的数组C[0...k]。算法的具体步骤如下:

(1)根据输入数组A中元素的值确定k的值,并初始化C[1....k]= 0;

(2)遍历输入数组A中的元素,确定每个元素的出现的次数,并将A中第i个元素出现的次数存放在C[A[i]]中,然后C[i]=C[i]+C[i-1],在C中确定A中每个元素前面有多个元素;

(3)逆序遍历数组A中的元素,在C中查找A中出现的次数,并结果数组B中确定其位置,然后将其在C中对应的次数减少1。

举个例子说明其过程,假设输入数组A=<2,5,3,0,2,3,0,3>,计数排序过程如下:

数中给出了计数排序的伪代码:

 1 COUNTING_SORT(A,B,k)
 2     for i=0 to k
 3       do C[i] = 0
 4     for j=1 to length(A)
 5         do C[A[j]] = C[A[j]]+1   //C[i]中包含等于元素i的个数
 6     for i=1 to k
 7         do C[i] = C[i] + C[i-1]  //C[i]中包含小于等于元素i的个数
 8     for j=length[A] downto 1
 9         do B[C[A[j]]] = A[j]
10              C[A[j]] = C[A[j]] -1

问题:在COUNTING_SORT过程中,第8行for循环为什么是 for j=length[A] downto 1,而不是 for j=1 to length[A]。

解答:虽然从改为 for j=1 to length[A]该算法仍然能够正常地工作,但是此时不能保证算法是稳定的。因为如果有两个元素相同,那么就导致排序后前面的出现在后面,后面的出现在前面,即相同值的元素在输出数组中的相对次序与它们在输入数组中的次序是不同的。而从for j=length[A] downto 1可以保证是稳定的算法。

C++代码如下:

#include<iostream>
using namespace std;

void CountingSort(int A[],int B[],int n,int k)
{
    int C[k];
    int i;
    for(i=0;i<k;i++)
        C[i]=0;
    for(i=0;i<n;i++)
        C[A[i]]++;
    for(i=1;i<k;i++)
        C[i]+=C[i-1];
    for(i=n-1;i>=0;i--)
    {
        B[C[A[i]]-1]=A[i];
        C[A[i]]--;
    }
}
int main()
{
    int arr[20]={1,2,2,4,5,6,3,2,5,3,4,7,8,6,8,5,0,2,8,9};
    int B[20];
    CountingSort(arr,B,20,10);
    for(auto a:B)
        cout<<a<<" ";
    cout<<endl;
}

从计数排序的思想及过程可以看出,当输入数组A中的数较大的时候,就不适合。因为需要开辟最大元个辅助数组,统计每个元素的出现次数。通常计数排序用在基数排序中,作为一个子程序。计数排序最重要的性质就是它是稳定的:具有相同值的元素在输出数组中的相对次序与它们在输入数组中的次序相同。


3 基数排序

 基数排序排序过程无须比较关键字,而是通过“分配”和“收集”过程来实现排序,它的时间复杂度可达到线性阶:O(n)。对于十进制数来说,每一位的在[0,9]之中,d位的数,则有d列。基数排序首先按低位有效数字进行排序,然后逐次向上一位进行排序,直到最高位排序结束。举例说明基数排序过程,如下图所示:

基数排序算法很直观,假设长度为n的数组A中,每个元素都有d位数字,其中第1位是最低位,第d位是最高位。书中给出了伪代码如下所示:

1 RADIX_SORT(A,d)
2    for i=1 to d
3          do usage a stable sort to sort array A on digit i

引理:给定n个d位数,每一个数位可以取k种可能值。如果所用的稳定排序需要θ(n+k)的时间,基数排序算法性能以θ(d(n+k))的时间正确对这些数进行排序。

4、桶排序

  计数排序假设输入是由一个小范围内的整数构成,而桶排序则假设输入由一个随机过程产生的,该过程将元素均匀而独立地分布在区间[0,1)上。当桶排序的输入符合均匀分布时,即可以线性期望时间运行。桶排序的思想是:把区间[0,1)划分成n个相同大小的子区间,成为桶(bucket),然后将n个输入数分布到各个桶中去,对各个桶中的数进行排序,然后按照次序把各个桶中的元素列出来即可。

  数中给出了桶排序的伪代码,假设输入是一个含有n个元素的数组A,且每个元素满足0≤A[i]<1,另外需要一个辅助数组B[0....n-1]来存放链表(桶)。伪代码如下所示:

1 BUCKET_SORT(A)
2    n = length(A)
3    for i= 1 to n
4        do insert A[i] into list B
5    for i=0 to n-1
6        do sort list B[i] with insertion sort
7    concatenate the list B[0]、B[1],,,B[n-1] together in order

举个来说明桶排序的过程,假设现在有A={0.78,0.17,0.39,0.26,0.72,0.94,0.21,0.12,0.23,0.68},桶排序如下所示:

时间: 2024-09-29 18:06:55

第八章 线性时间排序的相关文章

第八章 线性时间排序 8.3 基数排序

package chap08_Linear_Time_Sort; import static org.junit.Assert.*; import java.util.Arrays; import org.junit.Test; public class CopyOfSortAlgorithms { /** * 基数排序 * * @param n * @param digit */ static void radixSort(int[] n, int digit) { int[] b = new

算法导论 第八章 线性时间排序(python)

比较排序:各元素的次序依赖于它们之间的比较{插入排序O(n**2) 归并排序O(nlgn) 堆排序O(nlgn)快速排序O(n**2)平均O(nlgn)} 本章主要介绍几个线性时间排序:(运算排序非比较排序)计数排序O(k+n)基数排序O() 第一节:用决策树分析比较排序的下界 决策树:倒数第二层满,第一层可能满的二叉树,它用来表示所有元素的比较操作{于此来分析下界},忽略控制,移动操作 1:2 #A[1]和A[2]比 <= 走左边 >走右边 <3,1,2> 最后的结果 下标对应排

算法导论第八章线性时间排序

一.线性时间排序算法历史概览 计数排序首先是由 Harold H. Seward 于1954年提出,而且他还提出将计数排序和基数排序进行结合的思想:基数排序是L.J.Comrie于1929年首次在一篇描述卡片穿孔机文档中提出的一种方法,它是从最低有效位开始,对一个有多位数组成的数进行排序的方法:而桶排序的基本思想则由E.J.Isaac和R.C.Singleton于1956年提出的,之后很多研究人员在这三种算法的基础上针对不同的应用场景又进一步改进,到了今天一个很成熟.很通用的地步. 二.O(nl

算法导论笔记——第八章 线性时间排序

8.1 排序算法的下界 定理8.1 在最坏情况下,任何比较排序算法都需要做Ω(nlgn)次比较. 推论8.2 堆排序和归并排序都是渐进最优的比较排序算法. 8.2 计数排序 计数排序假设n个输入元素中的每一个都是在0到k区间内的一个整数,其中k为某个整数.当k=O(n)时,排序的运行时间为θ(n). 基本思想:对每一个输入元素x,确定小于x的元素个数.C[A[j]] = C[A[j]] + 1 8.3 基数排序 Radix Sort 从LSB到MSB,一位数排序算法必须是稳定的. 若b<(lgn

算法导论 第8章 线性时间排序

合并排序和堆排序的时间复杂度为O(nlgn),插入排序和冒泡排序的时间复杂度为O(n^2),快速排序的时间复杂度在平均情况下是O(nlgn),这些排序算法都是通过对元素进行相互比较从而确定顺序的,因此都叫比较排序. 比较排序可以看做是决策树(一个满二叉树),因为每一次比较都是一个分支.n个元素的序列,其排序的结果有 n! 种可能(n个元素的全排),所以这个决策树有 n! 个叶子结点,假设树的高度为h,则有:n! <= 2^h,所以h >= lg(n!) = Ω(nlgn).一次比较排序就是从决

排序算法(4)-线性时间排序

在前面三节排序算法中,我们分别分析了不同策略,思想用于排序,而这些算法都是基于数据间的比较来确定顺序的.假设我不用比较,换一种思路,那么就可以达到时间复杂度为O(n)的排序算法,当然是以付出额外的空间为代价的. 一.基本思想 线性时间排序的算法思想: (1):在计数排序中,利用比x小或等的元素个数和的来确定x位置.比如2 5 4 9 1 6.9比其余5个数都大,那就说明9 在排序后的第6个位置,这样我们只要得到比某个数大的元素个数就能得到元素在排序后数组中的位置了. (2):在桶排序中,是通过映

算法导论-- 线性时间排序(计数排序、基数排序、桶排序)

线性时间排序 前面介绍的几种排序,都是能够在复杂度nlg(n)时间内排序n个数的算法,这些算法都是通过比较来决定它们的顺序,这类算法叫做比较排序 .下面介绍的几种算法用运算去排序,且它们的复杂度是线性时间. -------------------------------------- 1.计数排序 计数排序采用的方法是:对每个元素x,去确定小于x的元素的个数,从而就可以知道元素x在输出数组中的哪个位置了. 计数排序的一个重要性质是它是稳定的,即对于相同的两个数,排序后,还会保持它们在输入数组中的

线性时间排序算法

线性时间排序算法列表 线性时间排序 Name Average Worst Memory Stable Description  计数排序 (Counting Sort) n + k n + k n + k Stable Indexes using key values.  基数排序 (Radix Sort) n * k  n * k n + k Stable Examines individual bits of keys.  桶排序 (Bucket Sort) n + k n2 n * k S

算法导论之六:线性时间排序之 决策树&amp;计数排序

本系列前五篇都是讲述的比较排序算法,从本文开始,将进入线性时间排序.什么是比较排序,简单的说,就是排序的过程依赖于数组中数据大小的比较,从而来确定数据在排好序输出时的位置. 比较排序法比较直观,但是也有它的不足,我们容易证明任何比较排序法,在最坏的情况下的时间复杂度的下限都是 nlgn.要证明这个问题,我们首先要搞清楚一个模型:决策树模型. 一.决策树模型 什么是决策树?决策树从形态上来讲,是一颗完全二叉树,它除叶子节点之外,其他层的节点都是满的.它的每一个叶子节点表示对输入数据组合的一种排序可