【算法】常用排序算法


需要云服务器等云产品来学习Linux的同学可以移步/-->腾讯云<--/-->阿里云<--/-->华为云<--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。


目录

 一、常见排序算法 

二、直接插入排序

1、直接插入排序思想

2、直接插入排序代码

二、希尔排序

1、希尔排序思想

2、希尔排序代码

三、选择排序

1、选择排序思想

2、选择排序的代码

四、堆排序

1、堆排序思想

2、堆排序代码

五、冒泡排序

1、冒泡排序思想

2、冒泡排序代码

六、快速排序

1、hoare版

1.1hoare版单趟排序动图

1.2代码

1.3思想

1.4三数取中,优化选key

1.5小区间优化,减少递归次数

2、挖坑法

2.1挖坑法单趟排序动图

2.2挖坑法思想

2.3挖坑法代码

3、前后指针法

3.1前后指针法单趟排序动图

3.2前后指针法思想

3.3前后指针法代码

4、快排的非递归方法

4.1非递归的思想

4.2非递归代码

七、归并排序

1、归并排序的递归方法

1.1归并排序的思想

1.2归并排序代码

2、归并排序的非递归

2.1归并排序的非递归思想

2.2归并排序的非递归代码

八、计数排序

1、计数排序的思想

2、计数排序代码


 一、常见排序算法 

类别

排序方法

时间复杂度

空间复杂度

稳定性

适用场景

平均

最好

最坏

插入排序

直接插入排序

O(N^2)

O(N)有序场景

O(N^2)逆序场景

O(1)

稳定

数据有序或接近有序,

时间O(N^2)中最强排序

希尔排序

O(N^(1.3))

O(N)有序场景

O(N^2)每两个数进行一次比较并交换

O(1)

不稳定

插入排序怕大量无序不重复数据,希尔是插入的改良

选择排序

直接选择排序

O(N^2)

~~

~~

O(1)

不稳定例如89855

最差排序

堆排序

O(N*logN)

~~

~~

O(1)

不稳定

1、首次建堆可以用于找最大/最小。2、top k问题

交换排序

冒泡排序

O(N^2)

~~

~~

O(1)

稳定

”那你先写个冒泡排序吧“

快速排序

O(N*logN)

~~

O(N^2)有序场景

O(logN)

不稳定

官方库采用的排序算法,遇事不决用快排

归并

归并排序

O(N*logN)

~~

~~

O(N)

稳定

外排序

非比较排序

计数排序

O(N+range)

~~

~~

O(range)

稳定

适合排序数据范围小的数据,不能排序浮点数

二、直接插入排序

1、直接插入排序思想

扑克牌玩过吧,扑克牌抽牌,边抽边排的思想。

虽然直接插入排序是一个N^2的算法,但可以认为它是所有算法中最优的。当直接插入排序遇到重复数据较多、有序、接近有序的数据时,时间复杂度将会降为O(N)。

直接插入排序的缺点就是时间复杂度过大,针对大量无序不重复数据,排序吃力,所以就有了希尔排序。

2、直接插入排序代码

void InsertSort(int* arr, int size)//插入排序O(N^2)
{
	for (int i = 0; i < size - 1; ++i)
	{
		int end = i;//end是最后一个有序数字的下标
		int tmp = arr[end + 1];
		while (end >= 0)
		{
			if (arr[end] > tmp)
			{
				arr[end + 1] = arr[end];
				--end;
			}
			else
				break;
		}
		arr[end + 1] = tmp;//最后把tmp放到数组end+1位置处
	}
}

二、希尔排序

1、希尔排序思想

希尔排序是直接插入排序的改进,gap可以除三加一,也可以除二。每次循环gap是变化的。

gap大于1,称为预排序。希尔排序的预排序是将gap间隔的数据分为一组,进行直接插入排序,让数组接近有序。

