这篇文章总结一下C语言数据结构中常见的几种排序算法。
1.直接插入排序
直接插入排序的算法思想是,
从第二个元素开始,逐个将元素插入到已排序部分。
对于每个待插入元素,从后向前扫描已排序部分,找到合适的位置并插入
void InsertSort(int* a, int n)
{
for (int i = 1; i < n; i++)//默认第一个元素作为第一次的比较序列
{
int end = i - 1;
int temp = a[i];
while (end >= 0)//挨个遍历判断大小
{
if (temp < a[end])//插入
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = temp;
}
}
直接插入排序极端的情况是输入数据完全逆序,这就相当于遍历两遍数组,时间复杂度是O(n^2),这里只用了一个额外的空间因此空间复杂度是O(1),并且这个排序是稳定的排序。
2.希尔排序
希尔排序是基于直接插入排序的优化,在直接插入排序的基础上设置一个增量,根据增量对待排序数组进行分组后插排,当这个增量减至1时排序完成。这极大的优化了插入排序的效率。下面给给出具体实现
//希尔排序
void ShellSort(int* a, int n)
{
int gap = n;
//多组一起排
while (gap > 1)
{
gap /= 2;
//当gap为1时,就为直接插入排序
for (int i = 0; i < n - gap; i++)
{
int end = i;
int temp = a[i + gap];
while (end >= 0)
{
if (temp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = temp;
}
}
}
这里事实上与直接插入排序类似,当gap为1时就是直接插入排序。希尔排序的时间复杂度是一个极为复杂的过程,因为这里的时间是和我们取的增量gap有关的一个函数,在查阅资料后。这里的时间复杂度可以认为是O(N^1.3),空间复杂度也是O(1),但是这个排序是不稳定的排序。
3.选择交换排序
选择交换排序的思想是,从待排序数组中选择最小(或者最大)的元素放在序列的起始位置,直到待排序数组排完。
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
while (left < right)
{
int mini = left;
int maxi = left;
//第一次遍历拿到最小和最大的下标
for (int i = left + 1; i <= right; i++)
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
//交换
Swap(&a[mini], &a[left]);
if (maxi == left)
{
maxi = mini;
}
Swap(&a[maxi], &a[right]);
left++;
right--;
}
}
选择排序的时间复杂度是O(N^2),空间复杂度是O(1)这个排序的思想很好理解,但是效率很低,因为无论数组是否有序都要遍历。这是一种不稳定的排序。
4.冒泡排序
冒泡排序的思想是,遍历数组,只要后面的元素符合排序的大小关系就交换。但是这里也可以给出优化,设置一个标记,只要发现没有产生交换那么这个数组就有序,直接跳出循环。
void BubbleSort(int* a, int n)
{
for (size_t j = 0; j < n; j++)
{
int flag = 0;//设置标记
for (size_t i = 1; i < n - j; i++)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
flag = 1;
}
}
if (flag == 0)//没有发生交换说明有序,跳出循环
{
break;
}
}
}
很容易想到的是,最坏的情况就是一直要遍历到最后一组数据交换,那么时间复杂度就是O(N^2),空间复杂度就是O(1),这是一种稳定的排序。
5.快速排序
快速排序的思想是,选取一个待排序数组中的元素作为基准值,然后根据这个基准值将待排序数组分为两部分,左边是小于这个基准值的数组,右边是大于这个基准值的数组,然后左右数组重复这个过程,直到所有元素在对应的位置上。
一般情况下这个基准值选取的是首元素,但是可以想象的到的是,如果这个元素恰好是最大或者最小的时候,那么此时有一边数组势必要浪费大量的时间。那么有一种解决方案是,在开始排序之前,先从数组中找到三个元素取中间值交换到第一个位置上。这能有效避免最差的情况发生,同时为了简便,这三个数这里选择数组首元素、尾元素和中间元素。
这里有一点是值得注意的,当选取左侧首元素做基准元素后,那就要先从对侧开始遍历。这是为了保证分区的正确性。
例如[3, 1, 2, 5, 4]这个数组,选3为基准元素。
首先从右侧开始找小于3的元素是2,然后右侧停下,从左侧开始找大于3的元素,但是不能发生交叉,那么就会在2处相遇这时候就代表着这一趟已经确定了左边小于3,右边大于3的元素位置,这时候交换3与2的位置,数组变为[2,1,3,5,4]。可以看到这是符合要求的
那么如果从同侧开始会有什么问题呢?从左侧开始找大于3的元素,找到5这个位置,然后从右侧开始找小于3的元素,但是在5这个位置相遇,交换5和3,这时候数组变为[5,1,2,3,4]。可以看到这里分区出现了错误,不符合要求。
快速排序的时间复杂度是O(N*logN),空间复杂度是O(logN),这是一种不稳定的排序
void QuickSort1(int* a, int left,int right)
{
if (left >= right)
{
return;
}
int begin = left;
int end = right;
//三数取中
int midi = GetMidNumi(a, left, right);
Swap(&a[left], &a[midi]);
//一趟
int keyi = left;
while (begin < end)
{
//先走右边,这里需要注意begin和end走的时候不能越界,而且需要保证小于的关系不能发生交叉
while (begin < end && a[keyi] <= a[end])
{
end--;
}
//后走左边,这里与上面同理
while (begin < end && a[keyi] >= a[begin])
{
begin++;
}
Swap(&a[begin], &a[end]);
}
Swap(&a[keyi], &a[begin]);
QuickSort1(a, left, begin - 1);
QuickSort1(a, begin + 1, right);
}
6.归并排序
归并排序的算法思想也是分治的思想,将一个序列分为子序列,使得每个子序列有序,之后在合并这些子序列再使合并后的序列有序这个最后就更归并到整个序列。
void _MergeSort(int* a,int left,int right,int* temp)
{
if (left >= right)
{
return;
}
int mid = (left + right) / 2;
//递归
_MergeSort(a, left, mid, temp);
_MergeSort(a, mid + 1,right,temp);
int begin1 = left,end1 = mid;
int begin2 = mid + 1,end2 = right;
int i = left;
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++];
}
//当递归右边时,区间不是从0开始的需要加上left
memcpy(a + left, temp + left, sizeof(int) * (right - left + 1));
}
void MergeSort(int* a, int n)
{
int* temp = (int*)malloc(sizeof(int) * n);
if (temp == NULL)
{
perror("malloc");
return;
}
_MergeSort(a,0,n-1,temp);
free(temp);
temp = NULL;
}