排序是对数据、元素或对象按照一定的规则或条件进行排列的操作。排序通常是根据一些特定的标准将项目从小到大或从大到小重新排列。
常见的排序算法包括:
- 冒泡排序(Bubble Sort)
- 选择排序(Selection Sort)
- 插入排序(Insertion Sort)
- 归并排序(Merge Sort)
- 快速排序(Quick Sort)
- 堆排序(Heap Sort)
在编程中,根据具体情况选择合适的排序算法是很重要的。每种排序算法都有其优势和局限性,比如在特定情况下可能会有更高的效率。
插入排序
插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作方式类似于整理扑克牌的方法。该算法逐步构建最终的排序列表,每次将一个元素插入到已经排序的列表中的正确位置。
以下是插入排序的基本思想:
- 从第一个元素开始,该元素可以认为已经被排序。
- 取出下一个元素,在已经排序的元素序列中从后向前扫描。
- 如果已排序元素大于/小于新元素,将已排序元素移到下一位置。
- 重复步骤 3,直到找到已排序的元素小于/大于或等于新元素的位置。
- 将新元素插入到该位置后。
- 重复步骤 2~5。
直接插入排序
//直接插入排序
//时间复杂度为O(n^2)
//空间复杂度为O(1)
void InsertSort(int* a, int n)
{
for (int i = 1; i < n; i++)
{
int tmp = a[i];
int j = i - 1;
while (j >= 0 && a[j] Sort_direction tmp)
{
a[j + 1] = a[j];
j--;
}
a[j + 1] = tmp;
}
}
希尔排序
希尔排序(Shell Sort)是一种改进的插入排序算法,旨在克服插入排序的缺点,尤其是在处理大规模数据时的效率问题。希尔排序的基本思想是将待排序的序列按照一定的增量(间隔)分成若干个子序列,对每个子序列进行插入排序,逐渐缩小增量直至为1,最后对整个序列进行一次插入排序。
希尔排序的具体步骤如下:
-
选择增量序列:选择一个增量序列,一般取n/2、n/4、n/8等。这个增量序列的选择对排序的效率有很大影响,不同的增量序列会导致不同的时间复杂度。
-
按增量分组:根据选定的增量,将序列分成多个子序列,每个子序列包含相隔增量个元素。
-
对每个子序列进行插入排序:对每个子序列分别进行插入排序,即将待排序序列分成若干个小部分,分别对这些小部分进行插入排序。
-
逐渐缩小增量:重复以上步骤,逐渐缩小增量直至为1。通过逐渐缩小增量,希尔排序能够先使序列局部有序,然后逐步扩大已排序序列的范围。
-
最终插入排序:当增量缩小至1时,进行最后一次插入排序,完成最终的排序。
希尔排序的时间复杂度取决于增量序列的选择,一般情况下在O(n log^2 n)到O(n^1.5)之间。其优点是相对简单且易于实现,对于中等大小的数据集合表现良好。然而,希尔排序的性能高度依赖于所选择的增量序列,不同的增量序列可能会导致不同的排序效率。
void ShellSort(int* arr, int n)
{
int gap, i, j, temp;
// 希尔排序的核心部分:循环遍历不同的增量序列
for (gap = n / 2; gap > 0; gap /= 2)
{
// 对每个子序列进行插入排序
for (i = gap; i < n; i++)
{
temp = arr[i]; // 保存当前待插入的元素
// 插入排序的一部分:将当前元素插入到已排序序列的正确位置
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap)
{
arr[j] = arr[j - gap]; // 移动元素
}
arr[j] = temp; // 将待插入元素放入正确位置
}
}
}
选择排序
直接选择排序
直接选择排序(Selection Sort)是一种简单直观的排序算法,它的基本思想是每次从未排序的部分中选取最小(或最大)的元素放到已排序部分的末尾。这个过程不断重复,直到所有元素都被排序。
以下是直接选择排序算法的伪代码描述:
- 从未排序部分中找到最小元素的索引。
- 将找到的最小元素与未排序部分的第一个元素进行交换。
- 将已排序部分末尾扩展一个元素,即原先的最小元素。
- 重复上述步骤,直到所有元素都被排序。
直接选择排序虽然简单,但效率较低,时间复杂度为O(n^2),不适用于大规模数据的排序。
直接选择排序算法的优化,同时找到未排序部分的最小值和最大值,然后将它们分别放到已排序部分的起始和末尾。
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini, maxi;
mini = maxi = begin;
for (int i = begin + 1; 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;
}
}
这个优化后的算法在每一轮循环中同时找到未排序部分的最小和最大值,并将它们分别放到已排序部分的起始和末尾,从而减少了比较和交换的次数,提高了排序效率。
堆排序
堆排序(Heap Sort)是一种高效的排序算法,它基于二叉堆数据结构实现。堆排序分为两个阶段:构建堆和堆调整。
堆排序的步骤:
- 构建堆:
- 将待排序序列看成一个完全二叉树,从最后一个非叶子节点开始,依次向前进行"下沉"操作,将序列调整为一个最大堆(或最小堆)。
- 堆调整:
- 将堆顶元素(最大值或最小值)与堆末尾元素交换,然后对剩余元素重新进行堆调整,直到所有元素都被排序完成。
堆排序的特点:
- 时间复杂度:堆排序的时间复杂度为 O(n log n),其中构建堆的时间复杂度为 O(n),每次调整堆的时间复杂度为 O(log n)。
- 空间复杂度:堆排序是原地排序,只需要常数级别的额外空间。
- 稳定性:堆排序是不稳定的排序算法。
// 对以节点 i 为根的子树进行堆化
void heapify(int arr[], int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
// 如果左子节点存在且大于根节点
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
// 如果右子节点存在且大于当前最大节点
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大值不是当前节点
if (largest != i) {
int temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;
// 递归地对受影响的子树进行堆化
heapify(arr, n, largest);
}
}
// 堆排序函数
void heapSort(int arr[], int n) {
// 构建最大堆(重新排列数组)
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 逐个从堆中提取元素
for (int i = n - 1; i > 0; i--) {
// 交换堆顶元素和数组末尾元素
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 对剩余元素进行堆化
heapify(arr, i, 0);
}
}
下沉操作(Heapify Down):
void heapifyDown(int arr[], int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
int temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;
heapifyDown(arr, n, largest);
}
}
上升操作(Heapify Up):
void heapifyUp(int arr[], int i) {
int parent = (i - 1) / 2;
if (parent >= 0 && arr[parent] < arr[i]) {
int temp = arr[i];
arr[i] = arr[parent];
arr[parent] = temp;
heapifyUp(arr, parent);
}
}
交换排序
冒泡排序
//时间复杂度:O(N^2)
//空间复杂度:O(1)
void BubbleSort(int* a, int n) {
int i, j;
int isSorted = 0; // 标志变量,用于检测数组是否已经有序
for (i = 0; i < n - 1; i++) {
isSorted = 1; // 假定数组已经有序
for (j = 0; j < n - i - 1; j++) {
if (a[j] > a[j + 1]) {
// 如果当前元素大于下一个元素,则交换它们
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
isSorted = 0; // 如果发生交换,说明数组仍然无序
}
}
// 如果一轮遍历中没有发生交换,说明数组已经有序
if (isSorted) {
break;
}
}
}
快速排序
快速排序(Quicksort)是一种常用的排序算法,基本思想是通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据小,然后对这两部分数据分别进行快速排序,递归地实现整个数据集的排序。
int PartSort(int* a, int begin, int end) {
int key = a[end];
int keyindex = end;
while (begin < end) {
while (begin < end && a[begin] <= key) {
++begin;
}
while (begin < end && a[end] >= key) {
--end;
}
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[keyindex]);
return begin;
}
void QuickSort(int* a, int left, int right) {
int* stack = malloc((right - left + 1) * sizeof(int));
int top = -1;
stack[++top] = left;
stack[++top] = right;
while (top >= 0) {
right = stack[top--];
left = stack[top--];
int div = PartSort(a, left, right);
if (div - 1 > left) {
stack[++top] = left;
stack[++top] = div - 1;
}
if (div + 1 < right) {
stack[++top] = div + 1;
stack[++top] = right;
}
}
free(stack);
}
归并排序
归并排序(Merge Sort)是一种分治算法,它将待排序的数组分为两部分,分别对这两部分进行排序,然后合并两部分已排序的数组,以达到整体有序的目的。
归并排序的基本思想是:将数组分为左右两部分,分别对左右两部分进行递归地排序,然后将两部分已经有序的数组合并成一个有序的数组。这个过程可以递归地进行,直到数组长度为1。
归并排序的步骤如下:
- 如果数组长度小于等于1,直接返回数组。
- 将数组一分为二,分别对左右两部分进行归并排序。
- 将排好序的左右两部分数组合并成一个有序数组。
void merge(int arr[], int l, int m, int r) {
int n1 = m - l + 1;
int n2 = r - m;
int* L = (int*)malloc(n1 * sizeof(int));
int* R = (int*)malloc(n2 * sizeof(int));
for (int i = 0; i < n1; i++)
L[i] = arr[l + i];
for (int j = 0; j < n2; j++)
R[j] = arr[m + 1 + j];
int i = 0, j = 0, k = l;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
}
else {
arr[k] = R[j];
j++;
}
k++;
}
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
free(L);
free(R);
}
void MergeSort(int arr[], int n) {
int curr_size;
int left_start;
for (curr_size = 1; curr_size <= n - 1; curr_size = 2 * curr_size) {
for (left_start = 0; left_start < n - 1; left_start += 2 * curr_size) {
int mid = left_start + curr_size - 1;
int right_end = left_start + 2 * curr_size - 1;
if (right_end >= n) {
right_end = n - 1;
}
merge(arr, left_start, mid, right_end);
}
}
}
排序稳定性
在排序算法中,"排序稳定性"是一个重要的概念,指的是当排序算法中存在多个具有相同键值的元素时,这些元素在排序后的相对位置是否保持不变。如果排序算法能够保持具有相同键值的元素的相对顺序不变,那么这个排序算法就被称为"稳定的排序算法"。
为什么排序稳定性很重要?
-
保持顺序:在某些情况下,我们希望在排序后保持相同键值的元素原始的相对顺序。例如,如果有一个列表包含学生的姓名和对应的分数,且需要按照分数排序,但是分数相同时希望按照姓名的字母顺序排序,这时候稳定的排序算法会很有用。
-
避免信息丢失:在某些应用中,元素的原始顺序可能包含了重要的信息。如果排序算法不是稳定的,可能导致相同键值的元素在排序后位置发生变化,从而丢失了原始的信息。
稳定排序算法的例子:
-
冒泡排序:冒泡排序是一种稳定的排序算法,当有相同元素时,它会保持它们原来的相对位置。
-
插入排序:插入排序也是一种稳定的排序算法,它在排序时会保持相同元素的相对位置不变。
不稳定排序算法的例子:
-
快速排序:快速排序是一种不稳定的排序算法,相同元素的相对位置在排序后可能会改变。
-
堆排序:堆排序同样是一种不稳定的排序算法,它可能改变相同元素的原始顺序。
在选择排序算法时,根据具体的应用需求,稳定性是一个需要考虑的重要因素。