一:常见的几种排序及其实现
二:各种排序的时间空间复杂度
三:各种排序的性能
//////
1.常见的几种排序:
排序的定义:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
///
1.1:插入排序(直接插入):
思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
在插入时,我们面临四种情况:
1.序列为空 ;
2.序列有值,但是插入的值大于序列中任意一个数;
3.序列有值,但是插入的值小于序列中最大值,大于序列中一部分数;
4.序列有值,但是插入的值小于序列中任意一个数;
具体实现代码如下所示:
void Insertsort(int* arr, int n)//将要排序的数组与数组内值的个数传过来;
{
for (int i = 0; i < n - 1; i++)
{
int end=i;//一个一个插入;
int temp = arr[end + 1];//创建临时变量,记录要插入的值,防止插入值被覆盖
while (end >= 0)//控制结束条件
{
if (arr[end] > temp)//如果要插入的值小于数组内的值
{
arr[end + 1] = arr[end];//将原数组的最大值放到要插入值的位置上,
end--;
}
else//如果要插入的值大于数组内的值,直接跳出循环,
{
break;
}
}
arr[end + 1] = temp;//这里主要解决两种情况:插入值大于数组中最大值,直接插入,
//数组中没有值,直接插入要插入的值,
}
}
///
1.1.2:插入排序(希尔排序):
思想:希尔排序法又称缩小增量法。基本思想是:先选定一个整数gap,把待排序文件中所有记录分成若干个组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后取重复上述分组和排序的工作。当到达gap=1时,所有记录在统一组内排好序;
当gap>1时,是预排序:预排序的作用就是让大的数尽可能尽快的到后面,让小的数尽可能尽快的到前面,尽可能尽快的接近有序,
当gap等于1时:此时已经接近有序,就可以很快的排序完成
(gap通常先赋值成数组个数,然后gap=gap/3+1);
具体实现代码如下所示:
void Shelltsort(int* arr, int n)
{
int gap = n;//先将数组元素个数赋值给gap;
while (gap >1)//控制每一次的步长;
{
gap = gap / 3 + 1;//每次步长是之前的步长除3加1;
for (int i = 0; i < n - gap; i++)//一次走一步,每走一步预排序完一次
{
//单趟排序:和插入排序的思想相同,不过一次要跳步长的距离;
int end = i;
int temp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > temp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end+gap] = temp;
}
}
}
///
1.2.1:选择排序(选择排序):
思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完,(在此,我们优化一次,每一次遍历我们选出最大值和最小值);
具体实现代码如下所示:
//选择排序:
void Selectsort(int* arr, int n)
{
int begin= 0;
int end = n - 1;
while (begin<end)//如果begin=end,表明数组选择最大最小值结束;
{
int max = arr[begin];//每次暂定最大值就是下标为begin对应的值
int min = arr[begin];//每次暂定最小值就是下标为begin对应的值
for (int i =begin; i <end+1; i++)//由于有已经排好的数,所以循环范围会跟着begin,end
//变化
{
if (arr[i] > arr[max])//找到最大值;
{
max = i;
}
if(arr[i] <= arr[min])//找到最小值;
{
min = i;
}
}
swap(&arr[begin], &arr[min]);//先交换最小值与下标为begin的值,以防出现先交换最大值与
//下标end而影响最小值的预交换对象被交换走;
swap(&arr[end], &arr[max]);//交换最大值与下标为end的值;
//成功选择后,缩小范围,为下一次选择准备;
begin++;
end--;
}
}
///
1.2.2:选择排序(堆排序):
思想:堆排序是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
1.先建堆(排升序要建大堆,排降序建小堆)
2.同时通过建大堆获取堆顶元素(也就是最大值),让堆顶元素与最后父节点的最小子节点交换,将最大值交换到数组结尾,数组范围-1,循环排序,直到数组大小为0,即完成排序;
具体实现代码如下所示:
void ADJUSTDown(int* arr, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (arr[child + 1] > arr[child] && child + 1 < n)//左孩子加一是右孩子,比左孩子大,让大孩子为右孩子;
{
child++; //此时child为右孩子;
}
if (arr[child] > arr[parent])//如果子节点大于父节点;
{
swap(&(arr[child]), &(arr[parent]));//交换父节点与子节点
parent = child;//让父节点成为新的子节点,
child = parent * 2 + 1;//通过形成的新的父节点再去找子节点去比较;
}
else
{
break;
}
}
}
//堆排序:
void Heapsort(int* arr, int n)
{
//先建堆:
int i = 0;
for (i = (n - 1 - 1) / 2; i >=0; i--)//在数组中,先找到父节点,通过父节点找到子节点,两者比较来建大堆;
{
ADJUSTDown(arr, n, i);//向下调整; 向下调整:(父节点找子节点比较),
}
//交换堆顶元素与数组最后位置,完成排序过程;
int end = n - 1;
while (end > 0)
{
swap(&(arr[0]), &arr[end]);//先交换,使大堆堆顶的数换到数组最后的位置,
ADJUSTDown(arr, end, 0);//重新建堆;
end--;//每一次调完堆后,让交换到数组最后位置的堆顶元素不参加下一次调堆;
}
}
///
1.3.1:交换排序(冒泡排序):
思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
具体实现代码如下所示:
//冒泡排序:
void Bubblesort(int* arr, int n)
{
for (int i = 0; i < n; i++)//循环数组元素次
{
int falg = 0;//优化,如果数组没发生交换,表明数组已经有序
for (int j = 0; j < n - i-1; j++)
{
if (arr[j] > arr[j + 1])//如果前一个元素大于后一个元素;
{
swap(&arr[j], &arr[j + 1]);//交换;
falg = 1;//改变falg的值;
}
}
if (falg == 0)//判断一次循环里面是否发生交换,如果没有发生交换,说明已经有序,直接break
{
break;
}
}
}
///
1.3.2:交换排序(快速排序):
思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止;
有三种单趟排序的方法;
1.霍尔单趟排序;
2.挖坑法单趟排序;
3.快慢指针法单趟排序;
两种优化方式:
1.三数取中(选出适中的数,防止接近有序情况);
2.小区间优化(减少大量递归,因为递归越往下,递归越多,当区间很小时,采取其他排序方式减少不必要的大量递归浪费);
具体实现代码如下所示:
//霍尔版单趟排序:
int PartSort1(int* arr, int begin, int end)
{
int mid = Getmidintdx(arr, begin, end);//三数取中,防止要排序的序列接近有序,所以每次取的基准值应该要适中;
swap(&arr[begin], &arr[mid]);//交换适中值与begin值;
int left = begin;
int right = end;
int key = left;//左边做基准值;
while (left < right)
{
//无论左边还是右边,都需要带上=和left<right,防止左右两边有与基准值相同的值,也防止一边都大于基准值,或者一边都小于基准值;
while (arr[right] >= arr[key] && left < right)//右边先走;
{
right--;
}
while (arr[left] <= arr[key] && left < right)//左边再走;
{
left++;
}
swap(&arr[left], &arr[right]);//左边找到比基准值大的,右边找到比基准值小的,两者交换;
}
swap(&arr[left], &arr[key]);//当right>=left时,说明一趟排序走完,将基准值与left(right)交换,使基准值回到排序后正确的位置上去;
key = left;
return key;//返回基准值的下标;
}
//挖坑法单趟排序:
int PartSort2(int* arr, int begin, int end)
{
int mid = Getmidintdx(arr, begin, end);//三数取中
swap(&arr[begin], &arr[mid]);//交换适中值与begin值;
int left = begin;
int right = end;
int key = left;//左边做基准值;同时将其形成一个坑位;
int temp = arr[key];//创建临时变量,记录下基准值的大小;
while (left < right)
{
while (arr[right] >= temp && left < right)//右边先走;
{
right--;
}
arr[key] = arr[right];//找到比基准值小的,将小的值放到基准值的位置,虽然将基准值覆盖了,但是我们之前创建了临时变量,没有丢失;
key = right;//将比基准值小的数的下标给基准值的下标,使这里形成新的坑位;
while (arr[left] <= temp && left < right)//左边再走;
{
left++;
}
arr[key] = arr[left];//找到比基准值大的,将大的值放到基准值的位置,
key = left;//将比基准值大的数的下标给基准值的下标,使这里形成新的坑位;
}
arr[key] = temp;//一趟结束后,将临时变量(之前记录下来的基准值)放到最后一个坑中,使基准值回到排序后的正确位置,
return key;//返回基准值的下标;
}
//双指针法单趟排序:
int PartSort3(int* arr, int begin, int end)
{
int mid = Getmidintdx(arr, begin, end);//三数取中;
swap(&arr[begin], &arr[mid]);交换适中值与begin值;
int slow = begin;//慢指针;
int fast = slow + 1;//快指针;
int key = begin;//左边做基准值;
while (fast < end)//如果快指针小于右边界;
{
if (arr[fast] < arr[key]&&++slow!=fast)//快指针小于基准值并且比慢指针快一步以上;
{
slow++;//慢指针先走一步;
swap(&arr[slow], &arr[fast]);//交换快慢指针所指向的数组的值;
}
fast++;//每次快指针都向前走一步;
}
swap(&arr[key], &arr[slow]);//最后交换基准值与慢指针的位置。使基准值回到排序后的正确位置,
key = slow;//将慢指针的下标给基准值,
return key;//返回基准值的下标;
}
//快速排序:
void quicksort(int* arr, int begin, int end)
{
if (begin >= end)
{
return;
}
if ((end - begin + 1) > 15)
{
//霍尔版单趟排序:
//int key = PartSort1(arr, begin, end);
//挖坑法单趟排序:
//int key = PartSort2(arr, begin, end);
//双指针法单趟排序:
int key = PartSort3(arr, begin, end);
//分割出左右区间,递归式的一个小区间一个小区间的分割排序;
quicksort(arr, begin, key - 1);//左区间;
quicksort(arr, key + 1, end);//右区间;
}
else//小区间优化,元素个数小于15个的直接用插入排序,减少大量下层递归次数,避免栈溢出,提高性能;
{
Insertsort(arr + begin, end - begin - 1);
}
}
///
1.3.3:交换排序(快速排序非递归(主要用栈实现)):
具体实现代码如下所示:
//快速排序非递归(用栈实现):
//void quicksortNonR(int* arr, int begin, int end)
//{
// ST st;//定义一个栈(注:ST是我自己实现的栈);
// StackInit(&st);//初始化栈;
// StackPush(&st, begin);//将要快速排序的数组的最左下标入栈;
// StackPush(&st, end);//将要快速排序的数组的最右下标入栈;
//
// while (stackempty(&st))//栈不为空就继续
// {
// int right = stacktop(&st);//先拿到栈中后入的最右下标;
// stackPop(&st);//删除已经拿走的最右下标,使先入栈的最左下标成为栈顶元素;
// int left= stacktop(&st);//再拿到栈中先入的最左下标;
// stackPop(&st);//删除已经拿走的最左下标;
//
// int key = PartSort1(arr, left, right);//单趟排序,使key到正确的位置;
// //分成了两个子区间:[left,key-1]; key; [key+1,right];
// if (key + 1 < right)//如果右子区间的空间大于1,则继续拆分左右区间;
// {
// StackPush(&st, key + 1);
// StackPush(&st, right);
// }
// if (key - 1 > left)如果左子区间的空间大于1,则继续拆分左右区间;
// {
// StackPush(&st, key - 1);
// StackPush(&st, left);
// }
// }
// StcakDestroy(&st);//最后销毁栈;
//}
///
1.4.1:归并排序:
思想:即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
(简单点来说,就是想让整体有序,那么先让局部有序,想让局部有序,就需要局部的局部有序,一层层的分治下来,形成有序,然后再将一层层的局部有序合并成一层层的‘小整体’,再让一层层的“整体”合并有序,最终整个序列有序);
具体代码如下:
//函数参数需要(归并的数组)(数组的左右下标,方便递归划分区间)(一个新的数组,周转数组元素)
void _Mergesort(int* arr, int* ps, int begin, int end)
{
if (begin >= end)//递归过程中,如果递归到最后每一次的小区间只有一个元素,则不需要再分区间
{
return;
}
//区间拆分:
int mid =(begin+end)/2;//对半分区间;
_Mergesort(arr, ps, begin, mid);//先递归左边,分左边的区间;
_Mergesort(arr, ps, mid+1, end);//再递归右边,分右边的区间;
//区间分治完后,开始两两归并:将两个要归并的区间左右下标记录下来,合并;
int begin1 = begin;
int begin2 = mid + 1;
int end1 = mid;
int end2 = end;
int i = begin;
while (begin1<=end1&&begin2<=end2)//两个小区间都有值,合并;
{
if (arr[begin1] < arr[begin2])//把小的拿下来放到新数组,临时存储;
{
ps[i++] = arr[begin1++];
}
else
{
ps[i++] = arr[begin2++];
}
}
while (begin1 <= end1)//如果区间长度不同,接着把归并的小区间的元素拿到新数组中去;
{
ps[i++] = arr[begin1++];
}
while (begin2 <= end2)//如果区间长度不同,接着把归并的小区间的元素拿到新数组中去;
{
ps[i++] = arr[begin2++];
}
//每一次把两个小区间在新数组中合并有序后,再拷贝到原数组,等待下一次更大的两个区间合并;
memcpy(arr + begin, ps+begin, sizeof(int)*(end - begin + 1));
}
///
1.4.2:归并排序(非递归):
思想:由上面的递归版本我们发现,归并的大思想就是分治,分到一个数与一个数合并,
两个数与两个数合并,4个数和4个数合并......;那么当我们控制要归并的数的区间大小,就可以模拟递归的方式;
非递归有两种情况:
1.理想情况:要归并的数组正好是由两个相同长度的小区间组成的,
例:长度为8的数组要归并,正好可以有两个相同长度为4的小区间组成的;
2.非理想情况:例: 长度为9的数组要归并,则无法由两个相同长度的小区间组成;
【N】:所以在解决非递归时,我们需要将非理想情况考虑进去;
具体代码如下:
//归并排序非递归1:每次归并完原数组的一部分就拷贝回去;
//void MergesortNonR(int* arr, int n)
//{
// int* ps = (int*)malloc(sizeof(int) * n);//创建一个新的临时数组;
// if (ps == NULL)
// {
// perror("malloc fail");
// exit(-1);
// }
// int Range = 1;//先暂时定义一个数与一个数交换;
// while (Range < n)//控制次数;
// {
// for (int i = 0; i < n; i += 2*Range)
// {
//两个小区间合并之前,都有自己的区间范围:
// int begin1 = i;
// int end1 = i + Range - 1;
// int begin2 = i + Range;
// int end2 = i + 2 * Range-1;
// int j = i;
//非理想情况下,三种越界的判定与修改;
// if (end1 > n)//如果end1,begin2,end2都越界,那么直接退出这次循环,防止刚归并完一部分就拷贝回去,使之前的数组元素丢失;
// {
// break;
// }
// else if (begin2 > n)//如果begin2,end2都越界,那么直接退出这次循环,防止刚归并完一部分就拷贝回去,使之前的数组元素丢失;
// {
// break;
// }
// else if (end2 > n)//如果end2越界,那么将end2截到n-1,继续归并;
// {
// end2 = n - 1;
// }
// while (begin1 <= end1 && begin2 <= end2)//正常合并两个小区间;
// {
// if (arr[begin1] < arr[begin2])
// {
// ps[j++] = arr[begin1++];
// }
// else
// {
// ps[j++] = arr[begin2++];
// }
// }
// while (begin1 <= end1)
// {
// ps[j++] = arr[begin1++];
// }
// while (begin2 <= end2)
// {
// ps[j++] = arr[begin2++];
// }
//归并排序后一部分后,拷贝回原数组,等待下一次的归并排序;
// memcpy(arr+i, ps+i, sizeof(int) * (end2 - i + 1));
// }
// //将要归并的两个小区间范围扩大(使里面比较的元素个数增加),
// Range *= 2;
// }
// free(ps);
// ps = NULL;
//}
///
1.5:计数排序:
思想:开辟一个数组arr,全部初始化为0;然后遍历一遍要排序的序列,序列的值为x,那么arr[x]的值加1,x在变化,那么对应的arr[x]也变化加+1,最后将arr数组里的值按下标的方式打印出来,即完成排序;
具体代码如下:
//计数排序:
void Countsort(int* arr, int n)
{
//选择数组中的最大最小值;
int max = arr[0];
int min = arr[0];
for (int i = 0; i < n; i++)
{
if (arr[i] < min)
{
min = arr[i];
}
if (arr[i] > max)
{
max = arr[i];
}
}
/开辟新数组:大小为max-min+1,初始化全为0;
int ret = max - min + 1;
int* ps = (int*)calloc(ret,sizeof(int));
if (ps == NULL)
{
perror("calloc file");
exit(-1);
}
//遍历原数组,得到的值在新数组对应的下标位置的元素+1;
for (int j = 0; j < n; j++)
{
ps[arr[j] - min]++;
}
//再根据新数组的值对应下标依次赋值给原数组;
for (int k = 0; k < ret; k++)
{
while (ps[k]--)
{
arr[k] = k + min;
}
}
free(ps);
ps = NULL;
}
///
///
2:各种排序的时间空间复杂度
///
///
3.各种排序的性能
[N]:通过上面的测试,感觉计数排序非常厉害,但是它有很大的局限性,它只能排序整形;
///
///