排序素来美如玉,层进理解获芳心

注:本篇博客排序算法的实现默认都以升序为例

目录

一、(直接)插入排序

第一层境界:一组有序数据 插入 数tmp 使其有序(预备工作)

第二层境界:一组数据从乱序到升序(真正的插入排序)(第一层境界的变形~)

二、希尔排序

第一层境界:gap组数据之每组数据的排序

第二层境界:循环实现gap组数据的各组排序

第三层境界:gap组数据的最终排序算法

三、冒泡排序

第一层境界:傻傻比较走全程

第二层境界:边走边看中途停(第一层境界的优化)

四、堆排序

五、选择排序

六、快速排序

第一层境界:递归实现(三大版本)

避坑:

以上三个版本代码缺陷:

快速排序递归版本选key优化

第二层境界:非递归实现(借助栈)

快速排序复杂度分析

七、归并排序

第一层境界:递归实现

思想:数组一分为二,整体有序则需左右有序,左右有序则递归即可~

代码要点:

第二层境界:非递归实现(循环实现)

八、计数排序

九、八大排序总结


一、(直接)插入排序

第一层境界:一组有序数据 插入 数tmp 使其有序(预备工作)

思想:倒着依次比较数组元素和tmp大小,a[end]>tmp,数组元素后移,end-- ; a[end]<tmp,直接插入到 end+1 位置

关键:情况一和情况二最后均要执行 a[end+1]=tmp 该语句

代码实现:

void InsertSort(int* a, int n)
{
	int tmp;//一堆已经有序的数中插入tmp
	int end;//end为数组中最后一个数的下标
	while (end >= 0)
	{
		if (a[end] > tmp)
		{
			a[end + 1] = a[end];
			--end;
		}
		else
		{
			break;
		}
	}
	a[end + 1] = tmp;
}

第二层境界:一组数据从乱序到升序(真正的插入排序)(第一层境界的变形~)

思想:把境界一套进for循环

过程阐释:这是数组数据逐个插入排序的过程,第一个数显然不需要参与排序,tmp是每次要插入的值

两种写法的不同仅在于下标的控制而已,本质一样

//写法一:

