排序问题是数据结构中很重要的一个问题,比如在查找的时候,在一个已经排好序的集合中可以采用二分查找的方法达到高效的查找,而在无序的集合中是没有比较快速查找元素的方法的。这里我将介绍几种常用的基本排序(插入排序,冒泡排序,选择排序)和几种效率比较好的排序(快速排序,归并排序,希尔排序),排序算法被写成了一个静态方法,可以直接调用。下面看具体的排序算法。
在介绍排序算法之前,先简单介绍下交换函数,
static void swap(E * datas, int p1, int p2){
E temp = datas[p1];
datas[p1] = datas[p2];
datas[p2] = temp;
}
参数datas是被交换元素所在的数组,p1,p2是交换元素对应的位置
一:选择排序
选择排序就是对数组中的元素进行N趟遍历,每次选出最小的值,然后放到合适的位置,下面是选择排序原理图
上面操作图展示了选择排序的步骤,下面给出选择排序代码和解释
/*
*选择排序
*参数:datas待排序集合,size元素集合大小
*/
static void selectionSort(E * datas, int size){
int min;
for (int i = 0; i < size; ++i){
min = i;
for (int j = i + 1; j < size; ++j){
if (datas[j] < datas[min]){
min = j;
}
}
if (i != min){
Sorts::swap(datas, i, min);
}
}
}
方法中E是模板参数,形式参数datas代表的是待排序的集合,size代表的是待排序集合的大小。选择排序采用的是遍历元素选择最小的元素,然后放在开始的位置,然后再对其余的元素中选择第二小的元素。
函数第一趟从datas[0]~datas[size – 1]中选出最小的放在datas[0]中,第二趟从datas[1]~datas[size -1]中选择最小的放在datas[1]中,后面类似,经过循环的嵌套调用实现排序。
二:插入排序
插入排序就是依次对数组中的每个元素处理,把他们插入到他们左边已经排好序的集合中,也就是说当前被处理元素左边总是已经排好序的集合,下面给出具体操作图:
上面操作图给出了具体的操作步骤,下面给出具体的实现代码和解释。
/*
*插入排序
*参数:datas待排序集合,size元素集合大小
*/
static void insertionSort(E * datas, int size){
for (int i = 1; i < size; ++i){
for (int j = i; j > 0; --j){
if (datas[j] < datas[j - 1]){
//交换数组中两个元素
Sorts<E>::swap(datas, j, j - 1);
}
else{
break;
}
}
}
}
方法中E是模板参数,形式参数datas代表的是待排序的集合,size代表的是待排序集合的大小。插入排序的原理是从左边开始一个元素一个元素地插入,插入元素左边的元素已经是有序的,右边的是待排序的。假设元素element为当前被插入的元素,方法采用循环的嵌套,element从第二个元素到最后一个元素进行迭代,element分别和element前面的元素对比,把element插入合适的位置(因为一开始element前面只有一个元素,所以element前面的序列总是有序的),然后选取下一个要插入的element,如此循环,最后达到排序的效果。
三:冒泡排序
冒泡排序正如其名,最大或者最小的元素就像一个气泡一样,可以飘到数组的最上面,也就是最前面。
下面是冒泡排序的操作图:
上面操作图说明了冒泡的操作过程,下面给出具体的实现:
/*
* 冒泡排序
* 参数:datas待排序集合,size元素集合大小
*/
static void bubbleSort(E * datas, int size){
for (int i = 1; i < size; ++i){
for (int j = 0; j < size - i; ++j){
if (datas[j] > datas[j + 1]){
Sorts<E>::swap(datas, j, j + 1);
}
}
}
}
方法中E是模板参数,形式参数datas代表的是待排序的集合,size代表的是待排序集合的大小。冒泡排序采用的是相邻元素相互比较,然后选出最大的放到集合尾部,然后对剩余元素执行相同操作选择第二大元素放到集合尾部,最后实现排序。
函数第一趟是集合中全部元素(datas[0]~datas[size – 1])相邻比较得出最大的数放到datas[size – 1]位置;第二趟是从datas[0]~datas[size – 2]中相邻比较得出最大的放在datas[size – 2]中,后面以此类推,最后的效果就是大的元素就像泡泡一样往上冒,然后达到排序的效果。
四:快速排序
快排是先找一个轴值,然后确定轴值的最终位置,然后再对轴值左边和右边的子序列递归调用快排实现排序。
快排先找一个轴值,然后放到数组的最后一个元素位置,然后从左边找到第一个比轴值大的元素,从右边找第一个比轴值小的元素,然后交换它们。执行查找和交换操作直到l >= r,然后交换l位置的元素和最后一个元素(轴值),轴值的位置就确定了,然后再对左右子序列递归调用。
下面是操作过程图:
上面给出了操作过程,下面给出具体代码和解释
/*
* 快速排序
* 参数:datas待排序集合,
* start排序集合开始元素,end排序集合结束元素
* 局部变量:pivot代表要轴值,
* 轴值前面元素小于等于等于轴值,右边元素大于轴值
* 局部变量:left 和 right分别用于从左边开始寻找比轴值大的元素和
* 从右边开始寻找比轴值小的元素
*/
static void quickSort(E * datas, int start, int end){
if (start >= end) //递归结束条件
return;
int pivot = (start + end) / 2; //初始化局部变量
int left = start;
int right = end;
/*
*三步确定轴值位置
*/
//步骤1,把轴值和end位置元素交换
Sorts<E>::swap(datas, pivot, end);
//步骤2,确定轴值位置,左边比它小,右边比它大
while (left < right){
while ((left < right) && (datas[left] <= datas[end]))
++left;
while ((left < right) && ((datas[right] > datas[end]) || (right == end)))
--right;
Sorts<E>::swap(datas, left, right);
}
//步骤3,把轴值位置上的元素和轴值(在end位置)交换
Sorts<E>::swap(datas, right, end);
/*
* 对轴值左边和右边元素集合进行快速排序调用
*/
quickSort(datas, start, left - 1);
quickSort(datas, left + 1, end);
}
方法中E是模板参数,形式参数datas代表的是待排序的集合,start代表要排序集合的开始元素,end表示要排序集合的结束元素。局部变量pivot代表轴值,轴值前面元素小于等于轴值,后面元素大于轴值,left,right分别用于代表左边第一个比povit值大的元素和右边第一个比povit值小的元素的位置。
函数首先选定轴值,然后确定轴值的位置,最后对轴值左右两边的元素集合进行quickSort的递归调用。确定轴值位置利用了left和right,先把轴值和最后一个元素交换,从左边开始选择一个比povit值大的元素,然后从右边开始选择一个比povit值小的元素,然后交换它们,重复上述操作指导left==right,最后将最后一个元素(轴值)与left位置元素交换,轴值的位置就确定了。
五:归并排序
归并排序是首先对整个序列进行切割,然后切割到一定程度(此处是只有一个元素时)时对切割后的子序列进行归并(合成一个有序的大序列)。
下面是操作的过程图:
此图只给出了归并的过程,切割成一个元素的过程没有再给出了,下面给出具体的实现和解释:
/*
* 归并排序
* 参数:datas待排序集合,
* temp用于储存切割后的元素并用于归并步骤,
* start排序集合开始元素,end排序集合结束元素
* 局部变量:mid代表元素集的中间位置的元素,
* lpos代表切割后左边部分集合的开始位置,
* rpos代表切割后右边部分集合元素开始位置
*/
static void mergeSort(E * datas, E * temp, int start, int end){
//只有一个元素时递归结束
if (start == end)
return;
//确定中值,对切割后的左右两边元素进行递归调用
int mid = (start + end) / 2;
mergeSort(datas, temp, start, mid);
mergeSort(datas, temp, mid + 1, end);
//储存datas元素,用于后面归并
for (int i = start; i <= end; ++i)
temp[i] = datas[i];
//归并两个子集合
int lpos = start;
int rpos = mid + 1;
for (int curr = start; curr <= end; ++curr){
//左子集合全部归并完时,把右子集合剩余元素归并
if (lpos == mid + 1)
datas[curr] = temp[rpos++];
//右子集合全部归并完时,把左子集合剩余元素归并
else if (rpos > end)
datas[curr] = temp[lpos++];
//左子集合开始元素小于或等于右子集合元素,将左子集合开始元素归并
else if (temp[lpos] <= temp[rpos])
datas[curr] = temp[lpos++];
//右子集合开始元素小于左子集合元素,将右子集合开始元素归并
else
datas[curr] = temp[rpos++];
}
}
方法中E是模板参数,形式参数datas代表的是待排序的集合,temp是归并时用到的数组,start代表开始元素,end代表结束元素。
归并排序首先将待排序元素进行切割,然后对切割后元素进行归并最后达到排序的效果。
六:希尔排序
希尔排序先确定步长,然后根据步长把数组分为几个子序列,然后再对子序列执行排序算法(此处采用插入排序)。希尔排序的目的就是为了减少排序时交换的次数。
下面是希尔排序的操作步骤说明:
//希尔排序
static void shellSort(E * datas, int size){
for (int i = size / 2; i > 2; i /= 2){ //确定步长
for (int j = 0; j < i; j++) //对每一个子序列执行innersort
innerSort(&datas[j], size - j, i);
}
innerSort(datas, size, 1); //最后对元素以步长为1执行插入排序
}
/*
* datas 是待处理数组
* size 是数组的大小
* increasement 是排序时的步长
*/
void static innerSort(E * datas, int size, int increasement){
//对每一个子序列执行插入排序
for (int i = increasement; i < size; i += increasement){
for (int j = i; j >= increasement && datas[j] < datas[j - increasement]; j -= increasement){
swap(datas, j, j - increasement);
}
}
}
七:总结
各种排序都有适用的场合,暂时没用一种排序是在每一个场合中都是最优的