gap越大,数组内的数字跳的越快,但排完还不是有序。gap越小,数字跳的越慢,但排完越接近有序。希尔排序gap先大后小。

在数据均不相等的情况下,数据较少,希尔排序优于O(N*logN),反之,希尔排序劣于O(N*logN)

2、希尔排序代码

void ShellSort(int* arr, int size)//希尔排序(N^1.3)
{
	int gap = size;
	while (gap > 1)//gap等于1时说明已经有序,退出循环
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < size - gap; ++i)
		{
			int end=i;//end是数组最后一个元素的下标
			int tmp = arr[end + gap];
			while (end >= 0)
			{
				if (arr[end] > tmp)
				{
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else
					break;
			}
			arr[end + gap] = tmp;
		}
	}
}

三、选择排序

1、选择排序思想

以升序为例,选择排序每次遍历数组,选出最大的那个数。可以认为它是最菜的排序算法。

以下代码为选择排序的优化,遍历一次选出最大和最小的数字。

2、选择排序的代码

void CelectSort(int* arr, int size)//选择排序
{
	int begin = 0, end = size - 1;
	while (begin < end)
	{
		int min = begin, max = begin;
		for (int i = begin+1; i <=end; ++i)
		{
			if (arr[i] > arr[max])
				max = i;
			if (arr[i] < arr[min])
				min = i;
		}
		if (max == begin)
		{
			max = min;
		}
		Swap(&arr[begin], &arr[min]);
		Swap(&arr[end], &arr[max]);
		++begin;
		--end;
	}
}

注意:一趟遍历选出最大和最小的写法,需要考虑max==begin的情况。防止max在初始位置,但该点的值被min换走的情况。

四、堆排序

1、堆排序思想

1、以升序为例:先找到最后一个非叶节点,采用向下调整算法,再找到倒数第二个非叶节点,重复向下调整算法,把一个数组建成堆。这个时候,堆顶就是数组中最大的数。该步时间复杂度O(N)

动图如下:

2、将数组调整成大堆后,将堆顶数据(最大的数)和堆底数据进行互换,锁定这个最大的数。这个时候原有的堆形态被破坏,需要重新使用向下调整算法对这个数组重建堆。迭代锁定。该步时间复杂度O(N*logN)

动图如下:

2、堆排序代码

void Adjustdown(int* arr, int size, int parent)//向下调整算法
{
	int maxChild = 2 * parent + 1;
	while (maxChild<size)
	{
		if (maxChild + 1 < size && arr[maxChild] < arr[maxChild + 1])
		{
			++maxChild;
		}
		if (arr[maxChild] > arr[parent])
		{
			Swap(&arr[maxChild], &arr[parent]);
		}
		parent = maxChild;
		maxChild = 2 * parent + 1;
	}
}
void HeapSort(int* arr, int size)//堆排序O(N*logN)
{
	//初始化建堆O(N)
	for (int i = (size - 1 - 1) / 2; i >= 0; --i)
	{
		Adjustdown(arr, size, i);
	}
	//重建堆O(N*logN)
	for (int i = 0; i < size - 1; ++i)
	{
		Swap(&arr[0], &arr[size - 1 - i]);
		Adjustdown(arr, size-1-i, 0);
	}
}

五、冒泡排序

1、冒泡排序思想

一对一对地交换

2、冒泡排序代码

void BubbleSort(int* arr, int size)//冒泡排序(N^2)
{
	for (int i = 0; i < size - 1; ++i)//趟数
	{
		int flag = 0;
		for (int j = 0; j < size - 1 - i; ++j)
		{
			if (arr[j] > arr[j + 1])
			{
				Swap(&arr[j], &arr[j + 1]);
				flag = 1;
			}
		}
		if (flag == 0)
			break;
	}
}

立个flag,当flag没变,说明一趟走完没有进行两两交换,数组已经有序,跳出循环结束排序。

六、快速排序

1、hoare版

