八大排序详解(动图演示)

图表总结

排序方式平均情况最好情况最坏情况辅助空间(空间复杂度)稳定性
冒泡排序O(N^2)O(N^2)O(N^2)O(1)稳定
选择排序O(N^2)O(N^2)O(N^2)O(1)不稳定
插入排序O(N^2)O(N)O(N^2)O(1)稳定
希尔排序O(N*logN)~O(N^2)O(N^1.3)O(N^2)O(1)不稳定
快速排序O(N*logN)O(N*logN)O(N^2)O(logN~N)不稳定
堆排序O(N*logN)O(N*logN)O(N*logN)O(1)不稳定
归并排序O(N*logN)O(N*logN)O(N*logN)O(N)稳定
计数排序O(max(range,N)O(max(range,N)O(max(range,N)O(range)稳定

请添加图片描述

冒泡排序

请添加图片描述

思想:

遍历数组,每次将最大或者最小的数放到最后一位
1.排升序时,将当前的数和其后面的数比较,如果比他小,就交换俩数的位置,如果比他大,就不做操作,继续进行这样的比较,直到到达比较的边界位置处。
2.排降序时,将将当前的数和其后面的数比较,如果比他大,就交换俩数的位置,如果比他小,就不做操作,继续进行这样的比较,直到到达比较的边界位置处。

代码实现:

//冒泡排序
void Bubblesort(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n - 1 - i; j++)
		{
			if (a[j] > a[j + 1])     //排升降序控制这里就好了
			{
				Swap(&a[j], &a[j + 1]);
			}
		}
	}
}

解释:
因为有n个数,所以需要遍历n次,但每一次遍历的数的个数都会比上一次遍历的个数要少一个,举例说明,假设n为10,比如说i等于1时,这相当于第二次遍历,因为前面已经将最大或者最小的数已经放到数组的末尾,所以我们这次遍历只需要遍历(n-i)个数就好了,即9个数,但在数组中坐标是从0开始的,而我们控制边界也是控制数组的下标,所以此时边界为(n-i-1),即8,即a[j+1]中的(j+1)最大就是8,即数组中的第9个位置。

选择排序

请添加图片描述

注:上面放的图是排升序的
思想:

假设要将n个数排成升序,我们就要遍历n次,每次都记录其中最大(或者最小)的数的下标,遍历结束后,将其丢到最后面(或者最前面)

  1. 升序:
  • 每次找最大丢后面
  • 每次找最小丢前面
  1. 降序
  • 每次找最大丢前面
  • 每次找最小丢后面

代码实现如下:

//普通版选择排序
void Selectsort2(int* a, int n)
{
	int maxi = 0;   //默认最大的数为a[0]
	int end = n - 1;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n - i; j++)
		{
			if (a[j] > a[maxi])       //排升降序控制这里就好了
			{
				maxi = j;
			}
		}
		--end;
		Swap(&a[maxi], &a[end]);     //每次将最大的值放到尾巴去
		//Swap(&a[maxi], &a[n-i-1]);
	}
}

下面也给出升级版的选择排序:
思想:

每次遍历将最大或者最小的数选出来,同时排好范围内最大或者最小的数

//选择排序 升级版
void Selectsort(int* a, int n)
{
	
	int begin = 0;     //规定未排序的区间
	int end = n - 1;   //规定已排序的区间
	int maxi = begin;  //将数组首元素赋给俩个人
	int mini = begin;
	while (end > begin)
	{
		for (int i = begin; i <= end; i++)
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
			if (a[i] < a[mini])
			{
				mini = i;
			}
		}
		Swap(&a[begin], &a[mini]);
		if (begin == maxi)    //处理最大的数在begin位置时 及时更新maxi的位置
		{
			maxi = mini;
		}
		Swap(&a[end], &a[maxi]);
		++begin;
		--end;
	}
}

注意: 如果我们先交换的begin位置和mini位置处的数,这个时候就可能会有最大的数刚好在begin位置,这时候如果我们直接交换,就会出现end位置放到并不是最大的数,就会使排序出错。
处理方法:每次begin和mini位置交换完之后,进行判断,如果maxi==begin的话,就及时更新maxi。

插入排序

请添加图片描述

思想:

将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。

下面我们先从将一个数插入到一个有序数组中形成新的数组开始。
思路:从有序数组的末尾开始遍历,如果比要插入的数大,就将其往后挪一位,直到找到比需要插入的数的小,就将要插入的数放在此数后面

