算法打基础——线性时间排序

算法打基础——线性时间排序

这一节主要讲线形时间排序的算法,主要的知识点是:

1. 基于比较的排序的下界及分析   (两种线性排序算法)2.计数排序   3.radix排序

1. 基于比较的排序的下界及分析

前面介绍了很多用于排序的算法(merge sort,quicksort,insertion sort等),目前这些算法最小的时间复杂度就是Θ(nlogn).

事实上,这些基于比较的排序算法最差情况下最好的时间复杂度就是O(nlogn).这点是可以通过决策树来帮助我们分析证明的。

决策树这个东西,学过人工智能之类的应该就很熟悉了,就是根据元素比较的不同情况往不同分支走,然后树将所有可能的情况都包括进去。

举一个三个元素的例子:<a1,a2,a3>=<9,4,6>

算法打基础——线性时间排序

注意这个决策树的叶子就是n个元素的所有可能排列。一个实例的运行时间就是它走的路径的长度。所以最

差情况下的运行时间就是决策树的高度。要证明基于比较的算法最差情况下运行时间是Θ(nlogn),就是要证明决策树的高度最小就是nlogn

Theorem: 排序n个元素的任意决策树的高度 Ω(nlgn).

Proof: 上面提到过树最少要有n!个叶子。而高度为h的二叉树的叶子数最多是(满的时候)2h个叶子,即

 n!≤2h

 h≥lg(n!)

   ≥ lg ((n/e)n)  (Stirling’s formula)

   = nlg n–nlg e

   = Ω(nlg n)

于是就得到结论,基于比较的排序算法下界是Ω(nlg n)

然后是两种线性时间排序

2.计数排序

不通过比较来确定元素的相对位置,从而实现线性时间的排序

输入: A[1 . . n], where A[j]∈{1, 2, …, k}.
输出: B[1 . . n], sorted.
辅助数组: C[1 . . k].

计数排序的思想是通过辅助数组记录这个元素及小于它的元素的个数,从而确定这个元素在排序后的数组中的位置,进而实现排序

下面给出计数排序的伪代码:

for  i←1 to k
  do  C[i] ←0                        ⊳初始化
for  j←1 to n
  do  C[A[j]] ←C[A[j]] + 1  ⊳C[i] = |{key = i}| 记录每个值得个数

for  i←2 to k
  do  C[i] ←C[i] + C[i–1]    ⊳C[i] = |{key ≤i}|  记录小于等于这个值的个数


for  j←n down to 1        ⊳从后往前遍历是为了排序算法是稳定的
  do  B[C[A[j]]] ←A[j]    ⊳将元素放到他应该在的位置上
        C[A[j]] ←C[A[j]] –1

下面给出一个排序实例

算法打基础——线性时间排序

根据伪代码也可以知道,计数排序的时间复杂度是Θ(n +k)。

另外,这个排序算法是稳定的,即它能保留排序前两个相同元素的相对位置

 3.radix排序

上面的计数排序虽然很快,但是其缺点是对数大小的限制比较大,当有很大的数时,其创建的辅助数组就会占很大内存

基数排序适用于很大的数,通过将数分割成几个digit,在digit上分别排序,最终实现所有的排序。而且基数排序另外一个重要的地方是,

直观上我们都是从高位到低位来排序的,但是这样我们需要将排好的数分到各个桶里面,

否则排序低位时这些数就乱了。当数位很多时,桶的数量就变得非常多,带来很大消耗。

基数排序通过从低位到高位来排序,使得数组几乎原地就排好了。

基数排序的伪代码非常简单,

RADIX-SORT(A; d)
1     for i from 1 to d
2    use a stable sort to sort array A on digit i

基数排序的一个实例

算法打基础——线性时间排序

下面对基数排序进行分析:

基数排序需要一个子排序算法,为了保证其线性的时间,且算法也要求它是stable的,所以一般我们选用计数排序。

