八大排序,倾囊而出(排序分类,核心思想,代码示例,复杂度理解,稳定性分析,适用场景)

八大排序可以分为划分为五种类型:

一、插入排序(直接插入排序,希尔排序)

二、选择排序(简单选择排序,堆排序)

三、交换排序(冒泡排序,快速排序)

四、二路归并排序

五、基数排序 

一、插入排序:

  • 直接插入排序(InsertSort):

1)核心思想:

       在要排序的一组数中,假设前面(n-1) [n>=2] 个数已经是排好顺序的,现在要把第n个数插到前面的有序数中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。

2)代码示例:

void InsertSort(int arr[],int len)
{//最好情况和最坏情况时间复杂度都为O(n^2)
	int i;//未排序序列待排序的元素下标
	int j;//已排序序列最大元素的下标
	int tmp;
	for (i = 1; i < len; i++)
	{
		tmp = arr[i];
		for (j = i - 1; j>=0 && arr[j]>tmp; --j)
		{
			arr[j + 1] = arr[j];
		}
		arr[j + 1] = tmp;
	}
}

3)复杂度理解:

      如果序列是完全有序的,插入排序只要比较n次,无需移动时间复杂度为O(n),如果序列是逆序的,插入排序要比较O(n²)和移动O(n²) ,所以平均复杂度为O(n²),最好为O(n),最坏为O(n²),排序过程中只要一个辅助空间,所以空间复杂度O(1)

4)稳定性分析:

        直接插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。刚开始这个小序列只包含第一个元素,事件复杂度O(n2)。比较是从这个小序列的末尾开始的。想要插入的元素和小序列的最大者开始比起,如果比它大则直接插在其后面,否则一直往前找它该插入的位置。如果遇见了一个和插入元素相等的,则把插入元素放在这个相等元素的后面。所以相等元素间的顺序没有改变,是稳定的

  • 希尔排序(ShellSort):

1)核心思想:

       希尔排序先将要排序的一组数按某个增量d(n/2,n为要排序数的个数)分成若干组,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。当增量减到1时,进行直接插入排序后,排序完成。

2)代码示例:

static void Shell(int *arr,int len,int gap)//一趟shell过程,gap为分组数量
{
	int i,j;
	int tmp;
	for(i=gap;i<len;i++)
	{
		tmp = arr[i];
		for(j=i-gap;j>=0&&arr[j]>tmp;j-=gap)
		{
			arr[j+gap] = arr[j];
		}
		arr[j+gap] = tmp;
	}
}

void ShellSort(int *arr,int len)//O(n^1.3~n^1.5),O(1),
{
	int d[] = {5,3,1};
	for(int i=0;i<sizeof(d)/sizeof(d[0]);i++)
	{
		Shell(arr,len,d[i]);
	}
}

3)复杂度理解:

       希尔排序的时间复杂度分析及其复杂,有的增量序列的复杂度至今还没人能够证明出来,只需要记住结论就行,{1,2,4,8,...}这种序列并不是很好的增量序列,使用这个增量序列的时间复杂度(最坏情形)是O(n²),Hibbard提出了另一个增量序列{1,3,7,...,2^k-1},这种序列的时间复杂度(最坏情形)为O(n^1.5),Sedgewick提出了几种增量序列,其最坏情形运行时间为O(n^1.3),其中最好的一个序列是{1,5,19,41,109,...},需要一个临时变量用来交换数组内数据位置,所以空间复杂度为O(1)

4)稳定性分析:

       希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的

二、选择排序:

  • 简单选择排序(SelectSort

1)核心思想:

        在要排序的一组数中,选出最小的一个数与第一个位置的数交换;然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。

2)代码示例:

void SelectSort(int arr[],int len)
{
	int i,j;
	int min;//待排序数中最小值的下标
	int tmp;
	for(i =0;i<len-1;i++)
	{
		min = i;
		for(j=i+1;j<len;j++)
		{
			if(arr[j] < arr[min])
			{
				min =j;
			}
		}
		tmp = arr[i];
		arr[i] = arr[min];
		arr[min] = tmp;
	}
}

