目录:
一、排序算法的性质
二、十大排序算法介绍及代码实现(C++版)
一、排序算法的性质
1.稳定性
如果在待排序的序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
2.时间复杂度
1)时间频度
一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。算法的时间复杂度是指执行算法所需要的计算工作量。
(2)时间复杂度
在刚才提到的时间频度中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念。
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),存在一个正常数c使得fn*c>=T(n)恒成立。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
在各种不同算法中,若算法中语句执行次数为一个常数,则时间复杂度为O(1),另外,在时间频度不相同时,时间复杂度有可能相同,如T(n)=n^2+3n+4与T(n)=4n^2+2n+1它们的频度不同,但时间复杂度相同,都为O(n^2)。
按数量级递增排列,常见的时间复杂度有:
常数阶O(1),对数阶O(log2n)(以2为底n的对数,下同),线性阶O(n),
线性对数阶O(nlog2n),平方阶O(n^2),立方阶O(n^3),...,
k次方阶O(n^k),指数阶O(2^n)。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
3.空间复杂度
与时间复杂度类似,空间复杂度是指算法在计算机内执行时所需存储空间的度量。记作:
S(n)=O(f(n))
算法执行期间所需要的存储空间包括3个部分:
(1)算法程序所占的空间;
(2)输入的初始数据所占的存储空间;
(3)算法执行过程中所需要的额外空间。
4.排序算法性质比较