1.1hoare版单趟排序动图

1.2代码

//[left,right]
int PartSort(int* arr, int left, int right)//单趟排完不是有序,只是key左边比key小,右边比key大
{
	int keyi = left;//选left做key,keyi是下标
	while (left < right)
	{
		//我们选了left做key,那么右边先走。反之左边先走。
		while (left < right && arr[right] >= arr[keyi])//R找小
		{
			--right;
		}
		while (left < right && arr[left] <= arr[keyi])//L找大
		{
			++left;
		}
		Swap(&arr[left], &arr[right]);
	}
	int meet = left;//meet是left和right的相遇点下标
	Swap(&arr[meet], &arr[keyi]);
	return meet;
}
//[begin,end]
void QuickSort(int* arr, int begin,int end)//快排
{
	if (begin >= end)
		return;
	int keyi = PartSort(arr, begin, end);
	//[begin,keyi-1] key [keyi+1,end]
	QuickSort(arr, begin, keyi - 1);//快排
	QuickSort(arr, keyi + 1, end);//快排
}

1.3思想

1、PartSort是单趟排序,我们先在数组中选择一个位置做key,一般是数组开头或末尾那个位置。

上面代码中,是用数组首元素做key,为了保证小人相遇位置的值小于key,必须让右边的小人先行动。如果用数组末尾做key,那么需要左边的小人先走。

右边小人一直走,遇到比key小的数则停下,左边小人再走,遇到比key大的数停下。再交换左右小人脚下的数字。交换完成后一直重复此步骤,直到左右小人相遇。

将相遇点与keyi位置交换(注意我们是要改变数组,所以需要交换相遇点与keyi位置的值,而不是交换相遇点与key的值)

单趟排序完成后,keyi左边的值都比key小,keyi右边的值都比key大。

2、QuickSort中利用二叉树的递归,完成排序。

该方法存在缺陷

1、递归层数过多有爆栈风险 2、面对有序或者接近有序的待排序数据,时间复杂度就变成了O(N^2)

所以需要作如下优化:

1.4三数取中,优化选key

1、随机选key(听着就很随机,虽然不靠谱,但有的场景还是可以使用随即选key的方法)

2、针对有序情况,选正中间数据做key(前提是知道有序)

3、三数取中(选出左中右三数中间大小的做key)(三数取中后,对于缺陷2,直接由最坏情况变成最好情况)

三数取中代码实现:

int GetMidIndex(int* arr, int left, int right)//三数取中
{
	int mid = left + (right-left) / 2;
	if (arr[left] >= arr[right])
	{
		if (arr[left] > arr[mid])
		{
			if (arr[mid] >= arr[right])
				return mid;
			else
				return right;
		}
		else
			return left;
	}
	else
	{
		if (arr[right] > arr[mid])
		{
			if (arr[mid] >= arr[left])
				return mid;
			else
				return left;
		}
		else
			return right;
	}
}
//[left,right]
int PartSort(int* arr, int left, int right)//单趟排完不是有序,只是key左边比key小,右边比key大
{
	int mid = GetMidIndex(arr, left, right);
	Swap(&arr[mid], &arr[left]);
	int keyi = left;//选left做key,keyi是下标
	while (left < right)
	{
		//我们选了left做key,那么右边先走。反之左边先走。
		while (left < right && arr[right] >= arr[keyi])//R找小
		{
			--right;
		}
		while (left < right && arr[left] <= arr[keyi])//L找大
		{
			++left;
		}
		Swap(&arr[left], &arr[right]);
	}
	int meet = left;//meet是left和right的相遇点下标
	Swap(&arr[meet], &arr[keyi]);
	return meet;
}
//[begin,end]
void QuickSort(int* arr, int begin,int end)//快排
{
	if (begin >= end)
		return;
	int keyi = PartSort(arr, begin, end);
	//[begin,keyi-1] key [keyi+1,end]
	QuickSort(arr, begin, keyi - 1);//快排
	QuickSort(arr, keyi + 1, end);//快排
}

