八大排序算法

目录

一.插入排序

二.希尔排序

三.选择排序 

四.堆排序

五.冒泡排序

六.快速排序

       1.递归

        2.非递归

七.归并排序

        1.递归

        2.非递归

八.计数排序


一.插入排序

        插入排序是一种比较基础的排序,类似于整理扑克牌。

        思想:把待排序的数据逐个插入到前面的有序序列中(一个数字本身就是有序序列)

代码实现:

// 插入排序 升序
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n; i++)//遍历每一个数字
	{
		for (int j = i; j > 0; j--)//与前面数据相比较,因为(比较的次数=数据长度-1)所以j>0
		{
			if (a[j] < a[j - 1])//遇到比自己小的就交换,否则就说明长度为i+1的子数组排好了,可以跳出循环
				swap(a + j, a + j - 1);
			else
				break;
		}
	}
}

// 插入排序 降序
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n; i++)//遍历每一个数字
	{
		for (int j = i; j > 0; j--)//与前面数据相比较,因为(比较的次数=数据长度-1)所以j>0
		{
			if (a[j] > a[j - 1])//遇到比自己小的就交换,否则就说明长度为i+1的子数组排好了,可以跳出循环
				swap(a + j, a + j - 1);
			else
				break;
		}
	}
}

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

空间复杂度:O(1)

稳定性:稳定

注意:如果序列越有序,那么插入排序的速度就越快,当序列完全有序时间复杂度为O(N),当序列完全逆序时间复杂度就为最坏值O(N^2)!

二.希尔排序

        上面说到,序列越接近有序插入排序的速度就越快,如果我们能先让序列接近有序,再进行插入排序,那么速度就会大大提升。

        思想:取一个值gap,序列中所有相隔gap元素的数据为一组,对每一组进行插入排序。排完后缩小gap,继续插入排序,直到gap等于1。当gap等于1时,整个序列就是一组的,相当于对整个序列进行插入排序,序列就是有序的。     

 代码实现:

// 希尔排序 升序
void ShellSort(int* a, int n)
{
	int gap = n;//令gap等于n
	while (gap /= 2)//gap/2最好。解释:当n>=2时,如果gap/(比gap大的数)时gap会直接为0,不进循环
	{
		for (int i = 0; i < n; i++)//下面的代码实际上就是把原来插入排序的1换成了gap
		{
			for (int j = i; j - gap >= 0; j -= gap)//注意循环进入条件,需要>=0而不是>0
			{
				if (a[j] < a[j - gap])//遇到比自己小的就交换
					swap(a + j, a + j - gap);
				else
					break;
			}
		}
	}
}

// 希尔排序 降序
void ShellSort(int* a, int n)
{
	int gap = n;//令gap等于n
	while (gap /= 2)//gap/2最好。解释:当n>=2时,如果gap/(比gap大的数)时gap会直接为0,不进循环
	{
		for (int i = 0; i < n; i++)//下面的代码实际上就是把原来插入排序的1换成了gap
		{
			for (int j = i; j - gap >= 0; j -= gap)//注意循环进入条件,需要>=0而不是>0
			{
				if (a[j] > a[j - gap])//遇到比自己小的就交换
					swap(a + j, a + j - gap);
				else
					break;
			}
		}
	}
}

时间复杂度:O(n^1.25)~O(1.6*n^1.25)

空间复杂度:O(1)

稳定性:不稳定

注意:希尔排序的时间复杂度极难证明,在严蔚敏的《数据结构》中说过“希尔排序的时间是所取‘增量’序列的函数,这涉及一些数学上尚未解决的难题”,也就是和gap有关。不过在Knuth的《计算机程序设计技巧》中利用了大量实验数据得出,当n很大时,时间复杂度大约在O(n^1.25)~O(1.6*n^1.25)之间!

以及关于插入排序第二个for循环的结束条件是j>0,而希尔排序的第二个for循环的结束条件是j-gap>=0的问题。实际上插入排序就是希尔排序gap等于1的时候,插入排序j-1>=0和j>0是相等的

三.选择排序 

        选择排序是一种基础的排序。

        思想:遍历一次待排序序列找出最大或最小值,然后和有序序列第一位或最后一位数据交换。(最大对应第一位,最小对应最后一位)            不过我们可以一次遍历同时找出最大或最小值,然后进行交换,这样能稍微提升速度。

代码实现:

// 选择排序 升序
void SelectSort(int* a, int n)
{
	int min = 0, max = 0, left = 0, right = n - 1;
	while (left < right)
	{
		for (int i = left; i <= right; i++)//遍历一次找到当前区间的最大值和最小值
		{
			if (a[min] > a[i])
				min = i;
			if (a[max] < a[i])
				max = i;
		}
		if (min == max)//如果min==max那么说明[left,right]中的所有数字都相同
			break;
		int tmp1 = a[min], tmp2 = a[max];//不能直接用swap交换,否则会出错
		a[min] = a[left], a[max] = a[right];//比如swap(a+left,a+min) swap(a+right,a+max)当max==left时
		a[left] = tmp1, a[right] = tmp2;
		left++, right--;//缩小区间
		min = max = left;
	}
}

// 选择排序 降序
void SelectSort(int* a, int n)
{
	int min = 0, max = 0, left = 0, right = n - 1;
	while (left < right)
	{
		for (int i = left; i <= right; i++)//遍历一次找到当前区间的最大值和最小值
		{
			if (a[min] > a[i])
				min = i;
			if (a[max] < a[i])
				max = i;
		}
		if (min == max)//如果min==max那么说明[left,right]中的所有数字都相同
			break;
		int tmp1 = a[min], tmp2 = a[max];//不能直接用swap交换,否则会出错
		a[min] = a[left], a[max] = a[right];//比如swap(a+left,a+min) swap(a+right,a+max)当max==left时
		a[right] = tmp1, a[left] = tmp2;
		left++, right--;//缩小区间
		min = max = left;
	}
}

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

空间复杂度:O(1)

稳定性:不稳定

注意:排序中记录的最大最小值实际上是记录其下标,交换时用两个临时变量存放,然后再和待排序序列的最大最小值交换!

四.堆排序

        堆排序是利用了数据结构堆来进行排序的,排升序建大堆,排降序建小堆。

        思想:建立大小堆,每次和待排序序列的最后一个值交换,然后对根节点进行向下调整,直到堆只剩下根节点就算排序完成。 

代码实现: 

// 堆排序 升序
void AdjustDown(int* a, int n, int root)//向下调整算法
{
	int parent = root, child = parent * 2 + 1;//假设左孩子是最大的
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child + 1])//比较左孩子右孩子谁大,需判断右孩子是否存在
			child++;
		if (a[parent] < a[child])//如果双亲节点比孩子节点小就交换
			swap(a + parent, a + child);
		else
			break;
		parent = child;//向下迭代
		child = child * 2 + 1;
	}
}
void HeapSort(int* a, int n)
{
	int size = n;
	for (int i = (n - 2) / 2; i >= 0; i--)//i的初始值为最后一个双亲节点
	{
		AdjustDown(a, n, i);//对每一个双亲节点进行向下调整
	}
	while (size--)
	{
		swap(a, a + size);//把待排序序列最后一个值和堆的根节点(最大值)交换
		AdjustDown(a, size, 0);//对堆的根节点进行向下调整算法
	}
}

// 堆排序 降序
void AdjustDown(int* a, int n, int root)//向下调整算法
{
	int parent = root, child = parent * 2 + 1;//假设左孩子是最大的
	while (child < n)
	{
		if (child + 1 < n && a[child] > a[child + 1])//比较左孩子右孩子谁小,需判断右孩子是否存在
			child++;
		if (a[parent] > a[child])//如果双亲节点比孩子节点大就交换
			swap(a + parent, a + child);
		else
			break;
		parent = child;//向下迭代
		child = child * 2 + 1;
	}
}
void HeapSort(int* a, int n)
{
	int size = n;
	for (int i = (n - 2) / 2; i >= 0; i--)//i的初始值为最后一个双亲节点
	{
		AdjustDown(a, n, i);//对每一个双亲节点进行向下调整
	}
	while (size--)
	{
		swap(a, a + size);//把待排序序列最后一个值和堆的根节点(最小值)交换
		AdjustDown(a, size, 0);//对堆的根节点进行向下调整算法
	}
}

时间复杂度:O(N*log2 N)

空间复杂度:O(1)

稳定性:不稳定

注意:堆是一个完全二叉树,所以可用数组表示!以下是一些性质:

节点i的双亲节点:(i-1)/2

节点i的左孩子节点:i*2+1

节点i的右孩子节点:i*2+2

最后一个叶子结点:N-1

最后一个双亲节点:(最后一个叶子结点-1)/2   -->   (N-2)/2

五.冒泡排序

        冒泡排序应该是最容易理解,入门第一个接触到的第一个排序算法了。

        思想:通过比较大小交换两个数据在序列中的位置,大的数据往后移动,小的数据往前移动。

 代码实现:

// 冒泡排序 升序
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n; i++)//遍历数组的数据
	{
		for (int j = 0; j < n - 1; j++)
		{
			if (a[j] > a[j + 1])//前一个数比后一个数大就交换
				swap(a + j, a + j + 1);
		}
	}
}

// 冒泡排序 降序
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n; i++)//遍历数组的数据
	{
		for (int j = 0; j < n - 1; j++)
		{
			if (a[j] < a[j + 1])//前一个数比后一个数小就交换
				swap(a + j, a + j + 1);
		}
	}
}

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

空间复杂度:O(1)

稳定性:稳定

六.快速排序

        1.递归

       快速排序是基于二叉树结构的交换排序方法。

       思想:在序列中任取一个值当做基准值,然后将整个序列分为两个子序列,左子序列小于等于基准值,右子序列大于等于基准值,然后左右子序列重复该过程,直到整个序列有序。

        代码实现:(这个是框架)

void QuickSort(int* a, int left, int right)//left和right是指当前区间的左右边界
{
	if (left >= right)//如果左边界大于等于右边界就不用排了
		return;
	int privot = PartSort1(a, left, right);//中间这里就是对当前区间进行排序,privot是已经排好的那个数的下标
	QuickSort(a, left, privot - 1);//递归排序privot左边的序列
	QuickSort(a, privot + 1, right);//递归排序privot右边的序列
}

        (1)hoare版本 

        思想1:定义左边界的数为基准值,定义两个指针分别指向左边界和右边界。右指针先走,遇到比基准值小的值停下来,再到左指针走,遇到比基准值大的停下来,然后交换两个指针指向的数,继续右指针先走...直到两个指针相遇才停下来,然后交换左边界的数和两指针相遇的数。(两指针相遇的数一定是小于等于基准值的)

        思想2:定义右边界的数为基准值,定义两个指针分别指向左边界和右边界。左指针先走,遇到比基准值大的值停下来,再到右指针走,遇到比基准值小的停下来,然后交换两个指针指向的数,继续左指针先走...直到两个指针相遇才停下来,然后交换右边界的数和两指针相遇的数。(两指针相遇的数一定是大于等于基准值的)

        以下是上述结论的证明:

第一种情况:

左指针和右指针不断的在向中间走,最后一定会变成这种情况:左指针和右指针中间有一个数,假如这个数大于等于基准值,那么right就会走到left的地方,left是小于等于基准值的,结论成立,假如这个数小于基准值,那么right就会停下来,然后left往右走就会碰到right,结论成立。又或者左指针和右指针中间有若干个等于基准值的数,那么right还是会走到left处,结论成立。


只有left和right相遇的数小于等于基准值,才能和基准值所在的位置交换,这样交换后基准值左边都是小于等于基准值的数,右边都是大于等于基准值的数。

 第二种情况:

 左指针和右指针不断的在向中间走,最后一定会变成这种情况:左指针和右指针中间有一个数,假如这个数小于等于基准值,那么left就会走到right的地方,right是大于等于基准值的,结论成立,假如这个数小于基准值,那么left就会停下来,然后right往左走就会碰到left,结论成立。又或者左指针和右指针中间有若干个等于基准值的数,那么left还是会走到right处,结论成立。


只有left和right相遇的数大于等于基准值,才能和基准值所在的位置交换,这样交换后基准值左边都是小于等于基准值的数,右边都是大于等于基准值的数。

代码实现:(这个是补全上面快排框架的PartSort1函数)

// 快速排序hoare版本
int PartSort1(int* a, int left, int right)//第一种情况
{
	int index = left, privot = a[left];//基准值是左边界的数
	while (left < right)
	{
		while (left < right && a[right] >= privot)//右指针先走
			right--;
		while (left < right && a[left] <= privot)//左指针后走
			left++;
		swap(a + left, a + right);//交换两数,在这个循环里left不会大于right所以不用判断
	}
	swap(a + left, a + index);//交换left和right相遇位置的数和左边界的数
	return left;//返回现在基准值所在的下标
}

// 快速排序hoare版本
int PartSort1(int* a, int left, int right)//第二种情况
{
	int index = right, privot = a[right];//基准值是右边界的数
	while (left < right)
	{
		while (left < right && a[left] <= privot)//左指针先走
			left++;
		while (left < right && a[right] >= privot)//右指针后走
			right--;
		swap(a + left, a + right);//交换两数,在这个循环里left不会大于right所以不用判断
	}
	swap(a + left, a + index);//交换left和right相遇位置的数和右边界的数
	return left;//返回现在基准值所在的下标
}

        (2)挖坑法

        挖坑法就是对基准值也进行交换,基准值最后停下来的位置就是最终位置。