int x = 4;//要插入的数据
	int end = n - 1;//指向数组末尾
	while (end >= 0)
	{
		if (a[end] > x)    //end指向的元素大于x 将往后挪一位
		{
			a[end + 1] = a[end];
			end--;
		}
		else       //找到比x小的数 跳出循环 将x排在该数后面
		{
			break;
		}
	}
	a[end + 1] = x;

而我们的插入排序的思路正是这个思路:将一个数插入到有序的数组中,形成新的有序数组。而这个怎么原地在数组中实现呢?也很简单,只要我们将排序想成(n-1)次插入数据:从将一个数插入到一个只有一个数的数组中,再到将一个数插入到这个新形成的含有俩个数的有序数组中,以次下去,就排好序了。
实现方法:end从0开始,需要插入的数为a[end+1]

   //N次遍历 真插入排序 时间复杂度O(N^2)
	for (int i = 0; i < n - 1; i++)    //这里end最大为n-1-1,是因为插入数的位置最大为n-1
	{
		int end = i;//指向数组末尾
		int x = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > x)    //end指向的元素大于x 将往后挪
			{
				a[end + 1] = a[end];
				end--;
			}
			else       //找到比x小的数 跳出循环 将x排在该数后面
			{
				break;
			}
		}
		a[end + 1] = x;
	}

希尔排序

请添加图片描述

  • 前言:发明这个排序的大佬发现插入排序在排接近有序的序列时,效率很高,时间复杂度几乎接近O(N),于是他就想出来插入排序的升级版,先将序列变得接近有序,最后再使用一次插入排序,而使序列变得更有序的方法就是将距离为gap的数分到一组进行插入排序,反复改变gap,进行操作,且保证最后一次操作时的gap为1,即用插入排序去排解决有序的数组。

思想:

先选定一个整数(gap),把待排序文件中所有记录分成几个组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后对gap进行处理,取到新的gap,重复上述分组和排序的工作。当到达gap=1时,所有记录在统一组内排好序。

下面我们举个例子希尔排序是怎么进行排序的说明一下:
请添加图片描述
第一次我们取gap为5,即距离为5的被分到同一组,所以10个数被分成了[9,4] [1,8] [2,6] [5,3] [7,5] 五组,然后对每组里面数进行排序操作后变成[4,9][1,8][2,6][3,5][5,7]
请添加图片描述接着我们对gap进行/2操作,得到gap=2,即距离为2的数被分到一组,10个数又被成[4,2,5,8,5] [1,3,9,6,7]俩组,重复上面的操作得到新的数组

gap为1,就是我们之前所说的插入排序
在这里插入图片描述

代码实现如下:

int gap = n;
	while (gap >1)  //此处应该为 gap>1 因为如果是gap=1时 以gap>0为条件 会时gap进去俩次 多进入一次
	{
		gap /= 2; //保证gap能等于1就行 gap等于1相当于 原来的插入排序
		/*gap = gap / 3 + 1;*/
		for (int i = 0; i < n - gap; i++)         //当i走到n-gap时就结束了 因为这是以gap为距离的几个小数组的末尾中最小的末尾
		{
			int end = i;//指向数组末尾
			int x = a[end + gap];  //指向end后gap的数据
			while (end >= 0)
			{
				if (a[end] > x)    //end指向的元素大于x 将往后挪
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else       //找到比x小的数 跳出循环 将x排在该数后面
				{
					break;
				}
			}
			a[end + gap] = x;
		}
	}

而上面的代码又是怎么来的呢?
解释:

灵魂一式

  • 我们先从对1组距离为gap的数进行插入排序的实现开始.
    我们先放出代码再进行解释
	int gap = 5;
	    //遍历一次以gap为距离的小区间
	for (int i = 0; i < n - gap; i+=gap)
	{
		int end = i;//指向数组末尾
		int x = a[end + gap];  //指向end后gap的数据
		while (end >= 0)
		{
			if (a[end] > x)    //end指向的元素大于x 将往后挪
			{
				a[end + gap] = a[end];
				end-=gap;
			}
			else       //找到比x小的数 跳出循环 将x排在该数后面
			{
				break;
			}
		}
		a[end + gap] = x;
	}

拿第一组数{9,4}来解释,end一开始为9的下标0,则x为a[0+gap]即4,然后进行插入排序,因为9是比x大的,就把9丢到end的后gap位去即a[0+gap],然后对end进行一次-=gap操作,此时end为0-gap,已经小于0了,所以会跳出循环,然后将x丢到a[end+gap]处,即a[0]处。

