目录
前言:
为什么是痛苦浅尝,因为解析的过程让博主感受到只能欣赏,不能触摸的感觉~~~
1.打印
这里提一下,默认排序都排成升序。
2.插入排序
a.思想:
其实为什么叫插入排序呢?就是将一个个数据往一个数组中插入进行排序,这里选了网上一个比较清晰的gif图帮助理解这个思想:
b.代码解释:
1.首先解释为什么循环的变量要从i开始,因为我们选取数据进行排序的时候,是选一个数据和这个数据的下一个数据进行比较,所以开始end从i-1开始,对于第一个数据,tmp则记录它的下一个数据。
2.外面每循环一次,相当于插入一个数据,相当于我们再[0,end]这个区间内插入数据并进行升序排序;如果前一个大于后一个,那就让前一个挪到后一个,并且--end,让end往前走,这是为了继续比较挪动完后前面的数据是否还需要挪到,最后我们再让记录下来的tmp给tmp的前一个位置即end+1(+1是因为end已经--了,要注意下标的对应关系)
3.如果插入后有序了,直接跳出循环,继续下一个数据。
c.时间复杂度:
1.如果是数组的顺序是排好的,那时间复杂度就是O(N),也就是遍历了一遍。
2.如果数组是逆序,那时间复杂度就是O(N^2)了,因为遍历一遍为N,在遍历的过程中,由于是逆序
所以会插入数据后一直往前交换,往前的每一个数据都会比较,然后交换,相当于外面遍历+里面比较构成等差数列,比如插入最后一个数据时前面全部数据都要往后挪,所以为O(N^2):
3.希尔排序
a.思想:
首先预排序即接近有序(大的数更快跳到后面,小的更快跳到前面) ,然后执行插入排序(间隔为gap的分为一组,一共gap组。希尔的思想文字细说实在不易或者有些抽象博主也不能理解的很清楚,主要理解是分组的插入排序即可,一样给上一张清晰的网图理解一下:
b.代码解释:
1.我们要分为gap组,这个gap要怎么确定?假设我们设n为7,数据为下:
2.大致思路就是这,j是控制gap组数据,也就是分组的;为什么i<n+gap,因为:1.如果end为n-gap,防止a[end+gap]越界 2.正好符合上方的控制gap组数据。逻辑差不多就是这,主要就是组内比较,再分为更细的组再比较,然后插入排序擦屁股,文字实在编不下去了,这个是真不好解释,看图能看懂就看看吧,解释一下gap得了:
这里下面的方法就是gap/3+1或者gap/2的意思,反正就是走预排序的思路,到1再走插入排序。
c.时间复杂度:
记住最后一条就行。
d.附加---多组并排:
//while (gap > 1)
//{
// gap /= 2;
// 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;
// }
// }
其实这个思路更符合分组再插入的理念,i<n-gap就是截止到倒数第gap个元素结束。
4.冒泡排序
a.思想:
终于来了个善茬,这个思路就是控制趟数与比较的对数即可。
趟数:趟数,有n个数需要排序,就走n-1趟,因为最后一趟只剩下一个数了,不需要再走了
比较的对数:开始需要走n趟就要比较n-1对数据,每走完一趟,最后一个数肯定有序,所以下一次就少走一趟,也就少比较一对数据,所以要减趟数i:
b.代码解释:
需要解释的就是如果是有序的,就不需要再排了,所以加一个标志flag,可以减少销毁。
c.时间复杂度:
每趟都要跟前面的比一遍,外面遍历跟里面比较构成(因为外面是N,控制里面比较就是N-1,N-2,N-3......1次)完美的等差数列,所以是O(N^2)。
5.选择排序
选一个版本:
//交换两个数据
void Swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
//选择排序
void SelectSort(int* arr, int size)
{
int i = 0;
for (i = 0; i < size-1; i++)
{
int min = i;
int j = 0;
for (j = i+1; j < size; j++)
{
if (arr[j] < arr[min])
{
min = j;
}
}
Swap(&arr[i], &arr[min]);
}
}
选两个版本:
void SelectSort(int* a, int n)
{
int begin = 0;
int 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[begin], &a[mini]);
if (begin == maxi)
{
maxi = mini;
}
Swap(&a[end],&a[maxi]);
++begin;
--end;
}
}
a.思想:
选择最小的数的下标记录下来即可,然后就放到最左边,一遍一遍的走;我们这里代码采用两侧的选最小最大,节省一点效率。
b.代码解释:
需要注意的就是如果先交换了begin与mini位置的值,并且maxi与begin重叠了,就会发生:
c.时间复杂度:
按选一个数算也是完美的等差数列O(N^2),因为选完就放到左边了,所以需要遍历的次数是(n-1)+(n-2)+...+1。
6.堆排序
a.思想:
在数据结构专栏中的对堆的讲解有详细解释,原文链接:https://blog.csdn.net/2301_79698419/article/details/136179286
如果我们要排成降序,就建小堆,小堆选出最小的,首尾交换,最小的放到最后的位置,最后一个数据不看做堆里面的,再次向下调整就可以选出次小的,以此类推,相当于一个一个头插;
调用一次是O(logN),N次就是O(N*logN),计算方法跟向下调整差不多;
向下调整建堆需要倒着调整,叶子节点不需要处理,倒数第一个非叶子节点即最后一个节点的父亲开始调整:
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])//如果右孩子存在(因为如果左孩子为n-1,那右孩子就为n了,就越界了)并且左孩子大于右孩子,下标就走到右孩子上
{
child = child + 1;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
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;
}
}
如果我们要排升序,只需要改动向下调整或者写两个建堆方法:
改动两处:选孩子时选大的那个;如果孩子大于父亲,交换:
b.代码解释:
建堆注意点:
1.我们既然默认左孩子为小的那一个,那结束条件就应该是左孩子不存在的情况即当左孩子等于n的时候就越界了,而又由于堆是完全二叉树,所以左孩子不存在,那右孩子一定不存在,所以只写这一个就行。
2.child + 1 < n && a[child] > a[child + 1],首先需要注意左孩子存在,但右孩子不存在的情况,所以判断child+1<n,其次这个条件要写到&&的前面,因为如果写到后面,a[child]>a[child+1]为假,就判断不出右孩子可能越界的情况了,所以右孩子的检查应该放到&&前面。
c.时间复杂度:
O(N*logN),详细见https://blog.csdn.net/2301_79698419/article/details/136179286
7.快速排序
a.霍尔法:
思想:
1.找一个基准值k(一般是最左或最右,是下标),它比左边都小,比右边的大,此时k就在最终排好序的位置了。
2.如何保证k的左边都比它小,右边都比它大?将k从最左边L开始,右边R先走,R找到比k小停下;左边再走,找到比k大停下,交换左右;如果相遇,交换左L对应的值和k对应的值(为什么相遇这样交换,下面解释)。
3.再取k的左右区间,分别递归,直到只剩一个数或者区间不存在停下(停下原因下面解释),往回反,直到全部有序。
霍尔法:
int PartSort1(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int k = left;//默认k在左边
while (left < right)//里面为什么还要判断left<right,因为left和right会一直变
{
//右边先走,找小;找到停下
while (left < right && a[right] >= a[k])
{
--right;
}
//左边找大,找到停下
while (left < right&&a[left]<=a[k])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left],&a[k]);//相遇这样交换
return left;//left现在就对于k的值
}
注意:
1.为什么外面要left小于right,里面还要判断,因为里面的值会变。
2. 如何解决问题1的死循环?取等于防止左右下标的值都等于k,一直反复交换死循环;和k
相等可以继续走,相等的值放左放右都行。
3.如何解决当left不变right一直--走到越界?判断left<right就行,但是要保证写在&&的前面,因为left=right再走就可能越界了并且顺序可能出错;然后这个条件要放在前面,防止上次++--已经越界了,然后先判断a[left]<=a[k]这个条件,还是越界了,放后面相当于没判断。
相遇问题:
1.左边做k,右边先走,保证了相遇的位置比k小
2.右边做k,左边先走,保证了相遇的位置比k大
3.拿我们左边做k举例:
相遇有两种情况:
第一种是右边走到与左边相等停下来,说明右边找比k小都没找到,此时交换k与左位置的值可以保证k右边都是比k大的,没问题。
第二种就是右边找小停下来了,左边走到了和右边相等的位置,此时说明左边找比k大没找到,此时交换k与左位置的值可以保证右边找到的那个比k小的换到了左边,并且k的右边都是比k大的,也没问题
递归结束条件的判断:
1.区间只有一个值,直接返回。
2.区间不存在,假设剩下一个值,end为1,begin为0,此时k由上一步得到为1,右区间k再加1就为2了,区间不存在再递归begin>end,直接返回;或者左区间相等,就没值了,返回
b.挖坑法:
//1.假设开头是h和L,结尾是R,将h的值存到k中,R找小,L找大
//2.R走找到小的往h里填,R的位置形成新的坑h,L开始找大,找到往h里填,L的位置形成新坑,以此往复,相遇了一定有坑,将k的值放到坑里即可
//[left,right]
int PartSort2(int* a, int left, int right)
{
int midi = GetMidIndex(a, left, right);
Swap(&a[left], &a[midi]);//防止有序,导致效率下降
int key = a[left];
int hole = left;
while (left < right)//外面判断里面还要判断?因为里面值会改变
{
//右边找小,找到停下
while (left < right && a[right] >= key)//等于情况防止左右下标对应的值都等于keyi,导致死循环
{
--right;
}
a[hole] = a[right];
hole = right;
//左边找大
while (left < right && a[left] <= key)//left<right防止极端情况越界(right==left,比如right一直走走到与left和keeyi相等,又++或--),且要放在&&的前面,不然都已经越界了才判断出来问题
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;//返回keyi的位置
}
c.前后指针法:
//前后指针法---单趟
//[left,right]
//1.假设k为起始位置,prev为首指针,cur为prev的后一位指针,cur找小于k的
//2.当cur找到小于k的,++prev,再交换prev与cur的位置,cur++
//3.当cur找到大于k的,cur++,当cur++到越界时,将prev与k交换(cur找到大的会拉开与prev的差距,导致与prev之间都是比k大的,找到小的,prev又++与cur交换,就起到了大的放后,小的放前的目的了)
int PartSort3(int* a, int left, int right)
{
int midi = GetMidIndex(a, left, right);
Swap(&a[left], &a[midi]);//防止有序,导致效率下降
int keyi = left;//返回的是下标,不能用a[left]
int prev = left;
int cur = left+1;
while (cur<=right)
{
if (a[cur] < a[keyi] &&++prev!=cur)//自己跟自己不用换
{
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
keyi=prev;
return keyi;
}
d.时间复杂度
最好是每次选k都能选到最中间的位置,都是中位数,效率很好,为O(N*logN):
最坏就是数组有序,每次选k都是返回最左边的也就是第一个,然后递归右区间,也就是等差数列,所以是O(N^2):
e.快排优化:
解决方法就是三数取中或者随机数取中,更快的帮助k来到中间的位置:
三数取中优化快排:
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) / 2;
//当遇到3数都很小的例子或者都很大的例子时间上也会变慢,可以随机数取中---这里一个注意点!!!
//int mid=left+(rand()%(right-left));
if (a[left] > a[mid])
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] > a[right])//现在左最大,而且右大于中
{
return right;
}
else
{
return left;
}
}
else//a[left]<a[mid]
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])//现在mid最大(因为上一句不成立),而且左又大于右
{
return left;
}
else
{
return right;
}
}
}
有些oj题会针对快排给一些极端的测试用例,所以再给一种优化:
void QuickSort(int* a, int begin,int end)//要分别递归左右两个区间,因此要给定区间范围
{
if (begin >= end)//区间只有一个值,即keyi==1,keyi-1为end,此时begin==end,直接返回;或者说分割到0-1这个区间了(此时这个区间end==1),keyi为1,keyi-1就等于begin了直接返回,或者右区间keyi+1>end了,不存在这样的区间直接返回
{
return;
}
//三路划分
int left = begin;
int right = end;
int cur = left + 1;
int midi = GetMidIndex(a, left, right);
Swap(&a[left], &a[midi]);//防止有序,导致效率下降
int key = a[left];
while (cur <= right)
{
if (a[cur] < key)
{
Swap(&a[left], &a[cur]);
++left;
++cur;
}
else if (a[cur] > key)
{
Swap(&a[right], &a[cur]);
--right;
}
else
{
++cur;
}
}
小 相等 大 不用递归相等的区间了
[begin,left-1] [left,right] [right+1,end]
QuickSort(a, begin, left - 1);
QuickSort(a, right + 1, end);
}
有些极端情况时间上会变大,所以可以采取优化1.三路划分(小于k的,等于k的,大于k的)
//1.起始为l和k,l的下一个为c,末尾为r
//2.a[c]<k,交换c与l的位置,++l,++c
//3.a[c]>k,交换c和r的位置,--r(不++c是因为交换过来r的值不确定,还要判断c的值)
//4.a[c]==k,++c(当一串数都相等的情况直接++到最后,就不会再递归了)
//5.cur>r结束
f.空间复杂度
O(logN),满二叉树高度为logN,往下要压logN层栈帧,不用担心递归回来会再建立,递归回来会复用之前开好的栈帧,所以对于非递归的快排也是往下要logN层,也是O(logN)
8.计数排序
a.思想:
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*)calloc(range, sizeof(int));
if (countA == NULL)
{
perror("calloc fail");
exit(-1);
}
memset(countA, 0, sizeof(int) * range);
//统计次数
for (int i = 0; i < n; i++)
{
countA[a[i] - min]++;
}
int k = 0;
for (int j = 0; j < range; j++)
{
while (countA[j]--)
{
a[k++] = j + min;
}
}
free(countA);
}
b.代码解释:
1.统计次数
统计每个数出现几次,次数放到countA对应的下标里(countA对应的下标因为减了最小的数,所以对应的再加上min正好是对应的数,没出现的是次数是0,就不会进下面的循环了)。
2.排序
每个数是被计了几次,就映射几次,如果对应坐标没有数据,就不映射。
3.缺陷
依据数据的范围,适用于范围集中的数组,数据不集中,range很大,比N大时间上就大了;
只能用于整形。
c.时间复杂度:
时间复杂度---O(N+Range),range大就算range
d.空间复杂度
空间复杂度---O(Range)
9.归并排序
a.思想:
归并排序---对两个有序数组进行归并排序
1.先分解成两个数组,看分别有没有序,无序接着再对左部分再分解
2.直到分解到有序,往回归并(类似递归,往深先走)
3.归并时对原数组copy到tmp数组,copy过程中用尾插,尾插完再copy回去
代码:
void _MergeSort(int* a,int begin,int end,int* tmp)
{
if (begin == end)
{
return;
}
if (end - begin + 1 < 10)
{
InsertSort(a+begin, end - begin + 1);//+begin是因为可以有很多区间的开始是begin,传过来的begin是多少,就从哪一段的begin开始
return;
}
int mid = (begin + end) / 2;
//[begin,mid] mid [mid+1,end]
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end,tmp);
//往回归并
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)//看左右哪一段区间先走完
{
if (a[begin1] <= a[begin2])//小的那一个放到新的空间上,相当于尾插;如果相等,取左右的哪一个都行
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//走到这,说明有一个没走完,即还剩数据,还有数据的那一个直接都给tmp就行,所以下面的while只会进去一个
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//再拷贝回来
// +begin是因为可以有很多区间的开始是begin,传过来的begin是多少,就从哪一段的begin开始往回拷
memcpy(a + begin, tmp + begin, sizeof(int)*(end - begin + 1));//+begin可能是一个局部的归并完然后拷贝的,所以要加begin;
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
b.代码解释:
注意:
1.递归结束条件是begin==end,说明只剩下两个数了,区间分到底了。
2.第二个if为优化,由于递归会走很深,很多的数递归到个数小于10,说明到了下层,类比二叉树,下层递归占比大(满二叉树最下层2^(h-1)就占总调用次数(2^h-1)的50%了),直接用插入排序直接排,减少递归次数。
3.往回归并时,小的那一个放到新的空间上,相当于尾插;如果相等,取左右的哪一个都行。
4.往回拷贝时,+begin是因为可以有很多区间的开始是begin,传过来的begin是多少,就从哪一段的begin开始往回拷,InsertSort这样写也是一样的道理。
c.时间复杂度:
时间复杂度---O(N*logN)
1.一直往下还是二分,高度还是logN
2.每一层归并为N
d.空间复杂度
空间复杂度---O(N)
1.开辟一个tmp为N
2.开辟到最深logN层栈帧,递归回来与之前栈帧复用还是logN,相加起来,logN相比N忽略不计
10.快排非递归
思路:
递归可能引起栈溢出,可以用非递归实现快排
1.类比递归,0-9区间找k,k下标为5,又分为左右区间0-4与6-9,再递归0左区间-4,找到k下标为2,再递归它的左区间0-1,找到为k下标为1,再递归0-1的左为0-0,右区间为2-1不存在,再递归前一个的右
2.用栈先压0-9,0-9出栈进行单趟处理,带出左右区间0-4与6-9入栈,为了类比递归顺序使用后进先出,先入6-9,再入0-4
3.0-4出栈单趟处理,它的左右子区间入栈,先入3-4再入0-1,以此类推
4.用队列就类似于层序遍历的思路,一层一层的排,先排一层的左区间右区间(递归与栈是深度优先,先往下处理;队列是广度优先,一层一层的处理)
void QuickSortNonR(int* a, int begin, int end)
{
ST st;
STInit(&st);
STPush(&st, end);//先入右再入左,出的时候就先出左再出右
STPush(&st, begin);
while (!STEmpty(&st))
{
int left = STTop(&st); //子区间先出左再出右,先出左再出右,开始先取堆顶0为左,9为右,假设走完一趟,现在取出堆顶就是0 - 4,再对0 - 4走单趟处理,然后再分左右子区间,就是为了模仿递归
STPop(&st);
int right = STTop(&st);
STPop(&st);
int k = PartSort1(a, left, right);
//[left,k-1] k [k+1,right]
if (k + 1 < right)//子区间也是先入右子区间
{
STPush(&st, right);
STPush(&st, k + 1);
}
if (left<k-1)//再入左子区间,出的时候先出,开始就是先出0-4
{
STPush(&st, k-1);
STPush(&st, left);
}
}
STDestroy(&st);
}
时间复杂度:
O(N*logN)
空间复杂度:
O(logN)
11.归并非递归
实际就是递归归并的反操作:
//归并排序---非递归
//1.两两归并,拷贝到tmp数组,再拷回原数组,再2个为一组与另一组归并,拷贝到tmp数组,再拷回原数组,再4个为一组与另一组归并,拷贝到tmp中,再拷回原数组
//2.销毁tmp数组
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
//1,2,4....
int gap = 1;//起始两个数归并,两个数之间距离为1,一组是gap个
while (gap<n)
{
for (int i = 0; i < n; i += 2 * gap)//第一组归完跳第二组,往下1,2,4....
{
//每组的合并数据
int begin1 = i, end1 = i + gap - 1;//注意区间下标,这是第一组
int begin2 = i + gap, end2 = i + 2 * gap - 1;//这是第二组
int j = i;//两组归并完要更新j使走向下两组,或者开始定义j为0
//不处理归并时数据会越界,下面归并注释掉打印可看越界的区间printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
//1.end1 与begin2-end2区间越界
//2.begin2-end2区间越界
//3.end2越界
//那越界的元素拷贝到tmp吗?如果不拷,再拷回原数组的时候之前不拷到tmp数组的那一个空间也会被拷到原数组,且那块空间是随机值,会覆盖原数组元素
//处理方法:1.归并一组,拷贝一组,没有归并的不拷,越界的跳出去
if (end1 >= n || begin2 >= n)//修正12两种情况的问题
{
break;
}
if (end2 >= n)//修正end2越界的情况
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];//这里begin++要注意,画图理解
}
else
{
tmp[j++] = a[begin2++];
}
}
//走到这,说明有一层的左部分或者右部分已经都放到tmp数组里了,再判断哪个没结束(没结束的可能是大的值),让没结束的一部分继续放到数组里,所以下面的循环只会进一个
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
memcpy(a+i, tmp+i, sizeof(int) * (end2-i+1));//处理方法1,归并一组拷贝一组,+i是找到不同组的起始位置
}
//memcpy(a, tmp, sizeof(int) * n);//拷贝回去,用处理方法1时屏蔽
gap *= 2;
}
free(tmp);
}
详细解释见其他大佬博客吧~~~
12.排序稳定性的分析
那稳定性有什么意义呢?
试想,如果有5个人参加考试,5个人的成绩都是一样的,那第一名的奖应该发给谁呢?当然是发给先交卷的人了;或者再按单科成绩排名,那如何保证分数相同的比较单科成绩呢?先将某一科成绩排名,此时再用一个稳定的排序对总分进行排序,这样总分相同单科成绩高就排在前面了,就能选出总分相同里单科成绩高的了,所以此时采用稳定的排序就有意义了。
a.插入排序---稳定
因为插入一个数比较,相等可以不挪到。
b.希尔排序---不稳定
因为分组的时候相同的数据分到不同的组,就会发生移动了,可能就不能保证相同数据原来的先候顺序了。
c.选择排序---不稳定
此场景没法保证交换后黑5还在红5前面,选数的时候可以保证稳定性,交换的时候不能。
d.堆排序---不稳定
例如大堆,交换再调整不能保证。
e.冒泡排序---稳定
相等不交换,稳定。
f.快速排序---不稳定
这样一交换就不稳定了。
g.归并排序---稳定
这一句代码保证等于就是稳定的,小的尾插,相等再让begin1尾插。