排序算法

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. 直接插入排序

以顺序法查找插入位置

算法思想:

  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

算法思想:

  1. 取左第一个元素为pivot并存储,并设置两个指针low、high
  2. 先令arr[high]和pivot比较,若小则将arr[high]赋值给arr[low],并开始从low方向向后比较pivot,若大则赋值给arr[high],再次交换顺序并比较,直到 low == high
  3. 按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. 堆调整

算法思想(以小根堆为例):

  1. 输出堆顶元素之后,以堆中的最后一个元素代替之
  2. 然后将根结点值与左、右子树的根结点值进行比较,并与其中小者进行交换
  3. 重复上述操作,直至叶子结点,将得到新的堆,称这个堆顶至叶子的调整过程为“筛选
2. 堆创建

单结点的二叉树是堆;在完全二叉树中所有以叶子结点(序号 i > n/2)为根的子树是堆

只需将序号为n/2,n/2 - 1,...,1的结点为根的子树均调整为堆即可

  1. 调整从第n/2个元素开始,将以该元素为根的二叉树调整为堆
  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)

稳定性:稳定