灵魂第二式

  • 然后我们接下来进行实现的就是对距离为gap的数进行插入排序
    思路:

我们发现每个小数组的首位数据都是俩俩相邻的,我们自然就会想到嵌套循环,而我们那层循环控制为多少次呢,我们会发现一个规律,gap为多少,数组就会被分成多少组数据,所以外层循环次数就为gap次

下面放代码 😗

    //遍历gap次以gap为距离的小区间
	for (int j = 0; j < gap; j++)
	{
		for (int i = j; i < n - gap; i += gap)
		{
			int end = i;//指向数组末尾
			int x = a[end + gap];  //指向end后gap的数据
			while (end >= 0)
			{
				if (a[end] > x)    //end指向的元素大于x 将往后挪
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else       //找到比x小的数 跳出循环 将x排在该数后面
				{
					break;
				}
			}
			a[end + gap] = x;
		}
	}

灵魂第三式

  • 现在的代码已经很接近源代码里面的样子,但还是能做出优化,我们发现可以将外层循环剥掉,将内循环的跳整距离gap改为1也是一样的,就此得出优化版本,我称之为乱炖型遍历
     //乱炖遍历数组 按照距离为gap遍历
	for (int i = 0; i < n - gap; i++)         //当i走到n-gap时将结束了 因为这是以gap为间隔的小数组的末尾中最小的末尾
	{
		int end = i;//指向数组末尾
		int x = a[end + gap];  //指向end后gap的数据
		while (end >= 0)
		{
			if (a[end] > x)    //end指向的元素大于x 将往后挪
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			else       //找到比x小的数 跳出循环 将x排在该数后面
			{
				break;
			}
		}
		a[end + gap] = x;
	}

灵魂第四式

  • 最后一步:就是控制我们的gap,而控制gap也是有讲究的,必须保证最后的gap=1,一般有俩种方法:
  1. 每次/2
  2. 每次/3+1

下面丢出代码:😃

	  //接下来开始控制gap的大小
	int gap = n;
	while (gap >1)  //此处应该为 gap>1 因为如果是gap=1时 以gap>0为条件 会时gap进去俩次 多进入一次
	{
		gap /= 2; //保证gap能等于1就行 gap等于1相当于 原来的插入排序
		/*gap = gap / 3 + 1;*/
		for (int i = 0; i < n - gap; i++)         //当i走到n-gap时就结束了 因为这是以gap为距离的几个小数组的末尾中最小的末尾
		{
			int end = i;//指向数组末尾
			int x = a[end + gap];  //指向end后gap的数据
			while (end >= 0)
			{
				if (a[end] > x)    //end指向的元素大于x 将往后挪
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else       //找到比x小的数 跳出循环 将x排在该数后面
				{
					break;
				}
			}
			a[end + gap] = x;
		}
	}

快速排序

请添加图片描述

基本思想:

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

主框架:

// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
 if(right - left <= 1)
 return;
 
 // 按照基准值对array数组的 [left, right)区间中的元素进行划分
 int div = partion(array, left, right);
 
 // 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
 // 递归排[left, div)
 QuickSort(array, left, div);
 
 // 递归排[div+1, right)
 QuickSort(array, div+1, right);
}

框架定了我们下面来实现里面的子函数:
:下面取基准值为key

单趟遍历三式

1. (hoare版本)左右指针法实现

请添加图片描述
思路:

Left指针每次找比基准值小大的数,Right指针每次找比基准值key小的数。然后交换俩个指针位置的值。当俩指针相遇是,将key位置的值和俩指针相遇处的值交换,从而使左边的值都小于key,右边的值都大于key

通常key有俩种选择:

  • 一是最左边left所在处
  • 二是最右边right所在处
    下面我们先以key取最left处解释,key为最左边元素时,我们先让R指针去寻找比key小的值,然后再找L指针去找比key大的值,这样可以保证最后左右指针相遇处的值永远小于或等于key位置的值,因为最坏的情况就是R指针没有找到比key小的值,一路走到L,L还未出发就相遇了,key原地换。
    key取最右边原理也是相同的,让L先走,去找比key位置处的值大的数,然后再让R出发去找比基准值小的值,这样可以保证最后左右指针相遇处的值永远大于或等于key位置的值,然后确保最后交换之后满足左边的值都小于key,右边的值都大于key。

