排序总结及详讲
排序的概念
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
在我们的日常生活中,也应用到很多地方,例如某地区考试完的全部排名。
1、 插入排序
1.1 直接插入排序
直接插入排序的基本思想就是把待排序的数值,插入到已经排序好的有序序列中,直到所有的数值都插入完为止,得到一个新的有序序列;实际上就跟我们平时玩扑克牌时是一样的,都是引用的插入排序的思想。
void InsertSort(int* a, int n)
{
for (int i = 1; i < n; i++)
{
int end = i-1;
int tmp = a[i];
while (end>=0)
{
if (a[end]>tmp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
就比如刚开始给了这样一个序列数值
以此类推,从上面的过程我们可以看出来,插入排序的时间复杂度最复杂的是O(n^2),最好情况为O(n)。
1.2 希尔排序(缩小增量排序)
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达= 1时,所有记录在统一组内排好序。
//希尔排序
void ShellSort(int* a, int n)
{
int gap = n;
while (gap>1)
{
gap = gap / 2;
for (int i = 0; i < n-gap; i++)
{
int end = i;
int tmp = a[i + gap];
while (end>=0)
{
if (a[end]>tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
希尔排序的特点:
1、希尔排序是对直接插入排序的优化。
2、当gap >1 时都是预排序,目的是让数组更接近于有序。当gap == 1时,数据已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
3、希尔排序的时间复杂度不是很好计算,因为gap的取值方法很多,导致很难去计算。暂时是按照O(n^1.25)来计算的。
2、选择排序
2.1选择排序
概念:
每一次从待排序的数据元素中选出最小(或最大)数据元素,存放在序列的起始或者末尾位置,直到全部待排序的数据元素排完。
//交换数据元素函数
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//选择排序
//时间复杂度永远为o(n^2)
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
while (left < right)
{
int mini = left;
int maxi = left;
for (int i = left; i <= right; i++)
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
Swap(&a[left], &a[mini]);
if (maxi == left)
{
maxi = mini;
}
Swap(&a[right], &a[maxi]);
left++;
right--;
}
}
while循环的作用是用来找数组中最小值和最大值坐标的,每次都是循环遍历一遍未排序过的数组。
当结束while循环后,开始交换数据。
这里来解释一下while循环里头的
if
语句
- 在元素集合 array[i] – array[n-1] 中选择关键码最大(小)的数据元素
- 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
- 在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
由此看来它的时间复杂度最好最坏的情况都是O(n^2)
2.2 堆排序
- 概念:堆排序是指利用堆积树这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择选择数据。需要注意的是排升序要建大堆,排降序建小堆。
//堆排序
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
}
parent = child;
child = parent * 2 + 1;
}
}
void HeapSort(int* a, int n)
{
int i;
for (i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
//自己实现排序
int end = n - 1;
while (end > 0)
{
Swap(&a[end], &a[0]);
AdjustDown(a, end, 0);
end--;
}
}
略…
从上面的简介图的过程来看,向下取整是从最后一个孩子结点的父亲结点开始向下取整,直到所有的父亲结点都过完一次才结束,大的数字往上浮,小的数字往下面沉,这种操作也叫建大堆
向下取整的时间复杂度为o(N)
- 当堆建好后,我们就可以自己来利用堆来建立有序数组了
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
3、交换排序
基本思想:所谓交换,就是根据序列中两个记录数组下标的元素大小,来对换这两个元素在数组中的位置,交换排序的特点是:将数据元素较大的记录向数组尾部移动,较小的往数组前部移动。
3.1 冒泡排序
冒泡排序就跟气泡往上浮一个倒立,每次运行把数组中最大的数据元素移动到数组尾部。最大的往上浮,最小的往下浮。
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n-1; i++)
{
bool exchange = false;
for (int j = 1; j < n-i; j++)
{
if (a[j-1]>a[j])
{
int tmp = a[j-1];
a[j-1] = a[j];
a[j] = tmp;
exchange = true;
}
}
if (exchange == false)
{
break;
}
}
}
每次都会选择两个数进行比较,最大的数往后移动,将数据元素较大的记录向数组尾部移动,较小的往数组前部移动。当内部的for循环完成一整次遍历后,就会排好一个最大数,所以第二次进入内部排序的时候,是不需要跟数组最后一个元素比较的,所以是 n - i。
3.2 快速排序
快速排序是从冒泡排序演变过来的,但是它的效率要比冒泡排序高效的多,它的算法和冒泡排序类似,像冒泡排序是将大的数或者小的数往上浮,而快速排序是选一个数组,然后大于这个数的数值放到这个数的右边,小于这个数的数值放到左边。
冒泡排序
冒泡排序是讲最大的数往最后放
快速排序
快速排序是提前选一个值,然后把这个值排到它有序的位置,它的左边的数值都比它小,它的右边都比它大。
冒泡排序我们上面已经讲述过是怎么移动的了,接下来,我们来讲述一下快速排序是怎么一步一步移动的,直到它为有序数组。
3.2.1 快速排序(霍尔Hoare)
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int begin = left;
int end = right;
int keyi = left;
while (left < right)
{
//右边找小
while (left < right && a[right] >= a[keyi])
{
--right;
}
//左边找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[right], &a[left]);
}
Swap(&a[keyi], &a[left]);
keyi = left;
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi + 1, end);
}
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子树序列,左子序列中的所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右序列重复该过程,直到所有元素都排列在相应位置上为止。
过程图
这时我们完成了第一趟排序,并使得基准值左子树都是小于它的,右子树都是大于它的,接下来,我们通过递归,来实现序列完全有序。
这时所有的左子树全部递归排序完
接下来,我们来考虑一下效率问题
上图看来,采用三数去中的方法效率是较为高的,我们这里是通过计算它的结束时间 减去 它的开始时间来分析程序的运行效率。
//选三个数,取中间数为key,并返回
int GetMidNumi(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left]>a[right])
{
if (a[right]>a[mid])
{
return right;
}
else
{ //a[right]<a[mid]
if (a[mid]>a[left])
{
return left;
}
else
{
return mid;
}
}
}
else
{
//a[left]<a[right]
if (a[left]>a[mid])
{
return left;
}
else
{//a[left]<a[mid]
if (a[right] < a[mid])
{
return right;
}
else
{
return mid;
}
}
}
}
//快速排序
void PartSort1(int* a, int left, int right)
{
if (left >= right)
{
return;
}
//随机选一个key
/*int randi = left + (rand() % (right - left));
Swap(&a[left], &a[randi]);*/
//选三个数,取中间数为key
int midi = GetMidNumi(a, left, right);
if (left!=midi)
{
Swap(&a[left], &a[midi]);
}
int begin = left;
int end = right;
int keyi = left;
while (left < right)
{
//右边找小
while (left < right && a[right] >= a[keyi])
{
--right;
}
//左边找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[right], &a[left]);
}
Swap(&a[keyi], &a[left]);
keyi = left;
return keyi;
}
因为每次都需要递归调用,所以我们把这个递归调用放到了单个的函数中,方便其他方法调用
//递归实现快速排序
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort1(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
3.2.2 快速排序(挖坑法)
首先先选出一个基准值,将这个基准值拷贝后,剩下的步骤跟霍尔排序逻辑一样,设置两个指针,一个指向尾部,一个指向头部,右边先走并且找比基准值小的,左边后走,并且找比基准值大的数。当右边找到比基准值小的数值的时候,直接将该数值放到指定的坑位,并且该值所在的位置成为最新的坑位,以此类推,直到while循环条件不成立,将基准值添到最后的坑位。
//挖坑法
int PartSort2(int* a, int left, int right)
{
//选三个数,取中间数为key
int midi = GetMidNumi(a, left, right);
if (midi!= left)
{
Swap(&a[left], &a[midi]);
}
int key = a[left];
int hole = left;
while (left<right)
{
//右边找小
while (left<right && a[right]>=key)
{
right--;
}
a[hole] = a[right];
hole = right;
//左边找大
while (left<right && a[left]<=key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
3.2.3 快速排序(前后指针)
采用前后指针也是可以完成快速排序的,当cur遇到了比基准值小的时候,prev后挪一位,并且交换两个所指向的值,当cur>rightde的时候,交换基准值和prev,并且更新基准值的下标,再进行下一次递归调用,以此类推,来实现有序数组。
//前后指针法
int PartSort3(int* a, int left, int right)
{
//选三个数,取中间数为key
int midi = GetMidNumi(a, left, right);
if (midi!=left)
{
Swap(&a[left], &a[midi]);
}
int keyi = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
/*if (a[cur]<a[keyi])
{
++prev;
if (cur != prev)
{
Swap(&a[cur], &a[prev]);
++cur;
}
else
{
++cur;
}
}
else
{
cur++;
}*/
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[cur], &a[prev]);
++cur;
}
Swap(&a[keyi], &a[prev]);
keyi = prev;
return keyi;
}
3.2.3 快速排序(非递归实现,利用栈)
根据栈的特点,先进后出,来模拟实现递归。原理根递归实现的原理基本相似,这里需要注意的是入栈的顺序。
//非递归实现快速排序(运用栈)
void QuickSortNonR(int* a, int left, int right)
{
ST st;
STInit(&st);
STPush(&st, right);
STPush(&st, left);
while (!(STEmpty(&st)))
{
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
int keyi = PartSort3(a, begin, end);
if (keyi + 1 < end)
{
STPush(&st, end);
STPush(&st, keyi + 1);
}
if (begin < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, begin);
}
}
STDestroy(&st);
}
4、归并排序
归并算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再是子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
4.1、归并排序(递归实现)
//递归实现归并排序
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
{
return;
}
int mid = (begin + end) / 2;
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid+1, end, tmp);
int begin1 = begin;
int end1 = mid;
int begin2 = mid+1;
int end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a+begin,tmp+begin,sizeof(int)* (end - begin + 1));
}
//归并排序
void MergeSort(int* a, int n)
{
int tmp = (int*)malloc(sizeof(a) * n);
assert(tmp);
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
4.2.1、归并排序(非递归实现,所有数值比较完后拷贝回去)
//归并排序非递归,for循环完成后,直接把tmp拷贝给数组a
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
assert(tmp);
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;//
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int j = i;
if (end1 >= n)
{
end1 = n - 1;
begin2 = n;
end2 = n - 1;
}
else
{
if (begin2 >= n)
{
begin2 = n;
end2 = n - 1;
}
}
if (end2 >= n)
{
end2 = n - 1;
}
while (begin1<= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
}
//for循环完成后,直接把tmp拷贝给数组a
memcpy(a, tmp, sizeof(int) * n);
gap = gap * 2;
}
free(tmp);
tmp = NULL;
}
此时将tmp数组的所有元素全部拷贝给a数组,并且让gap的值放大两倍
此时将tmp数组的所有元素全部拷贝给a数组,并且让gap的值再放大两倍
此时,数组已经有序,并且gap再放大二倍的话while循环条件也不成立了,所以程序会直接结束。
在这里我们要思考的问题是,当数组时偶数个的时候,这个方法是可行的,如果数组是奇数个时,是不是会存在指针所指向的位置不存在呢,答案是肯定的,这里我们也做了对应的解决办法,那就是修正路线。
在没有修正的时候,红色框框标注的都是越界了的,导致程序不能正常运行,这时我们就需要对程序进行修正了
if (end1 >= n)
{
end1 = n - 1;
begin2 = n;
end2 = n - 1;
}
else
{
if (begin2 >= n)
{
begin2 = n;
end2 = n - 1;
}
}
if (end2 >= n)
{
end2 = n - 1;
}
这就是我们修正以后的效果,当end1越界后,我们就不需要在对后边的值进行排序了,因为没有可比较的,直接拷贝回去即可,同样begin2也是一样的,但是单end2越界后,我们要将它修正到数组尾部,这样尾部的几个元素也就参与到排序了,就不会出现越界问题了
4.2.2、归并排序(非递归实现,比较以此拷贝一次)
//归并排序非递归,每比较一次,拷贝一次
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
assert(tmp);
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int j = i;
if (end1 >= n || begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
//每比较一次,拷贝一次
memcpy(a, tmp, sizeof(int) *j);
}
gap = gap * 2;
}
free(tmp);
tmp = NULL;
}
4、计数排序
概念:
计数排序是一种非比较排序,其核心是将序列中的元素作为键存储在额外的数组空间中,而该元素的个数作为值存储在空间中,通过遍历该数组排序。
这里值得注意的是:数组空间的大小是通过最大数值减去最小数值来计算数组的范围的,这个范围就是数组的大小,所以,这个差值不能过大,这主要是防止建立数组时造成内存的浪费。
序列中存在的元素是整数,因为我们使用的是该元素作为键存储在额外的数组空间中,如果不是整数,不能作为键。
当然,如果这个序列的数值都比较接近话,排序的效率还是很高的,反之,它的空间也会浪费很多
void CountSort(int* a, int n)
{
int min = a[0], max = a[0];
for (int i = 0; i < n; i++)
{
if (a[i]<min)
{
min = a[i];
}
if (a[i]>max)
{
max = a[i];
}
}
int range = max - min + 1;
int* countA = (int*)malloc(sizeof(int) * range);
assert(countA);
memset(countA, 0, sizeof(int) * range);
//统计各位数字出现的次数
for (int i = 0; i < n; i++)
{
countA[a[i] - min]++;
}
//排序
int j = 0;
for (int i = 0; i < range; i++)
{
while (countA[i]--)
{
a[j++] = i + min;
}
}
free(countA);
countA = NULL;
}
首先,我们会从已知的序列中找出最大值和最小值,来计算开辟多大的数组空间。
这时我们发现数组中的元素并没有12个,缺开辟了12位的数组,这就造成了一定的浪费,但如果元素都是接近的,效率会高很多。