八大排序
一、复杂度与稳定性
1、时间复杂度
1.希尔排序
《数据结构(C语言版)》— 严蔚敏《数据结构-用面相对象方法与C++描述》— 殷人昆
希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在很多书中给出的希尔排序的时间复杂度都不固定。对于本文给出的希尔排序代码,由于gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,可以按照O(n1.25)—O(1.6 x n1.25)来算
2、堆排序、归并排序、快速排序
- 堆排序:直接使用二叉堆(完全二叉树)的数据结构,通过数组来表示和操作。
- 归并排序:虽然不直接使用二叉树,但其递归过程可以被看作是一个递归树。
- 快速排序:同样不直接使用二叉树,但其递归过程也可以被看作是一个递归树。
这三种排序算法都采用了分治法的思想,但只有堆排序直接使用了二叉树(二叉堆)的数据结构。归并排序和快速排序的递归过程可以被抽象为递归树,但它们的实际实现并不依赖于显式的二叉树结构。
一般情况下这三种排序的时间复杂度都为二叉树的深度。
对于快速排序存在最坏情形,为正序或逆序排列,二叉树画出来应该是一棵斜树,并且需要经过n-1次递归调用才能完成,且第i次划分需要经过n‐i次关键字的比较才能找到第i个记录,也就是枢轴的位置,所以最终的时间复杂度应该O(n2)、空间复杂度为O(n)
3、稳定性
在排序算法中,稳定性指的是相等元素的相对顺序是否在排序后保持不变。简单来说,如果排序前两个相等的元素在数组中的相对位置是固定的,排序后它们的位置依然不变,那么这种算法就是稳定排序;如果排序后位置可能会变化,那就是不稳定排序。
堆排序、快速排序、希尔排序、直接选择排序是不稳定的排序算法,而基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。
1.冒泡排序
**冒泡排序就是把小的元素往前调或者把大的元素往后调。**比较是相邻的两个元素比较,**交换也发生在这两个元素之间。**所以,如果两个元素相等,不会发生交换;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
2.选择排序
**选择排序是给每个位置选择当前元素最小的,**比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n - 1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。
举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
3.插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
4.快速排序
选择第一个元素作为基准值。使用两个指针 i 和 j,分别从数组的两端向中间移动。
i 从左向右移动,直到找到一个大于基准值的元素。j 从右向左移动,直到找到一个小于基准值的元素。交换 i 和 j 位置的元素,继续移动指针,直到 i 和 j 交叉。
最后将基准值与 i 位置的元素交换。在数据交换时很容易出现不稳定的情况。
举个例子,比如序列为6 7 10 7 1 3 7 3 9 4 2,取6为key值进行第一趟交换后则出现不稳定,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。
5.归并排序
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
6.希尔排序(shell)
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。
8.堆排序
我们知道堆的结构是节点i的孩子为2 * i和2 * i + 1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, … 1这些个父节点选择元素时,就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
二、数据敏感与算法分类
1、归并排序、选择排序、堆排序
归并排序、选择排序和堆排序对数据不敏感,主要是因为它们的时间复杂度在不同情况下保持相对稳定,不受输入数据的初始状态(如是否已经部分排序或完全逆序)的影响。
冒泡排序、快速排序、插入排序和希尔排序对数据的初始状态较为敏感,这意味着它们的时间复杂度会因输入数据的不同而变化。以下是每种排序算法的具体分析:
2、 冒泡排序、快速排序、插入排序、希尔排序
1.冒泡排序
- 最好情况:如果数组已经排序,冒泡排序只需要一次遍历即可完成,时间复杂度为 O(n)。
- 最坏情况:如果数组完全逆序,每次遍历都需要进行 n-1 次比较和交换,时间复杂度为 O(n^2)。
2. 快速排序
- 最好情况:当每次选择的基准值都能将数组均匀分成两部分时,时间复杂度为 O(n* logn)。
- 最坏情况:当每次选择的基准值都是最小或最大值时,时间复杂度为 O(n2)。这种情况通常发生在已经排序或逆序的数组上。
3. 插入排序
- 最好情况:如果数组已经排序,插入排序只需要一次遍历即可完成,时间复杂度为 O(n)。
- 最坏情况:如果数组完全逆序,每次插入操作都需要进行 n-1 次比较和移动,时间复杂度为 O(n2)。
4. 希尔排序
- 最好情况:如果选择合适的增量序列,希尔排序的时间复杂度可以接近 O(n log n)。
- 最坏情况:如果选择不合适的增量序列,希尔排序的时间复杂度可能退化为 O(n^2)。
这些排序算法的时间复杂度会因输入数据的初始状态而变化,因此对数据较为敏感。选择合适的排序算法需要根据具体的应用需求和数据特性来决定。
三、总结
四、代码合集
sort.c
include"sort.h"
//交换
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
// 插入排序
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else break;
}
a[end + 1] = tmp;
}
}
// 希尔排序
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 2;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else break;
}
a[end + gap] = tmp;
}
}
}
// 简单选择排序
void SelectSort(int* a, int n)
{
int head = 0;
int end = n - 1;
while (head < end)
{
int mini = head;
int maxi = end;
for (int i = head; i <= end; i++)
{
if (a[i] > a[maxi])
maxi = i;
if (a[i] < a[mini])
mini = i;
}
Swap(&a[head], &a[mini]);
if (maxi == head)
maxi = mini;
Swap(&a[end], &a[maxi]);
head++;
end--;
}
}
// 堆排序
void AdjustDwon(int* a, int n, int root)
{
int child = root * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
child++;
if (a[child] > a[root])
{
Swap(&a[child], &a[root]);
root = child;
child = root * 2 + 1;
}
else break;
}
}
void HeapSort(int* a, int n)
{
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDwon(a, n, i);
}
for (int i = n - 1; i > 0; i--)
{
Swap(&a[0], &a[i]);
AdjustDwon(a, i, 0);
}
}
// 冒泡排序
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
if (a[j + 1] < a[j])
Swap(&a[j + 1], &a[j]);
}
}
}
// 快速排序递归实现
int GetMidIndex(int* a, int left, int right)
{
if (right - left == 0)
return left;
int mid = left + (rand() % (right - left));
if (a[left] < a[mid])
{
if (a[mid] < a[right])
return mid;
else if (a[right] > a[left])
return right;
else return left;
}
//a[left] > a[mid]
else
{
if (a[left] < a[right])
return left;
else if (a[mid] > a[right])
return mid;
else return right;
}
}
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
int keyi = GetMidIndex(a,left,right);
Swap(&a[left], &a[keyi]);
keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])
{
right--;
}
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int hole = GetMidIndex(a, left, right);
Swap(&a[hole], &a[left]);
int key = a[left];
hole = left;
while (left < right)
{
while (left < right && a[right] >= key)
{
right--;
}
Swap(&a[hole], &a[right]);
hole = right;
while (left < right && a[left] <= key)
{
left++;
}
Swap(&a[hole], &a[left]);
hole = left;
}
a[hole] = key;
return hole;
}
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int prev = GetMidIndex(a, left, right);
Swap(&a[left], &a[prev]);
prev = left;
int cur = prev + 1;
int key = a[left];
while (cur <= right)
{
if (a[cur] < key && prev++ != cur)
{
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[prev], &a[left]);
return prev;
}
//快速排序三路划分法
//int PartSort4(int* a, int left, int right);
void QuickSort_v1(int* a, int left, int right)
{
if (left >= right)
return;
int key = PartSort1(a, left, right);
QuickSort_v1(a, left, key - 1);
QuickSort_v1(a, key + 1, right);
}
void QuickSort_v2(int* a, int left, int right)
{
if (left >= right)
return;
int key = PartSort2(a, left, right);
QuickSort_v2(a, left, key - 1);
QuickSort_v2(a, key + 1, right);
}
void QuickSort_v3(int* a, int left, int right)
{
if (left >= right)
return;
int key = PartSort3(a, left, right);
QuickSort_v3(a, left, key - 1);
QuickSort_v3(a, key + 1, right);
}
//快速排序三路划分法
void QuickSort_v4(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left;
int end = right;
int cur = left + 1;
int key = GetMidIndex(a, left, right);
Swap(&a[left], &a[key]);
key = a[left];
while (cur <= right)
{
if (a[cur] < key)
{
Swap(&a[cur], &a[left]);
cur++;
left++;
}
else if (a[cur] > key)
{
Swap(&a[cur], &a[right]);
right--;
}
else cur++;
}
QuickSort_v4(a, begin, left - 1);
QuickSort_v4(a, right + 1, end);
}
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
Stack St;
StackInit(&St);
StackPush(&St, right);
StackPush(&St, left);
while (!StackEmpty(&St))
{
int begin = StackTop(&St);
StackPop(&St);
int end = StackTop(&St);
StackPop(&St);
int mid = PartSort1(a, begin, end);
if (mid < end - 1)
{
StackPush(&St, end);
StackPush(&St, mid + 1);
}
if (mid > begin + 1)
{
StackPush(&St, mid - 1);
StackPush(&St, begin);
}
}
StackDestroy(&St);
}
// 归并排序递归实现
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
_MergeSort(a, tmp, 0, n - 1);
free(tmp);
}
void _MergeSort(int* a, int* tmp, int begin, int end)
{
if (begin >= end)
return;
int mid = (end + begin) / 2;
_MergeSort(a, tmp, begin, mid );
_MergeSort(a, tmp, 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])
{
tmp[i++] = a[begin2++];
}
else
{
tmp[i++] = a[begin1++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
int j = 0;
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = begin1 + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
if (begin2 > n - 1 || end1 > n - 1)
break;
if (end2 >= n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] > a[begin2])
{
tmp[j++] = a[begin2++];
}
else
{
tmp[j++] = a[begin1++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
free(tmp);
}
// 计数排序
void CountSort(int* a, int n)
{
int max = a[0];
int 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));
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] != 0)
{
a[j] = i + min;
j++;
tmp[i]--;
}
}
free(tmp);
}
sort.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
#include"Stack.h"
//#include"Stack.h"
//交换
void Swap(int* a, int* b);
// 插入排序
void InsertSort(int* a, int n);
// 希尔排序
void ShellSort(int* a, int n);
// 选择排序
void SelectSort(int* a, int n);
// 堆排序
void AdjustDwon(int* a, int n, int root);
void HeapSort(int* a, int n);
// 冒泡排序
void BubbleSort(int* a, int n);
// 快速排序递归实现
//三数取中
int GetMidIndex(int* a, int left, int right);
// 快速排序hoare版本
int PartSort1(int* a, int left, int right);
// 快速排序挖坑法
int PartSort2(int* a, int left, int right);
// 快速排序前后指针法
int PartSort3(int* a, int left, int right);
//快速排序三路划分法
//int PartSort4(int* a, int left, int right);
void QuickSort_v1(int* a, int left, int right);
void QuickSort_v2(int* a, int left, int right);
void QuickSort_v3(int* a, int left, int right);
//快速排序三路划分法
void QuickSort_v4(int* a, int left, int right);
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right);
// 归并排序递归实现
void MergeSort(int* a, int n);
void _MergeSort(int* a, int* tmp, int begin, int end);
// 归并排序非递归实现
void MergeSortNonR(int* a, int n);
// 计数排序
void CountSort(int* a, int n);
void Arrayprint(int* a, int n);
stack.c
#include"Stack.h"
// 初始化栈
void StackInit(Stack* ps)
{
ps->a = NULL;
ps->capacity = 0;
ps->top = 0;
}
// 入栈
void StackPush(Stack* ps, STDataType data)
{
if (ps->capacity == ps->top)
{
int Newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, Newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity = Newcapacity;
}
ps->a[ps->top] = data;
ps->top++;
}
// 出栈
void StackPop(Stack* ps)
{
assert(ps);
assert(!StackEmpty(ps));
ps->top--;
}
// 获取栈顶元素
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(!StackEmpty(ps));
return ps->a[ps->top - 1];
}
// 获取栈中有效元素个数
int StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
int StackEmpty(Stack* ps)
{
assert(ps);
return ps->top == 0;
}
// 销毁栈
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top; // 栈顶
int capacity; // 容量
}Stack;
// 初始化栈
void StackInit(Stack* ps);
// 入栈
void StackPush(Stack* ps, STDataType data);
// 出栈
void StackPop(Stack* ps);
// 获取栈顶元素
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
int StackEmpty(Stack* ps);
// 销毁栈
void StackDestroy(Stack* ps);