下面是代码实现:

//子函数1 -- 前后指针
int Partion1(int* a, int left, int right)
{
	//三值取中 处理有序的情况 防止最坏情况出现
	int mid = getMid(a, left, right);
	//将此值换到key位置
	Swap(&a[mid], &a[left]);
	//默认key为left
	int keyi = left;
	while (left < right)   //左右指针还未相遇时
	{
		//1.右边先挪动找小
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}
		//2.左边找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}
		Swap(&a[left], &a[right]);
	}
	//最后交换key位置和俩指针相遇位置
	Swap(&a[left], &a[keyi] );

	return left;    //key值即为left和right相遇位置
}

代码实现时需要注意的点:
1.左指针找大和右指针找小时,注意越界问题。
例:右边找小时,一直没有找到比key小的值,这时候没有left<right的限制的话,right就会往前越界。
2.寻找比key值大或小的数,一定不要严格大于或者小于
例:假设Left找大时,我们把a[left] <= a[keyi],里面的=号去掉,第一次L就会继续停在key位置处,然后进行交换,就会把key值一开始就丢出去了。

2. 挖坑法

请添加图片描述
思路:

选择一个基准值:

  1. 最左边位基准值,将基准值所在位置处设为坑位,然后R指针出发找小,找到小的了,就将找到值丢到坑位处,然后将R当前所在处设为新坑位,接下来L指针开始找大,找到大的就将值丢到坑位中,然后就地形成新坑位,直接R,L指针相遇为止,然后将key值丢到俩指针相遇处。
    这里让右指针R先走也是为了确保R,L指针最后相遇时的值是比key值小的。*
  2. 最右边位基准值,将基准值所在位置处设为坑位,然后L指针出发找大的,找到大的了,就将找到值丢到坑位处,然后将L当前所在处设为新坑位,接下来R指针开始找小,找到小的就将值丢到坑位中,然后就地形成新坑位,直接R,L指针相遇为止,然后将key值丢到俩指针相遇处。
    这里让左指针L先走也是为了确保R,L指针最后相遇时的值是比key值大的。

代码:😃

//挖坑法
int Partion2(int* a, int left, int right)
{
	//初始坑设在最左边
	int pit = left;
	int key = a[pit];     //保留初始坑的值
	while (left < right)
	{
		//1.右边先找小,
		while (left<right && a[right] >key)
		{
			--right;
		}
		//将找到的值丢到已有都坑,形成新的坑
		a[pit] = a[right];
		pit = right;
		//2.左边再找大
		while (left < right && a[left] < key)
		{
			++left;
		}
		//将找到的值再丢到现有的坑 生成新的坑
		a[pit] = a[left];
		pit = left;
	}
	//将key的值丢到最后的坑位 
	a[pit] = key;
	return pit;
}

3. 前后指针法

请添加图片描述
前言:此方法也是最多人使用的最上面的动图使用的也是这个方法,这个方法实现较好控制。
思路: 将小的丢到左边,大的翻到右边

1.key值为最左边:
prev初始指向key值处,cur指针每次走在prev的前面,找比key值小的数,找到了之后,先++prev,然后交换俩位置的值,直到cur越界为止,一直重复上述操作,最后将key位置的值和prev所在位置的值交换。
2.key值为最右边:
prev初始指向最左边的前一个位置处,cur从数组首元素位置出发,cur指针每次走在prev的前面,找比key值小的数,找到了之后,先++prev,然后交换俩位置的值,直到cur走到key位置为止,一直重复上述操作,最后先++prve,再将key位置的值和prev所在位置的值交换。

好控制的原因:上面俩种方法都是不会遍历到key值处的

代码实现: 😃

//方法三:前后指针 cur prev
int Partion3(int* a, int left, int right)
{
	//默认key为最左边
	int keyi = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		//cur找比key值要小的
		if (a[cur] < a[keyi] && ++prev != cur)       //cur在prev前面时可不交换,因为++prev之后,俩指针就会在同一位置
		{
			Swap(&a[prev],&a[cur]);
		}
		++cur;
	}
	Swap(&a[prev], &a[keyi]);
	return prev;    //返回key值所在处
}

快排小优化

快排的最坏情况:处理已经有序的数组时,此时每次的key值会一直在最左端或者最右端
,时间复杂度就会到达O(N^2)
请添加图片描述
解决方法:三值取中,将L,R和中间位置处的值选择出一个大小居中的值作key

