排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i] = r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
时间复杂度(平均情况) | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 | |
---|---|---|---|---|---|
直接插入排序 | O(N2) | O(N) | O(N2) | O(1) | 稳定 |
希尔排序 | O(N*logN)~O(N2) 或O(n1.25)~1.6*O(n1.25) |
O(N1.3) | O(N2) | O(1) | 不稳定 |
简单选择排序 | O(N2) | O(N2) | O(N2) | O(1) | 不稳定 |
堆排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(1) | 不稳定 |
冒泡排序 | O(N2) | O(N) | O(N2) | O(1) | 稳定 |
快速排序 | O(N*logN) | O(N*logN) | O(N2) | O(logN)~O(N) | 不稳定 |
归并排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(N) | 稳定 |
计数排序 | O(N+Range) | O(N+Range) | O(N+Range) | O(Range) | 稳定 |
时间复杂度:O(1)<O( n \sqrt{n} n)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)
logn 以几为底无关紧要,因为它们之间只差一个常数。
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
tmp
往前插入,从后往前依次比较大小tmp
大,则将该元素往后移动一个位置再与前一个元素比较;如果该元素比tmp
小,则将tmp
置于该元素之后;如果tmp
前没有元素,则将tmp
置于第一个元素的位置tmp
,重复以上步骤以下代码全部默认为从小到大排序(升序)
void InsertSort(int* a, int n)
{
//[0,end]有序,插入end +1的位置,让[0,end+1]有序
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
a[end + 1] = tmp;
}
}
}
希尔排序法又称缩小增量法,是对直接插入排序的优化。
希尔排序法的基本思想是:先选定一个整数gap
,把待排序文件中所有记录分成个组,所有距离为gap
的记录分在同一组内,并对每一组内的记录进行排序。然后,取gap = gap/n + 1(n = 3/4...)或 gap/=2
重复上述分组和排序的工作。当gap=1
时,所有记录在统一组内排好序。
设置gap
的目的:使数组快速接近有序,当最后gap
到达1时,此时数组接近有序,便于直接插入排序。
gap
的值,每间隔gap
的元素视为一个数组,对每组元素进行插入排序;gap
的值倍数式减小,每次gap
的值减小时都重复步骤1;gap
的值必然减小到1,此时就是对整个数组进行一次直接插入排序。动图演示:
例如这组数据是
{8,9,1,7,2,3,5,4,6,0}
首次取gap=5
,即视间隔为5的数为一组进行插入排序:
{8 3 }->
{3 8 }
{ 9 5 }->
{ 5 9 }
{ 1 4 }->
{ 1 4 }
{ 7 6 }->
{ 6 7 }
{ 2 0}->
{ 0 2}
即原先的数组变为
{8917235460}->
{3516089472}
当gap=2
时,即视间隔为5的数为一组进行插入排序:
{3 1 0 9 7 }->
{0 1 3 7 9 }
{ 5 6 8 4 2}->
{ 2 4 5 6 8}
即
{3516089472}->
{0214357698}
当最后gap=1
时,即直接插入排序。
依照这个思路写的循环是这样的:
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap /= 2; // gap /= 3 + 1;
for (int j = 0; j < gap; j++)
{
for (int i = j; i < n - gap; i+=gap)
{
//...
}
}
方法二即将这两个循环整合在一起,先对每一组的第一段进行插入排序,再对每组的第二段插入排序…但效率上和以上代码是相同的。
以下代码便使用了方法二。
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap /= 2; // gap /= 3 + 1;
//gap > 1时都是预排序
// gap = 1时就是直接插入排序
//把间隔为gap的数据同时排
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
gap > 1
时都是预排序,目的是让数组更接近于有序。当gap == 1
时,数组已经接近有序的了,这样再排序就会很快。对整体而言,可以达到优化的效果。gap
的取值方法很多,导致很难去计算,因此在许多书中给出的希尔排序的时间复杂度都不固定:gap
是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:O(n1.25)——1.6*O(n1.25)来算。选择排序的基本思想是:每一趟从待排序的元素中选出最小(/最大)的记录,顺序放在已排好序的子文件的最后(/最前),直到全部记录排序完毕。
我们可以对此算法进行优化,在每次对未排序的元素进行遍历中同时选出最小和最大的元素,分别放在原序列的序列头和序列尾,这样只需遍历序列的一半次数即可。
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int maxi = begin, mini = begin;
for (int i = begin; i <= end; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[mini], &a[begin]);
if (maxi == begin)//考虑特殊情况
{
//当begin和maxi相同的时候,由于a[mini]和a[begin]进行了交换,最大值不在原本的位置,所以要换回来
maxi = mini;
}
Swap(&a[maxi], &a[end]);
++begin;
--end;
}
}
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
具体讲解见:C语言实现二叉树、堆、堆排序
对要排序的数组建堆,此时堆顶(也就是数组第一个元素)要么最大,要么最小;
交换数组第一个和最后一个元素;
对堆顶的元素进行向下调整,保持最大堆或最小堆的特性;
数组长度减1;
重复步骤2、3、4,直至数组长度为1(下标为0),结束。
动图演示:
步骤解析:
void HeapSort(int* a, int n)
{
//升序建大堆,降序建小堆,取决于AdjustDown函数中的符号
for (int i = (n-1-1)/2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;//最后一个元素的下标
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
冒泡排序是所有排序里最简单的一种,其基本思想是:对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部。
a[0]
开始,与下一个元素a[1]
比较,若是逆序则交换;再让a[1]
与下一个元素a[2]
比较,逆序则交换……重复此步骤,直到a[n-2]
与a[n-1]
比较完为止;a[n-1]
最大,且可视为有序,接下来只需要比较前n-1个数,找出最大的数挪到a[n-1]
前面;a[n-3]
与a[n-2]
为止,此时最后两个数有序;动图演示:
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];
a[j] = a[j - 1];
a[j - 1] = tmp;
exchange = true;
}
}
if (!exchange)
{
break;
}
}
}
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素
key
作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
比如给一组数据,选取一个值key
(通常是最左边的值或最右边的值),经过操作后使key
左边的数都比key
小,key
右边的数都比key
大,这样就把数据分成了两组,再对两边的数据进行如上操作,直到每组数据只剩1个数据或没有数据为止。
这是快速排序递归实现的主要思路,我们发现它和二叉树前序遍历有些像,学习二叉树过后会有更好的理解。
以下均以升序为例。
将区间按照基准值划分为左右两半部分有三种常见方式:
Hoare版本的快速排序是一种基于分治策略的排序算法,我们先考虑单趟排序的步骤:
key
;left
指向最左边的数,定义一个右下标right
指向最右边的数;right
先往左(left
先往右)遍历寻找一个比key
小(大)的数字,找到后停下来;left
向右(right
向左)遍历寻找一个比key
大(小)的数字,找到后停下来;left
和right
相遇,再把这个相遇的位置的数字和key
交换就完成了一趟快速排序(此时左边的数比key
小,右边的数比key
大)。图片演示:(以最左边的值为key
)
这样一次单趟排序就完成了。但在写代码时有几个坑一定要注意:
left
必须从key
开始不能从keyi+1
开始,假设key
是数组中最小的,代码就错误了。(同样right
不能从keyi-1
开始)
在二层循环中left
(right
)与key
比较时要加上等于号。 按照思路,arr[left]
大于key
时停止,反过来,继续走的判断条件即arr[left]
小于key
。但是当遇到等于key
的元素时,代码就会陷入死循环,比如这种情况:
也可能导致数组越界,比如这种情况,right
一直没遇到比0小的数字,且会越过0:
所以务必要加上等于号。
我们在二层循环比较left
与right
为置的元素时,是不受一层循环left < right
条件约束的,这样当left
与right
相遇时可能不会终止(加等号后),比如。因此要在内层循环中也加上条件
left < right
。但要注意的是,left < right
条件一定要加在前面,因为&&
有短路性质,如果加在后面就先越界再判断了。
若选最左边的数为key
,那么right
先走(若选最右边的数为key
,那么left
先走)。因为在交换后,left
指向的元素一定比key
小,right
指向的元素一定比key
大。
如果right
先走,要么在比key
小的元素位置停,left
与right
相遇时一定在比key
小的位置,要么right
先遇到left
,这时也在比key
小的位置,交换后这个比key
小的元素就在key
的左边了。
而如果left
先走,情况一:left
走到比key
大的元素位置,right
遇left
;情况二:left
直接遇到right
,这时它们指向的元素比key
大,这两种情况下,它们相遇位置的元素与key
交换后,就跑到了key
的左边,但是key
左边的要求是比key
小,因此要注意代码顺序。
单趟排序代码:
void Swap(int* a,int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void PartSort(int* arr,int left,int right)
{
int keyi = left;
while (left < right)//left等于right时退出循环
{
while (left < right && arr[right] >= arr[keyi])//防止越界加上left < right条件
{
right--;//right先走
}
while (left < right && arr[left] <= arr[keyi])
{
left++;
}
Swap(&arr[left], &arr[right]);
}
Swap(&arr[keyi], &arr[left]);//此时left与right相同
}
只要将单趟排序写出来,整个排序就变得简单了,递归即可。
在单趟排序后,再分别对key
左边的数据与右边的数据进行排序,使其变为有序,这样整个数组便有序了。因此我们需要知道key
的下标,所以将PartSort
的返回类型改为int
类型,返回keyi
。
别忘了写返回的条件,当每个部分的元素个数<=1时就意味着无需排序了。
图示:
void Swap(int* a,int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int PartSort(int* arr,int left,int right)
{
int keyi = left;
while (left < right)//left等于right时退出循环
{
while (left < right && arr[right] >= arr[keyi])//防止越界加上left < right条件
{
right--;
}
while (left < right && arr[left] <= arr[keyi])
{
left++;
}
Swap(&arr[left], &arr[right]);
}
Swap(&arr[keyi], &arr[left]);//此时left与right相同
return keyi;
}
void QuickSort(int* arr, int begin, int end)//使用时要传下标
{
if (begin >= end)
return;
int keyi = PartSort(arr, begin, end);
//[begin,keyi+1] keyi [keyi+1,end]
QuickSort(arr, 0, keyi - 1);
QuickSort(arr, keyi + 1, end);
}
挖坑法在效率上与hoare版本没有什么不同,但挖坑法更易于理解。挖坑法的单趟排序与hoare版本思路大体一样,但结果可能不同,但快排代码相同。
步骤:
key
,key
所在位置的下标就是一个坑hole
,这便是挖坑(理解为把它存到key
中,但这个位置上元素还在);left
和right
表示下标,它们分别指向数列的最左和最右两个元素;right
开始,把它所指向的元素和key
比较。如果比key
大或等于,则right
向左移动;如果比key
小,则把right
所指向的元素填入坑中,right
所指向的位置又挖了一个坑(挖一填一);left
指针进行比较。如果left
指向的元素等于小于key
,则left
向右移动;如果元素大于key
,则把left
指向的元素填入坑中,left
所在的位置成为坑;key
放到最后的坑中。int PartSort2(int* arr, int left, int right)
{
int key = arr[left];
int hole = left;
while (left < right)
{
while (left < right && arr[right] >= key)
{
right--;
}
arr[hole] = arr[right];
hole = right;
while (left < right && arr[left] <= key)
{
left++;
}
arr[hole] = arr[left];
hole = left;
}
arr[hole] = key;
return hole;
}
void QuickSort(int* arr, int begin, int end)
{
if (begin >= end)
return;
int keyi = PartSort2(arr, begin, end);
//[begin,keyi+1] keyi [keyi+1,end]
QuickSort(arr, 0, keyi - 1);
QuickSort(arr, keyi + 1, end);
}
前后指针法相较于前两种方法更简单,但理解上更抽象,没有前两种方法直观。三种方法在效率上是一样的。
步骤:
key
,初始化两个指针(下标),prev
指向序列的第一个元素,cur
指向prev
的下一个元素;cur
位置的元素大于key
,那么继续让cur
指针向右移动;cur
位置的元素小于key
,那么prev
位置加一,并交换prev
和cur
所指向的元素,再把cur
的位置加一;(prev是走了后再换,cur是换了后再走)cur
指针超过序列的最右侧位置;prev
指向的元素与key
所在的位置交换,就划分了左右区间。最开始
prev
和cur
是相邻的,当cur
遇到比key
大的元素时,prev
与cur
开始拉开距离,并且prev
与cur
之间的元素都是比key
大的元素,因为cur
一遇到比key
小的元素就与++prev
交换了(++prev
是先自增走到比key
大的位置再交换),这样保证prev
走过的位置都比key
要小,当cur
走完的时候,prev
正处在最右边一个比key
小元素的位置,与key
的位置交换后便完成了单趟排序。
完整代码:
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//前后指针法
int PartSort3(int* arr, int left, int right)
{
int keyi = left;//最后要交换取key的下标
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
//&&左边为真才会执行右边
if (arr[cur] < arr[keyi] && ++prev != cur)//相邻时交换就是自己和自己交换无意义
{
//注意++prev写条件里了,prev已经走到了比key大的位置
Swap(&arr[prev], &arr[cur]);
}
cur++;//不管遇到比key大的还是小的,cur都要自增
}
Swap(&arr[keyi], &arr[prev]);
return prev;
}
void QuickSort(int* arr, int begin, int end)
{
if (begin >= end)
return;
int keyi = PartSort3(arr, begin, end);
//[begin,keyi+1] keyi [keyi+1,end]
QuickSort(arr, 0, keyi - 1);
QuickSort(arr, keyi + 1, end);
}
动图演示:
在此之前,请大家试试用快排来通过这道题目:排序数组
但是你会发现快排过不了此题,反而别的排序可以过,因为这是一道专门针对快排出的题目!因此,我们来看看快排如何优化。
快速排序的时间复杂度在最好情况下是O(n*logn),但在最坏情况下是O(n2)(比如有序数列),三数取中是对有序序列情况的优化。
三数取中,是指选取数组最左、最右和中间三个元素,取中间大小的那个元素与
key
所在的位置交换,那么这个数就成了key
(key
的位置没变,不影响之前的代码思路),这样能让序列划分得更均匀一些,减小时间复杂度。
代码:
int GetMidIndex(int* arr, int left, int right)
{
int mid = (left + right) / 2;
if (arr[left] < arr[mid])
{
if (arr[mid] < arr[right])
return mid;
//arr[mid] > arr[right]
else if (arr[left] < arr[right])
return right;
//arr[left] > arr[right]
else return left;
}
//arr[left] > arr[mid]
else
{
if (arr[right] < arr[mid])
return mid;
else if (arr[right] < arr[left])
return right;
else return left;
}
}
通过
GetMidIndex()
函数找到中间大小元素的下标midi
,再把这个元素与序列最左边的元素交换位置(以key
为最左边元素为例),只需在PartSort()
函数开头加上代码:
int midi = GetMidIndex(arr,left,right);
Swap(&arr[left],&arr[midi]);
完整代码:
int GetMidIndex(int* arr, int left, int right)
{
int mid = (left + right) / 2;
if (arr[left] < arr[mid])
{
if (arr[mid] < arr[right])
return mid;
else if (arr[left] < arr[right])
return right;
else return left;
}
else
{
if (arr[right] < arr[mid])
return mid;
else if (arr[right] < arr[left])
return right;
else return left;
}
}
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//hoare法
int PartSort1(int* arr, int left, int right)
{
int midi = GetMidIndex(arr, left, right);
Swap(&arr[left], &arr[midi]);
int keyi = left;
while (left < right)
{
while (left < right && arr[right] >= arr[keyi])
{
right--;
}
while (left < right && arr[left] <= arr[keyi])
{
left++;
}
Swap(&arr[left], &arr[right]);
}
Swap(&arr[keyi], &arr[left]);
return keyi;
}
//挖坑法
int PartSort2(int* arr, int left, int right)
{
int midi = GetMidIndex(arr, left, right);
Swap(&arr[left], &arr[midi]);
int key = arr[left];
int hole = left;
while (left < right)
{
while (left < right && arr[right] >= key)
{
right--;
}
arr[hole] = arr[right];
hole = right;
while (left < right && arr[left] <= key)
{
left++;
}
arr[hole] = arr[left];
hole = left;
}
arr[hole] = key;
return hole;
}
//前后指针法
int PartSort3(int* arr, int left, int right)
{
int midi = GetMidIndex(arr, left, right);
Swap(&arr[left], &arr[midi]);
int keyi = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[prev], &arr[cur]);
}
cur++;
}
Swap(&arr[keyi], &arr[prev]);
return prev;
}
void QuickSort(int* arr, int begin, int end)
{
if (begin >= end)
return;
int keyi = PartSort3(arr, begin, end);
//[begin,keyi+1] keyi [keyi+1,end]
QuickSort(arr, 0, keyi - 1);
QuickSort(arr, keyi + 1, end);
}
但是!!!此题采用三数取中任然过不了,因为它同时也针对了三数取中(三个数都接近最大或最小),所以mid
就不能去中间值了,可以使用随机数(仅限此题),把PartSort
中的mid
定义为left + (rand()%(right - left))
就好。
int mid = left + (rand()%(right - left));
三路划分是针对重复元素情况的一种方法,我们知道原本的快排并没有特地去针对重复元素,所以在序列全是重复元素或大多重复元素时,效率就会变得很低。此题就有这样一个测试用例,所以快排会<超出时间限制>。
使用三路划分在应对普通数据时效率略低一些,日常不需要使用,但它能应对的场景更全面。
思路:
三路划分的目的是使key
位于中间,比key
小的元素在key
的左边,比key
大的元素在key
的右边,形成三个区间,这样递归时只用递归最左边和最右边的区间。
三路划分的实现思路与前后指针法的思想类似
步骤:
left
指针(下标)指向序列开头(key
),right
指针指向序列结尾,cur
指针指向left
的下一个位置;cur
位置的元素小于key
,那么交换left
位置与cur
位置的元素,left
右移一位,cur
右移一位;cur
位置的元素大于key
,那么交换cur
位置的元素与right
位置的元素,right
左移一位;cur
位置的元素等于key
,cur
右移一位;cur > right
为止,此时right
指向最后一个key
。不难发现,left
所指向的值永远是key
,left
的作用就是把key
换到后面去,而cur
的作用是把不是key
的值往前面或后面换。
代码:
相当于把PartSort
的代码优化了,这里直接写在了QuickSort
中
void QuickSort(int* arr,int begin,int end)
{
if(begin >= end)
return;
int left = begin,right = end;
int cur = left + 1;
//三数取中防有序(midi随机版)
int midi = left + (rand()%(right - left));
Swap(&arr[left], &arr[midi]);
//三路划分防重复
int key = arr[begin];
while(cur <= right)
{
if(arr[cur] < key)
{
Swap(&arr[left],&arr[cur]);
left++;
cur++;
}
else if(arr[cur] > key)
{
Swap(&arr[cur],&arr[right]);
right--;
}
else//=key
{
cur++;
}
//区间变成[begin,left-1],[left,right],[right + 1,end]
}
//中间区间不用递归
QuickSort(arr,begin,left-1);
QuickSort(arr,right+1,end);
}
相比于非递归形式,递归有一些可能的弊端:
- 栈溢出:递归形式的快速排序在处理大量数据时可能导致栈溢出。因为每次递归调用都会消耗一定的栈空间,当递归深度过大时,超出了系统栈空间限制,就会引发栈溢出错误。非递归形式的快速排序则不存在这个问题,因为它使用循环代替递归,不需要额外的栈空间。
- 效率问题:在某些情况下,递归形式的快速排序可能效率较低。当数据规模较大时,递归调用会产生一定的额外开销,这可能导致性能下降。非递归形式的快速排序可以避免这种开销,因为它通过循环实现,没有递归调用的额外成本。
快排非递归形式要用到手写的栈1(c++可直接使用),所以先要引入写好的栈"Stack.h"
思想与递归形式相同,都是先单趟排序整个区间[begin,end]
,再排[begin,keyi-1]
,再排[keyi+1,end]
…只不过非递归是用循环实现的。
步骤:
end
入栈,再将begin
入栈(这样出栈的时候先出begin
,方便理解,反着来也可以);left
,并出栈,再取栈顶元素定义为right
,并出栈;即单趟排序的区间,对区间进行单趟排序并取返回值keyi
;keyi - 1
大于left
,就表明这个区间有2个及以上的元素,将keyi - 1
和left
分别入栈;如果keyi + 1
小于end
,将end
与keyi + 1
分别入栈;代码:
void QuickSortNonR(int* arr, int begin, int end)
{
Stack st;
StackInit(&st);//初始化
StackPush(&st, end);
StackPush(&st, begin);
while (!StackEmpty(&st))
{
int left = StackTop(&st);//后入的begin,先取左
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
int keyi = PartSort(arr, left, right);//单趟排序,三种都可
if (keyi + 1 < right)
{
StackPush(&st, right);
StackPush(&st, keyi + 1);
}
if (keyi - 1 > left)
{
StackPush(&st, keyi - 1);
StackPush(&st, 0);
}
}
StackDestroy(&st);
}
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
对于两个有序序列合并,我们可以使用双指针的方法完成。
可以参考此题:牛客 - 合并有序序列
#include
int main()
{
int n,m;
scanf("%d%d",&n,&m);
int a[n],b[m];//牛客可用变长数组
for(int i = 0; i < n; i++)
scanf("%d",&a[i]);
for(int i = 0; i < m; i++)
scanf("%d",&b[i]);
int add[n+m];
int i = 0,j = 0;//分别指向两个数组的第一个元素
int l = 0;
while(i < n && j < m)//结束条件,防止越界
{
if(a[i] <= b[j])
add[l++] = a[i++];
else
add[l++] = b[j++];
}
//还要考虑数组a或数组b中剩下的元素
while(i < n)
{
add[l++] = a[i++];
}
while(j < m)
{
add[l++] = b[j++];
}
for(int e = 0; e < n+m; e++)
printf("%d ",add[e]);
return 0;
}
归并排序正是这个思路,我们要做的就是把序列平均分成左右两个序列,使左右两个序列有序。这时要用到递归分别对左右区间分割,直到每个区间只剩下两个元素,将这两个元素分割,那么左右元素即有序,对其进行合并,那么这个区间就有序了。这个思想类似二叉树的后序思想。
需要注意的是,归并排序并不能在原数组上进行,因为会覆盖掉原本的数据,所以要格外开辟一个数组用来储存排序后的元素,然后将替换回原数组,这也是归并排序的弊端。
tmp
,对原数组a
进行归并;begin
与尾下标end
的中间值mid
,分别对左区间[begin,mid]
与右区间[mid + 1,end]
递归,直到区间只有一个元素就返回;begin1
和end1
分别指向左区间的首下标和尾下标,定义begin2
和end2
分别指向右区间的首下标和尾下标,将这两个有序序列(区间)合并,存入tmp
;tmp
中合并的序列拷贝到原数组;void _Mergesort(int* a, int* tmp, int begin, int end)
{
if (begin == end)//递归返回条件
return;
int mid = (begin + end) / 2;//取中间下标
//后序递归
_Mergesort(a, tmp, begin, mid);//递归左区间
_Mergesort(a, tmp, mid + 1, end);//递归右区间
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;//记录tmp的下标
//合并两个有序序列
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++];
}
//替换,注意要从+begin的位置开始替换
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);//开辟
_Mergesort(a, tmp, 0, n - 1);//递归函数
free(tmp);
}
归并的非递归形式与递归形式略有不同,递归是先左后右区间,但是非递归是整个序列一起操作,先11归,再22归,44归…
·我们可以定义变量gap
表示每个区间的元素个数,比如gap
= 1,11归,gsp
= 2,22归,gap
呈 2n 增长,直到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;
//...
}
[begin1,end1]
,[begin2,end2]
表示要合并的两个区间,i走到下个要合并区间的起始位置。
但实际情况不会如上图演示的那么简单,只要数据量不是2n,代码就会出错。
出错情况有三种:
解决方式有两种:
方式一:归并一次拷贝一次
这种方法采用的是退出的思路
end2
的位置再进行归并。代码:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;//每个区间数据个数
while (gap < n)
{
int j = 0;//tmp数组的下标
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;
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 + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
free(tmp);
}
方法二:归并完拷贝整个序列
在所有组的gap
个数据归并完时,将tmp
整体拷贝回原序列,这种方法采用的是修正的思路。
end1
越界,end1
修正为n - 1
begin2、end2
越界,[begin2,end2]
修正为不存在(begin2 > end2)的区间end2
越界,end2
修正为n - 1
代码:
void MergeSortNonR2(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;//每个区间数据个数
while (gap < n)
{
int j = 0;//tmp数组的下标
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;
//修正
if (end1 >= n)
{
end1 = n - 1;
begin2 = n + 1;
end2 = n;
}
else if (begin2 >= n)
{
begin2 = n + 1;
end2 = n;
}
else 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) * n) ;
gap *= 2;
}
free(tmp);
}
小区间优化是一种策略,旨在在数组元素较少时避免过多的递归调用,从而减少资源消耗,提高算法效率,算是一种锦上添花的优化。
对于小规模的数组,插入排序可能会更快。当子数组的大小小于一定阈值(如10),可以切换到插入排序。
快排:
void QuickSort(int* arr, int begin, int end)
{
if (begin >= end)
return;
if(end - begin + 1 < 10)
{
InsertSort(a + begin, end - begin + 1);
return;
}
int keyi = PartSort3(arr, begin, end);
//[begin,keyi+1] keyi [keyi+1,end]
QuickSort(arr, 0, keyi - 1);
QuickSort(arr, keyi + 1, end);
}
归并:
void _Mergesort(int* a, int* tmp, int begin, int end)
{
if (begin == end)
return;
if(end - begin + 1 < 10)
{
InsertSort(a + begin, end - begin + 1);
return;
}
//...剩下代码与原代码相同
}
经常刷题的可能对计数更熟悉,因为它经常被用到。
计数排序(Counting Sort)是一种非基于比较的排序算法,它的优势在于在对一定范围内的整数排序时,它的复杂度为O(n+k),快于任何比较排序算法。
计数排序的基本思想是:通过统计每个元素出现的次数,然后根据这些统计信息构建有序的结果数组。
比如{1,5,2,1,7,5,5}这个数组中,1出现了2次,2出现了1次,5出现了3次,7出现了1次。
那么就排序为2个1,1个2,3个5,1个7。
[0 ,max - min]
,其所有元素都被初始化为0
,用来计数;#include
#include
void CountSort(int* a, int n)
{
int max = a[0], min = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
int range = max - min + 1;//范围
int* countA = (int*)malloc(sizeof(int) * range);
memset(countA, 0, range * sizeof(int));//初始化为0
//统计每个元素出现的次数
for (int i = 0; i < n; i++)
{
countA[a[i] - min]++;
}
//排序
int k = 0;
for (int i = 0; i < range; i++)
{
while (countA[i]--)
{
a[k++] = i + min;
}
}
}
关于C语言实现栈 ↩︎