八大排序算法有直接插入排序、希尔排序、冒泡排序、快速排序、直接选择排序、堆排序、归并排序、基数排序。
今天想把排序算法系统学习一下,所以上网搜索了下排序算法。真是个好家伙,那么多!不怕,一个个学习并用代码实现出来!
排序算法大体可以分为以下几类:
- 交换类排序:冒泡排序、鸡尾酒排序、奇偶换位排序、梳子排序、侏儒排序、快速排序、臭皮匠排序
- 选择类排序:选择排序、堆排序、Smooth排序、笛卡尔树排序、锦标赛排序、圈排序
- 插入类排序:插入排序、希尔排序、耐心排序
- 归并类排序:归并排序、Strand排序
分布类排序:基数排序、计数排序、桶排序
本次我都是按从小到大的排序方式来实现。以下是一个交换函数,每个排序算法都会用到。
template <typename T>
void Swap(T &a, T &b) {
T tmp = a;
a = b;
b = tmp;
}
接下来,让我们来一一攻破!
一、交换类排序
(一)冒泡排序
最原始的交换类排序方式,它的思想是:第一趟在序列(A[0]~A[n-1])中从前往后进行两个相邻元素的比较,若后者小,则交换,比较n-1次;第一趟排序结束,最大元素被交换到A(n-1)中(即沉底)。下一趟排序只需在子序列(A[0]~A[n-2])中进行。如果在某一趟排序中未交换元素,说明子序列已经有序,则不再进行下一趟排序。该方法最多进行n-1趟。
最好情况下:正序有序,则只需要比较n次。故,为O(n)
最坏情况下: 逆序有序,则需要比较(n-1)+(n-2)+……+1,故,为O(n^2)。
因为排序过程中只交换相邻两个元素的位置,所以当两个数相等时,是没必要交换两个数的位置的。因此,它们的相对位置并没有改变,冒泡排序算法是稳定的。
//冒泡排序
//最好情况O(n),最坏情况O(n^2)
template <typename T>
void BubbleSort(T array[],int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
int i,j,last;
i = n-1;
while (i) { //最多进入n-1趟
last = 0;
for (j = 0; j < i; j++) {
if (array[j + 1] < array[j]) {
Swap(array[j + 1], array[j]);
last = j;
}
}
i = last; //如果一趟排序中没有交换元素,则last为0,说明排序完成,之后就可以退出了
}
}
(二)鸡尾酒排序
鸡尾酒排序是冒泡排序的变形,也叫做双向冒泡排序。它是先从低到高排序,再从高到低排序。如此反复,直到子序列已经有序。它性能不会比冒泡排序差,相反,更多时候会更好。比如排序(2,3,4,5,1),用鸡尾酒排序只需要排序两趟(去一趟,回一趟)。而如果用冒泡排序,需要4趟。
//鸡尾酒排序(双向冒泡排序)
template <typename T>
void CocktailSort(T array[],int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
int low_index,high_index;
low_index = 0;
high_index = n - 1;
bool hasSwap = false;
int i, j;
int last;
while (low_index < high_index) {
last = 0;
for (i = low_index; i < high_index; i++) { //从左到右冒泡
if (array[i + 1] < array[i]) {
Swap(array[i + 1], array[i]);
hasSwap = true;
last = i;
}
}
if (hasSwap) {
hasSwap = false;
high_index = last;
last = 0;
}
for (i = high_index-1; i >= low_index; i--) { //从右到左冒泡
if (array[i+1] < array[i]) {
Swap(array[i + 1], array[i]);
hasSwap = true;
last = i+1;
}
}
if (hasSwap) {
hasSwap = false;
low_index = last;
} else {
break;
}
}
}
(三)奇偶换位排序
奇偶排序法的思路是在数组中重复两趟扫描。第一趟扫描排序所有奇-偶位置数字对,如果它们的关键字的值次序颠倒,就交换它们。第二趟扫描排序所有偶-奇位置数字对。重复进行这样两趟的排序直到数组全部有序。
奇偶排序实际上在多处理器环境中很有用,处理器可以分别同时处理每一个奇数对,然后又同时处理偶数对。因为奇数对是彼此独立的,每一刻都可以用不同的处理器比较和交换。这样可以非常快速地排序。
//奇偶换位排序
template <typename T>
void OddEvenSort(T array[],int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
bool isSorted = false;
int i;
while (!isSorted) {
isSorted = true;
for (i = 0; i < n - 1; i += 2) { //排序奇-偶位置数字对
if (array[i] > array[i + 1]) {
Swap(array[i], array[i + 1]);
isSorted = false;
}
}
for (i = 1; i < n - 1; i += 2) { //排序偶-奇位置数字对
if (array[i] > array[i + 1]) {
Swap(array[i], array[i + 1]);
isSorted = false;
}
}
}
}
(四)梳子排序
梳排序和希尔排序很类似。希尔排序是在直接插入排序的基础上做的优化,而梳排序是在冒泡排序的基础上做的优化。也是像希尔排序一样,将待排序序列通过增量分为若干个子序列,然后对子序列进行一趟冒泡排序,一步步减小增量,直至增量为1。所以梳排序的最后一次排序是冒泡排序。 增量的计算是待排数组长度除以1.3得到近似值,下次则以上次得到的近似值再除以1.3。1.3是该算法的作者通过研究得出来的能使效率最大化的数字。
//梳子排序
template <typename T>
void CombSort(T array[],int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
const float rate = 1.3;
int i, j, pace;
pace = n;
while (true) {
pace = (int) (pace / rate);
for (i = 0; i < pace; i++) {
for (j = i + pace; j < n; j += pace) {
if (array[j - pace] > array[j]) {
Swap(array[j - pace], array[j]);
}
}
}
if (pace <= 1) {
break;
}
}
}
(五)侏儒排序
从头开始遍历元素,如果当前元素比前一个元素大,就把它跟前一个元素互换并继续检查前一个元素(i–),否则检查下一个元素(i++)。当i=n时结束排序。
这个算法代码实现非常简单,只需要一个循环就好。但是!一重循环不代表时间复杂度更小,它的时间复杂度依然是O(n^2)。因为游标i并不总是往前推进的,当算法找到一组大小顺序错误的毗邻元素时,交换它们后,因为这个交换可能会带来新的顺序错误的毗邻元素对,所以需要通过(i–)来重新检查。
//侏儒排序
template<typename T>
void DwarfSort(T array[], int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
int i = 0;
while (i < n) {
if (0 == i || array[i - 1] <= array[i]) {
i++;
} else {
Swap(array[i - 1], array[i]);
i--;
}
}
}
(六)快速排序
轮到大名鼎鼎的快速排序了!
快速排序的基本思想是:对任意给定的序列中某个元素(称为基准元素),经过一趟排序后,将原序列分割成两个子序列,其中前一个子序列中的所有元素的关键字均小于或等于该元素的关键字,后一个子序列中元素的关键字大于或等于该元素的关键字。如此重复,直到子序列为空或只有一个元素时结束,最后得到有序序列。
最好的情况下:因为每次都将序列分为两个部分(一般二分的复杂度都和logn相关),要对n个数进行处理,故为 O(n*logn)
最坏的情况下:基本有序时,退化为冒泡排序。更本质上来讲,是基准元素选的不好。每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。因此,快速排序必须做 n-1 次划分,第 i 次划分开始时区间长度为 n-i-1, 所需的比较次数为 n-i(1<=i<=n-1), 故总的比较次数达到最大值 n(n-1)/2=O(n^2) 。所以要么选择一个随机的作基准,或者选择一个分区中间的下标作为基准,或者(特别是对于相比更长的分区)选择分区的第一个、中间、最后一个元素的中值作为基准。
不过本文我的实现就只是选最左一个元素作为基准。学习下思想就okay啦。
//快速排序的私有递归
template<typename T>
void QuickSort(T array[], int left, int right) {
int i, j;
if (left < right) {
i = left + 1;
j = right;
do { //将array[left]作为分割元素
while (array[i] < array[left]) {
i++;
}
while (array[j] > array[left]) {
j--;
}
if (i < j) {
Swap(array[i], array[j]);
}
} while (i < j);
Swap(array[left], array[j]);
QuickSort(array, left, j - 1);
QuickSort(array, j + 1, right);
}
}
//快速排序的外部接口
//最好情况O(nlogn),最坏情况O(n^2)
//不稳定排序
template <typename T>
void QuickSort(T array[], int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
QuickSort(array, 0, n - 1);
}
(七)臭皮匠排序
臭皮匠排序是一种低效的递归排序算法,甚至慢于冒泡排序。每个序列判断头尾大小并交换头尾两个后,分为三个子序列,也就是三个臭皮匠。
具体做法是:
(1)如果最后一个值小于第一个值,则交换它们
(2)如果当前子集元素数量大于等于3:
- 使用臭皮匠排序前2/3的元素
- 使用臭皮匠排序后2/3的元素
- 再次使用臭皮匠排序前2/3的元素
//臭皮匠排序
//私有接口
template <typename T>
void StoogeSort(T array[],int low,int high) {
if (array[high] < array[low]) {
Swap(array[high], array[low]);
}
if ((high - low + 1) >= 3) {
int t = (high - low + 1) / 3;
StoogeSort(array, low, high - t); //排序前2/3的元素
StoogeSort(array, low + t, high); //排序后2/3的元素
StoogeSort(array, low, high - t); //再次排序前2/3的元素
}
}
//臭皮匠排序的外部接口
template <typename T>
void StoogeSort(T array[], int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
StoogeSort(array, 0, n - 1);
}
二、选择类排序
(一)简单选择排序
基本思想是:将初始序列(A[0]~A[n-1])作为待排序序列,第一趟在待排序序列(A[0]~A[n-1])中找最小值元素,与该序列中第一个元素A[0]交换,这样子序列(A[0])有序,下一趟排序在待排序子序列(A[1]~A[n-1])中进行。也就是说,第i趟排序在待排序子序列(A[i-1]~A[n-1])中,找最小值元素,与该子序列中第一个元素A[i-1]交换。经过n-1趟排序后使得初始序列有序。
可见,该算法执行时间与元素的初始排列无关,无论初始排列如何,该算法都必须执行n-1趟,每趟执行n-i-1次关键字的比较。所以最好、最坏和平均情况的时间复杂度都为O(n^2)。
由于每次都是选取未排序序列A中的最小元素与A中的第一个元素交换,因此很可能破坏了元素间的相对位置,因此选择排序是不稳定的。
//选择类
//简单选择排序
//该算法执行时间与元素的初始排列无关,无论初始排列如何,该算法都必须执行n-1趟,每趟执行n-i-1次关键字的比较。
//所以最好、最坏和平均情况的时间复杂度都为O(n^2)。
//是不稳定的排序。
template <typename T>
void SelectSort(T array[], int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
int minIndex;
for (int i = 0; i < n - 1; ++i) { //执行n-1趟
minIndex = i;
for (int j = i + 1; j < n; ++j) { //每趟扫描待排序序列n-i-1次
if (array[j] < array[minIndex]) {
minIndex = j;
}
}
Swap(array[minIndex], array[i]); //最小元素与待排序序列中第一个元素交换
}
}
(二)堆排序
因为本文我是按照从小到大进行排序,所以此处使用的是最大堆。
堆排序的基本思想是:将初始序列通过向下调整函数构造成最大堆,第一趟排序,将堆顶元素A[0]和堆底元素A[n-1]交换位置,然后再调用向下调整函数将A[0]向下调整,使得剩余的前n-1个元素还是最大堆。如此反复操作,直到堆中只剩一个元素。
最好、最坏、平均情况下时间复杂度都是O(nlogn)。
堆排序也是不稳定的排序。
//下沉,也就是向下调整
template <typename T>
void Sink(T array[],int n,int i) {
int j;
while (2 * i + 1 < n) {
j = 2 * i + 1;
if (j < n-1 && array[j] < array[j + 1]) { //找两个孩子中较大的一个
j++;
}
if (array[i] >= array[j]) { //父结点比孩子大,向下调整结束
break;
} else {
Swap(array[i], array[j]);
i = j;
}
}
}
//堆排序(此处实现的是最大堆)
//时间复杂度O(nlogn)
//不稳定排序
template <typename T>
void HeapSort(T array[],int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
int i;
for (i = (n - 2) / 2; i >= 0; i--) { //初始化最大堆
Sink(array, n, i);
}
for (i = n - 1; i > 0; i--) {
Swap(array[0], array[i]); //堆顶和堆底元素交换位置
Sink(array, i, 0); //从堆顶开始向下调整成最大堆
}
}
三、插入类排序
(一)直接插入排序
基本思想:将序列中第一个元素作为一个有序序列,然后将剩下的n-1个元素按关键字大小依次插入该有序序列,每插入一个元素后依然保持该序列有序,经过n-1趟排序后即成为有序序列。
最好的情况下:正序有序(从小到大),这样只需要比较n次,不需要移动。因此时间复杂度为O(n)
最坏的情况下:逆序有序,这样每一个元素就需要比较n次,共有n个元素,因此实际复杂度为O(n2)
平均情况下:O(n2)
若k1和k2相等,在插入排序中,设k1是已排序部分中的元素,当k2和k1比较时,直接插到k1的后面(没有必要插到k1的前面,这样做还需要移动),因此,插入排序是稳定的。
//插入类
//直接插入排序
//最好情况O(n),最坏情况O(n^2)
template <typename T>
void InsertSort(T array[],int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
T tmp;
int j;
for (int i = 0; i < n; ++i) { //执行n-1趟
j = i;
tmp = array[j];
while (j > 0 && tmp < array[j - 1]) { //从后往前查找插入位置
array[j] = array[j - 1];
j--;
}
array[j] = tmp; //待插入元素存入找到的插入位置
}
}
(二)希尔排序
对于大规模乱序数组插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点地从数组的一端移动到另一端。例如,如果主键最小的元素正好在数组的尽头,要将它挪到正确的位置就需要n-1次移动。希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。
希尔排序的思想是使数组中任意间隔为h的元素都是有序的,每一次都需要对h个子数组独立地排序。所以希尔排序就相当于对每个子数组进入插入排序。
希尔排序算法的性能和h的选取有很大关系。下面代码我采用的是使用了序列1/2(3^k - 1),从n/3开始递减至1。也就是用了序列1,4,13,40,121,364,1093,….
希尔排序比插入排序和选择排序要快得多,并且数组越大,优势越大。
我们知道直接插入排序是稳定的,那希尔排序呢?由于希尔排序其实是不同的插入排序过程,这样在不同的排序过程中相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
//希尔排序
//不稳定排序
template <typename T>
void ShellSort(T array[],int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
int h = 1;
while (h < n / 3) { //1,4,13,40,121,364,1093,...
h = 3 * h + 1;
}
while (h >= 1) {
for (int i = h; i < n; ++i) {
//将array[i]插入到array[i-h],array[i-2*h],array[i-3*h]...之中
for (int j = i; j >= h && array[j] < array[j - h]; j -= h) {
Swap(array[j], array[j - h]);
}
}
h = h / 3;
}
}
(三)耐心排序
耐心排序是对插入排序的优先,先将数据调整为基本有序,再进入插入排序。调整为基本有序的方法是使用桶,入桶规则是从左到右比较要入桶的数字和桶里最上面的数字,如果能找到第一个桶的最上面数字比待入桶数字大,就放入桶的顶部,如果找不到,就新建一个桶并放入该数字。将所有数字都放入桶后,再依次将所有桶中的数据从上到下取出来存到数组中,这样该数组就基本有序了。对于基本有序的数组,插入排序是个不错的选择。
//耐心排序
template <typename T>
void PatienceSort(T array[],int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
//链表结点
struct Node{
T data;
Node *next;
};
//每个桶是一个链表
Node **bucket = new Node *[n];
for (int i = 0; i < n; ++i) {
bucket[i] = NULL;
}
int i,j;
Node *p = NULL;
for (i = 0; i < n; ++i) { //将n个数放入桶中
j = 0;
//找到第一个最上面的数据比关键字大的桶,如果没找到则指向一个新的桶
while (bucket[j] != NULL && bucket[j]->data < array[i]) {
j++;
}
p = new Node;
p->data = array[i];
if (bucket[j] != NULL) { //表示找到了第一个最上面的数据比关键字大的桶
p->next = bucket[j];
bucket[j] = p;
} else { //表示需要放入新桶中
p->next = NULL;
bucket[j] = p;
}
}
i = j = 0;
//顺序从所有桶中将数据取出放入数组
while (bucket[j] != NULL) {
p = bucket[j];
while (p != NULL) {
array[i++] = p->data;
p = p->next;
}
j++;
}
//因为此时数组已经基本有序,所以最后再执行一次插入排序就可以了。
InsertSort(array, n);
for (i = 0; i < n; ++i) {
delete (bucket[i]);
}
}
四、归并类排序
(一)两路合并排序
两路合并排序的时间复杂度是O(nlogn),基本思想是:将有n个元素的序列看成是n个长度为1的有序子序列,然后两两合并子序列,得到ceiling(n/2)个长度为2或1的有序子序列;再两两合并 ,…,直到得到一个长度为n的有序序列时结束。
需进行logN 趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。
//归并类排序
//两路合并
template <typename T>
void Merge(T array[],int i1,int j1,int i2,int j2) {
//i1,ji是子序列1的下、上界,i2,j2是子序列2的下、上界
T *tmp = new T[j2 - i1 + 1]; //分配能存入两个子序列的临时数组
int i = i1, j = i2, k = 0; //i,j是两个子序列的游动指针,k是tmp的游动指针
while (i <= j1 && j <= j2) { //将array[i]和array[j]中较小的存入tmp[k]
if (array[i] < array[j]) {
tmp[k++] = array[i++];
} else {
tmp[k++] = array[j++];
}
}
while (i <= j1) { //若子序列1还有剩余,则存入tmp
tmp[k++] = array[i++];
}
while (j <= j2) { //若子序列2还有剩余,则存入tmp
tmp[k++] = array[j++];
}
for (i = 0; i < k; i++) {
array[i1++] = tmp[i]; //将临时数组中的元素倒回array
}
delete[] tmp;
}
//合并排序
//时间复杂度O(nlogn)
template <typename T>
void MergeSort(T array[],int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
int i1, j1, i2, j2; //i1,ji是子序列1的下、上界,i2,j2是子序列2的下、上界
int size = 1; //子序列中的元素个数,初始化为1
while (size < n) {
i1 = 0;
while (i1 + size < n) { //若i1+size<n,则说明存在两个子序列,需两两合并
i2 = i1 + size; //确定子序列2的下界
j1 = i2 - 1; //确定子序列1上界
if (i2 + size - 1 > n - 1) { //若子序列2中不足size个元素,则置子序列2的上界j2=n-1
j2 = n - 1;
} else {
j2 = i2 + size - 1; //否则有size个,则置j2=i2+size-1
}
Merge(array, i1, j1, i2, j2); //合并相邻两个子序列
i1 = j2 + 1; //确定下一次合并时子序列1的下界
}
size *= 2; //元素个数扩大一倍
}
}
(二)Strand排序
基本思想是:
(1)从原始序列中取出第一个最长非递减子序列。
(2)从剩下序列中取出下一个最长非递减子序列,与第一个子序列进行合并,存到第一个子序列中。重复此操作,直到原始序列没有元素为止。
//对两个递增子序列进行合并
template <typename T>
void InnerMerge(T result[],int resLen,T sublist[],int subLen) {
T *tmp = new T[resLen + subLen];
int i=0, j=0, k;
for (k = 0; i < resLen && j < subLen; k++) {
if (result[i] < sublist[j]) {
tmp[k] = result[i++];
} else {
tmp[k] = sublist[j++];
}
}
if (i < resLen) {
memcpy(tmp + k, result + i, (resLen - i) * sizeof(T));
} else if (j < subLen) {
memcpy(tmp + k, sublist + j, (subLen - j) * sizeof(T));
}
memcpy(result, tmp, (resLen + subLen) * sizeof(T));
delete[] tmp;
}
//Strand排序
template <typename T>
void StrandSort(T array[],int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
T *result = new T[n]();
T *sublist = new T[n]();
int i;
int resLen = 0;
int flag = 1 << 31;
result[0] = array[0];
array[0] = flag; //置为flag,表示已从数组中去除
//取出第一个最长非递减子序列
// 比如原始序列{4,1,5,3,2},第一个最长非递减子序列为{4,5},原序列变成了{1,3,2}
for (i = 1; i < n; i++) {
if (array[i] >= result[resLen]) {
result[++resLen] = array[i];
array[i] = flag;
}
}
resLen++;
int subLen = -1;
bool finished = true;
int begin = 0;
while(true) {
finished = true;
subLen = -1;
//从原序列中取出下一个最长非递减子序列
for (i = begin; i < n; i++) {
if (array[i] != flag) {
if (-1 == subLen) {
begin = i+1; //标志当前array中第一个非flag之后的位置
sublist[++subLen] = array[i];
array[i] = flag;
finished = false;
} else if (array[i] >= sublist[subLen]) {
sublist[++subLen] = array[i];
array[i] = flag;
}
}
}
if (finished) { //再取不出子序列了,表示已经排序完毕
break;
}
subLen++;
InnerMerge(result, resLen, sublist, subLen); //将两个子序列进行归并,结果存入result中
resLen += subLen;
}
memcpy(array, result, n * sizeof(T));
delete[] result;
delete[] sublist;
}
五、分布类排序
(一)基数排序
基数排序是借助于多关键字的思想进行排序。
多关键字的排序,比如将一副扑克牌按花色和面值排序,这样的话我们可以先根据花色进行排序,再根据面值进行排序。当然,我们也可以先根据面值进行排序,再根据花色进行排序。这可以归纳成多关键字排序中的两种方法:最高位优先法MSD(Most Significant Digit first)和最低位优先法LSD(Least Significant Digit first)。
对于非负整数的排序,运用基数排序的最低位优先法LSD,其操作就是先个位数进行排序,再根据十位数进行排序,依次类推。
具体做法可以是准备10个桶,表示基数0~9,每个桶可以是一个队列。然后找到待排序数中最大的数,求出其位数。根据位数来控制排序趟数,依次对每个数提取出具体位,根据该位上的数字将该数存入新桶里,该趟排序结束后将所有桶中的数倒回原数组中。之后开始新的一趟排序。
分配需要O(n),收集为O(r),其中r为分配后桶的个数,以r=10为例,则有0~9这样10个桶来将原来的序列分类。而d,也就是位数(如最大的数是1234,位数是4,则d=4),即”分配-收集”的趟数。因此时间复杂度为O(d*(n+r))。
//分布类排序
//基数排序。LSD:最低位优先法
template <typename T>
void RadixSort(T array[],int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
T max_value = array[0];
vector<queue<T>> buckets(10); //10个桶,表示基数0-9,每个桶中是一个队列
for (int i = 1; i < n; ++i) { //找出最大的数
if (max_value < array[i]) {
max_value = array[i];
}
}
int loopTime = 0; //循环趟次,也是最大数的位数
while (max_value != 0) {
loopTime++;
max_value /= 10;
}
int k; //控制趟数
int bucketIndex; //桶的下标
int size; //桶中元素的个数
int elementIndex; //桶中元素的下标
T element; //某个元素
int j; //具体位上的数字
int tmp; //用于求具体位上的数字。比如798,个位桶是(798/1)%10=8;十位桶是(798/10)%10=9;百位桶是(798/100)%10=7
int index = 0; //数组中元素的下标
for (k = 1; k <= loopTime; k++) { //循环loopTime趟
tmp = (int) pow(10, k - 1);
for (index = 0; index < n; index++) { //将数组中每个元素放入对应的桶中
element = array[index];
j = (element / tmp) % 10; //提取出具体位
buckets.at(j).push(element); //存入新桶
}
//将所有桶中的元素都倒回数组中
index = 0;
for (bucketIndex = 0; bucketIndex < 10; bucketIndex++) {
while (!buckets.at(bucketIndex).empty()) {
array[index++] = buckets.at(bucketIndex).front();
buckets.at(bucketIndex).pop();
}
}
}
}
(二)计数排序
计数排序使用一个额外的数组counts,其中第i个元素是待排序数组array中值等于i的元素的个数。空间开销比较大,一般适合整数的范围为0-99之间。
//计算排序
//空间开销比较大,一般适合整数的范围为0-99之间
/*
* k:表示array中的元素范围为0~k-1之间的整数
*/
void CountingSort(int array[],int n, int k) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
//进行计数
int *counts = new int[k]();
for (int i = 0; i < n; ++i) {
counts[array[i]]++;
}
//存回原数组
int l = 0;
for (int i = 0; i < k; i++) {
for (int j = 0; j < counts[i]; ++j) {
array[l++] = i;
}
}
}
(三)桶排序
桶排序 也叫做箱排序,基本思想是将数组分到有限数量的桶子里。每个桶子再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。
//桶排序
template <typename T>
void BucketSort(T array[],int n) {
if (n <= 0) {
cout<<"input error"<<endl;
return;
}
int min,max;
min = max = array[0];
//先求出待排序列的范围
for (int i = 0; i < n; ++i) {
if (array[i] > max) {
max = array[i];
} else if (array[i] < min) {
min = array[i];
}
}
//求出需要桶的个数
int count = (max - min + 1) / 10 + 1;
vector<T> buckets[count];
//将待排序列存入对应的桶中
int j;
for (int i = 0; i < n; ++i) {
j = (array[i] - min + 1) / 10;
buckets[j].push_back(array[i]);
}
//分别对每个桶进行快排
for (int i = 0; i < count; ++i) {
sort(buckets[i].begin(), buckets[i].end()); //这是用了c++库中的排序函数
}
//将所有桶中的数据存回原数组
j = 0;
for (int i = 0; i < count; ++i) {
if (!buckets[i].empty()) {
for (auto begin = buckets[i].cbegin(); begin != buckets[i].cend(); begin++) {
array[j++] = *begin;
}
}
}
}
————————————————————测试用例————————————————————
int main() {
int n = 10;
int array[n];
default_random_engine e;
e.seed(time(0));
uniform_int_distribution<int> u(0, 100);
for (int i = 0; i <n; ++i) {
array[i] = u(e);
}
cout<<"before sort: ";
for (int i = 0; i < n; ++i) {
cout<<array[i]<<" ";
}
cout<<endl;
cout<<"after sort: ";
BucketSort(array, n);
for (int i = 0; i < n; ++i) {
cout<<array[i]<<" ";
}
cout<<endl;
}
————————————————————从网上找的排序算法复杂度与稳定性表—————————————————————