目录
一、归并排序(递归版)(时间:O(N*log(N)))
1、思路
在学C语言的时候,我们都知道把两个数组归并成一个数组的方法:就是先开一个 tmp 数组;然后给每个数组一个指针去维护;然后比较两个指针指向的值,把小的那个拿下来放到 tmp 数组;当全部元素都放到 tmp 数组后,最后把 tmp 数组的数据都拷回原数组中。
但是,上述归并排序的条件十分苛刻——只有当两个数组都是升序或降序时,才能用归并排序。
因此,归并排序的思路就出来了。对于一个数组,我们可以以中间为界,看成两个无序的数组。而要使用归并排序的前提是要这两个数组有序,因此只有让这两个区间的数组都有序,才能对这两个数组归并。因此,它的归并展开图是这样的:
2、代码
void _MergeSort(int* a, int left, int right, int *tmp)
{
if (left == right)
return;
int mid = (left + right) / 2;
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
/* 归并 */
int curL = left, beginL = left, endL = mid;
int curR = mid + 1, beginR = mid + 1, endR = right;
int i = left;
while ((curL <= endL) && (curR <= endR))
{
if (a[curL] < a[curR])
{
tmp[i++] = a[curL++];
}
else
{
tmp[i++] = a[curR++];
}
}
while (curL <= endL)
{
tmp[i++] = a[curL++];
}
while (curR <= endR)
{
tmp[i++] = a[curR++];
}
memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)calloc(n, sizeof(int));
assert(tmp != NULL);
_MergeSort(a, 0, n - 1, tmp); /* 由于递归时每个栈帧储存的是排序区间的下标,因此传下标就可以了,不用每次都开一个新数组 */
free(tmp);
}
二、归并排序(非递归版)
1、总体思路
从上述递归版的归并排序中可以看出,我们是先让两段数组有序,然后再对这两段数组做动作。因此与二叉树的后序遍历相似。而且,递归时都是递归到最底层(即当数组长度为1时)才返回。
所以在非递归版的实现时,我们可以直接从递归的最底层开始模拟,即一一归并;然后再两两归并;然后再四四归并,最后再整体归并。
2、每趟归并完再整体拷回去(梭哈,但不推荐)
(1)思路
对于单趟来说,如果我们选择先把整个数组进行归并,最后再整体拷回原数组的话,那么就会遇到这三种情况:
第一种:右区间部分越界
对于这种情况,我们可以直接调整 e2 的位置,然后把处于数组内的两部分进行归并。
第二种:左区间没越界,右区间完全越界
对于这种情况,就不应该对右区间进行归并了,应该直接把左区间拷贝到 tmp 数组。最后再把 tmp 数组的数据拷回原数组。
那么,怎么才能使右区间不归并呢?很简单,直接把右区间改成一个不存在的区间就好了,这样它就不会进入拷贝的部分了。
第三种:左区间部分越界,右区间完全越界
对于这种情况,我们应该只拷贝左边没越界的区间到 tmp 数组,最后再把 tmp 数组的数据拷回原数组中。
那么,怎么才能使右区间不归并呢?很简单,直接把右区间改成一个不存在的区间就好了,这样它就不会进入拷贝的部分了。
(2)代码
/* 单趟梭哈 */
void MergeSortNonR1(int* a, int n)
{
int* tmp = (int*)calloc(n, sizeof(int));
assert(tmp != NULL);
for (int gap = 1; gap < n; gap *= 2)
{
/* 单趟归并 */
for (int beginL = 0; beginL < n; beginL += 2 * gap)
{
int endL = beginL + gap - 1, curL = beginL;
int beginR = beginL + gap, endR = beginR + gap - 1, curR = beginR;
int i = beginL; /* 维护tmp */
/* 调整拷贝范围 */
if ((endL >= n) || (beginR >= n)) /* 右半部分不归并 */
{
endL = n - 1;
beginR = n;
endR = n - 1;
}
else if (endR >= n) /* 归并没越界的部分 */
{
endR = n - 1;
}
//拷贝
while ((curL <= endL) && (curR <= endR))
{
if (a[curL] < a[curR])
tmp[i++] = a[curL++];
else
tmp[i++] = a[curR++];
}
while (curL <= endL)
tmp[i++] = a[curL++];
while (curR <= endR)
tmp[i++] = a[curR++];
}
memcpy(a, tmp, sizeof(int) * n);
}
free(tmp);
}
3、每趟每归并一次就拷一次回去
(1)思路
对每趟归并排序来说,当两个区间都没越界时,每次对两个区间归并,然后把在 tmp 归并后的区间拷回原数组原来的位置。
但是,我们同样需要处理相关区间越界的问题。
第一种:左区间没越界,右区间部分越界
针对这种情况,我们要调整右区间右端的值,然后归并没越界的部分。
if (endR >= n)
endR = n - 1;
第二种:左区间没越界,右区间完全越界
对于这种情况,我们就可以不用对左区间进行归并了。
if (beginR >= n)
break;
第三种:左区间部分越界,右区间完全越界
对于这种情况,与第二种的处理相同,即直接放弃归并。
if (beginR >= n)
break;
(2)代码
//分步拷贝
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)calloc(n, sizeof(int));
assert(tmp != NULL);
for (int gap = 1; gap < n; gap *= 2)
{
/* 单趟归并 */
for (int beginL = 0; beginL < n; beginL += 2 * gap)
{
int endL = beginL + gap - 1, curL = beginL;
int beginR = beginL + gap, endR = beginR + gap - 1, curR = beginR;
int i = beginL; /* 维护tmp */
if (beginR >= n)
break;
if (endR >= n)
endR = n - 1;
while ((curL <= endL) && (curR <= endR))
{
if (a[curL] < a[curR])
tmp[i++] = a[curL++];
else
tmp[i++] = a[curR++];
}
while (curL <= endL)
tmp[i++] = a[curL++];
while (curR <= endR)
tmp[i++] = a[curR++];
memcpy(a + beginL, tmp + beginL, sizeof(int) * (endR - beginL + 1));
}
}
free(tmp);
}
三、计数排序(时间:O(N))
1、思路
思路很简洁,就是新开一个数组用来储存原数组每个元素出现的次数,然后根据映射规律从小到大把值赋到原来的数组上。
2、使用条件
但是,计数排序的适用条件非常苛刻。只能对整型,且数据的极差不大的数组进行排序。
3、代码
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* tmp = (int*)malloc(sizeof(int) * (max - min + 1));
assert(tmp != NULL);
memset(tmp, 0, sizeof(int) * (max - min + 1));
for (int i = 0; i < n; i++)
{
tmp[a[i] - min]++;
}
int j = 0;
for (int i = 0; i < max - min + 1; i++)
{
while (tmp[i]--)
{
a[j++] = i + min;
}
}
free(tmp);
}