三数取中后,我们继续进行下一个优化:

1.5小区间优化,减少递归次数

我们知道。二叉树的最后一层节点个数近乎占整棵树节点的一半,倒数第二层占25%,倒数第三层占12.5%。也就是说,后三层就占了整颗二叉树节点的87.5%。

那么,我们可以在递归时加一个限制条件:当递归时,区间剩余元素少于8个时(可自定义剩余个数),改为使用插入排序来排序这些小区间。

hoare版本最终代码:

int GetMidIndex(int* arr, int left, int right)//三数取中
{
	int mid = left + (right-left) / 2;
	if (arr[left] >= arr[right])
	{
		if (arr[left] > arr[mid])
		{
			if (arr[mid] >= arr[right])
				return mid;
			else
				return right;
		}
		else
			return left;
	}
	else
	{
		if (arr[right] > arr[mid])
		{
			if (arr[mid] >= arr[left])
				return mid;
			else
				return left;
		}
		else
			return right;
	}
}
//[left,right]
int PartSort(int* arr, int left, int right)//单趟排完不是有序,只是key左边比key小,右边比key大
{
	int mid = GetMidIndex(arr, left, right);
	Swap(&arr[mid], &arr[left]);
	int keyi = left;//选left做key,keyi是下标
	while (left < right)
	{
		//我们选了left做key,那么右边先走。反之左边先走。
		while (left < right && arr[right] >= arr[keyi])//R找小
		{
			--right;
		}
		while (left < right && arr[left] <= arr[keyi])//L找大
		{
			++left;
		}
		Swap(&arr[left], &arr[right]);
	}
	int meet = left;//meet是left和right的相遇点下标
	Swap(&arr[meet], &arr[keyi]);
	return meet;
}
//[begin,end]
void QuickSort(int* arr, int begin,int end)//递归快排
{
	if (begin >= end)
		return;
	if (end - begin <= 8)//小区间优化
	{
		InsertSort(arr+begin, end -begin + 1);//插入排序O(N^2)
		return;
	}
	else
	{
		int keyi = PartSort3(arr, begin, end);
		//[begin,keyi-1] key [keyi+1,end]
		QuickSort(arr, begin, keyi - 1);//快排
		QuickSort(arr, keyi + 1, end);//快排
	}
}

2、挖坑法

2.1挖坑法单趟排序动图

2.2挖坑法思想

三数取中后,我们把数组最左边当成坑,并记录这个位置的值key(动态中坑的初始下标是0,key是6)

同样的,右边小人先走,找到比key小的数字停下来,将脚下位置的数字填入坑中,同时,脚下变成了新的坑。

左边小人再行动,找到比key大的数字停下来,将脚下位置的数字填入坑中,同时,脚下变成新的坑。

循环上述过程,直到两小人相遇。相遇时,脚下必定是坑,那么把刚开始的key填入这个坑中。完成了单趟排序。

挖坑法同样可以适用于三数取中和小区间优化。

2.3挖坑法代码

//挖坑法
int PartSort(int* arr, int left, int right)
{
	int mid = GetMidIndex(arr, left, right);
	Swap(&arr[mid], &arr[left]);
	int key = arr[left];//选left做key,key是值
	int hole = left;//坑
	while (left < right)
	{
		while (left < right && arr[right] >= key)//右边找小,填到左边坑
		{
			--right;
		}
		arr[hole] = arr[right];
		hole = right;
		while (left < right && arr[left] <= key)//左边找小,填到右边坑
		{
			++left;
		}
		arr[hole] = arr[left];
		hole = left;
	}
	arr[hole] = key;//最后把key放到坑里
	return hole;//返回最后的坑位
}
//[begin,end]
void QuickSort(int* arr, int begin,int end)//递归快排
{
	if (begin >= end)
		return;
	if (end - begin <= 8)//小区间优化
	{
		InsertSort(arr+begin, end -begin + 1);//插入排序O(N^2)
		return;
	}
	else
	{
		int keyi = PartSort3(arr, begin, end);
		//[begin,keyi-1] key [keyi+1,end]
		QuickSort(arr, begin, keyi - 1);//快排
		QuickSort(arr, keyi + 1, end);//快排
	}
}