假设现在我们要排序n个长度是b bits的元素

如果我们1个bit 作为一个digit排序的话,排序的轮数将非常多,且计数排序中数的范围才是0-1,这也是不经济的

所以我们可以采用将r bits作为一个digit,则我们需要排序b/r轮,计数排序数的范围是2r

比如:算法打基础——线性时间排序这个32bit长的元素,我们取8bits为一个digit,就只需要4轮,范围是0-28-1的计数排序

事实上,这个r的选择非常关键,它决定着基数排序算法的复杂度。 而我们知道计数排序复杂度是Θ(n +k),所以基数排序的复杂度为

T(n,b) = Θ(b/r(n+2r)), 我们的目标就是选择r使得复杂度最小。

这个目标我们可以通过T(n,b)关于r导数等于0来求得。同时,直观上,我们希望(n+2r)中n能决定这一项的大小,即n≥2r

取最大值,即r=lgn, 从而T(n,b)=Θ(bn/lgn). 为了表达的方便,我们假设数的范围是0-nd-1

即b=dlgn,所以T(n,b)=Θ(dn). 

下面给出自己实现的两种排序算法

Counting_Sort

/////////////////////////CLRS video lec5 计数排序/////////////////////////////////////////////////
//线性时间排序,但是对输入做了假设 /////////////////////////////////////////////////////
//假设输入的数范围是0~99

#include<iostream>
#include<cstdlib>
#include<ctime>
using namespace std;

#define random(x)(rand()%x)

int main()
{
srand(int(time(0)));
int a[200],b[200];
int c[100],i,k;
for(i=0;i<200;i++)
{
a[i]=random(100);
}
memset(c,0,sizeof(c));
for(i=0;i<200;i++)
{
c[a[i]]++;
}
for(i=1;i<100;i++)
{
c[i]+=c[i-1];
}
for(i=200-1;i>=0;i--)
{
b[c[a[i]]-1]=a[i]; //一定要注意是从0开始计数的,这里
c[a[i]]--; //c计数最大到200了,所以用的时候要减一!!
}
for(i=0;i<200;i++)
{
cout<<b[i]<<" ";
}
cout<<endl;
return 0;
}

Counting_Sort

Radix_sort

/////////////////////////CLRS video lec5 基数排序/////////////////////////////////////////////////
//线性时间排序,排序要注意digit位数的选择 /////////////////////////////////////////////////////

#include<iostream>
#include<cstdlib>
#include<ctime>
#include<cmath>
using namespace std;

#define random(x)(rand()%x)

// 以计数排序作为辅助排序方法;且假设范围是0-99
int* countsort(int* a,int scale,int divide)
{
int* b = new int[scale];
int c[100],i;
memset(c,0,sizeof(c));

for(i=0;i<scale;i++)
{
c[(a[i]/divide)%100]++;
}
for(i=1;i<100;i++)
{
c[i]+=c[i-1];
}
for(i=scale-1;i>=0;i--)
{
b[c[(a[i]/divide)%100]-1]=a[i]; //一定要注意是从0开始计数的,这里
c[(a[i]/divide)%100]--; //c计数最大到200了,所以用的时候要减一!!
}

a = b; //数组指针部分必须认真复习,还如:不知道可以这样赋值
//做的时候出现了很多问题,如不返回a,其实原数组未修改!

return a;
}

int main()
{
srand((int)time(0));
int scale = 100000,max=1000000;
int* a = new int [scale];
int i,j,divide;
for(i=0;i<scale;i++)
{
a[i]=random(max);
}
//采用每两个十进制位为一个digit,即需要3轮
//实际上,总共有十万个数,log(10)100000=5;最大可以以5位为一个digit
for(i=0;i<3;i++)
{
divide=int(pow(100.0,i));
a=countsort(a,scale,divide);
}
for(i=0;i<1000;i++)
cout<<a[i]<<" ";
return 0;
}

