数据结构初阶——排序

本文我们讲解数据结构中与排序有关的内容。

目录

排序的概念及其应用

排序的概念

常见的排序算法

常见排序算法的实现

直接插入排序

图形演示:

代码实现:

特性总结: 

希尔排序( 缩小增量排序 )

图形演示:

代码实现:

选择排序

图形演示:

代码实现:

堆排序

图形演示:

代码实现:

冒泡排序

图形演示:

代码实现:

快速排序 (递归版)

单次排序(hoare版本)图形演示:

单次排序(hoare版本)代码实现:

单次排序(挖坑版)图形演示:

单次排序(挖坑版)代码实现:

单次排序(双指针法)图形演示:

单次排序(双指针法)代码实现:

快速排序全部代码(递归实现):

快速排序(非递归版)

快速排序(非递归版)图形演示:

快速排序(非递归版)代码实现:

快排的会遇到的一些问题及优化:

归并排序 

排序算法复杂度及稳定性

排序的概念及其应用

排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序

常见的排序算法

在文章接下来就会介绍下图中的各种算法:

常见排序算法的实现

直接插入排序

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。

图形演示:

代码实现:

//插入排序
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 (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
			a[end + 1] = tmp;
		}
	}
}

特性总结: 

1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定(相同元素的相对位置可以不发生改变)

希尔排序( 缩小增量排序 )

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。

希尔排序的思想简单来说就是,先进行分组预排序,然后在gap = 1的时候进行插入排序。

图形演示:

代码实现:

// 希尔排序
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
				a[end + gap] = tmp;
			}
		}
	}
}

选择排序

图形演示:

代码实现:

// 选择排序
void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	int Maxi = begin;
	int Mini = begin;

	while (begin < end)
	{
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[Maxi] < a[i])
			{
				Maxi = i;
			}
			if (a[Mini] > a[i])
			{
				Mini = i;
			}
		}
 		Swap(&a[begin], &a[Mini]);
		if (Maxi == begin)
			Maxi = Mini;
		Swap(&a[end], &a[Maxi]);
		begin++;
		end--;
	}
}

堆排序

图形演示:

在使用堆排序时需要注意,在向下调整的时候堆的右节点时可能是不存在的要添加判断的条件。

代码实现:

// 向下调整,最后形成大堆
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 - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDwon(a, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDwon(a, end--, 0);
	}
}

冒泡排序

图形演示:

代码实现:

// 冒泡排序
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = 1; j < n - i; j++)
		{
			if (a[j-1] > a[j])
			{
				Swap(&a[j-1], &a[j]);
			}
		}
	}
}

快速排序 (递归版)

快速排序的思想就是,先确定Keyi值得位置,然后在对Keyi的左右子区间进行同样的操作,最后可以得到排序后的结果。

单次排序(hoare版本)图形演示:

单次排序(hoare版本)代码实现:

// 快速排序hoare版本
int PartSort1(int* a, int begin, int end)
{
	int mid = FindMidNum(a, begin, end);
	Swap(&a[mid], &a[begin]);
	/*int keyi = begin;*/

	int left = begin;
	int right = end;
	int 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]);
	keyi = left;

	return keyi;
}

在hoare的这个版本之中,我们需要注意在进行移动寻找时需要先启动Right,进行寻找;然后再启动Left。在寻找的同时需要时刻注意Left的数值需要小于Right。

单次排序(挖坑版)图形演示:

单次排序(挖坑版)代码实现:

// 挖坑法
int PartSort2(int* a, int begin, int end)
{
	int mid = FindMidNum(a, begin, end);
	Swap(&a[mid], &a[begin]);

	int left = begin;
	int right = end;
	int hole = begin;
	int key = a[begin];

	while (left < right)
	{
		//右边找小,填到左边的坑里
		while (left < right && a[right] > key)
		{
			--right;
		}
		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 begin, int end)
{
	int mid = FindMidNum(a, begin, end);
	Swap(&a[mid], &a[begin]);
	
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;

	while (cur <= end)
	{
		//找到比k小的值跟++prev进行交换,小的往前翻,大的往后翻
		//if (a[cur] < a[keyi] && ++prev != cur)
		//	Swap(&a[prev], &a[cur]);

		if (a[cur] < a[keyi])
		{
			Swap(&a[++prev], &a[cur]);
		}
		++cur;
	}

	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}

快速排序全部代码(递归实现):

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	if ((end - begin + 1) < 15)
	{
		InsertSort(a + begin, end - begin + 1);
	}

	else
	{
		int keyi = PartSort2(a, begin, end);

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

快速排序(非递归版)

在这里实现快速排序的非递归版,我们使用的是栈这种数据结构。

快速排序(非递归版)图形演示:

根据上图所示,快速排序的非递归版本的过程与递归是类似的,首先将整个数组进行排序,然后对其keyi值左端的数组以及keyi值右端的数组进行排序。我们需要使用栈来记录下每次排序时数组的最左端与最右端的值。

快速排序(非递归版)代码实现:

// 快排非递归
void QuickSortNonR(int* a, int begin, int end)
{
	Stack st;
	StackInit(&st);

	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st))
	{
		int right = StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);
		int keyi = PartSort3(a, left, right);
		
		if (keyi - 1 > left)
		{
			StackPush(&st, left);
			StackPush(&st, keyi - 1);
		}

		if (right > keyi + 1)
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, right);
		}
	}
	StackDestroy(&st);
}

