C语言八大排序

排序

        排序就是将一堆杂乱无章的数据,按照每个元素的大小,以递增或者递减的形式排列数据。

        稳定性:如果多个相同的元素,排序完,它们的前后顺序保持不变,则这个算法是稳定的;如果它们的前后顺序不一致,算法是不稳定。

        本次博客中算法都是排序升序的。

插入排序

        每次将一个新的元素插入到已经有序的序列中,直到全部元素插入完就排序好了。

        第一趟插入,从第二个数开始,5比3大,直接插入。

        第二趟,4和5比,4比5小,第三个位置的值赋值为5,4比3大,直接插入。

// 插入排序
void InsertSort(int* a, int size)
{
	// size个数据排序size-1次
	for (int i = 0; i < size - 1; ++i)
	{
		// 下标0到end有序,从下标为end数据开始比较
		int end = i;
		// end位置的下个数据
		int t = a[end + 1];

		// 当比较完下标为0的第一个元素结束
		while (end >= 0)
		{
			// 如果比插入数据小,覆盖
			if (t < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			// 比插入数据大,跳出循环插入数据
			else
			{
				break;
			}
		}

		// t是最小的时候,end为-1,循环里不插入数据,统一在循环外面插入数据
		a[end + 1] = t;
	}
}

        时间复杂度:O(N^2)。

        空间复杂度:O(1)。

        稳定性:稳定。

        元素集合越接近有序,使用插入排序算法效率越高。

希尔排序

        希尔排序是建立在插入排序的基础上的。多次将元素集合进行分组,间距为gap的为一组,每组的元素先自行排序。

        gap越大的时候,大的元素更快到后面,小的元素更快到前面,但是越不接近有序;gap越小的时候,大的元素更慢到后面,小的元素更慢到前面,但是越接近有序。

        多次预排序,让元素更快的排在有序位置的附近,最后gap为1进行插入排序,完成排序。

int gap = size;
gap = gap / 3 + 1;

        gap是一个变化的值,并且要与元素集合个数有关,+1是为了一定会有gap为1,当gap为1排序完结束。 这里gap是3,相同颜色的为一组。

        预排序。

        gap为1。

        插入排序

// 希尔排序
void ShellSort(int* a, int size)
{
	int gap = size;

	// gap为1排序完成
	while (gap > 1)
	{
		// 分组
		gap = gap / 3 + 1;
		// size个数据比较size-gap次
		for (int i = 0; i < size - gap; ++i)
		{
			// 该组有序的最后一个数据
			int end = i;
			// 同一个组的下个数据
			int t = a[end + gap];

			// 当end小于0的时候结束
			while (end >= 0)
			{
				// 如果比插入数据小,覆盖
				if (t < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				// 比插入数据大,跳出循环
				else
				{
					break;
				}
			}

			// 插入数据
			a[end + gap] = t;
		}
	}
}

        不建议直接将gap分组的直接排序好,会有嵌套三层循环,每次每组插入一个。整体思路和插入排序一样,把-1的地方换为-gap,要理解为什么是这样。

        时间复杂度:O(N*log3(N))。(N乘以3为底N的对数)。希尔排序的时间复杂度计算很多种,是一个复杂的问题,可以查阅一下。

        空间复杂度:O(1)。

        稳定性:不稳定。 

选择排序

        在待排序元素集合中,每次找出一个最小或最大的元素,和待排序元素集合的第一个元素交换数据,直到全部元素排序完。

        第一趟找待排序元素集合中最小的元素0,和下标为0的元素交换数据。

        第二趟找待排序元素集合中最小的元素1,和下标为1的元素交换数据。

void Swap(int* x, int* y)
{
	int t = *x;
	*x = *y;
	*y = t;
}

// 选择排序
void SelectSort(int* a, int size)
{
	// size个元素排序size-1次
	for (int i = 0; i < size - 1; ++i)
	{
		// 从i开始,i前面的已经排序好
		int mini = i;

		// 找待排序元素集合中最小元素的下标
		for (int j = i + 1; j < size; ++j)
		{
			if (a[j] < a[mini])
				mini = j;
		}

		// 交换数据
		Swap(&a[i], &a[mini]);
	}
}

        时间复杂度:O(N^2)。

        空间复杂度:O(1)。

        稳定性:不稳定。

        选择排序效率非常的低,即使待排序元素集合已经有序,时间复杂度也是O(N^2)。

堆排序

        利用堆的性质,从堆顶中选数,大堆的堆顶数据是堆里面最大的,小堆的堆顶数据是堆里面最小的。每次将堆顶的数据和堆的最后一个数据交换,排序好最后一个位置,重复操作,直到堆只剩下一个数据。

// 向下调整算法,排升序建大堆
void AdjustDown(int* a, int size, int parent)
{
	int child = parent * 2 + 1;
	// 当没有孩子节点的时候,结束
	while (child < size)
	{
		// 找到左右孩子中数据小的那个,先判断是否有右孩子
		if (child + 1 < size && a[child + 1] > a[child])
			++child;

		// 如果孩子节点数据小于父节点数据,交换数据
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		// 孩子节点数据大于等于父节点数据, 结束
		else
		{
			break;
		}
	}
}

// 堆排序
void HeapSort(int* a, int size)
{
	// 建堆
	for (int i = (size - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, size, i);
	}

	// 选堆顶数据排序到最后位置
	int end = size - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

        时间复杂度:O(N*log(N))。

        空间复杂度:O(1)。

        稳定性:不稳定。

冒泡排序

        依次比较两个相邻元素的大小,如果前面的元素比较大,交换数据,一趟排序排好一个位置,将最大的元素或者最小的元素交换到待排序元素结合的最后一个。

// 冒泡排序
void BubbleSort(int* a, int size)
{
	// sz个元素需要排序sz-1趟
	for (int i = 0; i < size - 1; ++i)
	{
		bool flag = true;
		// sz个数,排序sz-1次,每次排序一个数,下一趟少排一个数
		for (int j = 0; j < size - 1 - i; ++j)
		{
			// 前一个比后一个,如果前一个大,交换数据
			if (a[j] > a[j + 1])
			{
				flag = false;
				int tmp = a[j];
				a[j] = a[j + 1];
				a[j + 1] = tmp;
			}
		}

		// 如果没有进行交换,已经有序
		if (flag)
			break;
	}
}

        时间复杂度:O(N^2)。

        空间复杂度:O(1)。

        稳定性:稳定。

快速排序

        任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。核心思想:交换数据。

        快排有三种实现形式:hoare版本、挖坑法、前后指针法

hoare版本

        先选出一个key,一般这个基准值是待排序元素集合的第一个元素(最左边)或者最后一个元素(最右边)。这里选第一个元素做key,用一个变量记录key的下标(因为要交换数据,所以记录的下标)。

        定义left是待排序元素集合的第一个元素的下标,定义right是待排序元素集合的最后一个元素下标。左边做key,建议要右边先找(右边先找的好处是,左右相等的位置就是key有序的存储位置)。

        排升序,右边找比key值小的元素,左边找比key值大的元素,交换两个元素,重复执行,结束条件是左和右同一个下标,即左等于右,结束后要将key的元素和结束位置的下标的元素交换,一趟排序结束。

        一趟排序后key所在的位置就是排序好的位置,因为左边的数据都小于等于key,右边的数据都大于等于key。递归排序左区间和右区间,递归结束条件左大于等于右。

        右边找小,左边找大,交换数据。

         右边找小,左边找大,交换数据。

        右边找小,左边找大,左右相等,结束,交换keyi下标的元素和相遇位置下标的元素,将keyi的位置更新到left位置,递归左区间和右区间。

        左区间递归。

        左边做key,右边找小,左边找大。

        左等于右结束,交换keyi位置的数据和相遇位置下标的元素,更新keyi为left。继续递归左区间和右区间。

// hoare版本
void Hoare(int* a, int begin, int end)
{
	// 如果该区间不存在或者只有一个数据,不需要排序
	if (begin >= end)
		return;

	// 左边做key
	int keyi = begin;

	int left = begin;
	int right = end;

	// 单趟排序,left等于right结束
	while (left < right)
	{
		// 找小
		while (left < right && a[right] >= a[keyi])
			--right;

		// 找大
		while (left < right && a[left] <= a[keyi])
			++left;

		// 交换数据
		Swap(&a[left], &a[right]);
	}

	// left的位置就是key排序好的位置
	Swap(&a[keyi], &a[left]);
	// key的位置变为left
	keyi = left;

	// 递归左区间和右区间
	Hoare(a, begin, keyi - 1);
	Hoare(a, keyi + 1, end);
}

        找小和找大需要注意是大于等于和小于等于。如果大于和小于,排序的时候,第一个元素和最后一个元素相等的时候,会死循环,不会改变left和right。

// 找小
while (left < right && a[right] >= a[keyi])

// 找大
while (left < right && a[left] <= a[keyi])

挖坑法

        先选出一个key,待排序元素集合的第一个元素做key,记录key的值和下标。定义left是待排序元素集合的第一个元素的下标,定义right是待排序元素集合的最后一个元素下标。左边做key,建议要右边先找(右边先找的好处是,左右相等的位置就是key有序的存储位置)。

        排升序,右边找比key值小的元素,找到后将该元素的值赋值到keyi的位置,更新keyi的位置;左边找比key值大的元素,找到后将该元素的值赋值到keyi的位置,更新keyi的位置;结束条件是左等于右,相遇的位置就是keyi的位置,将key值存储到keyi位置。

        右边找小,将找到的值赋值到keyi位置,更新keyi为right。

        左边找大,将找到的值赋值到keyi位置,更新keyi为left。

        重复执行,左等于右结束,相遇的位置就是keyi的位置,将key存储到keyi位置。一趟排序结束。

        一趟排序后key所在的位置就是排序好的位置,因为左边的数据都小于等于key,右边的数据都大于等于key。递归排序左区间和右区间,递归结束条件左大于等于右。

// 挖坑法
void Pit(int* a, int begin, int end)
{
	// 递归结束条件
	if (begin >= end)
		return;

	// 记录坑的位置和坑的值
	int keyi = begin;
	int key = a[keyi];

	int left = begin;
	int right = end;

	// 单趟排序,左等于右结束
	while (left < right)
	{
		// 找小
		while (left < right && a[right] >= a[keyi])
			--right;
		
		// 将小的值赋值给坑位置
		a[keyi] = a[right];
		// 小的值的位置变成坑
		keyi = right;

		// 找大
		while (left < right && a[left] <= a[keyi])
			++left;

		// 将大的值赋值给坑位置
		a[keyi] = a[left];
		// 大的值的位置变为坑
		keyi = left;
	}

	// 把坑的值存储到坑位置,这个位置就是有序的位置
	a[keyi] = key;

	Pit(a, begin, keyi - 1);
	Pit(a, keyi + 1, end);
}

前后指针法

        先选出一个key,待排序元素集合的第一个元素做key,记录key的值和下标。定义slow是待排序元素集合的第一个元素的下标,slow记录的是比key小的最后一个位置,定义fast是待排序元素集合的第二个元素的下标,fast找比key小的数据。

        fast位置的数据如果遇到比key小,交换到slow的下一个位置,slow自加1,当fast找完全部数据结束,将key的数据和slow位置上的数据交换,更新keyi。

        一趟排序后key所在的位置就是排序好的位置,因为左边的数据都小于key,右边的数据都大于等于key。递归排序左区间和右区间,递归结束条件左大于等于右。

         fast遇到比key小的,slow自加一,交换slow位置和fast位置的值。

        fast遇到比key小的,slow自加一,交换slow位置和fast位置的值。

 

         fast遇到比key小的,slow自加一,交换slow位置和fast位置的值。

        fast走完全部元素,交换keyi位置和slow位置的数据。

        一趟排序后key所在的位置就是排序好的位置,因为左边的数据都小于key,右边的数据都大于等于key。递归排序左区间和右区间,递归结束条件左大于等于右。

// 前后指针法
void Pointer(int* a, int begin, int end)
{
	// 递归结束条件
	if (begin >= end)
		return;

	int keyi = begin;

	// slow 记录的是比key小的最后一个元素,
	int slow = begin;
	// fast找比key小的,找到小的就交换到slow的下一个位置
	int fast = begin + 1;

	while (fast <= end)
	{
		// 如果fast位置的元素比key小,交换到slow的下一个位置
		// 如果slow的下一个位置跟fast位置一样,不需要交换
		if (a[fast] < a[keyi] && ++slow != fast)
			Swap(&a[slow], &a[fast]);

		++fast;
	}

	// slow的位置就是key存储的位置,交换数据
	Swap(&a[keyi], &a[slow]);
	// 更新keyi
	keyi = slow;

	// 递归左区间和右区间
	Pointer(a, begin, keyi - 1);
	Pointer(a, keyi + 1, end);
}

        选待排序元素集合的第一个元素作为key,在待排序元素集合本身有序或者接近有序的时候,快排的效率非常慢。需要修改key的选择。

        时间复杂度:O(N^2)。时间复杂度高。

        空间复杂度:O(N)。递归深度太深,可能会栈溢出,程序崩溃。

        稳定性:不稳定。

        解决办法:1.随机取key。2.三数取中。

优化 

三数取中

        左边做key,找待排序元素集合中的中位数,跟第一个元素和最后一个元素比较,找中间大小的值,修改key,key和中间大小的值交换数据。避免选取待排序元素集合中最小值或最大值。

        3、8、0的中间大小的值是3,3做key,交换3和key的位置。 

// 三数取中
int GetMid(int* a, int begin, int end)
{
	// 找中位数
	int midi = begin + (end - begin) / 2;
	int max = a[begin];
	int mid = a[midi];
	int min = a[end];

	// 将max变为三个数中最大
	if (max < mid)
		Swap(&max, &mid);
	if (max < min)
		Swap(&max, &min);

	// 将mid变为中间的,min变为最小
	if (mid < min)
		Swap(&mid, &min);

	// 返回mid值对应三个数的下标
	if (mid == a[begin])
		return begin;
	else if (mid == a[midi])
		return midi;
	else
		return end;
}

         时间复杂度:O(N*log(N))。

        空间复杂度:O(logN)。

小区间优化

        小区间递归次数是非常多的。

        如果十个数据,每次的key值都是数组中间大小的值,递归展开图。

        小区间的递归十分消耗时间,所以将小区间的排序使用别的排序算法,这里建议使用插入排序,插入排队对已经有序和接近有序的效率非常高。

        前后指针法的优化。

// 前后指针法
void Pointer(int* a, int begin, int end)
{
    // 递归结束条件
	if (begin >= end)
		return;

	// 小区间优化
	if (end - begin < 10)
	{
		SelectSort(a + begin, end - begin + 1);
		return;
	}
	
	// 左边做key
	//int keyi = begin;
	
	// 优化:三数取中
	int keyi = begin;
	int mid = GetMid(a, begin, end);
	Swap(&a[keyi], &a[mid]);

	// slow 记录的是比key小的最后一个元素,
	int slow = begin;
	// fast找比key小的,找到小的就交换到slow的下一个位置
	int fast = begin + 1;

	while (fast <= end)
	{
		// 如果fast位置的元素比key小,交换到slow的下一个位置
		// 如果slow的下一个位置跟fast位置一样,不需要交换
		if (a[fast] < a[keyi] && ++slow != fast)
			Swap(&a[slow], &a[fast]);

		// 上面那个不能理解看这个
		//if (a[fast] < a[keyi])
		//{
		//	++slow;
		//	if (slow != fast)
		//		Swap(&a[slow], &a[fast]);
		//}

		++fast;
	}

	// slow的位置就是key存储的位置,交换数据
	Swap(&a[keyi], &a[slow]);
	// 更新keyi
	keyi = slow;

	// 递归左区间和右区间
	Pointer(a, begin, keyi - 1);
	Pointer(a, keyi + 1, end);
}

        没有小区间优化

        小区间优化 

        时间复杂度:O(N*log(N))。

        空间复杂度:O(logN)~O(N)。

        稳定性:不稳定。 

快速排序非递归

        使用栈数据结构模拟非递归。复制一份之前写的栈的头文件和源文件到当前项目下。函数的参数入栈顺序是从右往左入的。

        模拟实现前后指针法快速排序的非递归。

// 快排非递归
void QuickSortNon(int* a, int size)
{
	Stack s;
	StackInit(&s);
	StackPush(&s, size - 1);
	StackPush(&s, 0);

	// 当栈不为空的时候继续排序
	while (!StackEmpty(&s))
	{
		int left = StackTop(&s);
		StackPop(&s);

		int right = StackTop(&s);
		StackPop(&s);

		// 小区间优化
		if (right - left < 10)
		{
			// a+begin是待排序的起始位置,end-begin+1是待排序的个数
			SelectSort(a + left, right - left + 1);
			continue;
		}

		// 优化:三数取中
		int keyi = left;
		int mid = GetMid(a, left, right);
		Swap(&a[keyi], &a[mid]);

		// slow 记录的是比key小的最后一个元素,
		int slow = left;
		// fast找比key小的,找到小的就交换到slow的下一个位置
		int fast = left + 1;

		while (fast <= size - 1)
		{
			// 如果fast位置的元素比key小,交换到slow的下一个位置
			// 如果slow的下一个位置跟fast位置一样,不需要交换
			if (a[fast] < a[keyi] && ++slow != fast)
				Swap(&a[slow], &a[fast]);

			++fast;
		}

		// slow的位置就是key存储的位置,交换数据
		Swap(&a[keyi], &a[slow]);
		// 更新keyi
		keyi = slow;

		// 先入栈右区间,后入栈左区间,先出栈左区间排序
		
		// 如果右区间存在,且不是一个数,入栈
		if (keyi + 1 < right)
		{
			StackPush(&s, right);
			StackPush(&s, keyi + 1);
		}

		// 如果左区间存在,且不是一个数,入栈
		if (left < keyi - 1)
		{
			StackPush(&s, keyi - 1);
			StackPush(&s, left);
		}
	}
	StackDestroy(&s);
}

        注意小区间优化的return变成了continue。

归并排序

        需要申请跟原数组一样的空间大小,归并排序数组到申请空间,将排序好的数据拷贝回数组。将待排序元素集合分为两个区间,递归排序左区间和右区间,左区间和右区间已经有序,将数组数据归并排序申请的空间上,最后拷贝回原数组。递归结束条件是该区间不存在或只有一个数。

// 归并排序需要的子函数
void MergeSortSub(int* a, int* tmp, int begin, int end)
{
	// 归并结束条件
	if (begin >= end)
		return;

	// 找中间下标
	int mid = begin + (end - begin) / 2;

	// 归并左区间和右区间
	MergeSortSub(a, tmp, begin, mid);
	MergeSortSub(a, tmp, mid + 1, end);

	// 左区间和右区间归并结束,拷贝回原数组
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;

	// 左区间和右区间归并
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
			tmp[i++] = a[begin1++];
		else
			tmp[i++] = a[begin2++];
	}

	// 左区间或右区间没有归并完
	// 剩余数据归并
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}

	// 剩余数据归并
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}

	// 拷贝回原数组
	for (int j = begin; j <= end; ++j)
	{
		a[j] = tmp[j];
	}
}