void InsertSort(int* a, int n)
{
	for (int i = 1;i < n;i++)
	{
		int tmp = a[i];
		int end = i - 1;
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

//写法二:

void InsertSort(int* a, int n)
{
	for (int i = 0;i < n-1;i++)
	{
		int tmp = a[i+1];//保存待比较数据,防止数据被覆盖丢失
		int end = i;
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

插入排序算法性能分析:

时间复杂度:

最坏 : 逆序最坏---O(N^2)

最好:顺序最好---O(N)(√)  O(1)(×)  因为至少每个数都要和前一个数据比较一次

二、希尔排序

两大步骤:

1.预排序---使数组元素接近有序

实现预排序方法:分组,将数据每隔gap分一组,一共可以分gap组,利用直接插入排序的思想,实现每组数据有序,整体就接近有序

2.直接插入排序

每组排好序后,再用直接插入排序就可以更快地实现排序

第一层境界:gap组数据之每组数据的排序

每组数据的排序本质就是直接插入排序,但是每一组元素不是紧邻了,改一改代码即可~

以第1组数据的排序为例:

1.类比直接插入排序即可,注意相邻元素之间下标间隔gap即可,i+=gap

2.注意i范围,由于进入循环后要执行tmp = a[i+gap] 这句代码,因此 i <n-gap

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

第二层境界:循环实现gap组数据的各组排序

写法一:三层循环(两层for,一层while)

第一层境界代码示例只解决了第1组数据排序,那其他组呢?毫无疑问,外层套上for循环

一共有gap组,外层循环j的范围就从0到gap即可,内层循环i每次从j开始即可,这样就实现了第1组数据有序--->第二组数据有序--->····················第gap组数据有序

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

写法二:两层循环,更加简洁

写法一是每组数据彻底排好序后才进行下一组排序,而写法二是多组并排,不同组同时进行排序

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

第三层境界:gap组数据的最终排序算法

       前两层境界都是在实现希尔排序的第一个步骤---预排序,第三层境界就要实现最后的总体排序了,方法就是直接插入法,难道还需要再写一段直接插入排序代码吗?  no!

       直接插入排序的本质其实就是gap取1,所以我们只需要让gap最终取到1,就实现了目的~

       关键代码就是 gap = gap/3+1, 因为gap最终越除越小,最后gap/3必然可以取到0,再加1就是gap=1的情况,就自动实现了最终的直接插入排序

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
        //也可以写成gap = gap / 2;
		for (int i = 0;i < n - gap;i++)
		{
			int end = i;
			int tmp = a[i + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

希尔排序的特性总结:

1.希尔排序是对直接插入排序的优化

2.gap取值对排序的影响:

总结:gap>1都是预排序,gap==1时,数组已经接近有序,整体而言达到了优化的效果

细节:①gap越大,大的数可以更快到后面,小的数可以更快到前面,但越不接近有序

           ②gap越小,大数据和小数据都挪动越慢,但是越接近有序

           ③gap == 1, 就是直接插入排序

3.希尔排序的时间复杂度:

这是一个尚未证明的问题,有人总结出了经验公式,我们且认为是在O(n^1.25) 到 O(1.6*n^1.25)

三、冒泡排序

第一层境界:傻傻比较走全程

内层循环控制比较次数,外层循环控制趟数,冒泡本质是相邻数比较,两两交换,大数下沉~

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

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

第二层境界:边走边看中途停(第一层境界的优化)

第一层境界要比较n-1趟,无论中间排序排的如何了,甚至有序了,还是会走完余下趟数~

而第二层境界 是当某一趟走完发现没有发生任何元素的交换,那就说明已经有序了,直接跳出

void BubbleSort(int* a, int n)
{
	for (int i = 0;i < n - 1;i++)
	{
		bool exchange = true;
		for (int j = 0;j < n - 1 - i;j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], a[j + 1]);
				exchange = false;
			}
		}
		if (exchange)
		{
			break;
		}
	}
}

插入排序与冒泡排序算法性能比较分析

插入排序与冒泡排序时间复杂度都是经典的O(N^2),但插入更优一点,这时就要关注细节了~

结论:插入排序比冒泡排序适应性更强,尤其是当大部分数据有序,局部无序的时候,插入排序相比冒泡排序优势就显现出来了~

四、堆排序

堆排序在我的博客《独树一帜的完全二叉树---堆》已经详细讲解过了,这里只给出核心代码

void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child+1 < n && a[child] < a[child + 1])
		{
			child++;
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapSort(int* a, int n)
{
	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0;i--)
	{
		AdjustDown(a, n, i);
	}
	//向下调整
	int end = n - 1;
	while (end >= 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

五、选择排序

思想:每一轮待排序数据中选出最小的数据,依次放在最左边

第1趟遍历:选出最小放在a[0]位置;第2趟遍历:选出次小放在a[1]位置;依次类推~

void SelectSort(int* a, int n)
{
	for (int i = 0;i < n;i++)
	{
		int mini = i;
		for (int j = i + 1;j < n;j++)
		{
			if (a[mini] > a[j])
			{
				mini = j;
			}
		}
		Swap(&a[mini], &a[i]);
	}
}

选择排序优化:每次都要遍历一趟,找出了小的,不妨也把大的找出来,小的放左边,大的放右边

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mini = begin;
		int maxi = end;
		//该循环确定每一趟中的最大值下标和最小值下标
		for (int i = begin;i <= end;i++)
		{
			if (a[mini] > a[i])
			{
				mini = i;
			}
			if (a[maxi] < a[i])
			{
				maxi = i;
			}
		}
		//循环结束后应该交换最小值和begin位置元素,交换最大值和end位置元素
		Swap(&a[begin], &a[mini]);
		Swap(&a[end], &a[maxi]);
		//缩小区间范围
		begin++;
		end--;
	}
}

 避坑:当maxi和begin位置的值重合时,上述代码有问题

改进方法:在mini和begin位置元素交换完之后修正一下即可

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mini = begin;
		int maxi = end;
		//该循环确定每一趟中的最大值下标和最小值下标
		for (int i = begin;i <= end;i++)
		{
			if (a[mini] > a[i])
			{
				mini = i;
			}
			if (a[maxi] < a[i])
			{
				maxi = i;
			}
		}
		//循环结束后应该交换最小值和begin位置元素,交换最大值和end位置元素
		Swap(&a[begin], &a[mini]);
		//处理特殊情况---maxi和begin重叠
		if (begin == maxi)
		{
			maxi = mini;
		}	
		Swap(&a[end], &a[maxi]);
		//缩小区间范围
		begin++;
		end--;
	}
}