快排的会遇到的一些问题及优化:

在使用快速排序的时候,我们希望的是我们寻找的keyi值要尽量处在中间的位置,这样我们就可以在理想的情况下以O(N*logN)的时间复杂度来进行排序。但是显示往往不是这样,很有可能我们选到的数是最大的或者是最小的,那么快速排序的时间复杂度就会变为O(N^2)。反而效率会下降。针对这个问题,解决方案就是采用三数取中的方案。

三数取中:我们在最左端,左右端以及中间的三个位置进行比较,然后取出中间大小的那个数,这样就可以尽量避免发生keyi值取到最大或者最小的情况。

//三数取中
int FindMidNum(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[begin] > a[mid])
	{
		if (a[end] > a[begin])
		{
			return begin;
		}
		else if (a[end] < a[mid])
		{
			return mid;
		}
		else
		{
			return end;
		}
	}
	if (a[begin] < a[mid])
	{
		if (a[end] < a[begin])
		{
			return begin;
		}
		else if (a[end] > a[mid])
		{
			return mid;
		}
		else
		{
			return end;
		}
	}
}

第二种情况是小区间优化,快速排序的整个过程类似于二叉树的样子,当快速排序递归进去的区间比较小时,可以直接使用插入排序进行替换,因为大部分的递归调用都是在小区间的情况下进行的,当我们将小区间进行处理之后,可以大大减少递归次数。

	if ((end - begin + 1) < 15)
	{
		InsertSort(a + begin, end - begin + 1);
	}

归并排序 

基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

图形演示(递归):

排序(冒泡排序,选择排序,插入排序,归并排序,快速排序,计数排序,基数排序) - VisuAlgo

 自己对于递归进行的图形演示:

代码实现(递归):

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
	{
		return;
	}
	
	// 去中间的值进行分割
	int mid = (begin + end) / 2;

	// 类似于二叉树后序遍历
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);

	// 使用begin1和begin2来记录左右区间的起始位置
	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[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}

	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 MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
	tmp = NULL;
}

图形演示(非递归):

代码实现(非递归):

// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	int rangeN = 1;
	while (rangeN < n)
	{
		for (int j = 0; j < n; j += rangeN * 2)
		{
			int begin1 = j, end1 = j + rangeN - 1;
			int begin2 = j + rangeN, end2 = j + 2 * rangeN - 1;
			int i = j;

			//需要对越界的情况进行判断
			//有两种针对越界的方案
			//1、修正
			//2、跳出
			//不能使用end1 = begin2 = end2 = n-1 来进行修正,会导致问题出现
			if (end1 >= n)
			{
				end1 = n - 1;
				begin2 = n; 
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}

			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++];
			}

			//将排序好的数组拷贝回原数组
			memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));
		}
			//将排序好的数组拷贝回原数组
			//若是使用break的方式需要将拷贝整体进行。
			//memcpy(a, tmp, sizeof(int) * (n));
			rangeN *= 2;
	}

	free(tmp);
	tmp = NULL;
}

排序算法复杂度及稳定性

下面我们来看一下各个排序算法的时间复杂度和稳定性。

直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定

希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定:暂时可以以O(n^1.25)~O(1.6*n^1.25)

4. 稳定性:不稳定

选择排序的特性总结:
1. 选择排序思考非常好理解,但是效率不是很好。实际中很少使用
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定

堆排序的特性总结:
1. 堆排序使用堆来选数,效率就高了很多。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(1)
4. 稳定性:不稳定

冒泡排序的特性总结:
1. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定

快速排序的特性总结:
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定

归并排序的特性总结:
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值