int getMid(int* a, int left, int right)
{
	int mid = left + (right - left) / 2;    //防止 left加right 越界
	if (a[mid] > a[left])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if(a[right]>a[left])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[right] > a[left])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

然后在上面3个方法前加上

//三值取中 处理有序的情况 防止最坏情况出现
	int mid = getMid(a, left, right);
	//将此值换到key位置
	Swap(&a[mid], &a[left]);

堆排序

详细堆排序见之前的文章
链接
下面直接丢出代码:😃

void Adjustdown(int* a,int parent,int n)
{
	int child = parent*2+1;
	while (child < n)
	{
		 //默认较大或者较小的孩子是左孩子
		if ( (child+1)<n &&a[child + 1] > a[child])  //控制好 防止越界
		{
			child++;
		}
		if (a[child] > a[parent])    
		{
			Swap(&a[child], &a[parent]);
		}
		//迭代
		parent = child;
		child = parent * 2 + 1; 
	}
}
//堆排序   法一:先搓个堆 然后top k次 法二:原地造堆
void Heapsort(int* a, int n)
{
    //原地建堆 向下调整原地建堆
	
	for (int i = (n - 1 - 1) / 2; i>=0 ; i--)
	{
		Adjustdown(a, i, n );
	}
	//然后进行调整位置,进行向下调整
	int end = n - 1;
	while (end>= 1)
	{
		Swap(&a[0],&a[end]);
		Adjustdown(a, 0, end);
		//Print(a, n);
		end--;
	}
}

归并排序


基本思想:

分治的思想:将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有
序,再使子序列段间有序

代码: 😃

void _Mergesort(int* a, int left, int right, int* tmp)
{
	//递归结束条件
	if (left >= right)
	{
		return;
	}
	//先划分区间
	int Mid = left + (right - left) / 2;
	_Mergesort(a,left, Mid,tmp);
	_Mergesort(a, Mid + 1,right, tmp);![请添加图片描述](https://img-blog.csdnimg.cn/4467403de4e942fcb799857c37fb8d3b.gif)


    //开始归并数组 俩个有序数组合并
	int begin1 = left; int end1 = Mid;
	int begin2 = Mid+1; int end2 = right;
	int cur = left;
	while (begin1 <= end1 && begin2 <= end2)   //有一个子数组遍历完了就结束了
	{
		if (a[begin1] < a[begin2])
		{
			tmp[cur++] = a[begin1++];
		}
		else
		{
			tmp[cur++] = a[begin2++];
		}
	}
	//处理还没结束的数组
	while (begin1 <= end1)
	{
		tmp[cur++] = a[begin1++];
	}
	while (begin2 <= end2)    //此处不可写 begin!=end2 会造成越界访问
	{
		tmp[cur++] = a[begin2++];
	}
	//将合并好的数组tmp再拷回原数组a
	for (int i = left; i <= right; i++)
	{
		a[i] = tmp[i];
	}
}
void Mergesort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
	}

	_Mergesort(a, 0, n - 1, tmp);
	//释放归并的数组
	free(tmp);
	tmp = NULL;
}

计数排序

请添加图片描述
操作步骤:

  1. 统计相同元素出现次数
  2. 根据统计的结果将序列回收到原来的序列中

代码实现: 😃

// 时间复杂度:O(Max(N, Range))
// 空间复杂度:O(range)
// 适合范围比较集中的整数数组
// 范围较大,或者是浮点数等等都不适合排序了
void CountSort(int* a, int n)
{
	int max = a[0], min = a[0];
	for (int i = 1; i < n; ++i)      //遍历确定范围
	{
		if (a[i] > max)
		{
			max = a[i];
		}

		if (a[i] < min)
		{
			min = a[i];
		}
	}

	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int)*range);
	memset(count, 0, sizeof(int)*range);
	if (count == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	// 统计次数
	for (int i = 0; i < n; ++i)
	{
		count[a[i] - min]++;
	}

	// 根据次数,进行排序
	int j = 0;
	for (int i = 0; i < range; ++i)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
}

:上面也可以处理负数,因为使用的是相对映射
例:假设里面最小的数是-2,min即为-2,然后-2就会被放在count[-2-(-2)],即count[0]处,而在拷回原数组时,会以(0+(-2))的形式拷回去。

未完待续……还有归并和快排的非递归版本,俩者都可以用栈实现,归并还可以用循环实现。

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值