选择排序时间复杂度:O(N^2)

选择排序在实际中没有太多应用,没有自己的一技之长~

六、快速排序

第一层境界:递归实现(三大版本)

版本一:hoare版本

       被称为二叉树结构的快速排序---实际并非二叉树,只是递归过程类似二叉树结构,并且所有操作都是在原数组上进行的(以下图示多个数组只是为了形象展示过程)

思想:

纵观全局:随机选取一个基准值key,再使得key的左区间值均小于key,右区间值均大于key。如此key值就被调整到了正确位置,接下来对左右区间进行相同的操作,显然就是递归调用函数了~

庖丁解牛:如何让每一个区间选取的key来到正确位置呢?(注:hoare版本的key一般都选取区间左端点值或右端点值),下面以key取区间左端点为例

 接下来整个数组将被分成三部分:[begin,keyi-1]  keyi  [keyi+1, end],对左右区间进行递归~

 代码:(以下代码存在问题)

int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		while (a[right] > a[keyi])
		{
			right--;
		}
		while (a[left] < a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}

	Swap(&a[left], &a[keyi]);
    return left;
}
void QuickSort(int* a, int begin, int end)
{
	//结束条件两种情况
	//1.区间长度为0,代表只剩下一个数据
	//2.区间不存在
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort1(a, begin, end);
	//区间划分:[begin,keyi-1] && keyi && [keyi+1, end]
	//递归
	PartSort1(a, begin, keyi - 1);
	PartSort1(a, keyi + 1, end);
}

细节阐释:递归的结束条件:

当区间中只有一个数时就不需要排序了,即begin == end

当区间不存在时也应该结束递归 即begin >  end

避坑:

坑1:left起始位置可以是begin+1(×)

因此left下标必须从begin开始,不能从begin+1开始

但是代码仍存在问题,以下是某些特殊情况的分析(坑2与坑3)

坑:2:死循环

填坑:>改>=  ,  <改 <=

坑3:越界

一波刚平,一波又起,就在刚在填了坑1和坑2之后坑3就产生了,下标有可能会越界

 填坑:内层while循环加入left与right大小判断

 到此为止,坑就填完了,代码也已经全部正确~

int PartSort1(int* a, int left, int right)
{
	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[left], &a[keyi]);
	return left;
}
void QuickSort(int* a, int begin, int end)
{
	//结束条件两种情况
	//1.区间长度为0,代表只剩下一个数据
	//2.区间不存在
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort3(a, begin, end);
	//区间划分:[begin,keyi-1] && keyi && [keyi+1, end]
	//递归
	PartSort3(a, begin, keyi - 1);
	PartSort3(a, keyi + 1, end);
}

hoare版本难点分析:

问:当left和right相遇之后,会交换a[left]和a[keyi],如何保证相遇位置的值一定比a[keyi]要小?

答:当keyi取的是区间左端点的时候,要让right先走,同理,当keyi取的是区间右端点的时候,要让left先走

证明:(以keyi选取左端点下标为例)

left和right相遇,只有两种情况:

1. right处于停止状态,left遇right ;

right停止,说明right遇到了比a[keyi]小的值,此时left遇right, a[left] 就 < a[keyi]

2. left处于停止状态,   right遇left ;

right遇left,则在相遇的这一轮left没有动,left之前可能压根没动或者交换了一些轮次