Radix_sort

 
 
 
标签: 算法基础

这一节主要讲线形时间排序的算法,主要的知识点是:

1. 基于比较的排序的下界及分析   (两种线性排序算法)2.计数排序   3.radix排序

1. 基于比较的排序的下界及分析

前面介绍了很多用于排序的算法(merge sort,quicksort,insertion sort等),目前这些算法最小的时间复杂度就是Θ(nlogn).

事实上,这些基于比较的排序算法最差情况下最好的时间复杂度就是O(nlogn).这点是可以通过决策树来帮助我们分析证明的。

决策树这个东西,学过人工智能之类的应该就很熟悉了,就是根据元素比较的不同情况往不同分支走,然后树将所有可能的情况都包括进去。

举一个三个元素的例子:<a1,a2,a3>=<9,4,6>

算法打基础——线性时间排序

注意这个决策树的叶子就是n个元素的所有可能排列。一个实例的运行时间就是它走的路径的长度。所以最

差情况下的运行时间就是决策树的高度。要证明基于比较的算法最差情况下运行时间是Θ(nlogn),就是要证明决策树的高度最小就是nlogn

Theorem: 排序n个元素的任意决策树的高度 Ω(nlgn).

Proof: 上面提到过树最少要有n!个叶子。而高度为h的二叉树的叶子数最多是(满的时候)2h个叶子,即

 n!≤2h

 h≥lg(n!)

   ≥ lg ((n/e)n)  (Stirling’s formula)

   = nlg n–nlg e

   = Ω(nlg n)

于是就得到结论,基于比较的排序算法下界是Ω(nlg n)

然后是两种线性时间排序

2.计数排序

不通过比较来确定元素的相对位置,从而实现线性时间的排序

输入: A[1 . . n], where A[j]∈{1, 2, …, k}.
输出: B[1 . . n], sorted.
辅助数组: C[1 . . k].

计数排序的思想是通过辅助数组记录这个元素及小于它的元素的个数,从而确定这个元素在排序后的数组中的位置,进而实现排序

下面给出计数排序的伪代码:

for  i←1 to k
  do  C[i] ←0                        ⊳初始化
for  j←1 to n
  do  C[A[j]] ←C[A[j]] + 1  ⊳C[i] = |{key = i}| 记录每个值得个数

for  i←2 to k
  do  C[i] ←C[i] + C[i–1]    ⊳C[i] = |{key ≤i}|  记录小于等于这个值的个数


for  j←n down to 1        ⊳从后往前遍历是为了排序算法是稳定的
  do  B[C[A[j]]] ←A[j]    ⊳将元素放到他应该在的位置上
        C[A[j]] ←C[A[j]] –1

下面给出一个排序实例

算法打基础——线性时间排序

根据伪代码也可以知道,计数排序的时间复杂度是Θ(n +k)。

另外,这个排序算法是稳定的,即它能保留排序前两个相同元素的相对位置

 3.radix排序

上面的计数排序虽然很快,但是其缺点是对数大小的限制比较大,当有很大的数时,其创建的辅助数组就会占很大内存

基数排序适用于很大的数,通过将数分割成几个digit,在digit上分别排序,最终实现所有的排序。而且基数排序另外一个重要的地方是,

直观上我们都是从高位到低位来排序的,但是这样我们需要将排好的数分到各个桶里面,

否则排序低位时这些数就乱了。当数位很多时,桶的数量就变得非常多,带来很大消耗。

基数排序通过从低位到高位来排序,使得数组几乎原地就排好了。

基数排序的伪代码非常简单,

RADIX-SORT(A; d)
1     for i from 1 to d
2    use a stable sort to sort array A on digit i

基数排序的一个实例

算法打基础——线性时间排序

下面对基数排序进行分析:

基数排序需要一个子排序算法,为了保证其线性的时间,且算法也要求它是stable的,所以一般我们选用计数排序。