3)复杂度理解:

      简单选择排序是冒泡排序的改进,同样选择排序无论序列是怎样的都是要比较n(n-1)/2次的,最好、最坏、平均时间复杂度也都为O(n²),需要一个临时变量用来交换数组内数据位置,所以空间复杂度为O(1)

4)稳定性分析:

       每个元素都与第一个元素相比,产生交换,两重循环O(n2);举个例子,5 8 5 2 9,第一遍之后,2会与5交换,那么原序列中两个5的顺序就被破坏了。所以是不稳定的排序算法。

  • 堆排序(HeapSort

1)核心思想:

       堆排序是一种树形选择排序,是对直接选择排序的有效改进。

       堆的定义如下:具有n个元素的序列(h1,h2,...,hn),当且仅当满足(hi>=h2i,hi>=2i+1)或(hi<=h2i,hi<=2i+1)(i=1,2,...,n/2)时称之为堆。在这里只讨论满足前者条件的堆。

       由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项(大顶堆)。完全二叉树可以很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个堆,这时堆的根节点的数最大。然后将根节点与堆的最后一个节点交换。然后对前面(n-1)个数重新调整使之成为堆。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。

       从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。

2)代码示例:

void Adjust(int *arr,int start,int end)//O(logn)//一次堆调整
{
	int tmp = arr[start];
	int parent = start;
	for(int i=2*start+1;i<=end;i=2*i+1)//
	{
		if((i+1<=end) && (arr[i]<arr[i+1]))
		{
			++i;
		}//i为左右孩子较大值的下标
		if(tmp < arr[i])
		{
			arr[parent] = arr[i];
			parent = i;
		}
		else
		{
			break;
		}
	}
	arr[parent] = tmp;
}

void HeapSort(int *arr,int len)
{
	int i;
	for(i=(len-1-1)/2;i>=0;i--)//第一次建大根堆,O(nlogn)
	{
		Adjust(arr,i,len-1);
	}

	int tmp;
	for(i=0;i<len-1;i++)//O(nlogn)
	{
		tmp = arr[0];
		arr[0] = arr[len-1-i];
		arr[len-1-i] = tmp;
		Adjust(arr,0,len-1-i-1);
	}
}

3)复杂度理解:

        堆排序的时间复杂度,主要在初始化堆过程和每次选取最大数后重新建堆的过程,初始化建堆时的时间复杂度为O(n),更改堆元素后重建堆的时间复杂度为O(nlogn),所以堆排序的平均、最好、最坏时间复杂度都为O(nlogn),堆排序是就地排序,空间复杂度为O(1)

4)稳定性分析:

        我们知道堆的结构是节点i的孩子为2*i和2*i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n/2-1, n/2-2, ...1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序是不稳定的排序算法。

三、交换排序:

  • 冒泡排序(BubbleSort

1)核心思想:

        在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。

2)代码示例:

void BubbleSort (int arr[],int len)
{
	int i,j;
	int tmp;
	for(i =0;i<len-1;i++)
	{
		for(j=0;j<len-1-i;j++)
		{
			if (arr[j+1] < arr[j])
			{
				tmp = arr[j];
				arr[j] = arr[j+1];
				arr[j+1] = tmp;
			}
		}
	}
}

3)复杂度理解:

      冒泡排序不管序列是怎样,都是要比较n(n-1)/2 次的,最好、最坏、平均时间复杂度都为O(n²),需要一个临时变量用来交换数组内数据位置,所以空间复杂度为O(1)。有很多人说冒泡排序的最优的时间复杂度为O(n),其实这是在代码中使用一个标志位来判断是否已经排序好的,是冒泡排序的优化版,如果元素已经排序好,那么循环一次就直接退出。

4)稳定性分析:

        冒泡排序是相邻元素之间的比较和交换,两重循环O(n2);如果两个相邻元素相等,是不会交换的。所以它是一种稳定的排序方法。

  • 快速排序(QuickSort

1)核心思想:

        选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。

