归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。它将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
归并排序的基本思想是分治思想,它包括以下三个步骤:
归并排序通过不断地将大问题分解成小问题来解决,即把大的数组拆分成若干个小的数组,然后逐一合并这些有序的小数组来得到最终排序好的整体数组。这种算法非常适用于链表等数据结构,在处理大规模数据时尤其高效。
// 归并排序递归函数
void _MergeSort(int* a, int begin, int end, int* temp)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
// [begin, mid] [mid+1, end]
_MergeSort(a, begin, mid, temp);
_MergeSort(a, mid+1, end, temp);
// ... 归并 [begin,mid] [mid+1,end]
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
temp[i++] = a[begin1++];
}
else
{
temp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
temp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
temp[i++] = a[begin2++];
}
// 拷贝回原数组
memcpy(a + begin, temp + begin, sizeof(int) * (end - begin + 1));
}
// 归并排序
void MergeSort(int* a, int n)
{
// 申请一个与原数组同样大小的空间
int* temp = (int*)malloc(sizeof(int) * n);
if (temp == NULL)
{
perror("malloc fail");
return;
}
_MergeSort(a, 0, n - 1, temp);
free(temp);
}
现在我们来分析一下以上代码:
这段代码是归并排序(Merge Sort)的实现。归并排序是一种分治算法,它将一个数组分成两半,对每一半进行排序,然后将两个有序的部分合并成一个有序的数组。以下是这段代码的算法思想和步骤分析:
递归划分:
_MergeSort
函数中,首先检查基准条件,即如果begin
大于或等于end
,则数组已经完全有序,所以直接返回。mid
,将数组分成两个子数组:[begin, mid]
和[mid+1, end]
。合并:
begin1
和begin2
分别指向两个子数组的开始位置,而指针end1
和end2
分别指向两个子数组的结束位置。temp
中,直到其中一个子数组被完全取完。拷贝回原数组:
memcpy
函数将临时数组中的元素复制回原数组。这一步是必要的,因为临时数组是在堆上分配的,而原数组是在栈上。主函数:
MergeSort
函数是归并排序的入口点。它首先在堆上为原数组分配一个同样大小的临时数组。如果分配失败(即malloc
返回NULL),则输出错误信息并返回。_MergeSort
对原数组进行排序。稳定性:
时间复杂度:
O(nlogn)
,其中n
是数组的大小。这是因为每次递归调用都会将问题规模减半(logn)
,并且需要进行n
次这样的递归调用(n)
。空间复杂度:
O(n)
,因为需要一个与原数组同样大小的临时数组来存储合并过程中的中间结果。// 归并排序(非递归)
void MergeSortNonR(int* a, int n)
{
int* temp = (int*)malloc(sizeof(int) * n);
if (temp == NULL)
{
perror("malloc fail");
return;
}
int gap = 1; // 通过gap来控制归并的两个区间的大小,表示的是这两个区间的大小
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;
// [begin1, end1] [begin2, end2] 归并
// 边界处理
if (end1 >= n || begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
// 归并
int j = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
temp[j++] = a[begin1++];
}
else
{
temp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
temp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
temp[j++] = a[begin2++];
}
// 拷贝回原数组(边归并边拷贝) --- 因为最后可能有一个区间不需要归并,所以这一个区间的元素是不需要改变的,即不需要拷贝回去,若一次性拷贝回原数组,会使这个区间的元素全部变为随机值
memcpy(a + i, temp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
free(temp);
}
对于上述代码我们接着来分析一下它的算法步骤:
【算法步骤】:
初始化:
temp
,其大小为输入数组a
的大小。gap
为1,它表示每次归并时每组数据的个数。归并循环:
gap
小于输入数组的长度n
时,进入循环。gap
),并对这两个子数组进行归并。子数组归并:
begin1
到end1
和begin2
到end2
。temp
来存储归并的结果。拷贝回原数组:
memcpy
函数将临时数组中的数据拷贝回原数组。这一步是为了在归并过程中更新原数组。扩大gap:
gap
乘以2,以便在下一次循环中处理更大的子数组。释放内存:
temp
的内存。O(N)
的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。O(N*logN)
O(N)
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
这段代码是实现计数排序算法的C语言代码。以下是该代码的算法步骤和思想分析:
算法步骤:
// 计数排序
// 时间复杂度:O(N+range) 空间复杂度:O(range)
void CountSort(int* a, int n)
{
int min = a[0], max = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] < min)
{
min = a[i];
}
if (a[i] > max)
{
max = a[i];
}
}
int range = max - min + 1;
int* count = (int*)calloc(range, sizeof(int));
if (count == NULL)
{
perror("calloc fail");
return;
}
// 统计次数
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
// 排序
int i = 0;
for (int j = 0; j < range; j++)
{
while (count[j]--)
{
a[i++] = j + min;
}
}
}
O(MAX(N,范围))
,由于算法只涉及到一次遍历输入数组和一次遍历计数数组,所以时间复杂度为O(MAX(N,范围))
。O(范围)
,由于需要创建一个与范围大小相等的计数数组,所以空间复杂度为O(范围)
。