假设现在我们要排序n个长度是b bits的元素

如果我们1个bit 作为一个digit排序的话,排序的轮数将非常多,且计数排序中数的范围才是0-1,这也是不经济的

所以我们可以采用将r bits作为一个digit,则我们需要排序b/r轮,计数排序数的范围是2r

比如:算法打基础——线性时间排序这个32bit长的元素,我们取8bits为一个digit,就只需要4轮,范围是0-28-1的计数排序

事实上,这个r的选择非常关键,它决定着基数排序算法的复杂度。 而我们知道计数排序复杂度是Θ(n +k),所以基数排序的复杂度为

T(n,b) = Θ(b/r(n+2r)), 我们的目标就是选择r使得复杂度最小。

这个目标我们可以通过T(n,b)关于r导数等于0来求得。同时,直观上,我们希望(n+2r)中n能决定这一项的大小,即n≥2r

取最大值,即r=lgn, 从而T(n,b)=Θ(bn/lgn). 为了表达的方便,我们假设数的范围是0-nd-1

即b=dlgn,所以T(n,b)=Θ(dn). 

下面给出自己实现的两种排序算法

Counting_Sort

/////////////////////////CLRS video lec5 计数排序/////////////////////////////////////////////////
//线性时间排序,但是对输入做了假设 /////////////////////////////////////////////////////
//假设输入的数范围是0~99

#include<iostream>
#include<cstdlib>
#include<ctime>
using namespace std;

#define random(x)(rand()%x)

int main()
{
srand(int(time(0)));
int a[200],b[200];
int c[100],i,k;
for(i=0;i<200;i++)
{
a[i]=random(100);
}
memset(c,0,sizeof(c));
for(i=0;i<200;i++)
{
c[a[i]]++;
}
for(i=1;i<100;i++)
{
c[i]+=c[i-1];
}
for(i=200-1;i>=0;i--)
{
b[c[a[i]]-1]=a[i]; //一定要注意是从0开始计数的,这里
c[a[i]]--; //c计数最大到200了,所以用的时候要减一!!
}
for(i=0;i<200;i++)
{
cout<<b[i]<<" ";
}
cout<<endl;
return 0;
}

Counting_Sort

Radix_sort

/////////////////////////CLRS video lec5 基数排序/////////////////////////////////////////////////
//线性时间排序,排序要注意digit位数的选择 /////////////////////////////////////////////////////

#include<iostream>
#include<cstdlib>
#include<ctime>
#include<cmath>
using namespace std;

#define random(x)(rand()%x)

// 以计数排序作为辅助排序方法;且假设范围是0-99
int* countsort(int* a,int scale,int divide)
{
int* b = new int[scale];
int c[100],i;
memset(c,0,sizeof(c));

for(i=0;i<scale;i++)
{
c[(a[i]/divide)%100]++;
}
for(i=1;i<100;i++)
{
c[i]+=c[i-1];
}
for(i=scale-1;i>=0;i--)
{
b[c[(a[i]/divide)%100]-1]=a[i]; //一定要注意是从0开始计数的,这里
c[(a[i]/divide)%100]--; //c计数最大到200了,所以用的时候要减一!!
}

a = b; //数组指针部分必须认真复习,还如:不知道可以这样赋值
//做的时候出现了很多问题,如不返回a,其实原数组未修改!

return a;
}

int main()
{
srand((int)time(0));
int scale = 100000,max=1000000;
int* a = new int [scale];
int i,j,divide;
for(i=0;i<scale;i++)
{
a[i]=random(max);
}
//采用每两个十进制位为一个digit,即需要3轮
//实际上,总共有十万个数,log(10)100000=5;最大可以以5位为一个digit
for(i=0;i<3;i++)
{
divide=int(pow(100.0,i));
a=countsort(a,scale,divide);
}
for(i=0;i<1000;i++)
cout<<a[i]<<" ";
return 0;
}

Radix_sort