二、十大排序算法介绍
1.冒泡排序法
冒泡排序(Bubble Sort)是一种较简单的排序算法,重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)不同就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同水中的气泡最终会上浮到顶端一样,故名“冒泡排序”。
冒泡排序的原理如下:
a. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
b. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
c. 针对所有的元素重复以上的步骤,除了最后一个。
d. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
流程图:
冒泡排序C++代码:
#include<iostream>
#include<Windows.h>
using namespace std;
//冒泡过程
template< class T>
void bubble(T A[], int n)
{//将最大元素冒到最尾部或者顶部
for (int i = 0; i < n - 1; i++)
{
if (A[i] > A[i + 1])
swap(A[i], A[i + 1]);
}
}
//冒泡排序
template <class T>
void bubbleSort(T A[], int n)
{
for (int i = n; i>1; i--)
{
bubble(A, i);
}
}
//测试代码
int main(int argc, char ** argv)
{
int a[] = { 6, 4, 7, 2, 3, 1, 5 };
cout << "原数组是:" << endl;
for (int i = 0; i < 7; i++)
{
cout << a[i] << " ";
}
bubbleSort(a, 7);
cout << endl;
for (int i = 0; i < 7; i++)
{
cout << a[i] << " ";
}
Sleep(10000);
}
2.选择排序法
选择排序法是一种不稳定的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。
流程图:
选择排序C++代码:
#include<iostream>
#include<Windows.h>
using namespace std;
template <class T>
int indexOfMax(T A[], int n)
{
int temp = A[0];
//找出最大值
for (int i = 0; i < n; i++)
{
if (temp < A[i])
temp = A[i];
}
//找出最大值的索引
int i = -1;
for (i = 0; i < n; i++)
{
if (temp == A[i])
break;
}
return i;
}
//选择排序
template <class T>
void selectSort(T A[], int n)
{
//使用选择排序算法给数组排序
for (int i = n; i > 1; i--)
{
int max = indexOfMax(A, i);
swap(A[max], A[i - 1]);
}
}
int main(int argc, char ** argv)
{
int a[] = {6,5,8,4,3,1};
selectSort(a, 6);
for (int i = 0; i<6; i++)
{
cout << " ," << a[i] << endl;
}
Sleep(10000);
}
3.插入排序法
插入排序,一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法 。插入排序是一种简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而形成一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。
流程:
插入排序C++代码:
#include<iostream>
using namespace std;
//插入元素
template<class T>
void insert(T a[], int n, const T &x)
{
int i;
for (i = n - 1; i >= 0 && x < a[i]; i--)
{
a[i + 1] = a[i];
}
a[i + 1] = x;
}
template <class T>
void insertSort(T a[], int n)
{
for (int i = 1; i < n; i++)
{
T t = a[i];
insert(a, i, t);
}
}
4.快速排序法
快速排序由C. A. R. Hoare在1960年提出。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
流程:
快速排序C++代码:
int Paritition1(int A[], int low, int high) {
int pivot = A[low];
while (low < high) {
while (low < high && A[high] >= pivot) {
--high;
}
A[low] = A[high];
while (low < high && A[low] <= pivot) {
++low;
}
A[high] = A[low];
}
A[low] = pivot;
return low;
}
void QuickSort(int A[], int low, int high) //快排母函数
{
if (low < high) {
int pivot = Paritition1(A, low, high);
QuickSort(A, low, pivot - 1);
QuickSort(A, pivot + 1, high);
}
}
5.希尔排序法
希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键字越来越多,当增量减至 1 时,整个文件恰被分成一组,算法终止。
流程:
希尔排序C++代码:
template<typename T>
void shell_sort(T array[], int length) {
int h = 1;
while (h < length / 3) {
h = 3 * h + 1;
}
while (h >= 1) {
for (int i = h; i < length; i++) {
for (int j = i; j >= h && array[j] < array[j - h]; j -= h) {
std::swap(array[j], array[j - h]);
}
}
h = h / 3;
}
}
6.计数排序法
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)), 如归并排序,堆排序)。它是计算一个序列中的值在正常排好序中的序列所处的位置,怎么求解一个数的位置呢?就是利用下脚标进行求解,新建一个数组resu[],数组的长度要比序列中的最大值大1,数组中的值全部初始化为0,然后遍历原序列,将原序列的值i作为新建数组resu[]的下脚表,对resu[i]++操作,这样就得出值 i 出现的次数;然后再利用resu[i]=resu[i]+resu[i-1]求解出i在原序列中的位置。
流程:
计数排序的C++代码:
#include<iostream>
#include<Windows.h>
using namespace std;
/**
求数组元素的名次,结果存入r中
**/
template <class T>
void rankarr(T arr[], int n, int r[])
{
for (int i = 0; i < n; i++)
{
r[i] = 0; //初始化r
}
//从第一个元素开始两两比较所有元素
for (int i = 1; i < n; i++)
{
for (int j = 0; j < i; j++)
{
if (arr[j] <= arr[i])
r[i]++;
else
r[j]++;
}
}
}
//名次排序,计数排序
template <class T>
void rankSort(T a[], int n, int r[])
{
//新建一个数组
T *b = new T[n];
//把a中的元素移到b中的位置,按照r的名次
for (int i = 0; i < n; i++)
{
b[r[i]] = a[i];
}
//把b中元素移回到a
for (int i = 0; i < n; i++)
{
a[i] = b[i];
}
}
//测试代码
int main(int argc, char ** argv)
{
//int a[] = { 3, 4, 2, 1, 6, 7, 2, 3, 5 ,4};
//cout << "原数组:" << endl;
//for (int i = 0; i < 10; i++)
//{
// cout << a[i] << " ";
//}
//int b[10];
排名次
//rankarr(a, 10, b);
按名次排序
//rankSort(a,10,b);
//cout<< endl;
//cout << "排序后:" << endl;
//for (int i = 0; i < 10; i++)
//{
// cout << a[i] << " ";
//}
int a = 1;
a <<= 33;
cout << a << endl;
Sleep(10000);
}
7.堆排序法
堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
(1)最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点。
(2)创建最大堆(Build Max Heap):将堆中的所有数据重新排序。
(3)堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算。
[1] 首先,从要排序的数组开始建立一个大顶堆(这里以大顶堆说明,如何建大顶堆,下面详解),这样由大顶堆的性质可知,堆顶的数最大;
[2] 然后,交换堆顶key[0]和堆的最后一个元素Key[n-1],Key[0]<->Key[n-1] ,这样最大的数就放到堆的最后Key[n-1]了;由于交换了元素,0 ~ (n-2)的节点可能不满足大顶堆要求,所以然后对堆的Key[0]--Key[n-2]调整为大顶堆;
[3] 将调整好的大顶堆[0~(N-2)],交换堆顶Key[0]和堆的最后一个元素Key[n-2],Key[0]<->Key[n-2],这样,Key[n-2]、Key[n-1]最大的两个数都已经排好了;重新调整[0~(n-3)]为大顶堆;不断重复此过程直到整个排序过程完成
如何建大顶堆呢?
[1] 首先,从最大的非叶节点(设为Key[i])开始,往下进行调整,如果子孩子节点Key[2*i+1]和Key[2*i+2]较大的值比Key[i]的值大,则,交换该节点Key[i]与子节点中较大的节点。然后 以交换后的子节点作为当前节点,往下进行调整,直到遇到叶节点;
[2] 最大非叶节点Key[i]调整完毕后,对节点Key[i-1]进行往下调整,直到出现叶节点;不断重复此过程,直到对根节点往下调整完毕;
下面是建大顶堆的实例:待排序数组为[5 16 3 20 17 4]
堆排序流程实例:
演示
堆排序C++代码:
#include <iostream>
#include <algorithm>
using namespace std;
void max_heapify(int arr[], int start, int end) {
// 建立父节点和子节点指针
int dad = start;
int son = dad * 2 + 1;
while (son <= end) { // 如果子节点指针在范围内才作比较
if (son + 1 <= end && arr[son] < arr[son + 1]) //先比较两个字节点大小,选择最大的
son++;
if (arr[dad] > arr[son]) // 如果父節點大於子節點代表調整完畢,直接跳出函數
return;
else { // 否則交換父子內容再繼續子節點和孫節點比較
swap(arr[dad], arr[son]);
dad = son;
son = dad * 2 + 1;
}
}
}
void heap_sort(int arr[], int len) {
// 初始化,i從最後一個父節點開始調整
for (int i = len / 2 - 1; i >= 0; i--)
max_heapify(arr, i, len - 1);
// 先將第一個元素和已经排好的元素前一位做交換,再從新調整(刚调整的元素之前的元素),直到排序完畢
for (int i = len - 1; i > 0; i--) {
swap(arr[0], arr[i]);
max_heapify(arr, 0, i - 1);
}
}
int main() {
int arr[] = { 3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6 };
int len = (int) sizeof(arr) / sizeof(*arr);
heap_sort(arr, len);
for (int i = 0; i < len; i++)
cout << arr[i] << ' ';
cout << endl;
return 0;
}
8.归并排序
归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并操作,也叫归并算法,指的是将两个顺序序列合并成一个顺序序列的方法。
归并排序实例:
归并排序C++代码:
#include <iostream>
void Merge(int r[], int r1[], int s, int m, int t)
{
int i = s;
int j = m + 1;
int k = s;
while (i <= m && j <= t)
{
if (r[i] <= r[j])
r1[k++] = r[i++];
else
r1[k++] = r[j++];
}
if (i <= m)
while (i <= m)
r1[k++] = r[i++];
else
while (j <= t)
r1[k++] = r[j++];
for (int n = s; n <= t; n++)
r[n] = r1[n];
}
void MergeSort(int r[], int r1[], int s, int t)
{
if (s < t)
{
int m = (s + t) / 2;
MergeSort(r, r1, s, m);
MergeSort(r, r1, m + 1, t);
Merge(r, r1, s, m, t);
}
}
int main()
{
int r[8] = {10, 3, 5, 1, 9, 34, 54, 565}, r1[8];
MergeSort(r, r1, 0, 7);
for (int q = 0; q < 8; q++)
std::cout << r[q] << std::ends;
return 0;
}
9.桶排序
桶排序 (Bucket sort)或所谓的箱排序,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(O(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。
桶排序算法要求,数据的长度必须完全一样,程序过程要产生长度相同的数据,使用下面的方法:Data=rand()/10000+10000上面提到的,每次下一次的扫描顺序是按照上次扫描的结果来的,所以设计上提供相同的两个桶数据结构。前一个保存每一次扫描的结果供下次调用,另外一个临时拷贝前一次扫描的结果提供给前一个调用。
数据结构设计:链表可以采用很多种方式实现,通常的方法是动态申请内存建立结点,但是针对这个算法,桶里面的链表结果每次扫描后都不同,就有很多链表的分离和重建。如果使用动态分配内存,则由于指针的使用,安全性低。所以,笔者设计时使用了数组来模拟链表(当然牺牲了部分的空间,但是操作却是简单了很多,稳定性也大大提高了)。共十个桶,所以建立一个二维数组,行向量的下标0—9代表了10个桶,每个行形成的一维数组则是桶的空间。
平均情况下桶排序以线性时间运行。像基数排序一样,桶排序也对输入作了某种假设, 因而运行得很快。具 体来说,基数排序假设输入是由一个小范围内的整数构成,而桶排序则 假设输入由一个随机过程产生,该过程将元素一致地分布在区间[0,1)上。 桶排序的思想就是把区间[0,1)划分成n个相同大小的子区间,或称桶,然后将n个输入数分布到各个桶中去。因为输入数均匀分布在[0,1)上,所以一般不会有很多数落在一个桶中的情况。为得到结果,先对各个桶中的数进行排序,然后按次序把各桶中的元素列出来即可。
桶排序流程实例:
桶排序C++代码:
#include<iostream>
usingnamespace std;
int a[]={1,255,8,6,25,47,14,35,58,75,96,158,657};
const int len=sizeof(a)/sizeof(int);
int b[10][len+1]={0};//将b全部置0
void bucketSort(int a[]);//桶排序函数
void distribute Elments(int a[],int b[10][len+1],int digits);
void collectElments(int a[],int b[10][len+1]);
int numOfDigits(int a[]);
void zeroBucket(int b[10][len+1]);//将b数组中的全部元素置0
int main()
{
cout<<"原始数组:";
for(int i=0;i<len;i++)
cout<<a[i]<<",";
cout<<endl;
bucketSort(a);
cout<<"排序后数组:";
for(int i=0;i<len;i++)
cout<<a[i]<<",";
cout<<endl;
return 0;
}
void bucketSort(int a[])
{
int digits=numOfDigits(a);
for(int i=1;i<=digits;i++)
{
distributeElments(a,b,i);
collectElments(a,b);
if(i!=digits)
zeroBucket(b);
}
}
int numOfDigits(int a[])
{
int largest=0;
for(int i=0;i<len;i++)//获取最大值
if(a[i]>largest)
largest=a[i];
int digits=0;//digits为最大值的位数
while(largest)
{
digits++;
largest/=10;
}
return digits;
}
void distributeElments(int a[],int b[10][len+1],int digits)
{
int divisor=10;//除数
for(int i=1;i<digits;i++)
divisor*=10;
for(int j=0;j<len;j++)
{
int numOfDigist=(a[j]%divisor-a[j]%(divisor/10))/(divisor/10);
//numOfDigits为相应的(divisor/10)位的值,如当divisor=10时,求的是个位数
int num=++b[numOfDigist][0];//用b中第一列的元素来储存每行中元素的个数
b[numOfDigist][num]=a[j];
}
}
void collectElments(int a[],int b[10][len+1])
{
int k=0;
for(int i=0;i<10;i++)
for(int j=1;j<=b[i][0];j++)
a[k++]=b[i][j];
}
void zeroBucket(int b[][len+1])
{
for(int i=0;i<10;i++)
for(int j=0;j<len+1;j++)
b[i][j]=0;
}
10.基数排序
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
实现方法:
最高位优先(Most Significant Digit first)法,简称MSD法:先按k1排序分组,同一组中记录,关键码k1相等,再对各组按k2排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd对各子组排序后。再将各组连接起来,便得到一个有序序列。
最低位优先(Least Significant Digit first)法,简称LSD法:先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列
实现原理:
基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine)上的贡献。它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
基数排序流程:
基数排序C++代码:
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
int maxData = data[0]; ///< 最大数
/// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
for (int i = 1; i < n; ++i)
{
if (maxData < data[i])
maxData = data[i];
}
int d = 1;
int p = 10;
while (maxData >= p)
{
//p *= 10; // Maybe overflow
maxData /= 10;
++d;
}
return d;
/* int d = 1; //保存最大的位数
int p = 10;
for(int i = 0; i < n; ++i)
{
while(data[i] >= p)
{
p *= 10;
++d;
}
}
return d;*/
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int *tmp = new int[n];
int *count = new int[10]; //计数器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进行d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete []tmp;
delete []count;
}