2)代码示例:

int Partition(int *arr,int low,int high)//快排的一次划分,返回基准点,low和high的下标可取
{
	int tmp = arr[low];
	while(low<high)
	{
		while((low<high)&&arr[high]>=tmp)//从后往前找比基准小的数字
		{
			high--;
		}
		arr[low] = arr[high];

		while((low<high)&&arr[low]<=tmp)//从前往后找比基准大的数字 
		{
			low++ ;
		}
		arr[high]=arr[low];
	}
	arr[low] = tmp;
	return low;
}

递归与非递归俩种方法(任选其一即可):

①递归:

//递归快速排序
static void Quick(int *arr,int low,int high)//快排
{
	int par = Partition(arr,low,high); //一次快排后返回的基准点的位置
	if(par>low+1)
	{
		Quick(arr,low,par-1);//递归一次快排
	}
	if(par<high-1)
	{
		Quick(arr,par+1,high);//递归一次快排
	}

}
void QuickSort(int *arr,int len)//O(nlogn),O(logn),不稳定
{
	Quick(arr,0,len-1);
}

②非递归:

//非递归快速排序
void QuickSort(int *arr,int len)
{
	stack<int> s;//int *s = (int *)malloc(len*sizeof(int));
	int low = 0;
	int high = len-1;
	int par = Partition(arr,low,high);

	if(low < par-1)
	{
		s.push(low);
		s.push(par-1);
	}
	if(par+1 < high)
	{
		s.push(par+1);
		s.push(high);
	}

	while(!s.empty())
	{
		high = s.top();//获取栈顶值,不删
		s.pop();
		low = s.top();
		s.pop();

		par = Partition(arr,low,high);

		if(low < par-1)
		{
			s.push(low);
			s.push(par-1);
		}
		if(par+1 < high)
		{
			s.push(par+1);
			s.push(high);
		}
	}
}

3)复杂度理解:

       快速排序的时间复杂度最好是O(nlogn),平均时间复杂度也是O(nlogn),最坏情况是序列本来就是有序的,此时时间复杂度为O(n²),快速排序的空间复杂度可以理解为递归的深度,而递归的实现依靠栈,平均需要递归logn次,所以平均空间复杂度为O(logn)

4)稳定性分析:

        快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j]交换的时刻。

四、二路归并排序(MergeSort):

1)核心思想:

        二路归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

2)代码示例:

void Merge(int *arr,int len,int gap)//一趟归并,gap为归并段的长度
{
	int low1 = 0; //第一个归并段的起始下标
	int high1 = low1+gap-1;//第一个归并段的结束下标
	int low2 = high1+1;//第二个归并段的起始下标
	int high2 = low2+gap<len?low2+gap-1:len-1;//第二个归并段的结束下标
	int *brr = (int *)malloc(len*sizeof(int));
	int i = 0;//brr下标

	//有两个归并段
	while(low2 < len)//保证第二个归并段至少一个数字
	{
		//两个归并段都还有数据
		while(low1<=high1 && low2<=high2)
		{
			if(arr[low1] <= arr[low2])
			{
				brr[i++] = arr[low1++];
			}
			else
			{
				brr[i++] = arr[low2++];
			}
		}

		//一个归并段已完成,另一个还有数据
		while(low1<=high1)
		{
			brr[i++] = arr[low1++];
		}
		while(low2<=high2)
		{
			brr[i++] = arr[low2++];
		}
		low1 = high2+1;
		high1 = low1+gap-1;
		low2 = high1+1;
		high2 = low2+gap<len?low2+gap-1 : len-1;
	}

	//不足两个归并段
	while(low1 < len)
	{
		brr[i++] = arr[low1++];
	}

	for(i=0;i<len;i++)
	{
		arr[i] = brr[i];
	}

	free(brr);
}

