目录
插入排序
直接插入排序
思路与实现
思路:从前到后遍历,遍历的每一个位置的数都去比较前面的数,找到合适的位置插入
如下为动图:(来源于网络)
实现:
单趟排序
int tmp = a[end+1];
//end从tmp位置前一个开始向前遍历
while (end >= 0)
{
if (tmp < a[end])//找大,找到的值往后移动
{
a[end+1] = a[end];
end--;
}
else
{
break;//找到小的跳出
}
}
a[end+1] = tmp;//找到的比tmp小的值的上一个值,赋予它tmp的值
完整排序
在单趟排序基础上加一个循环。
我设定end的下一个位置的值tmp从前向后遍历,tmp每次保存end的下一个位置数组的值,end作用是每次循环确定tmp的值后,end向前遍历,得到的值与tmp比较,比tmp大的值赋给后一个,直到找到比tmp小的值,跳出循环,将找到的比tmp小的值的上一个赋成tmp。
void InsertSort(int* a, int n)
{
//tmp向后遍历
for (int i = 0;i < n-1;i++)//i只到n-1,因为end最多给到n-2,tmp给到a[n-1]
{
int end = i;
int tmp = a[end+1];//保存起来
//end从tmp位置前一个开始向前遍历
while (end >= 0)
{
if (tmp < a[end])//找大,找到的值往后移动
{
a[end+1] = a[end];
end--;
}
else//找到小的跳出
{
break;
}
}
a[end+1] = tmp;//找到的比tmp小的值的上一个值,赋予它tmp的值
}
}
分析
时间复杂度
最坏情况:
该组数据全是逆序。
tmp前面全是比它大的,每遍历一次都要将该处end的值移到end+1,一直要遍历的end=0,才能插入。
一开始的 tmp 与 end=0 位置的值有 1 次比较,最后一次的 tmp 与 end=n-2,n-3……1,0 位置的值有 n-1 次比较。
总体来说:
最好情况:
该组数据全是顺序。
每次都是比较,不用挪动数据,仅仅只是end(本质上是 tmp=end+1 移动,从开始到最后)从开始移到了最后,。
实际上直接插入排序针对于接近有序的情况会十分快。
空间复杂度
没有申请额外的空间。
稳定性
稳定。
注意到之前的代码 tmp < a[end] 不能取等,若取等后相等的数比较时移到后面,导致不稳定。
while (end >= 0)
{
if (tmp < a[end])//不能取等,取等后不稳定
{
a[end+1] = a[end];
end--;
}
else
{
break;
}
}
a[end+1] = tmp;
希尔排序
思路与实现
在直接插入排序中,直接插入排序针对于接近有序的情况会十分快。希尔排序也是这样产生的。
思路:对数据进行多次预排序,使数据接近有序,再用直接插入排序,进行排序。
实现:
step1:
本质上是直接插入排序,只不过引入了步长,划分成好几个组,每个组组内进行直接插入排序。
给定一个gap(步长),从第一个元素开始,每gap步长的设为一组,进行直接插入排序
gap = 3;//例子
//剩下与直接插入排序一样,只不过步长从1变成了gap
for (int i = 0;i < n - gap;i+=gap)
{
int end = i;
int tmp = a[end+gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end-=gap;
}
else
{
break;
}
}
a[end+gap] = tmp;
}
实际上gap=1时就是直接插入排序。
step2:
i=1 , i=2 …… i=n ,将所有的元素全部分组,后进行直接插入排序。
int gap = 3;
for (int j = 0;j < gap;j++)//j如果取到gap,就与第一组分组的情况重合
{
for (int i = j;i < n - gap;i+=gap)
{
int end = i;
int tmp = a[end+gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end-=gap;
}
else
{
break;
}
}
a[end+gap] = tmp;
}
}
step3:
改变gap,将gap减小(我取的是gap/3+1),达到多次预排序的目的,最后一次gap=1直接插入排序,将所有接近有序元素排好。
while (gap > 1)//取等死循环,最后一直gap=1
{
gap = gap/3 + 1;//加1使最后除以3时一定为1,从而进行最后的直接插入排序
for (int j = 0;j < gap;j++)
{
for (int i = j;i < n - gap;i+=gap)
{
int end = i;
int tmp = a[end+gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end-=gap;
}
else
{
break;
}
}
a[end+gap] = tmp;
}
}
}
分析
时间复杂度
接近于
空间复杂度
,没有开辟额外的空间
稳定性
不稳定。
原因:不同的分组会导致一些相等的数分在不同的组,这些不同的组在直接插入排序时互不干扰,可能导致原本相等排在前面的数现在排在后面。
例子:
选择排序
选择排序
思路与实现
思路:遍历数组找到最小的(最大的),使其与第一个数交换,将剩下的数看成一个整体,执行之前步骤。
动图演示:(来源于网络)
实现:
单趟排序
begin在数组最开始,end在数组最末尾,begin赋给min。
i依次遍历找到比min对应的值小的数就把i赋给min,直到数组结束,这样min指向的是整个数组最小的数。
交换begin与min指向的值,begin向后挪动,单趟排序完成。
//单趟排序代码
min = begin;
for (int i = begin;i <= end;i++)
{
if (a[i] < a[min])
{
min = i;
}
}
Swap(&a[begin], &a[min]);
begin++;
完整排序
外面加一个循环,多次进行单趟排序。每次进行遍历后排除掉第一个已经排好位置的数,剩下的数进行单趟排序。
while (begin < end)
{
//单趟排序
min = begin;
for (int i = begin;i <= end;i++)
{
if (a[i] < a[min])
{
min = i;
}
}
Swap(&a[begin], &a[min]);
begin++;//起到了每次排除第一个已经排好的数的作用
}
完整代码如下:
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
int min = begin;
while (begin < end)
{
min = begin;
for (int i = begin;i <= end;i++)
{
if (a[i] < a[min])
{
min = i;
}
}
Swap(&a[begin], &a[min]);
begin++;
}
}
改进
为了提升效率,在每次遍历时,找到最大和最小的数,替换掉最开始和结束的位置。
但要注意到这里一个坑,在交换的时候,如果begin索引是max索引,当min指向的值交换掉begin指向的值的时候,begin指向的值反而是最小了,max指向的值被换到min指向的值,这样交换会将最小的交换到最后,导致出错。
图示:当第一个是最大值时,
为了解决这样的问题,在这里我加入一个判断,min指向的值与begin指向的值交换完成时,如果max索引是begin的索引,说明原本的max指向的值被交换到min指向的值,这时只要再交换一下min与max索引即可。
//进行交换
Swap(&a[begin], &a[min]);
//添加的判断
if (max == begin)
{
Swap(&min, &max);
}
Swap(&a[end], &a[max]);
整体代码如下:
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
//设定两个值分别存储最大和最小
int min=begin;
int max=end;
while (begin < end)
{
min = begin;
max = end;
for (int i = begin;i <= end;i++)
{
//比较,找到最大,最小
if (a[i]>a[max])
{
max = i;
}
if (a[i] < a[min])
{
min = i;
}
}
//进行交换
Swap(&a[begin], &a[min]);
//添加的判断
if (max == begin)
{
Swap(&min, &max);
}
Swap(&a[end], &a[max]);
begin++;
end--;
}
}
分析
时间复杂度
空间复杂度
,没有申请额外的空间
稳定性
不稳定
原因:以未改进的选择排序为例,
堆排序
思路与实现
思路:建大堆排升序,对数据进行建大堆,将第一个元素与最后一个元素交换,排除掉最后一个已经排好位置的数,将其他元素再次建大堆(实际上就只有堆顶不符合大堆的条件),利用堆的向下调整算法,对堆顶的数进行调整再次形成大堆,以此类推。
实现:
step0:向下调整算法
适用于parent节点两边都是大堆,通过该算法可以直接调整成大堆。
向下调整过程:
通过孩子节点计算父节点:parent = ( child - 1 ) / 2。
通过父节点计算孩子节点:
左孩子:leftchild = parent × 2 + 1
右孩子:rightchild = parent × 2 + 2
void AdjustDwon(int* a, int n, int parent)//排大堆的向下调整算法
{
//先用左孩子,若右孩子更多,再换成右孩子
int child = parent * 2 + 1;
while (child < n)
{
//找最大孩子过程
//1.若在倒数第一个节点,该节点是左孩子,child+1会导致越界
//2.判断是否为最大孩子,若有右孩子,且右孩子大于左孩子,进入循环
//该循环作用是将child变成左右孩子最大的那一个的下标
if (child + 1 < n && a[child + 1] > a[child])
{
child++;
}
//调整过程
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = root * 2 + 1;
}
else
{
break;
}
}
}
step1:建大堆
建大堆原因是堆顶数最大,移到堆尾后,刚好为升序的最后一个数。
i 从最后一个节点的父节点开始,数组从0开始计数,最后一个节点的下标为 child = n - 1,其父节点的下标为parent = ( child - 1 )/ 2,故从(n-1-1)/2开始。
for (int i = (n - 1 - 1) / 2;i > 0;i--)//i从最后一个节点的父节点开始
{
AdjustDwon(a, n, i);
}
step2:将堆顶数移到堆尾,继续向下调整
int end = n - 1;//记录最后一个元素的位置,每一次循环都要end--,即将最后一个元素排除
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDwon(a, end, 0);
end--;
}
完整代码:
void AdjustDwon(int* a, int n, int root)
{
int child = root * 2 + 1;
while (child < n)
{
//判断是否为最大孩子
if (child + 1 < n && a[child + 1] > a[child])
{
child++;
}
//孩子与父节点比较
if (a[child] > a[root])
{
Swap(&a[child], &a[root]);
root = child;
child = root * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
//建大堆
for (int i = (n - 1 - 1) / 2;i > 0;i--)
{
AdjustDwon(a, n, i);
}
//交换堆顶和最后一个元素,并继续调整
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDwon(a, end, 0);
end--;
}
}
分析
时间复杂度
分析:向下调整算法时间复杂度是常数次
空间复杂度
,没有申请额外空间。
稳定性
不稳定。
原因:堆是一种树状结构,在调整的时候,左右两个子树互不干扰,但是把堆写成数组形式是左右两个子树的元素相互交叉组合。
例子:在堆顶和最后一个元素交换时导致不稳定。
交换排序
冒泡排序
思路与实现
思路:每次从开始遍历将较大的元素向后移,这样第一次遍历就能将最大的元素移到最后的位置,
再遍历一遍就能将倒数第二个元素的位置归位,以此类推。
动图演示:(来源于网络)
实现:
单趟排序
将最大的元素移到最后,以下面这个为例:
for (int j = 0;j < n - 1;j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
}
}
完整排序
在每次单趟排序后,都去掉最后一个元素进入下一轮遍历。
for (int i = 0;i < n;i++)
{
for (int j = 0;j < n - i - 1;j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
}
}
}
完整代码如下:
void BubbleSort(int* a, int n)
{
for (int i = 0;i < n;i++)
{
for (int j = 0;j < n - i - 1;j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
}
}
}
}
改进
进入一个flag变量,初始化为0,在交换处令其为1,如果一趟排序后flag仍然为0,说明这一趟排序中没有交换,该序列是顺序的,所以没有必要进行后续排序。
代码如下:
void BubbleSort(int* a, int n)
{
int flag = 0;//flag变量
for (int i = 0;i < n;i++)
{
for (int j = 0;j < n - i - 1;j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
flag = 1;
}
}
if (flag == 0)//若第一趟排序没有交换说明是顺序的,直接跳出结束
{
break;
}
}
}
分析
时间复杂度
最坏情况:全部逆序,每一个都要比较交换。
结果是。
最好情况:全部顺序,改进的情况下,只需遍历一遍,时间复杂度是。
但是稍微有一点逆序,改进的情况失效,虽然很多地方不会进行交换,但是都要遍历,同最坏情况一样的计算过程,结果是。
空间复杂度
,没有申请额外的空间。
稳定性
稳定。
这一步中,相等不交换,大于才交换,保证了稳定性。
if (a[j] > a[j + 1])//大于才交换
{
Swap(&a[j], &a[j + 1]);
}
快速排序
思路与实现
思路:选择一个key,通过单趟排序直接让key到其准确位置上,则其左边是比它小的数,右边是比它大的数
实现:
单趟排序
1.hoare版本
动图演示:(来源于网络)
设定left,key在开头,right指向末尾,右边right先走,遇到小于key的停下(不能小于等于,会导致死循环),然后左边再走,遇到大于key的停下,交换right和left指向的数。
注意:在 right 找小,left 找大的过程中,必须要有 left < right 条件否则可能发生 left 与 right 错过的现象。
例如:
代码如下:
while (a[right] >= a[keyi] && left < right)//右边找大停止,注意要保证left始终小于right
{
right--;
}
while (a[left] <= a[keyi] && left < right)//左边找小停止
{
left++;
}
Swap(&a[left], &a[right]);
再继续右边先走,找小停下,左边再走,找大停下,交换……以此类推,最终一定相遇,相遇时交换key指向的值和相遇时left与right指向的值。
代码如下:
while (left < right)
{
while (a[right] >= a[keyi] && left < right)//右边找大停止
{
right--;
}
while (a[left] <= a[keyi] && left < right)//左边找小停止
{
left++;
}
Swap(&a[left], &a[right]);
}
//最后交换相遇位置left指向的值和keyi指向的值
Swap(&a[left], &a[keyi]);
这样,6的左边都是比它小的,右边都是比它大的,6到了正确的位置,以后都不用调整。
一个问题:为什么左边作为key,让右边先走?
答:保证相遇位置指向的值比key指向的值小,这样交换后,使得左边的值都比相遇位置指向的值小。
情况:这里不考虑中间来来回回的过程,只考虑最后相遇前的那一趟过程。
1.right先走,right停下来,left去遇到right。相遇的位置就是right停下的位置,既然right是找小那自然相遇的位置指向的值比key指向的值小。
2.right先走,right没找到比key指向的值小的值,却遇到了left(由于left<right导致停止),相遇停止。
这有有两种可能:一种是left是上一轮中停下的位置,此时Left指向的值已经与上一轮中right指向的比key小的值交换,此时自然比key小。
另一种可能是该数组是顺序的或者key选到了最小的值,right从一开始就到了最左边与left相遇,这种情况单趟排序自然是排好的。
总结下来就是,左边做key,右边先走,右边做key,左边先走。
hoare版本单趟排序的总代码如下:
int PartSort1(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
//右边先走
while (a[right] >= a[keyi] && left < right)//右边找大停止
{
right--;
}
//左边后走
while (a[left] <= a[keyi] && left < right)//左边找小停止
{
left++;
}
Swap(&a[left], &a[right]);
}
//最后交换相遇位置left指向的值和keyi指向的值
Swap(&a[left], &a[keyi]);
keyi = left;
return keyi;//返回相遇位置的值
}
2.挖坑法
动图演示:(来源于网络)
与hoare版本类似,把key储存起来,原来的key位置为坑,右边先走,找到小后,将小的数填入坑中,这时右边这个位置变成坑,再左边走,找到大后,将大的数填入坑中,这时左边这个位置变成坑,以此类推,直到left与right相遇后停下,此时相遇位置是坑,再将储存的key值填入坑中,结束。
代码如下:
int PartSort2(int* a, int left, int right)
{
int key = a[left];//用key保存值,而不用keyi保存索引,原因是keyi索引的值会由于填坑发生改变
int hole = left;
while (left < right)
{
while (a[right] >= key && left < right)
{
right--;
}
a[hole] = a[right];
hole = right;
while (a[left] <= key && left < right)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
3.前后指针法
动图演示:(来源于网络)
这个方法与前面的方法有明显不同。
设定prev=left,cur=prev+1,cur找小于key指向的值,找到后停下,并且prev++,然后prev对应的值与cur交换。让这个过程循环起来,当cur越过数组(cur>right)时结束。
剩下过程如下:
代码如下:
int PartSort3(int* a, int left, int right)
{
int prev = left;
int cur = prev + 1;
int keyi = left;
while (cur <= right)//cur越界时停止循环
{
if (a[cur] < a[keyi])//cur找小
{
prev++;
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
完整排序
选取其中一个单趟排序,利用单趟排序后,返回相遇位置的值,这个值左边都比他小,右边都比他大,我把这个值记为keyi。
实际上通过这个相遇位置keyi把数组分成了两个部分[ left , keyi-1 ] ,[ keyi+1 , right ],再对这两个区间进行单趟排序,以此类推,就可以排好整个数组。
void QuickSort(int* a, int left, int right)
{
int keyi = PartSort1(a, left, right);
//int keyi = PartSort2(a, left, right);
//int keyi = PartSort3(a, left, right);
if (left>= right)//跳出条件
{
return;
}
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
改进
1.三数取中
在单趟排序中,我选左边作为key,但是遇到一些极端情况,比如选取的key的最终位置就在开头或结尾,这样排序没有充分利用到递归折半的良好性质,这样排序的效率是很低的,我们更加希望选取的key的最终位置在中间。
例如:
这样的复杂度达到 ,自然不如key选的数在中间,这样折半递归来得快。
所以选取key时用三数取中,即选取开头中间结尾三个数中,选取中间大小的数,让其与left最左边的数交换,这样选到的数就不会特别偏左或者偏右了。
三数取中代码如下:
int GetMidi(int* a, int left, int right)//三数取中
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right]) // mid是最大值
{
return left;
}
else
{
return right;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right]) // mid是最小
{
return left;
}
else
{
return right;
}
}
}
int PartSort1(int* a, int left, int right)
{
//三数取中
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left;
while (left < right)
{
while (a[right] >= a[keyi] && left < right)
{
right--;
}
while (a[left] <= a[keyi] && left < right)
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
return keyi;
}
2.小区间优化
对于大数据量的排序,递归次数多,递归深度深,可能导致栈溢出,或者导致时间开销和空间开销大。
为了解决这个问题,我在递归到每个数组足够小时(数组长度小于等于10),不在用递归,而是用直接插入排序,这样可以减少大量的调用次数。
代码如下:
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
// 小区间优化,小区间不再递归分割区间进行排序,降低递归次数
if ((right- left + 1) > 10)//区间长度 right - left + 1
{
int keyi = PartSort3(a, left , right);
QuickSort1(a, left , keyi - 1);
QuickSort1(a, keyi + 1, right);
}
else
{
InsertSort(a + left , right- left + 1);//区间够小时用直接插入排序
//注意是a+left不是a,因为多次分割区间后区间,不一定是从a开始,要根据left确定
}
}
总的改进后的快速排序代码:(hoare版本为例)
//三数取中
int GetMidi(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
//单趟排序
int PartSort1(int* a, int left, int right)
{
//三数取中
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left;
while (left < right)
{
while (a[right] >= a[keyi] && left < right)
{
right--;
}
while (a[left] <= a[keyi] && left < right)
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
return keyi;
}
//快速排序
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
if ((right- left + 1) > 10)
{
int keyi = PartSort3(a, left , right);
QuickSort1(a, left , keyi - 1);
QuickSort1(a, keyi + 1, right);
}
else
{
InsertSort(a + left , right- left + 1);
}
}
快排非递归形式
对于快速排序递归形式在极端条件下,可能会递归深度太深。为了解决这样的现象,可以将递归形式改成非递归形式。
递归改非递归通常有两种方式:1.直接改成循环,比如求斐波那契数列第n项,归并排序非递归形式。2.用数据结构模拟递归过程。
用栈模拟递归,注意先进后出,本质上就是把每一次递归中分割的节点装进栈里面,每次取出进行单趟排序。
代码如下:
void QuickSortNonR(int* a, int left, int right)
{
ST ps;
STInit(&ps);
STPush(&ps, right);
STPush(&ps, left);
while (!STEmpty(&ps))
{
int left = STTop(&ps);
STPop(&ps);
int right = STTop(&ps);
STPop(&ps);
int keyi = PartSort3(a, left, right);
if (keyi + 1 < right)
{
STPush(&ps,right);
STPush(&ps,keyi + 1);
}
if (keyi - 1 > left)
{
STPush(&ps, keyi - 1);
STPush(&ps, left);
}
}
STDestroy(&ps);
}
用队列模拟递归,注意先进先出。
void QuickSortNonR(int* a, int left, int right)
{
Queue ps;
QueueInit(&ps);
QueuePush(&ps, right);
QueuePush(&ps, left);
while (!StackEmpty(&ps))
{
int left = QueueFront(&ps);
QueuePop(&ps);
int right = QueueFront(&ps);
QueuePop(&ps);
int keyi = PartSort1(a, left, right);
if (keyi - 1 > left)
{
QueuePush(&ps, left);
QueuePush(&ps, keyi - 1);
}
if (keyi + 1 < right)
{
QueuePush(&ps,keyi + 1);
QueuePush(&ps,right);
}
}
QueueDestroy(&ps);
}
分析
时间复杂度
最好情况:每次选取的key指向的值是刚好整组数据的中位数。
每一层遍历的和是 ,又根据二分的特性,共有logN层,所以复杂度为。
最坏情况:每次选取的key是整组数据的最小值或者是最大值。
递归n次,每一次遍历一遍数组。
结果是
空间复杂度
稳定性
不稳定
left与right相遇时,相遇位置与key位置的值交换会导致不稳定。
归并排序
思路与实现
思路:
step1:进行区间划分
将n个区间对半分,分出来的区间继续对半分,直到划分至一个数, 不在分割。
step2:
将每次分割的数进行归并,返回,这使得每一次归并都是有序的。
动图演示:(来源于网络)
实现:与思路有不同,毕竟思路是比较理想化的做法。
大致实现过程:
举一个例子:
tmp是用来归并时临时拷贝的数组 。
step1:
找到中间 mid=(begin+end)/2 ,将整个区间分成 [begin,mid] [mid+1,end] ,以此类推。
step2:
从划分到最小的开始,进行归并,归并到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;
int end1 = mid;
int begin2 = mid + 1;
int end2 = end;
int index = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
memcpy(a + begin, tmp + begin, (end - begin + 1)*sizeof(int));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc failed\n");
return;
}
_MergeSort(a, tmp, 0, n - 1);
free(tmp);
}
归并排序非递归形式
我是通过模拟循环过程解决,更像是忽略掉拆分过程,直接进行合并,11归并,22归并,44归并…
通过设定一个gap来确定是几几归并,gap是几就是几几归并。然后就是在每一个归并中从哪里开始的问题,这里我是通过设定i,在一次归并中,归并完第一组,就跳过2倍gap个找到第二组再进行归并,以此类推。
gap=8,只有一个数组,无法归并,结束。
代码如下:
for (int gap = 1;gap < n;gap *= 2)//11归并,22归并,44归并……
{
for (int i = 0;i < n;i += 2 * gap)
{
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + gap + gap - 1;
int index = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
}
特殊情况:归并时出现奇数组。
遇到奇数组时最后一组不要理会,排着排着总会变成偶数组,这时最后一组与其他组可能长度不一样,只要修改一下最后一组结束的下标就好了(反正都是有序的都可以进行归并)。
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc failed\n");
return;
}
for (int gap = 1;gap < n;gap *= 2)
{
for (int i = 0;i < n;i += 2 * gap)
{
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + gap + gap - 1;
int index = i;
//出现奇数组情况,该判断都在最后一组
if (begin2 >= n)
{
break;
}
//若begin2<n,则此时匹配成两两一组(偶数组)
//但若最后一组末尾下标越界,修正即可。
if (end2 >= n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
}
}
分析
时间复杂度
原因:标准的,与快排最好情况类似。
每一次归并都是对数组所有进行遍历,根据二分特性,长度为N的数组划分完需要logN次。
所以是
s空间复杂度
原因:申请一个与排序数组一样大小的tmp数组用于储存每次归并后的结果。
稳定性
稳定。
原因:没有大幅度的交换,主要看归并,每次归并时,相同的数始终在前面。
if (a[begin1] <= a[begin2])//取等,不取不稳定
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
//因为在begin1到end1中的元素在前面,若出现相同的元素,自然是begin1到end1中的元素优先
计数排序
思路与实现
思路:创建一个全为零的数组,遍历排序的数组,将数组的值作为全零数组的索引,每到一个数组的值,其对应全零数组的索引的值加一,最后遍历全零数组,将大于一的值的索引重新填回排序的数组。这是一种非比较排序,建立了一种数组值到索引的映射关系。
实现:
step1:
我们要开一个数组,要确定数组的范围,因为是排序数组的值映射到新开辟数组的索引,所以我希望数组的范围是从排序数组的最小值到最大值。
int max = a[0];
int min = a[0];
for (int i = 0;i < n;i++)
{
if (max < a[i])//找到数组的最大值
{
max = a[i];
}
if (min > a[i])//找到数组的最小值
{
min = a[i];
}
}
int size = max - min + 1;//数组大小
//根据范围创建一个全零的tmp数组
int* tmp = (int*)calloc(size,sizeof(int));
step2:
将排序的数据填充到tmp数组。
for (int i = 0;i < n;i++)
{
tmp[a[i] - min]++;
}
step3:
遍历tmp数组填充数据。
int j = 0;
for (int i = 0;i < size;i++)
{
while(tmp[i]--)
{
a[j++] = i + min;
}
}
完整代码:
void CountSort(int* a, int n)
{
//找最大元素最小元素,计算范围
int max = a[0];
int min = a[0];
for (int i = 0;i < n;i++)
{
if (max < a[i])
{
max = a[i];
}
if (min > a[i])
{
min = a[i];
}
}
int range = max - min + 1;
//根据范围开辟数组
int* tmp = (int*)calloc(size,sizeof(int));
//已有数据填充数组
for (int i = 0;i < n;i++)
{
tmp[a[i] - min]++;
}
//依次拷贝回排序数组
int j = 0;
for (int i = 0;i < range ;i++)
{
while(tmp[i]--)
{
a[j++] = i + min;
}
}
}
分析
时间复杂度
1.当开辟的数组tmp长度range小于排序数组长度N时,排序数组主导
for (int i = 0;i < n;i++)
{
if (max < a[i])
{
max = a[i];
}
if (min > a[i])
{
min = a[i];
}
}
最少要遍历一遍排序数组找最大最小值,所以是
2.当开辟的数组tmp长度range大于排序数组长度N时,tmp数组主导
int j = 0;
for (int i = 0;i < range ;i++)
{
while(tmp[i]--)
{
a[j++] = i + min;
}
}
拷贝回排序数组需要根据tmp数组的长度来确定时间复杂度。
空间复杂度
int range = max - min + 1;
//根据范围开辟数组
int* tmp = (int*)calloc(size,sizeof(int));
开辟 range = max - min + 1 个空间。
总结
排序 | 时间复杂度 | 空间复杂度 | 稳定性 | |
插入排序 | 直接插入排序 | 稳定 | ||
希尔排序 | 不稳定 | |||
选择排序 | 选择排序 | 不稳定 | ||
堆排序 | 不稳定 | |||
交换排序 | 冒泡排序 | 稳定 | ||
快速排序 | 不稳定 | |||
归并排序 | 归并排序 | 稳定 | ||
非交换排序 | 计数排序 | × |