3、前后指针法

3.1前后指针法单趟排序动图

3.2前后指针法思想

prev指向初始位置,cur指向prev的下一个位置

prev先走,cur后走,当cur遇到比key大的值,prev停下,cur继续走,直到cur遇到比key小的数,prev再往前走,prev向前走完后,交换cur与prev的值

循环上述过程,直到cur>right,交换keyi和prev的值

动图看起来就像是prev在后面推着key大的数字往前走,当然该方法也要用三数取中和小区间优化。

3.3前后指针法代码

int PartSort(int* arr, int left, int right)
{
	int mid = GetMidIndex(arr, left, right);
	Swap(&arr[mid], &arr[left]);
	int keyi = left;//选left做key,keyi是下标
	int prev = left, cur = prev + 1;
	while (cur <= right)
	{
		if (arr[cur] < arr[keyi] && ++prev!=cur)
		{
			Swap(&arr[cur], &arr[prev]);
		}
		++cur;
	}
	Swap(&arr[prev], &arr[keyi]);
	return prev;
}
//[begin,end]
void QuickSort(int* arr, int begin,int end)//快排
{
	if (begin >= end)
		return;
	int keyi = PartSort(arr, begin, end);
	//[begin,keyi-1] key [keyi+1,end]
	if (end - begin <= 8)//小区间优化
	{
		InsertSort(arr+begin, end -begin + 1);//插入排序O(N^2)
		return;
	}
	else
	{
		QuickSort(arr, begin, keyi - 1);//快排
		QuickSort(arr, keyi + 1, end);//快排
	}
}

4、快排的非递归方法

4.1非递归的思想

从递归的方法可以看出,在第一次选出keyi时,可以对左右区间进行递归操作,重复选出keyi,不断对小区间进行排序,直至有序。同样的,非递归方法使用栈来模拟递归的过程,每次将左右区间的下标压入栈中,就执行一次该区间的单趟排序。直到栈空,说明数组已经有序。

4.2非递归代码

void NonFQuickSort(int* arr, int begin, int end)//非递归快排,使用栈
{
	ST 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(arr, left, right);//keyi是下标,单趟排序
		//[left,keyi-1] keyi [keyi+1,right]
		if (keyi + 1 < right)
		{
			StackPush(&st, keyi + 1);//压栈
			StackPush(&st, right);//压栈
		}
		if (left < keyi - 1)
		{
			StackPush(&st, left);//压栈
			StackPush(&st, keyi - 1);//压栈
		}
	}
	StackDestroy(&st);//销毁
}

七、归并排序

1、归并排序的递归方法

1.1归并排序的思想

现在想对一组无序数组排序,那么将这个数组均分成左右两个部分,只要左区间有序,右区间有序,那就可以借助额外的空间,将左右两个数组中小的数不断尾插,形成有序,最后将额外空间中的数据拷贝回原数组即可。那么左右区间怎么才能有序呢?通过递归的方法,对左右区间不断均分至区间内仅剩一个数,开始归并。

归并的缺点在于需要额外O(N)的空间,常用于解决磁盘的外排序。

1.2归并排序代码