void MergeSort(int *arr,int len)//O(nlogn),O(n),稳定
{
	for(int i=1;i<len;i*=2)
	{
		Merge(arr,len,i);
	}
}

3)复杂度理解:

      二路归并排序需要一个临时temp[]来储存归并的结果,空间复杂度为O(n),时间复杂度为O(nlogn),可以将空间复杂度由 O(n) 降低至 O(1),然而相对的时间复杂度则由 O(nlogn) 升至 O(n²)

4)稳定性分析:

    二路归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。

五、基数排序(RadixSort):

1)核心思想:

        基数排序就是将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

2)代码示例:

int FindMaxDigit(int arr[], int len)
{
	int maxnumber = arr[0];
	for (int i = 1; i < len; ++i)
	{
		if (arr[i] > maxnumber)
		{
			maxnumber = arr[i];
		}
	}
	int count = 0;
	while (maxnumber != 0)
	{
		maxnumber /= 10;
		count++;
	}
	return count;
}
/*
digit :
0   个位
1   十位
num  /10^0 %10;
num / 10^1 %10;
num / 10^2 %10;

double pow(double,int)
(int)pow(10.0,digit);
*/
int FindNumberDigit(int num, int digit)
{
	return num / (int)pow(10.0, digit) % 10;
} 

void Radix(int arr[], int **bucket,
	int len, int digit)
{
	//每个桶中现有的元素的个数
	int count[10] = { 0 };
	for (int i = 0; i < len; ++i)
	{
		int digitnumber = FindNumberDigit(arr[i], digit);
		bucket[digitnumber][count[digitnumber]] = arr[i];
		count[digitnumber]++;
	}
	int index = 0;
	for (int j = 0; j < 10; ++j)
	{
		for (int k = 0; k < count[j]; ++k)
		{
			arr[index++] = bucket[j][k];
		}
	}
}

void RadixSort(int arr[], int len)
{
	int** bucket = (int **)malloc(sizeof(int*)* 10);
	for (int j = 0; j < 10; ++j)
	{
		bucket[j] = (int *)malloc(sizeof(int)*len);
	}
	int maxdigit = FindMaxDigit(arr, len);
	for (int i = 0; i < maxdigit; ++i)
	{
		Radix(arr,bucket,len,i);
	}
	for (int j = 0; j < 10; ++j)
	{
		free(bucket[j]);
	}
	free(bucket);
}

3)复杂度理解:

       基数排序对于 n 个记录,执行一次分配和收集的时间为O(n+r),如果关键字有 d 位,则要执行 d 遍,所以总的时间复杂度为 O(d(n+r))。该算法的空间复杂度就是在分配元素时,使用的桶空间,空间复杂度为O(n)

4)稳定性分析:

   基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。


 

上述排序算法适用场景:


        若n较小(如n≤50),可采用直接插入简单选择排序。当记录规模较小时,直接选择较好,否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。

        若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序

        若序列初始状态基本有序,则直接插入和冒泡最佳,随机的快速排序也不错。插入排序对部分有序的数组很有效,所需的比较次数平均只有选择排序的一半。

       希尔排序比插入排序和选择排序要快得多,并且数组越大,优势越大。如果需要解决一个排序问题而又没有系统排序函数可用(例如直接接触硬件或者运行于嵌入式系统中的代码),可以先用希尔排序,再考虑是否替换为更复杂的排序算法。

       直接插入排序是而对于部分有序和小规模的数组比较实使用。

        归并排序可以处理数百万甚至更大规模的数组,但是插入排序和选择排序做不到。归并排序的主要缺点是辅助数组所使用的额外空间和n的大小成正比。

        快速排序的优点是原地排序(只需要一个很小的辅助栈),但是基准的选取是个问题,对于小数组,快速排序要比插入排序慢,但他是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短。

       堆排序的优点是在排序时可以将需要排序的数组本身作为堆,无需任何额外空间,与选择排序有些类似,但所需的比较要少得多,堆排序适合例如嵌入式系统或低成本移动设备中容量有限的场景。
 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值