代码实现:

// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	int index = left, privot = a[left];//基准值是左边界的数
	while (left < right)
	{
		while (left < right && a[right] >= privot)//右指针先走,找比基准值小的数
			right--;
		swap(a + right, a + index);//交换基准值和right指向的数
		index = right;//更新基准值所在下标
		while (left < right && a[left] <= privot)//左指针找比基准值大的数
			left++;
		swap(a + left, a + index);//交换基准值和left指向的数
		index = left;//更新基准值所在的下标
	}
	return left;//返回基准值所在的下标,也就是left和right相遇的地方
}

         (3)前后指针法

         思想:先指定一个基准值,定义两个指针,维护两个指针之间是一个大于等于基准值的序列,直到右指针走完整个序列。 

代码实现:

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int index = GetMid(a, left, right), privot = a[index], begin = left, end = left;//三数取中避免遇到最坏情况(取到最小值或者最大值)
	while (end <= right)
	{
		if (a[end] < privot)//遇到了比基准值小的数
		{
			if (begin == index)//中途可能会移动基准值,所以要更新index的值
				index = end;
			swap(a + begin, a + end);
			begin++;
		}
		end++;
	}
	swap(a + index, a + begin);//交换基准值与左指针的数,基准值就排好了
	return begin;
}

//三数取中
int GetMid(int* a, int left, int right)
{
	int num1 = a[left], num2 = a[right], num3 = a[(left + right) / 2];
	if (num1 >= num2)
	{
		if (num3 >= num1)
			return left;
		if (num3 <= num2)
			return right;
		return (left + right) / 2;
	}
	else
	{
		if (num3 >= num2)
			return right;
		if (num3 <= num1)
			return left;
		return (left + right) / 2;
	}
}

        (4)快速排序的优化

        每次递归都需要开辟函数栈帧,需要空间和时间,而最后几层递归实际上都在排序小区间,我们可以用其他排序(插入排序)来完成小区间的排序,这样可以大大减少函数栈帧的开辟,节省一点时间和空间。

        代码实现:

void QuickSort(int* a, int left, int right)//left和right是指当前区间的左右边界
{
	if (left >= right)//如果左边界大于等于右边界就不用排了
		return;
	/*int privot = PartSort1(a, left, right);*/
	/*int privot = PartSort2(a, left, right);*/
	if (right - left <= 8)//如果当前区间数的个数比较少就可以直接插入排序
		InsertSort(a + left, right - left + 1);
	else
	{
		int privot = PartSort1(a, left, right);//中间这里就是对当前区间进行排序,privot是已经排好的那个数的下标
		QuickSort(a, left, privot - 1);//递归排序privot左边的序列
		QuickSort(a, privot + 1, right);//递归排序privot右边的序列
	}
}

        2.非递归

        非递归就是用循环实现快速排序,需要借助栈实现,排之前先从栈中取出需要排序的区间,排完后就把左区间和右区间存入栈中,直到栈为空就说明排好了。

代码实现:

// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
	Stack st;//创建一个栈
	StackInit(&st);//初始化
	StackPush(&st, right);//存入当前区间右边界
	StackPush(&st, left);//存入当前区间左边界
	while (!StackEmpty(&st))//栈不为空就一直循环
	{
		int begin = StackTop(&st);//取区间左边界
		StackPop(&st);//出栈
		int end = StackTop(&st);//取区间右边界
		StackPop(&st);//出栈
		int privot = PartSort1(a, begin, end);//对区间进行排序
		if (privot + 1 < end)//如果右区间存在就存入左右边界
		{
			StackPush(&st, end);
			StackPush(&st, privot + 1);
		}
		if (begin < privot - 1)//如果左区间存在就存入左右边界
		{
			StackPush(&st, privot - 1);
			StackPush(&st, begin);
		}
	}
	StackDestroy(&st);//销毁栈
}

时间复杂度:O(log2N * N)

空间复杂度:O(log2N) (递归开辟栈帧)

稳定性:不稳定

七.归并排序

        1.递归

        归并排序是一种运用了分治法的排序。属于外排序,通常用于磁盘的排序。

        思想:把原序列分解为有序的子序列,再把有序的子序列合并成一个序列,直到整个序列有序。(一个数视为有序序列)

 代码实现:

// 归并排序递归实现
void PartSort4(int* a, int* tmp, int left, int right)
{
	if (left >= right)//如果左边界大于右边界就返回
		return;
	int begin1 = left, end1 = (right + left) / 2, begin2 = end1 + 1, end2 = right, index = 0;//定义两个子区间的左右指针
	PartSort4(a, tmp, left, end1);//继续递归左区间,直到子序列只有一个数为止
	PartSort4(a, tmp, begin2, right);//继续递归右区间,直到子序列只有一个数为止
	while (begin1 <= end1 && begin2 <= end2)//开始归并两个子序列
	{
		if (a[begin1] <= a[begin2])//取两个子序列小的那个数放入创建的数组中
			tmp[index++] = a[begin1++];
		else
			tmp[index++] = a[begin2++];
	}
	while (begin1 <= end1)//走到这里说明有一个子序列已经遍历完了,检查第一个子序列有没有遍历完,没有就遍历剩下的数放入创建的数组中
		tmp[index++] = a[begin1++];
	while (begin2 <= end2)//检查第二个子序列有没有遍历完,没有就遍历剩下的数放入创建的数组中
		tmp[index++] = a[begin2++];
	for (int i = left, index = 0; i <= right; i++, index++)//把创建的数组中刚才放进去的数放回原序列
		a[i] = tmp[index];
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);//创建一个和原序列大小相等的数组
	PartSort4(a, tmp, 0, n - 1);//归并排序
	free(tmp);//释放创建的数组
}

        2.非递归

        递归的写法把原序列分成一个一个子序列,一个数就是一个有序的子序列,然后俩俩合并,直到整个序列有序,非递归写法可以控制每次合并的子序列的大小来实现这样的效果。

// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);//创建一个和原序列大小相等的数组
	int gap = 1, begin1, end1, begin2, end2;//定义gap控制每次比较子序列的长度,以及两个子序列的左右边界
	while (gap < n)//当子序列的大小超过原序列的长度时即可跳出循环
	{
		begin1 = 0, end1 = begin1 + gap - 1, begin2 = end1 + 1, end2 = begin2 + gap - 1;//初始化两个子序列的左右边界
		while (begin2 < n)//如果右子序列不存在即可跳出循环
		{
			if (end2 >= n)//如果右子序列存在但是右子序列的右边界不存在就缩小,令其等于原序列最后一个数也就是n-1
				end2 = n - 1;
			int left = begin1, right = end2, index = 0;//记录左右两个子序列合成之后序列的左右边界
			while (begin1 <= end1 && begin2 <= end2)//和递归版的合并过程并无二致
			{
				if (a[begin1] <= a[begin2])
					tmp[index++] = a[begin1++];
				else
					tmp[index++] = a[begin2++];
			}
			while (begin1 <= end1)
				tmp[index++] = a[begin1++];
			while (begin2 <= end2)
				tmp[index++] = a[begin2++];
			for (int i = left, index = 0; i <= right; i++, index++)
				a[i] = tmp[index];
			begin1 = end2 + 1, end1 = begin1 + gap - 1, begin2 = end1 + 1, end2 = begin2 + gap - 1;//迭代四个指针
		}
		gap *= 2;//迭代比较数组的长度
	}
	free(tmp);//释放创建的数组
}

时间复杂度:O(log2N*N)

空间复杂度:O(N)

稳定性:稳定

八.计数排序

        计数排序就是对待排序数组中所有出现过的数字进行统计,然后根据统计结果重新写入到原数组中完成排序,用于数据比较集中的情况下效果比较好。

        思想:创建一个数组存放原数组出现过所有数字的次数,然后根据创建数组的数据把数字有序的写入原数组中。

代码实现:

// 计数排序
void CountSort(int* a, int n)
{
	int min = INT_MAX, max = INT_MIN;
	for (int i = 0; i < n; i++)//找到数组中最大最小值
	{
		if (min > a[i])
			min = a[i];
		if (max < a[i])
			max = a[i];
	}
	int* tmp = (int*)calloc((max - min + 1), sizeof(int));//创建一个大小为max-min+1长度的数组
	for (int i = 0; i < n; i++)//遍历数组,记录原数组数字出现的个数,映射下标就是数字-最小值,比如最小值的下标就是min-min为0
		tmp[a[i] - min]++;
	for (int i = 0, index = 0; i <= max - min; i++)//将数据从大到小写入原数组
	{
		while (tmp[i]--)//这个数出现几次就在原数组中写入几个这个数
			a[index++] = i + min;//小标+最小值就是原数组中的那个数大小
	}
}

时间复杂度: O(N)

空间复杂度: O(max-min+1)(数组中的最大值-最小值+1)

稳定性:稳定

 以上就是八大排序算法的原理和实现了,如有错误和建议可在评论区提出。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值