①left 没动的话,就是keyi的位置,那么right也就来到了keyi位置,自己和自己交换结果不受影响

②交换了一些轮次的话,则left始终存储了比keyi小的值~

版本二:挖坑法(更好理解,"坑"也更少)---hoare版本的变形---代码大同小异

       为什么说挖坑法更好理解,因为整个过程left和right其中一个必然是坑,无论是right找到了小,还是left找到了大,直接填坑即可,然后形成新的坑~

代码:

int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
	return hole;
}
void QuickSort(int* a, int begin, int end)
{
	//结束条件两种情况
	//1.区间长度为0,代表只剩下一个数据
	//2.区间不存在
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort2(a, begin, end);
	//区间划分:[begin,keyi-1] && keyi && [keyi+1, end]
	//递归
	PartSort2(a, begin, keyi - 1);
	PartSort2(a, keyi + 1, end);
}

版本三:前后指针法(最推荐的版本)---天才设想

第一轮操作如下图所示,后序仍然是递归调用自己~

 前后指针法本质分析:

       无论a[cur]与a[keyi]大小关系如何,cur一直向后走;而a[cur]<a[keyi]时,prev才向后走,a[cur]>=a[keyi]时,prev不动,只有cur向后走

       这就导致如果prev和cur中间间隔数据,那么这些数据必然比a[keyi]要大,因此当cur遇到比a[keyi]小的数据就交换prev后面一个数据和当前cur的值,使得小数换到前面,大数去到后面, 所以大数会跳跃式的往后移动,小数也会跳跃式的往前移动~

       当cur越界跳出循环时,prev前面的所有值就都比a[keyi]小了,再交换a[prev]与a[keyi],就使得左区间<a[keyi], 右区间>a[key],继续递归即可

 代码实现

int PartSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if(a[cur] < a[keyi])
		{
			prev++;
			Swap(&a[prev], &a[cur]);
		}
        //更好的if写法
        if(a[cur] < a[keyi] && ++prev != cur) //自己和自己交换没有意义
        {
            Swap(&a[prev], &a[cur]);
        }
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}
void QuickSort(int* a, int begin, int end)
{
	//结束条件两种情况
	//1.区间长度为0,代表只剩下一个数据
	//2.区间不存在
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort3(a, begin, end);
	//区间划分:[begin,keyi-1] && keyi && [keyi+1, end]
	//递归
	PartSort3(a, begin, keyi - 1);
	PartSort3(a, keyi + 1, end);
}
以上三个版本代码缺陷:

主要体现在选key上,我们默认都是区间左端点值做key,但是当数组有序的时候每次左区间不存在,右区间长度减少1,因此时间复杂度就是标准的等差数列---O(N^2),效率低下~

快速排序递归版本选key优化

1.三数取中:区间的左端点值,右端点值,中间位置的值三个数中选大小适中的数作为key

三数取中代码实现:

int GetMidIndex(int* a, int left, int right)
{
	int midi = (left + right) / 2;
	if (a[left] < a[midi])
	{
		if (a[midi] < a[right])
		{
			return midi;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else//a[left] > a[midi]
	{
		if (a[midi] > a[right])
		{
			return midi;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

三个版本在函数开始加上这两句代码就实现了三数取中的优化方案~ 

2.随机数选key---三数取中进一步优化

int GetMidIndex(int* a, int left, int right)
{
	int midi = left + (rand() % (right - left));
	if (a[left] < a[midi])
	{
		if (a[midi] < a[right])
		{
			return midi;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else //a[left] > a[midi]
	{
		if (a[midi] > a[right])
		{
			return midi;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

3.三路划分---专门解决快速排序遇到有大量重复元素的问题

大量重复元素会导致什么问题呢?

         大量重复元素导致的问题和数组有序导致的问题有异曲同工之处,都是每次右区间长度只减少1,导致了时间复杂度是标准的等差数列---O(N^2),而三路划分可以解决这个问题

三路划分:

三路划分核心逻辑:

1.a[cur]<key,交换a[cur]和a[left],++left,++cur

2.a[cur]>key,交换a[cur]和a[right],--right

3.a[cur] == key ,++cur

理解:left一直指向了key的位置,而整个过程本质是把left甩左边,right甩右边,把和key相等的值往中间推的过程

代码:

//三路划分实现快排
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int left = begin;
	int right = end;
	int cur = left + 1;
	
	int key = a[left];
	while (cur <= right)
	{
		if (a[cur] < key)
		{
			Swap(&a[left], &a[cur]);
			++left;
			++cur;
		}
		else if (a[cur] > key)
		{
			Swap(&a[right], &a[cur]);
			--right;
		}
		else
		{
			cur++;
		}
	}
	//子区间
	//[begin, left-1], [left, right], [right+1, end]
	//递归调用
	QuickSort(a, begin, left - 1);
	QuickSort(a, right + 1, end);
}

ps:三路划分中key的选取同样可以采用随机数选key来实现

第二层境界:非递归实现(借助栈)

"梅须逊雪三分白,雪却输梅一段香",快速排序递归法外强中干,却有着栈溢出的风险~,高手写代码,能用非递归,不用递归~

递归无非也是在递归每段区间罢了,利用栈的后进先出,我们同样可以采用循环处理每段区间

思想:每次从栈里面拿出一段区间,单趟分割处理,左右区间入栈

用栈处理区间实现快速排序过程示意图

 四大问题:

1.一段区间如何入栈?

答:分成右端点和坐端点依次入栈即可(左端点先入栈,右端点后入栈也可以)

2.一个区间划分为左右区间后哪个区间先入栈?

答:都可以,就看你想先处理左区间还是想先处理右区间

先处理左区间---右区间先入栈;先处理右区间---左区间先入栈;

3.什么时候压栈结束?

答:当区间左端点>=右端点,即不需要再划分区间也就不需要区间入栈了

4.什么时候循环结束?

答:栈为空

代码(以先处理左区间为例)---栈的实现参考我的博客《别样的数据结构---栈》

#include"Sort.h"
#include"Stack.h"
void PrintArray(int* a, int n)
{
	for (int i = 0;i < n;i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

int PartSort3(int* a, int left, int right)
{
	int keyi = left;
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi])
		{
			prev++;
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	return prev;
}


void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	STInit(&st);
	STPush(&st, end);
	STPush(&st, begin);
	while (!STEmpty(&st))
	{
		int left = STTop(&st);
		STPop(&st);

		int right = STTop(&st);
		STPop(&st);

		int keyi = PartSort3(a, left, right);

		if (keyi+1 < right)
		{
			STPush(&st, right);
			STPush(&st, keyi + 1);

		}
		
		if (left < keyi - 1)
		{
			STPush(&st, keyi-1);
			STPush(&st, left);
		}
	}
	STDestroy(&st);
}
快速排序复杂度分析

1.时间复杂度:

最好:每次找到的key都是中位数,快排过程的逻辑结构就是完美的二叉树---O(N*logN)

最坏:数组有序(若选取左端点做key)---O(N^2)

按理来说时间复杂度是按照最坏情况来算的,但是快排的时间复杂度是O(N*logN),因为我们一般不会直接选取左端点选key,而是采取随机数选k或者三数取中进行了优化

2.空间复杂度:

O(logN)---递归调用消耗logN层栈帧

七、归并排序

第一层境界:递归实现

思想:数组一分为二,整体有序则需左右有序,左右有序则递归即可~

其实归并排序的思想本质就是二叉树遍历方法中后序的思想---左右子树遍历完才遍历根

那么“归并”二字体现在哪?就体现在左右区间有序之后的合并过程从而使整体有序

而归并肯定要新建一个数组,归并好了之后拷贝回原数组,直接在原数组归并会产生覆盖值的问题

 

 代码:

//归并排序递归实现
void _MergeSort(int* a, int begin, int end, int* tmp)
{
	//begin不可能>end
	if (begin == end)
	{
		return;
	}
	//分割区间
	int mid = (begin + end) / 2;
	//[begn,mid] [mid + 1, end]
	
	//递归
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);
	
	//归并
	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);
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}
代码要点:

1.递归结束条件:begin == end ,也就是区间只有一个数;begin不可能>end, 因为区间划分是[begin, mid] [mid+1, end] ,而mid=(begin+end) / 2 , 当begin和end相等时mid最大等于end,而此时就是区间只有一个数的情况,已经不需要再分了

2.memcpy函数要注意src和dest位置,因为每次二分区间之后区间下标不一定是从0开始;还要注意数据个数,不是i, i是单纯的下标,应该用end-begin+1来算

归并排序复杂度分析:

时间复杂度:O(N*logN)---完美的二叉树

空间复杂度:O(N)       开辟tmp数组:O(N) ;  递归调用:O(logN)   总共:O(N+logN)---O(N)

归并排序递归优化版本:小区间优化

        归并排序每次都是把区间一分为二,然后递归,但是慢慢到后面,一个区间数据就很少了,这时仍然去递归调用归并排序效率就比较低了,这时可以调用插入排序函数解决剩下的排序~

区间长度小于多少开始调用插入函数不是固定的,一般取10差不多

进阶理解:可以看到,最下面三层递归调用次数占了总次数的87.5%,而倒数第四层每组数据差不多就是10个,因此没必要再提前调用插入函数了,意义不大~

第二层境界:非递归实现(循环实现)

思路:通过控制归并每组数据个数gap的值,实现不断地归并,直至整个数组有序

代码建立一:gap取特定值时归并实现

好风凭借力,送我上青天~,cv一下归并核心代码

注意归并的两组数据每组数据是gap个,因此归并的两段区间begin与end位置要注意改一改(画图)

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail\n");
		return;
	}
	//gap表示要归并的两个组的每个组的数据个数
	//gap ==1 表示一一归
	int gap = 1;
	int j = 0;
	for (int i = 0; i < n;i+=2*gap)
	{
		//每组数据的归并
		int begin1 = i, end1 = i + gap - 1;
		int begin2 = i + gap, end2 = i + 2 * gap - 1;
		while (begin1 <= end1 && begin2 <= end2)
		{
			if (a[begin1] < a[begin2])
			{
				tmp[j++] = a[begin1++];
			}
			else
			{
				tmp[j++] = a[begin2++];
			}
		}
		while (begin1 <= end1)
		{
			tmp[j++] = a[begin1++];
		}
		while (begin2 <= end1)
		{
			tmp[j++] = a[begin2++];
		}
	}
}

代码建立二:控制gap变化实现归并排序

//归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail\n");
		return;
	}
	//gap表示要归并的两个组的每个组的数据个数
	//gap ==1 表示一一归
	int gap = 1;
	while (gap < n)
	{
		int j = 0;
		for (int i = 0; i < n;i += 2 * gap)
		{
			//每组数据的归并
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
		}
		//每合并一次把数据拷贝回原数组
		memcpy(a, tmp, sizeof(int) * n);
		gap *= 2;
	}
	free(tmp);
}

8个数据测试确实没有问题,但是当测试9个数据,10个数据的时候,程序直接崩溃了

错误分析:越界问题,只要当数据个数不是2^N次方都会越界~

 打印区间下标分析越界情况:

错误解决方案1:遇到越界情况直接break跳出for循环

错误原因:因为最后要把tmp数组拷贝回a数组,如果直接break跳出循环,则tmp数组最后面数据就是随机值了,拷贝回原数组,不但后面数据没有排序,还覆盖了原数据

正确解决方案1:归并一段,拷贝一段

无非就是要处理越界的三种情形,可以发现,情形1,2属于一类,都是要归并的右区间整个不在数组范围内,压根不需要归并,直接break即可;而情形3的右区间部分越界,因此我们重新修正一下边界end2取值,使其等于数组最后一个数据下标即可(n-1),然后归并之后往回拷贝时只拷贝在数组的那一段就行

ps:归并的两数组并没有要求数据个数相同~

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail\n");
		return;
	}
	//gap表示要归并的两个组的每个组的数据个数
	//gap ==1 表示一一归
	int gap = 1;
	while (gap < n)
	{
		int j = 0;
		for (int i = 0; i < n;i += 2 * gap)
		{
			//每组数据的归并
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			//处理越界情况1、2
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			//处理越界情况3
			//修正end2边界
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
			//每归并一段,就拷贝该段到原数组
			memcpy(a+i, tmp+i, sizeof(int) * (end2 - i + 1));
		}
		gap *= 2;
	}
	free(tmp);
}

错误解决方案2:仍采用归并完一趟后整体拷贝的方法,若越界,则修正边界到n-1

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail\n");
		return;
	}
	//gap表示要归并的两个组的每个组的数据个数
	//gap ==1 表示一一归
	int gap = 1;
	while (gap < n)
	{
		int j = 0;
		for (int i = 0; i < n;i += 2 * gap)
		{
			//每组数据的归并
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			if (end1 >= n)
			{
				end1 = n - 1;
				begin2 = n - 1;
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
				begin2 = n - 1;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
		}
		memcpy(a, tmp, sizeof(int) * n);
		gap *= 2;
	}
	free(tmp);
}

