文章介绍了插入排序、希尔排序、选择排序、堆排序、冒泡排序、归并排序的实现思路与使用c编写的代码,同时对排序算法的三个评价要素:时间复杂度、空间复杂度、稳定性,分别进行了具体分析。

1、插入排序

实现思想:确定一个有序的数组,将后续的元素逐一插入此有序数组,确定其相对位置,直到所有元素插入完成; 代码如下:

void InsertSort(int* a, int n)
{
	for (int i = 1; i < n; i++)
	{
		int tmp = a[i];
		int end = i-1;
		// 将有序元素与待插入元素比较,确定其最终插入位置
		while (end >= 0)
		{
			// 若之前的元素大于待插入元素,则将其后移
			if (a[end] > tmp)
				a[end + 1] = a[end--];
			// 否则跳出循环
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

容易发现,当数组越接近有序,元素之间的比较与交换越少,时间效率越高;越接近倒序,发生的交换与比较越多,时间效率越低; 时间复杂度:$O(n^{2})$;第i个元素需要与前$i-1$个元素比较; 空间复杂度:$O(1)$; 稳定性:稳定;

2、希尔排序

实现思想:希尔排序是对插入排序的优化。将原数组的元素按照某个间隔$gap$划分为一组,同组的元素进行插入排序,$gap>1$时,实际上是对数组中的元素进行预排序,从而减少元素之间的比较和交换。   以升序为例,假设有10个元素,最大的元素恰巧在序列头部,此时采用插入排序,确定其最后位置时,需要与剩下的每个元素都进行比较,也就是9次;而采用希尔排序,若$gap=2$,此时需要比较预排序($gap=2$)时的4次,加上最后进行排序时($gap=1$)时的1次,共5次;   代码如下:

void ShellSort(int* a, int n)
{
	int gap = n / 2;
	while (gap > 0)
	{
		// i++时就进行了不同分组的切换
		for (int i = gap; i < n; i++)
		{
			// 在其内部仍然是插入排序的逻辑
			int tmp = a[i];
			int end = i - gap;
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
					break;
			}
			a[end + gap] = tmp;
		}
		// gap慢慢减小到1
		gap /= 2;
	}
}

时间复杂度:$O(nlogn)$; 空间复杂度:$O(1)$; 稳定性:不稳定;当两个相同的元素存在在不同组时,在预排序阶段它们的先后顺序可能会产生变化;

3、选择排序

实现思想:以升序为例,每次找剩余序列中最小的元素,将其插入最终位置; 代码如下:

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
void SelectSort(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		int min = i;
		// 找到后续元素中最小的元素位置
		for (int j = i + 1; j < n; j++)
		{
			if (a[j] <= a[min])
			{
				min = j;
			}
		}
		// 交换
		Swap(&a[i], &a[min]);

时间复杂度:$O(n^2)$; 空间复杂度:$O(1)$; 稳定性:不稳定;进行元素交换时,会变换元素顺序,从而导致不稳定,例如{5,5,1};当5和1进行交换时,两个5的先后顺序已发生变化;

4、堆排序

实现思想:根据堆的性质,升序建大堆,每次选出最大的数,随后调整堆,让次大的数浮到堆顶,依次类推;降序建小堆,原理相同; 代码如下:

// 向下调整成大根堆,前提:左右子树都是堆
void AdjustDwon(int* a, int n, int root)
{
	int child = root * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child])
		{
			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);
	}
	int end = n - 1;
	// 开始排序,每次出一个元素,随后调整剩下的元素
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDwon(a, end, 0);
		end--;
	}
}

时间复杂度:$O(nlogn)$;每次调整的复杂度为$logn$,共需调整n次;建堆的时间消耗小于排序时的消耗,根据时间复杂度的计算规则,略去; 空间复杂度:$O(1)$;原地调整堆排序即可,无需更多空间; 稳定性:不稳定;在调整堆再到排序的过程中,原在前的数可能会排序到后方,例如21 20a 20b 12 11 8 7,堆排序后为7 8 11 12 20a 20b 21(其中a,b仅为区分顺序)。

5、冒泡排序

实现思想:每趟排序都会将最大(小)的数据放到队尾,临近的两个数相比,将大(小)的数继续往后比,比较完成也就选出了最大(小)的元素;如此继续,经过n-1趟排序,序列便会有序; 增加优化:当一趟排序已经不发生交换时,此时序列已经有序,不用在进行多余比较,可防止元素有序后仍进行比较的时间消耗; 代码:

void BubbleSort(int* a, int n)
{
	for (int i = n - 1; i >= 0; i--)
	{
		int exchange = 0;
		for (int j = 0; j < i; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
				exchange = 1;
			}
		}
		// 一趟排序中,未发生交换,表明元素已有序,无须继续
		if (exchange == 0)
		{
			break;
		}
	}
}

时间复杂度:$O(n^2)$;共需要排序$n-1$趟,每次需要比较$n-i$次,其中i为排序的趟数; 空间复杂度:$O(1)$;原地排序,无需更多空间; 稳定性:稳定;当元素大小相同时,可不发生交换;

6、归并排序

实现思想:将本来无序的序列划组排序,随着组内元素的增多,最终有序;如下图: image.png 代码:

void MergeSort(int* a, int begin, int end)
{
	int n = end - begin + 1;
	int* tmp = (int*)malloc(sizeof(int) * n);
	int gap = 1;
	while (gap < n)
	{
		// 两两归并
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			// 第二组不存在
			if (begin2 >= n)
				break;
			// 第二组结尾越界,进行修正
			if (end2 >= n)
				end2 = n - 1;
			// 测试在归并的区间
			// printf("[%d,%d][%d,%d]", begin1, end1, begin2, end2);
			// 将归并结果放到tmp数组中
			int cur = 0; 
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
					tmp[i + cur++] = a[begin1++];
				else
					tmp[i + cur++] = a[begin2++];
			}
			while (begin1 <= end1)
			{
				tmp[i + cur++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[i + cur++] = a[begin2++];
			}
			// 将归并好的数据拷贝回原数组
			memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int));
		}
		// printf("\n");
		gap *= 2;
	}
}

时间复杂度:$O(nlogn)$;共需$logn$层,每层都需访问n个元素; 空间复杂度:$O(n)$;需建立一个与原序列相同大小的空间,用于归并数据; 稳定性:稳定;比较排序都发生在相邻的空间,因此不会影响序列的稳定性。