排序算法
1、排序概念
1. 分类
- 按存储介质:
- 内部排序:数据量不大、数据在内存无需内存外数据交换
- 外部排序:数据量较大,数据在硬盘(文件排序)
- 按比较器个数:
- 串行排序:单处理器(同一时刻比较一个元素)
- 并行排序:多处理器(同一时刻比较多个元素)
- 按主要操作:
- 比较排序:主要操作为比较(如插入排序、交换排序、选择排序等)
- 基数排序:不比较元素的大小,仅根据元素本身取值确定位置
- 按辅助空间:
- 原地排序:空间复杂度O(1)
- 非原地排序:空间复杂度超过O(1)
- 按稳定性:
- 稳定排序:能够使任何数值相等的元素,排序后相对次序不变
- 非稳定排序:不是稳定排序的排序
- 按自然性:
- 自然排序:输入数据越有序,排序的速度越快
- 非自然排序:不是自然排序的排序
2. 数据存储
以顺序表存储待排序的数据
// 定义记录的最大值
#define MAXSIZE 20
// 定义关键字
typedef int KeyType;
// 定义其它数据
typedef struct InfoType
{
int num;
char string[10];
}InfoType;
// 定义每个记录的结构
typedef struct RedType
{
KeyType key;
InfoType otherinfo;
}RedType;
// 定义存储每个记录所用的顺序表
typedef struct SqList
{
RedType arr[MAXSIZE + 1];
int length;
}SqList;
2、插入排序
1. 基本思想
每一步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止
找插入位置 ---> 移动原元素 ---> 插入
2. 直接插入排序
以顺序法查找插入位置
算法思想:
- 复制插入元素
- 记录后移,查找插入位置
void SeqInsertSort(SqList* l)
{
int i = 0;
int j = 0;
for (i = 2; i <= l->length; i++)
{
if (l->arr[i].key < l->arr[i - 1].key)
{
// 记录插入元素
memcpy(l->arr, l->arr + i, sizeof(RedType));
// 移动
memcpy(l->arr + i, l->arr + i - 1, sizeof(RedType));
for (j = i - 2; l->arr[0].key < l->arr[j].key; j--)
memcpy(l->arr + j + 1, l->arr + j, sizeof(RedType));
memcpy(l->arr + j + 1, l->arr, sizeof(RedType));
}
}
}
注:可借助“哨兵arr[0]”
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
3. 折半插入排序
以折半查找法查找插入位置
算法思想同上,折半查找部分见“查找”章节
void BinaryInsertSort(SqList* l)
{
int low = 0;
int high = 0;
int mid = 0;
int i = 2;
for (i = 2; i <= l->length; i++)
{
// 将插入元素存放至哨兵中
memcpy(l->arr, l->arr + i, sizeof(RedType));
low = 1;
high = i - 1;
while (low <= high)
{
mid = (low + high) / 2;
if (l->arr[0].key > l->arr[mid].key)
{
low = mid + 1;
}
else if (l->arr[0].key < l->arr[mid].key)
{
high = mid - 1;
}
else
{
break;
}
} // 循环结束,high + 1为插入位置
// 后移元素
for (int j = i - 1; j >= high + 1; j--)
{
memcpy(l->arr + j + 1, l->arr + j, sizeof(RedType));
}
// 插入元素
memcpy(l->arr + high + 1, l->arr, sizeof(RedType));
}
}
性能分析:
相较于直接插入排序,折半插入排序减少了比较次数,但没有减少移动次数,平均性能由于直接插入排序
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
4. 希尔排序
先将整个待排序的记录分割为若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序
特点:
- 缩小增量
- 多遍插入排序
// 希尔插入排序
void ShellInsert(SqList* l, int dk)
{
int i = 0;
int j = 0;
for (i = dk + 1; i <= l->length; i += dk)
{
if (l->arr[i].key < l->arr[i - dk].key)
{
memcpy(l->arr, l->arr + i, sizeof(RedType));
for (j = i - dk; j > 0 && l->arr[0].key < l->arr[j].key; j -= dk)
{
memcpy(l->arr + j + dk, l->arr + j, sizeof(RedType));
}
memcpy(l->arr + j + dk, l->arr, sizeof(RedType));
}
}
}
// 希尔排序
void ShellSort(SqList* l, int* delta, int len)
{
for (int i = 0; i < len; i++)
{
ShellInsert(l, delta[i]);
}
}
性能分析:
希尔排序的性能与其增量序列有关,暂无定论
空间复杂度:O(1)
稳定性:不稳定
3、交换排序
1. 基本思想
两两比较,若发生逆序则交换,直到所有记录都排好序为止
2. 冒泡排序
每趟不断将记录两两比较,并按照“前小后大”的顺序进行交换
void BubbleSort(SqList* l)
{
RedType tmp = { 0 };
for (int i = l->length - 1; i >= 1; i--)
{
// 定义判断符,以减少交换次数
int flag = 0;
for (int j = 0; j <= i - 1; j++)
{
// 若逆序则交换
if (l->arr[j].key > l->arr[j + 1].key)
{
memcpy(&tmp, l->arr + j, sizeof(RedType));
memcpy(l->arr + j, l->arr + j + 1, sizeof(RedType));
memcpy(l->arr + j + 1, &tmp, sizeof(RedType));
// 若进行交换则改变交换符的值
flag = 1;
}
}
// 若交换符不变则说明后续序列已有序
if (!flag)
break;
}
}
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
3. 快速排序
确定中心元素pivot的位置:小于pivot的元素放在其前面,大于pivot的元素放在其后面
再对每个左表和右表进行上述操作,直到每个子表的元素个数为1
算法思想:
- 取左第一个元素为pivot并存储,并设置两个指针low、high
- 先令arr[high]和pivot比较,若小则将arr[high]赋值给arr[low],并开始从low方向向后比较pivot,若大则赋值给arr[high],再次交换顺序并比较,直到 low == high
- 按2的方法可确定pivot的位置,而被pivot切分的左右子表可再进行查询其自身的中心元素,直到每个子表只剩下一个元素
// 确定pivot元素位置
int Partition(SqList* l, int low, int high)
{
RedType pivot = { 0 };
memcpy(&pivot, l->arr + low, sizeof(RedType));
while (low < high)
{
// high大于pivot继续查找
while (low < high && pivot.key < l->arr[high].key)
high--;
memcpy(l->arr + low, l->arr + high, sizeof(RedType));
// 查询方向调换,low小于pivot继续查找
while (low < high && pivot.key > l->arr[low].key)
low++;
memcpy(l->arr + high, l->arr + low, sizeof(RedType));
}
memcpy(l->arr + low, &pivot, sizeof(RedType));
return low;
}
void QSort(SqList* l, int low, int high)
{
if (low < high)
{
// 确定中间元素
int mid = Partition(l, low, high);
// 左表继续查找
QSort(l, low, mid - 1);
// 右表继续查找
QSort(l, mid + 1, high);
}
else
{
return;
}
}
// 快速排序
void QuickSort(SqList* l)
{
QSort(l, 0, l->length - 1);
}
时间复杂度:O(nlogn)
空间复杂度:O(logn)(递归)
稳定性:不稳定
自然性:非自然
4、选择排序
1. 简单选择排序
在待排序的数据中选出最大(小)的元素放在其最终的位置
void SelectSort(SqList* l)
{
int min = 0;
RedType tmp = { 0 };
for (int i = 1; i <= l->length - 1; i++)
{
// 记录最小值位置
min = i;
// 找到最小值
for (int j = i + 1; j <= l->length; j++)
{
if (l->arr[min].key > l->arr[j].key)
min = j;
}
// 交换
if (min != i)
{
memcpy(&tmp, l->arr + min, sizeof(RedType));
memcpy(l->arr + min, l->arr + i, sizeof(RedType));
memcpy(l->arr + i, &tmp, sizeof(RedType));
}
}
}
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定
2. 堆排序
堆的定义:
- 小根堆:左右孩子大于根的完全二叉树
- 大根堆:左右孩子小于根的完全二叉树
基本思想:
由于堆顶元素大于(小于)左右孩子,故可得到最大值(最小值),再将剩余元素重新组成堆,得到次大(小)值,以此类推,实现排序
1. 堆调整
算法思想(以小根堆为例):
- 输出堆顶元素之后,以堆中的最后一个元素代替之
- 然后将根结点值与左、右子树的根结点值进行比较,并与其中小者进行交换
- 重复上述操作,直至叶子结点,将得到新的堆,称这个堆顶至叶子的调整过程为“筛选”
2. 堆创建
单结点的二叉树是堆;在完全二叉树中所有以叶子结点(序号 i > n/2)为根的子树是堆
只需将序号为n/2,n/2 - 1,...,1的结点为根的子树均调整为堆即可
- 调整从第n/2个元素开始,将以该元素为根的二叉树调整为堆
- 依次往上调整,直到n/2为1时结束
时间复杂度:O(nlogn)
空间复杂度:O(1)
稳定性:不稳定
5、归并排序
基本思想:将两个或两个以上的有序子序列“归并”为一个有序序列
关键问题:如何将两个有序序列合并为一个有序序列?
解决方案:
设置两个指针分别指向两个线性表,比较两指针指向的元素大小,取较大(小)值并向后移动该指针继续比较,直到其中一个指针超出线性表长度,则复制另一个指针的线性表的剩余元素
时间复杂度:O(nlogn)
空间复杂度:O(n)
稳定性:稳定
6、基数排序
基本思想:分配+收集
分配:将关键字k的记录放入第k个箱子
收集:按序号将非空的连接
时间复杂度:O(k * (n + m))(k为关键字个数,m为桶个数)
空间复杂度:O(m + n)
稳定性:稳定