打印边界下标分析错误原因:

 正确解决方案2:修正边界,直接使得右区间不存在,就不会执行归并相关逻辑代码,直接把第一个区间数据拷依次拷贝到tmp中

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail\n");
		return;
	}
	//gap表示要归并的两个组的每个组的数据个数
	//gap ==1 表示一一归
	int gap = 1;
	while (gap < n)
	{
		int j = 0;
		for (int i = 0; i < n;i += 2 * gap)
		{
			//每组数据的归并
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 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[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
		}
		memcpy(a, tmp, sizeof(int) * n);
		gap *= 2;
	}
	free(tmp);
}

归并排序的硬伤在于数据拷贝~

归并排序复杂度

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

空间复杂度:O(N)

八、计数排序

       计数排序相比其他七大排序就比较特别了,因为其他七大排序都用到了数据的比较,也就是数据都要比大小,但是计数排序的实现不用比较数据大小,计数就是统计数据出现个数

步骤:

1.统计每个数据出现的个数

2.根据统计次数排序

初阶理解:绝对映射

进阶理解:相对映射

  代码:

void CountSort(int* a, int n)
{
	int min = a[0], max = a[0];
	for (int i = 0;i < n;i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}
		if (a[i] > max)
		{
			max = a[i];
		}
	}
	int range = max - min + 1;
	int* countA = (int*)malloc(sizeof(int) * range);
	memset(a, 0, sizeof(int) * range);
	//统计次数
	for(int i = 0;i<n;i++)
	{
		countA[a[i] - min]++;
	}
	//排序
	int k = 0;
	for (int j = 0;j < range;j++)
	{
		while (countA[j]--)
		{
			a[k++] = j + min;
		}
	}
}

计数排序复杂度分析:

时间复杂度:O(N+range) ---  当N >> range --- O(N)

空间复杂度:O(Range)

计数排序缺陷:

缺陷1:依赖数据范围,适用于范围集中的数组

缺陷2:只能用于整形数据排序

九、八大排序总结

问世间情为何物,不过是一物降一物,除了及个别排序外,剩下的排序都是各有千秋的~

比较排序具有普适性,可以适用于任意数据样本,而非比较排序---计数排序就比较挑剔了~

排序稳定性:

简单来说,数组中有若干相同的值,排序完之后这些相同的值前后位置保持不变,就说该排序是稳定的,否则就是不稳定的

稳定性意义何在?值不都相同嘛,在前在后有意义吗?当然有!

比如说考试3个同学成绩相同,那么根据交卷时间前后就可以分出一二三等奖~

再比如说两个同学总分相同,但语文成绩高低不同,这时要排名的话,可以先按照语文成绩排序,然后采用一个稳定的排序算法对总分排序,那么总分相同的同学排名高低立现~

本篇博客关于排序算法的分享就到这了,日拱一卒无有尽,功不唐捐终入海,不弃微末,久久为功,欢迎大家交流指正~

 

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值