1. 直接插入排序
- 原理:就像整理扑克牌一样,假设你手里已经有一些按顺序排好的牌,当拿到一张新牌时,你会从右到左依次比较,找到它应该插入的位置,然后把它插入进去,让手中的牌始终保持有序。
- 过程:从数组的第二个元素开始,把它和前面已经排好序的元素依次比较,如果它比前面的元素小,就把前面的元素往后移,直到找到合适的位置插入这个元素,重复这个过程,直到整个数组都排好序。
- 稳定性:稳定,因为相同元素的相对顺序不会改变。
- 时间复杂度:O(n²),意味着随着数组元素数量的增加,排序所需的时间会以平方的速度增长。
2. 冒泡排序
- 原理:想象有一群人站成一排,按照身高从低到高排序。每次比较相邻的两个人,如果前面的人比后面的人高,就交换他们的位置。这样经过一轮比较后,最高的人就会“冒泡”到最后面。重复这个过程,直到所有人都排好序。
- 过程:从数组的第一个元素开始,依次比较相邻的两个元素,如果顺序不对就交换它们的位置。每一轮比较都会把当前最大的元素放到数组的末尾。重复这个过程,直到整个数组都排好序。
- 稳定性:稳定,因为相同元素的相对顺序不会改变。
- 时间复杂度:O(n²)。
3. 简单选择排序
- 原理:还是以一群人排队为例,每次从剩下的人中选出最矮的人,让他和最前面还没排好序的人的位置交换。重复这个过程,直到所有人都排好序。
- 过程:从数组的第一个元素开始,遍历整个数组,找到最小的元素,然后把它和第一个元素交换位置。接着从第二个元素开始,重复这个过程,直到整个数组都排好序。
- 稳定性:不稳定,因为在交换元素的过程中,相同元素的相对顺序可能会改变。
- 时间复杂度:O(n²)。
4. 希尔排序
- 原理:它是对直接插入排序的一种改进。先将数组分成若干个子数组,对每个子数组进行直接插入排序,这样可以让数组大致有序。然后逐渐缩小子数组的间隔,再次进行插入排序,直到间隔为1,此时整个数组就基本有序了,最后再进行一次直接插入排序就可以完成排序。
- 过程:首先选择一个间隔序列,比如先以数组长度的一半为间隔,将数组分成若干个子数组,对每个子数组进行直接插入排序。然后逐渐缩小间隔,重复这个过程,直到间隔为1。
- 稳定性:不稳定,因为在分组插入排序的过程中,相同元素的相对顺序可能会改变。
- 时间复杂度:O(n^1.3),比直接插入排序的效率要高。
5. 快速排序
- 原理:选择一个基准元素,把数组分成两部分,一部分比基准元素小,另一部分比基准元素大。然后分别对这两部分再进行快速排序,直到整个数组都排好序。就像玩游戏分组一样,先选一个组长,比组长能力弱的人在一组,比组长能力强的人在另一组,然后对这两组再分别选组长,继续分组,直到每个人都在合适的位置。
- 过程:选择一个基准元素,通常选择数组的第一个元素。然后从数组的两端开始,分别找到比基准元素大的元素和比基准元素小的元素,交换它们的位置。重复这个过程,直到左右指针相遇,此时把基准元素放到相遇的位置。接着分别对基准元素左边和右边的子数组进行快速排序。
- 稳定性:不稳定,因为在交换元素的过程中,相同元素的相对顺序可能会改变。
- 时间复杂度:平均为O(nlog₂n),最坏情况下为O(n²)。
6. 堆排序
- 原理:把数组看成一个完全二叉树,通过构建最大堆(每个节点的值都大于或等于其子节点的值),将堆顶元素(最大值)和数组的最后一个元素交换位置,然后把剩下的元素重新调整成最大堆,再把堆顶元素和倒数第二个元素交换位置,以此类推,直到整个数组都排好序。
- 过程:首先构建最大堆,然后将堆顶元素和数组的最后一个元素交换位置,接着把剩下的元素重新调整成最大堆,重复这个过程,直到整个数组都排好序。
- 稳定性:不稳定,因为在交换元素的过程中,相同元素的相对顺序可能会改变。
- 时间复杂度:O(nlog₂n)。
7. 归并排序
- 原理:采用分治法,将一个大数组分成两个小数组,分别对这两个小数组进行排序,然后将排好序的小数组合并成一个大数组。就像把一堆书分成两堆,分别整理好,然后再把这两堆整理好的书合并成一堆。
- 过程:首先将数组不断地二分,直到每个子数组只有一个元素。然后将这些子数组两两合并,每次合并时比较两个子数组的元素,将较小的元素依次放入新的数组中,直到两个子数组都合并完。重复这个过程,直到整个数组都排好序。
- 稳定性:稳定,因为在合并的过程中,相同元素的相对顺序不会改变。
- 时间复杂度:O(nlog₂n)。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#define NUMOFARR(arr) sizeof(arr)/sizeof(arr[0])
void printArr(int arr[], int n)
{
int i;
for (i = 0;i < n;i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int* getDelta(int n, int* count)
{
int* delta;
int i;
*count = 0;
for (i = n / 2;i != 1;i /= 2)
{
(*count)++;
}
(*count)++;
delta = (int*)malloc(*count * sizeof(int));
if (!delta)
{
exit(-1);
}
for (i = 1;i <= *count;i++)
{
delta[i - 1] = (int)(n / pow(2, i));
}
if (delta[*count - 1] != 1)
{
exit(-1);
}
return delta;
}
void insertSort(int data[], int n)
{
int i, j;
int temp;
for (i = 1;i < n;i++)
{
if (data[i] < data[i - 1])
{
temp = data[i];
data[i] = data[i - 1];
for (j = i - 2;j >= 0 && data[j] > temp;j--)
{
data[j + 1] = data[j];
}
data[j + 1] = temp;
}
}
}
void bubbleSort(int data[], int n)
{
int i, j;
int tag = 1;
int temp;
for (i = 1;tag && i < n;i++)
{
tag = 0;
for (j = 0;j < n - i;j++)
{
if (data[j] > data[j + 1])
{
temp = data[j];
data[j] = data[j + 1];
data[j + 1] = temp;
tag = 1;
}
}
}
}
void selectSort(int data[], int n)
{
int i, j, k;
int temp;
for (i = 0;i < n - 1;i++)
{
k = i;
for (j = i + 1;j < n;j++)
{
if (data[j] < data[k])
{
k = j;
}
}
if (k != i)
{
temp = data[i];
data[i] = data[k];
data[k] = temp;
}
}
}
void shellSort(int data[], int n, int delta[], int m)
{
int k, i, j;
int dk;
int temp;
for (i = 0;i < m;i++)
{
dk = delta[i];
for (k = dk;k < n;k++)
{
if (data[k] < data[k - dk])
{
temp = data[k];
for (j = k - dk;j >= 0 && temp < data[j];j -= dk)
{
data[j + dk] = data[j];
}
data[j + dk] = temp;
}
}
}
}
int partition(int data[], int low, int high)
{
int i, j;
int pivot;
pivot = data[low];
i = low;
j = high;
while (i < j)
{
while (i < j && data[j] >= pivot)
{
j--;
}
data[i] = data[j];
while (i < j && data[i] <= pivot)
{
i++;
}
data[j] = data[i];
}
data[i] = pivot;
return i;
}
void quickSort(int data[], int low, int high)
{
if (low < high)
{
int loc = partition(data, low, high);
quickSort(data, low, loc - 1);
quickSort(data, loc + 1, high);
}
}
void adjustDown(int data[], int n, int index)
{
int cur = data[index];
int parent, child;
for (parent = index;(parent * 2 + 1) < n;parent = child)
{
child = parent * 2 + 1;
if ((child + 1) < n && data[child] < data[child + 1])
{
child++;
}
if (cur >= data[child])
{
break;
}
else
{
data[parent] = data[child];
data[child] = cur;
}
}
}
void buildHeap(int data[], int n)
{
int i;
for (i = (n - 2) / 2;i >= 0;i--)
{
adjustDown(data, n, i);
}
}
void heapSort(int data[], int n)
{
int i = n;
int temp;
buildHeap(data, n);
while (i > 0)
{
temp = data[0];
data[0] = data[i - 1];
data[i - 1] = temp;
i--;
adjustDown(data, i, 0);
}
}
void merge(int data[], int low, int mid, int high)
{
int i, p, k;
int* tmp;
tmp = (int*)malloc((high - low + 1) * sizeof(int));
if (!tmp)
{
exit(0);
}
k = 0;
for (i = low, p = mid;i < mid && p <= high;)
{
if (data[i] < data[p])
{
tmp[k++] = data[i++];
}
else
{
tmp[k++] = data[p++];
}
}
while (i < mid)
{
tmp[k++] = data[i++];
}
while (p <= high)
{
tmp[k++] = data[p++];
}
i = low;
p = 0;
while (p < k)
{
data[i++] = tmp[p++];
}
}
void mergeSort(int data[], int s, int t)
{
int m;
if (s < t)
{
m = (s + t) / 2;
mergeSort(data, s, m);
mergeSort(data, m + 1, t);
merge(data, s, m + 1, t);
}
}
int main()
{
int arr[] = { 1,3,5,7,9,2,4,6,8,10,-1,-3,-2,99 };
int test[NUMOFARR(arr)];
int count;
int* delta = getDelta(NUMOFARR(arr), &count);
printf("-----直接插入排序-----\n");
memcpy(test, arr, sizeof(arr));
printArr(test, NUMOFARR(test));
insertSort(test,NUMOFARR(test));
printArr(test, NUMOFARR(test));
printf("\n-----冒泡排序-----\n");
memcpy(test, arr, sizeof(arr));
printArr(test, NUMOFARR(test));
bubbleSort(test, NUMOFARR(test));
printArr(test, NUMOFARR(test));
printf("\n-----简单选择排序-----\n");
memcpy(test, arr, sizeof(arr));
printArr(test, NUMOFARR(test));
selectSort(test, NUMOFARR(test));
printArr(test, NUMOFARR(test));
printf("\n-----希尔排序-----\n");
printArr(delta, count);
memcpy(test, arr, sizeof(arr));
printArr(test, NUMOFARR(test));
shellSort(test, NUMOFARR(test), delta, count);
printArr(test, NUMOFARR(test));
printf("\n-----快速排序-----\n");
memcpy(test, arr, sizeof(arr));
printArr(test, NUMOFARR(test));
quickSort(test, 0, NUMOFARR(test) - 1);
printArr(test, NUMOFARR(test));
printf("\n-----堆排序-----\n");
memcpy(test, arr, sizeof(arr));
printArr(test, NUMOFARR(test));
heapSort(test, NUMOFARR(test));
printArr(test, NUMOFARR(test));
printf("\n-----简单选择排序-----\n");
memcpy(test, arr, sizeof(arr));
printArr(test, NUMOFARR(test));
mergeSort(test, 0, NUMOFARR(test) - 1);
printArr(test, NUMOFARR(test));
free(delta);
delta = NULL;
return 0;
}
如何选择排序方式
数据规模
- 小规模数据(数据量较少,如 n < 100)
- 直接插入排序:实现简单,对于小规模数据,代码简单易懂,性能损失不大。在文件代码中,
insertSort
函数实现了直接插入排序,时间复杂度为 O(n²),但在数据量较小时,其常数因子较小,排序效率尚可。 - 冒泡排序:原理直观,容易理解和实现。文件中的
bubbleSort
函数实现了冒泡排序,时间复杂度也是 O(n²),适合初学者理解排序概念,对于小规模数据能满足基本需求。 - 简单选择排序:同样实现简单,虽然不稳定,但在小规模数据排序中表现良好。文件中的
selectSort
函数实现了简单选择排序,时间复杂度为 O(n²)。
- 中等规模数据(数据量适中,如100 ≤ n < 10000)
- 希尔排序:是对直接插入排序的改进,通过分组插入排序,使数组更快地接近有序状态,时间复杂度为 O(n^{1.3}),在中等规模数据上的性能优于直接插入排序。文件中的
shellSort
函数实现了希尔排序。
- 大规模数据(数据量较大,如 n ≥ 10000)
- 快速排序:平均时间复杂度为 O(nlog₂n),性能在大多数情况下表现出色。文件中的
quickSort
函数实现了快速排序,但在最坏情况下时间复杂度会退化为 O(n²),例如当数据已经有序时。 - 堆排序:时间复杂度稳定在 O(nlog₂n),不依赖数据的初始状态,适合处理大规模数据。文件中的
heapSort
函数实现了堆排序。 - 归并排序:时间复杂度同样为 O(nlog₂n),并且是稳定排序算法,适合对稳定性有要求的大规模数据排序。文件中的
mergeSort
函数实现了归并排序。
数据初始状态
- 数据基本有序
- 直接插入排序:在数据基本有序的情况下,直接插入排序的时间复杂度接近 O(n),效率较高。因为大部分元素已经在正确的位置,只需要少量的插入操作。
- 冒泡排序:如果数据基本有序,冒泡排序在一轮比较中可能不会发生元素交换,此时可以提前结束排序,减少不必要的比较。
- 数据随机分布
- 快速排序:平均性能最优,能够快速地将数据分成两部分并递归排序。但要注意避免最坏情况的发生,可以通过随机选择基准元素等方法进行优化。
- 数据分布极端(如逆序)
- 堆排序:由于其时间复杂度稳定,不受数据初始状态的影响,在数据分布极端的情况下,堆排序的性能优势明显。
稳定性要求
- 需要稳定排序
- 直接插入排序:在排序过程中,相同元素的相对顺序不会改变,是稳定的排序算法。
- 冒泡排序:同样是稳定的排序算法,适合对稳定性有要求的场景。
- 归并排序:也是稳定排序算法,且时间复杂度较低,在需要稳定排序的大规模数据场景中表现出色。
- 对稳定性无要求
- 简单选择排序:在交换元素时可能会改变相同元素的相对顺序,是不稳定的排序算法,但实现简单,性能在小规模数据排序中可以接受。
- 希尔排序:分组插入排序的过程可能会破坏相同元素的相对顺序,是不稳定的排序算法,但在中等规模数据上性能较好。
- 快速排序:平均性能优秀,但在排序过程中是不稳定的。
- 堆排序:也是不稳定的排序算法,但其时间复杂度稳定,适合大规模数据排序。
空间复杂度要求
- 对空间要求严格
- 直接插入排序:只需要常数级的额外空间,空间复杂度为 O(1)。
- 冒泡排序:同样只需要常数级的额外空间,空间复杂度为 O(1)。
- 简单选择排序:空间复杂度也是 O(1),适合对空间要求较高的场景。
- 堆排序:空间复杂度为 O(1),在排序过程中只需要常数级的额外空间。
- 可以接受一定的额外空间开销
- 归并排序:需要额外的 O(n) 空间来合并两个有序子数组。文件中的
merge
函数使用了额外的数组 tmp
来存储合并结果。 - 快速排序:递归调用会使用栈空间,平均空间复杂度为 O(log₂n),最坏情况下为 O(n)。