// 归并排序
void MergeSort(int* a, int size)
{
	int* tmp = (int*)malloc(sizeof(int) * size);

	MergeSortSub(a, tmp, 0, size - 1);

	free(tmp);
}

        时间复杂度:O(N*log(N))。

        空间复杂度:O(N)。

        稳定性:稳定。

归并排序非递归

        归并排序的非递归不好模拟成跟递归的顺序一样。

// 直接模拟
void MergeSortNon(int* a, int size)
{
	int* tmp = (int*)malloc(sizeof(int) * size);

	// 一(gap)个数据和一(gap)个数据开始归并,因为一个数据本身就是有序的
	int gap = 1;
	// gap小于size,数组没有归并完
	while (gap < size)
	{
		// 当i<size的时候,还有数据没有归并
		for (int i = 0; i < size; i += 2 * gap)
		{
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;

			// 修正边界
			if (end1 >= size)
			{
				end1 = size - 1;
				begin2 = size;
				end2 = size - 1;
			}
			else if (begin2 >= size)
			{
				begin2 = size;
				end2 = size - 1;
			}
			else if (end2 >= size)
			{
				end2 = size - 1;
			}

			// 归并两个区间数据
			int j = begin1;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
					tmp[j++] = a[begin1++];
				else
					tmp[j++] = a[begin2++];
			}

			// 剩余数据归并
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

			// 剩余数据归并
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}

		}

		// 归并完拷贝回原数组
		for (int k = 0; k < size; ++k)
		{
			a[k] = tmp[k];
		}

		// gap*2个数据为一组的已经有序,下次归并gap*2和gap*2个数据归并
		gap *= 2;
	}

	free(tmp);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值