本文参考自一博文与《算法导论》。

    

  《算法导论》之前介绍了合并排序、堆排序和快速排序的特点及运行时间。合并排序和堆排序在最坏情况下达到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)时,计数排序的运行时间为O(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>,计数排序过程如下:

  

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

 COUNTING_SORT(A,B,k)
let C[..k] be a new array
for i= to k
C[i] =
for j= to length(A)
C[A[j]] = C[A[j]]+ //C[i]中包含等于元素 i 的个数
for i= to k
C[i] = C[i] + C[i-] //C[i]中包含小于等于元素 i 的个数
for j=length[A] downto
B[C[A[j]]] = A[j]
C[A[j]] = C[A[j]] -

  问题:在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可以保证是稳定的算法。

  实现程序如下:

 #ifndef COUNTINGSORT_H
#define COUNTINGSORT_H #include "Sorting.h" const int MAX = ; class CountingSort : public Sorting
{
public:
CountingSort();
CountingSort(int * initArr, int initLen);
virtual ~CountingSort();
virtual void sort(); private:
int TEMP[MAX]; }; #endif

CountingSort.h

 #include "CountingSort.h"
#include <iostream>
using namespace std; CountingSort::CountingSort()
{
for (int i = ; i < MAX; i++)
{
TEMP[i] = ;
}
} CountingSort::CountingSort(int * initArr, int initLen) : Sorting(initArr, initLen)
{
for (int i = ; i < MAX; i++)
{
TEMP[i] = ;
}
} CountingSort::~CountingSort()
{ } void CountingSort::sort()
{
int * outArr = new int[len]; for (int i = ; i < len; i++)
{
if (arr[i] >= MAX)
{
cout << "Out of range!" << endl;
return;
}
TEMP[arr[i]] += ;
}
// TEMP[i] now contains the number of elements equal to arr[i]; for (int i = ; i < MAX; i++)
{
TEMP[i] += TEMP[i - ];
}
// TEMP[i] now contains the number of elements less than or equal to arr[i]. for (int i = len - ; i > -; i--)
{
outArr[TEMP[arr[i]] - ] = arr[i];
TEMP[arr[i]]--;
} for (int i = ; i < len; i++)
{
arr[i] = outArr[i];
} delete outArr;
}

CountingSort.cpp

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

  计数排序跟《编程珠矶》第2版在第1章提到的问题是类似的,可见于博客“《编程珠玑》(第2版)第1章”:整数排序

  3、基数排序

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

  

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

 RADIX_SORT(A,d)
for i= to d
use a stable sort to sort array A on digit i

  引理:给定n个d位数,每一个数位可以取k种可能值。如果所用的稳定排序需要θ(n+k)的时间,基数排序算法性能以θ(d(n+k))的时间正确对这些数进行排序。
  为了完整的理解基数排序,结合上面的计数排序,采用C++语言实现一个程序,运用计数排序算法对一组3位数进行排序,程序如下:

 #ifndef RADIXSORT_H
#define RADIXSORT_H #include "Sorting.h" const int MAXDIGITS = ; // 最多多少个“十位”数 class RadixSort : public Sorting
{
public:
RadixSort();
RadixSort(int * initArr, int initLen);
virtual ~RadixSort();
virtual void sort(); private:
int getDigit(int data, int digit);
int getDigitNum(int data); }; #endif

RadixSort.h

 #include "RadixSort.h"
#include <math.h> RadixSort::RadixSort()
{ } RadixSort::RadixSort(int * initArr, int initLen) : Sorting(initArr, initLen)
{ } RadixSort::~RadixSort()
{ } void RadixSort::sort()
{
for (int d = ; d < MAXDIGITS; d++)
{
for (int i = ; i < len; i++)
{
for (int j = i; j > && less(getDigit(arr[j], d), getDigit(arr[j - ], d)); j--)
{
exchange(j, j - );
}
}
}
} int RadixSort::getDigit(int data, int digit)
{
int temp;
temp = data;
while (digit)
{
temp /= ;
digit--;
}
return (temp % );
} int RadixSort::getDigitNum(int data)
{
int digitNum = ;
while (data)
{
digitNum++;
data = data / ;
}
return digitNum;
}

RadixSort.cpp

 

  4、桶排序

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

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

 BUCKET_SORT(A)
n = length(A)
let B[..n-] be a new array
for i = to n-
make B[i] an empty list
for i= to n
insert A[i] into list B[nA[i]]
for i= to n-
sort list B[i] with insertion sort
concatenate the list B[]、B[],,,B[n-] together in order

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

  

  为了更好的理解桶排序,采用C++语言,借助STL中的map、vector进行操作,完整程序如下:

 #ifndef BUCKETSORT_H
#define BUCKETSORT_H class BucketSort
{
public:
BucketSort();
BucketSort(float * initArr, int initLen);
virtual ~BucketSort();
virtual void sort();
virtual void show();
virtual bool isSorted(); public:
float * arr;
int len; }; #endif

BucketSort.h

 #include "BucketSort.h"
#include <iostream>
#include <cassert>
#include <map>
#include <vector>
#include <algorithm>
using namespace std; BucketSort::BucketSort()
{ } BucketSort::BucketSort(float * initArr, int initLen)
{
len = initLen;
arr = new(nothrow) float[len];
assert(arr != nullptr);
for (int i = ; i < len; i++)
{
*(arr + i) = *(initArr + i);
}
} BucketSort::~BucketSort()
{
delete[] arr;
arr = nullptr;
} void BucketSort::sort()
{
map<int, vector<float> > bucket;
for (size_t i = ; i < len; i++)
{
int pos = (int)(arr[i] * ) % ;
bucket[pos].push_back(arr[i]);
} int sum = ;
map<int, vector<float> >::iterator itr = bucket.begin();
for (; itr != bucket.end(); itr++)
{
std::sort((itr->second).begin(), (itr->second).end()); int sz = (itr->second).size();
for (size_t j = ; j < sz; j++)
{
arr[sum + j] = (itr->second)[j];
}
sum += sz;
} } void BucketSort::show()
{
cout << "************************ Show Datas ************************" << endl;
for (int i = ; i < len; i++)
{
cout << *(arr + i) << " ";
}
cout << endl;
cout << "*************************** End ****************************" << endl;
} bool BucketSort::isSorted()
{
bool ret = true;
for (int i = ; i < len; i++)
{
if (arr[i] < arr[i - ])
{
ret = false;
break;
}
}
return ret;
}

BucketSort.cpp

  桶排序的期望运行时间为:O(n)。

  以上完整程序请见于Github.

"《算法导论》之‘排序’":线性时间排序的更多相关文章

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

    比较排序:各元素的次序依赖于它们之间的比较{插入排序O(n**2) 归并排序O(nlgn) 堆排序O(nlgn)快速排序O(n**2)平均O(nlgn)} 本章主要介绍几个线性时间排序:(运算排序非比 ...

  2. 算法导论学习之线性时间求第k小元素+堆思想求前k大元素

    对于曾经,假设要我求第k小元素.或者是求前k大元素,我可能会将元素先排序,然后就直接求出来了,可是如今有了更好的思路. 一.线性时间内求第k小元素 这个算法又是一个基于分治思想的算法. 其详细的分治思 ...

  3. 《算法导论》 — Chapter 8 线性时间排序

    序 到目前为止,关于排序的问题,前面已经介绍了很多,从插入排序.合并排序.堆排序以及快速排序,每一种都有其适用的情况,在时间和空间复杂度上各有优势.它们都有一个相同的特点,以上所有排序的结果序列,各个 ...

  4. 排序算法的C语言实现(下 线性时间排序:计数排序与基数排序)

    计数排序 计数排序是一种高效的线性排序. 它通过计算一个集合中元素出现的次数来确定集合如何排序.不同于插入排序.快速排序等基于元素比较的排序,计数排序是不需要进行元素比较的,而且它的运行效率要比效率为 ...

  5. Python线性时间排序——桶排序、基数排序与计数排序

    1. 桶排序 1.1 范围为1-M的桶排序 如果有一个数组A,包含N个整数,值从1到M,我们可以得到一种非常快速的排序,桶排序(bucket sort).留置一个数组S,里面含有M个桶,初始化为0.然 ...

  6. 《算法导论》 — Chapter 7 高速排序

    序 高速排序(QuickSort)也是一种排序算法,对包括n个数组的输入数组.最坏情况执行时间为O(n^2). 尽管这个最坏情况执行时间比較差.可是高速排序一般是用于排序的最佳有用选择.这是由于其平均 ...

  7. 《算法导论》读书笔记之排序算法—Merge Sort 归并排序算法

    自从打ACM以来也算是用归并排序了好久,现在就写一篇博客来介绍一下这个算法吧 :) 图片来自维基百科,显示了完整的归并排序过程.例如数组{38, 27, 43, 3, 9, 82, 10}. 在算法导 ...

  8. &quot;《算法导论》之‘线性表’&quot;:基于静态分配的数组的顺序表

    首先,我们来搞明白几个概念吧(参考自网站数据结构及百度百科). 线性表 线性表是最基本.最简单.也是最常用的一种数据结构.线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外, ...

  9. &quot;《算法导论》之‘线性表’&quot;:双向循环链表

    本文双链表介绍部分参考自博文数组.单链表和双链表介绍 以及 双向链表的C/C++/Java实现. 1 双链表介绍 双向链表(双链表)是链表的一种.和单链表一样,双链表也是由节点组成,它的每个数据结点中 ...

随机推荐

  1. [译]理解Javascript的异步等待

    原文链接: https://ponyfoo.com/articles/understanding-javascript-async-await 作者: Nicolás Bevacqua 目前async ...

  2. PostgreSQL-数据目录与pg_ctl

    # tail /etc/profile PATH="$PATH":/usr/lib/postgresql/9.2/bin/ export PATH export PGDATA=/v ...

  3. [vijos P1524] 最小监视代价

    历时四天(本周三至本周六),本人的第一道网络流题目终于通过了…虽然这么慢才搞懂很大程度是因为脑子笨,但是还是要吐槽一下: (1)选的这道题吧居然是无向图,对于初学者我表示呵呵,昨晚到现在一直在纠结怎么 ...

  4. 扩展Date的format方法--格式化日期时间

    Date.prototype.format = function (format) { var o = { "M+": this.getMonth() + 1, "d+& ...

  5. NDK Dev

    1.cdt下载(http://www.eclipse.org/cdt/downloads.php) cdt-8.8.0.zip http://mirrors.opencas.cn/eclipse//t ...

  6. ECOS-Ecstore 后台管理地址修改

    ECStore默认出厂的后台管理地址是: http://域名/index.php/shopadmin http://域名/shopadmin [配置过rewrite后,并开启伪静态] 如果想要更个性的 ...

  7. js架构设计模式——从angularJS看MVVM

    javascript厚积薄发走势异常迅猛,导致现在各种MV*框架百家争雄,MVVM从MVC演变而来,为javascript注入了全新的活力.我工作的业务不会涉及到 angularJS[ng] 这么重量 ...

  8. vue-----表单与组件

    <!DOCTYPE html><html><head> <meta charset="utf-8"> <meta name=& ...

  9. 使用OpenLDAP部署目录服务

  10. intellij idea 2018

    intellij idea 输入System.out.println()的快捷方法是:输入sout然后按tab键. 由于项目中引入了spring-boot-starter-test的依赖,也就是集成了 ...