void _MergeSort(int* arr, int begin, int end, int* tmp)
{
	if (begin >= end)
		return;
	int mid = begin + (end - begin) / 2;//将左右区间二分
	//[begin,mid][mid+1,end]
	_MergeSort(arr, begin, mid, tmp);
	_MergeSort(arr, mid+1, end, tmp);
	//升序归并,取小的尾插,重新定义起始位置,防止begin和end被改变
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (arr[begin1] <= arr[begin2])
		{
			tmp[i++] = arr[begin1++];
		}
		else
		{
			tmp[i++] = arr[begin2++];
		}
	}
	//左右区间必定有一个还没有尾插完
	while (begin1 <= end1)
	{
		tmp[i++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = arr[begin2++];
	}
	//将tmp部分区域数据拷贝回原数组
	memcpy(arr + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* arr, int size)
{
	int* tmp = (int*)malloc(sizeof(int) * size);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	_MergeSort(arr, 0, size - 1, tmp);
	free(tmp);
}

2、归并排序的非递归

2.1归并排序的非递归思想

因为递归版本是将数组内元素不断均分,直至区间内仅剩一个元素时,开始归并。那么非递归版本直接一一归并···两两归并····四四归并······直至整体有序。注意分类讨论边界问题:begin1是不可能越界的,当end1和begin2越界时,直接break即可;当end2越界时,修正end2=size-1继续进行归并。

2.2归并排序的非递归代码

void MergeSortNonR(int* arr, int size)
{
	int* tmp = (int*)malloc(sizeof(int) * size);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	int gap = 1;
	while (gap < size)
	{
		for (int j = 0; j < size; j += 2 * gap)
		{
			int begin1 = j, end1 = j + gap - 1;
			int begin2 = j + gap, end2 = j + 2 * gap - 1;
			//判断边界
			if (end1 >= size)
			{
				break;
			}
			if (begin2 >= size)
			{
				break;
			}
			if (end2 >= size)//修正end2,继续归并
			{
				end2 = size - 1;
			}
			int i = j;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (arr[begin1] <= arr[begin2])
				{
					tmp[i++] = arr[begin1++];
				}
				else
				{
					tmp[i++] = arr[begin2++];
				}
			}
			//还剩一个区间没有尾插完
			while (begin1 <= end1)
			{
				tmp[i++] = arr[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[i++] = arr[begin2++];
			}
			//将tmp数组中的数据拷贝回原数组
			memcpy(arr + j, tmp + j, sizeof(int) *(end2-j+1));//end2-j-1千万不可以写成2*gap,因为当end2被修正时没救不等于2*gap了
		}
		gap *= 2;;
	}
	free(tmp);
	tmp = NULL;
}

八、计数排序

1、计数排序的思想

计数排序适用于排序数组极差小的数据。此时range忽略,时间复杂度O(N)的话,也是一个非常强大的排序。

1、找出数组中的最大值和最小值,求出range=max-min+1

2、开辟range个空间并对这块区域初始化为0

3、通过相对映射对每个数出现的频次计数(同时数据已经被排序)

4、根据频次覆盖写入数据至原数组

5、free临时空间

2、计数排序代码

void CountSort(int* arr, int size)//计数排序,时间O(N+range),空间O(range)
{
	int min = arr[0], max = arr[0];
	//遍历找出最大、最小
	for (int i = 0; i < size; ++i)
	{
		if (arr[i] < min)
			min = arr[i];
		if (arr[i] > max)
			max = arr[i];
	}
	//malloc一个相对映射数组
	int range = max - min + 1;//malloc数组的元素个数
	int* Count = (int*)malloc(sizeof(int) * range);
	if (Count == NULL)
	{
		perror("malloc fail:");
		return;
	}
	memset(Count,0, sizeof(int) * range);
	//相对映射计数
	for (int i = 0; i < size; ++i)
	{
		++Count[arr[i] - min];
	}
	//将映射数据覆盖至原数组
	int j = 0;
	for (int i = 0; i < range; ++i)
	{
		while (Count[i]--)
		{
			arr[j++] = i + min;
		}
	}
	//释放malloc的空间
	free(Count);
}
  • 109
    点赞
  • 87
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 77
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蒋灵瑜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值