八大排序总结

目录

插入排序

直接插入排序

思路与实现

单趟排序

完整排序

 分析

 时间复杂度

空间复杂度

稳定性

希尔排序

思路与实现

分析 

时间复杂度

空间复杂度

稳定性

选择排序

选择排序

思路与实现

单趟排序

完整排序

改进

分析 

时间复杂度

空间复杂度

稳定性

堆排序

思路与实现

分析

时间复杂度

空间复杂度

稳定性

交换排序

冒泡排序

思路与实现

单趟排序

完整排序

改进

分析

时间复杂度

空间复杂度

稳定性

快速排序

思路与实现

单趟排序

完整排序 

改进 

快排非递归形式

分析

时间复杂度

空间复杂度

稳定性

归并排序

思路与实现

 归并排序非递归形式

分析

时间复杂度

空间复杂度

稳定性

计数排序

思路与实现

分析 

时间复杂度

空间复杂度


插入排序

直接插入排序

思路与实现

思路:从前到后遍历,遍历的每一个位置的数都去比较前面的数,找到合适的位置插入

如下为动图:(来源于网络)

实现

单趟排序

int tmp = a[end+1];
//end从tmp位置前一个开始向前遍历
while (end >= 0)
{
	if (tmp < a[end])//找大,找到的值往后移动
    {
        a[end+1] = a[end];
        end--;
    }
    else
    {
    	break;//找到小的跳出
    }
}
a[end+1] = tmp;//找到的比tmp小的值的上一个值,赋予它tmp的值
完整排序

        在单趟排序基础上加一个循环。

        我设定end的下一个位置的值tmp从前向后遍历,tmp每次保存end的下一个位置数组的值,end作用是每次循环确定tmp的值后,end向前遍历,得到的值与tmp比较,比tmp大的值赋给后一个,直到找到比tmp小的值,跳出循环,将找到的比tmp小的值的上一个赋成tmp。

void InsertSort(int* a, int n)
{	
    //tmp向后遍历
	for (int i = 0;i < n-1;i++)//i只到n-1,因为end最多给到n-2,tmp给到a[n-1]
	{
		int end = i;
		int tmp = a[end+1];//保存起来
        //end从tmp位置前一个开始向前遍历
		while (end >= 0)
		{
			if (tmp < a[end])//找大,找到的值往后移动
			{
				a[end+1] = a[end];
				end--;
			}
			else//找到小的跳出
			{
				break;
			}
		}
		a[end+1] = tmp;//找到的比tmp小的值的上一个值,赋予它tmp的值
	}
}
 分析
 时间复杂度

O( N^2)

最坏情况:O( N^2)

        该组数据全是逆序。

        tmp前面全是比它大的,每遍历一次都要将该处end的值移到end+1,一直要遍历的end=0,才能插入。

        一开始的 tmp 与 end=0 位置的值有 1 次比较,最后一次的 tmp 与 end=n-2,n-3……1,0 位置的值有 n-1 次比较。

        总体来说:

1+2+\cdots+\left( n-2 \right) +\left( n-1 \right) \\ =\frac{n\left( n-1+1 \right)}{2} \\ =\frac{n^2}{2}

最好情况:O( N)

        该组数据全是顺序。

        每次都是比较,不用挪动数据,仅仅只是end(本质上是 tmp=end+1 移动,从开始到最后)从开始移到了最后,n-1

        实际上直接插入排序针对于接近有序的情况会十分快。

空间复杂度
O\left( 1 \right)

        没有申请额外的空间。

稳定性

        稳定。

        注意到之前的代码 tmp < a[end] 不能取等,若取等后相等的数比较时移到后面,导致不稳定。

while (end >= 0)
{
	if (tmp < a[end])//不能取等,取等后不稳定
	{
		a[end+1] = a[end];
		end--;
	}
	else
	{
		break;
	}
}
a[end+1] = tmp;

希尔排序

思路与实现

        在直接插入排序中,直接插入排序针对于接近有序的情况会十分快。希尔排序也是这样产生的。

思路:对数据进行多次预排序,使数据接近有序,再用直接插入排序,进行排序。

实现:

step1:

        本质上是直接插入排序,只不过引入了步长,划分成好几个组,每个组组内进行直接插入排序。

        给定一个gap(步长),从第一个元素开始,每gap步长的设为一组,进行直接插入排序

gap = 3;//例子
//剩下与直接插入排序一样,只不过步长从1变成了gap
for (int i = 0;i < n - gap;i+=gap)
{
	int end = i;
	int tmp = a[end+gap];
	while (end >= 0)
	{
		if (tmp < a[end])
		{
			a[end + gap] = a[end];
			end-=gap;
		}
		else
		{
			break;
		}
	}
	a[end+gap] = tmp;
}

        实际上gap=1时就是直接插入排序。 

step2:

        i=1 , i=2 …… i=n ,将所有的元素全部分组,后进行直接插入排序。

int gap = 3;
for (int j = 0;j < gap;j++)//j如果取到gap,就与第一组分组的情况重合
{
	for (int i = j;i < n - gap;i+=gap)
	{
		int end = i;
		int tmp = a[end+gap];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + gap] = a[end];
				end-=gap;
			}
			else
			{
				break;
			}
		}
		a[end+gap] = tmp;
	}
}

step3:

         改变gap,将gap减小(我取的是gap/3+1),达到多次预排序的目的,最后一次gap=1直接插入排序,将所有接近有序元素排好。

while (gap > 1)//取等死循环,最后一直gap=1
{
	gap = gap/3 + 1;//加1使最后除以3时一定为1,从而进行最后的直接插入排序
	for (int j = 0;j < gap;j++)
    {
		for (int i = j;i < n - gap;i+=gap)
		{
			int end = i;
			int tmp = a[end+gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end-=gap;
				}
				else
				{
					break;
				}
			}
			a[end+gap] = tmp;
		}
	}
}
分析 
时间复杂度

        接近于O\left( N^{1.3} \right)

空间复杂度

        O\left( 1 \right),没有开辟额外的空间

稳定性

        不稳定。

        原因:不同的分组会导致一些相等的数分在不同的组,这些不同的组在直接插入排序时互不干扰,可能导致原本相等排在前面的数现在排在后面。

例子:

选择排序

选择排序

思路与实现

思路:遍历数组找到最小的(最大的),使其与第一个数交换,将剩下的数看成一个整体,执行之前步骤。

动图演示:(来源于网络)

实现:

单趟排序

        begin在数组最开始,end在数组最末尾,begin赋给min。

         i依次遍历找到比min对应的值小的数就把i赋给min,直到数组结束,这样min指向的是整个数组最小的数。

         

        交换begin与min指向的值,begin向后挪动,单趟排序完成。

//单趟排序代码
min = begin;
for (int i = begin;i <= end;i++)
{
	if (a[i] < a[min])
	{
		min = i;
	}
}
Swap(&a[begin], &a[min]);
begin++;
完整排序

        外面加一个循环,多次进行单趟排序。每次进行遍历后排除掉第一个已经排好位置的数,剩下的数进行单趟排序。

while (begin < end)
{
    //单趟排序
	min = begin;
	for (int i = begin;i <= end;i++)
	{
		if (a[i] < a[min])
		{
			min = i;
		}
	}
	Swap(&a[begin], &a[min]);
	begin++;//起到了每次排除第一个已经排好的数的作用
}

 完整代码如下:

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	int min = begin;
	while (begin < end)
	{
		min = begin;
		for (int i = begin;i <= end;i++)
		{
			if (a[i] < a[min])
			{
				min = i;
			}
		}
		Swap(&a[begin], &a[min]);
		begin++;
	}
}
改进

为了提升效率,在每次遍历时,找到最大和最小的数,替换掉最开始和结束的位置。

        但要注意到这里一个坑,在交换的时候,如果begin索引是max索引,当min指向的值交换掉begin指向的值的时候,begin指向的值反而是最小了,max指向的值被换到min指向的值,这样交换会将最小的交换到最后,导致出错。

        图示:当第一个是最大值时,

      为了解决这样的问题,在这里我加入一个判断,min指向的值与begin指向的值交换完成时,如果max索引是begin的索引,说明原本的max指向的值被交换到min指向的值,这时只要再交换一下min与max索引即可。 

//进行交换
Swap(&a[begin], &a[min]);

//添加的判断
if (max == begin)
{
	Swap(&min, &max);
}

Swap(&a[end], &a[max]);

整体代码如下:

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
    //设定两个值分别存储最大和最小
	int min=begin;
	int max=end;
	while (begin < end)
	{
		min = begin;
		max = end;
		for (int i = begin;i <= end;i++)
		{
            //比较,找到最大,最小
			if (a[i]>a[max])
			{
				max = i;
			}
			if (a[i] < a[min])
			{
				min = i;
			}
		}
        //进行交换
		Swap(&a[begin], &a[min]);

        //添加的判断
		if (max == begin)
		{
			Swap(&min, &max);
		}

		Swap(&a[end], &a[max]);
		begin++;
		end--;
	}
}
分析 
时间复杂度

        O\left( N^{2} \right)

空间复杂度

        O\left( 1 \right),没有申请额外的空间

稳定性

        不稳定

        原因:以未改进的选择排序为例,

堆排序

思路与实现

        思路:建大堆排升序,对数据进行建大堆,将第一个元素与最后一个元素交换,排除掉最后一个已经排好位置的数,将其他元素再次建大堆(实际上就只有堆顶不符合大堆的条件),利用堆的向下调整算法,对堆顶的数进行调整再次形成大堆,以此类推。

        实现

step0:向下调整算法

        适用于parent节点两边都是大堆,通过该算法可以直接调整成大堆。

        向下调整过程: 

        通过孩子节点计算父节点:parent = ( child - 1 ) / 2

        通过父节点计算孩子节点:

        左孩子:leftchild = parent × 2 + 1

        右孩子:rightchild = parent × 2 + 2

void AdjustDwon(int* a, int n, int parent)//排大堆的向下调整算法
{
    //先用左孩子,若右孩子更多,再换成右孩子
	int child = parent * 2 + 1;

	while (child < n)
	{
        //找最大孩子过程
        //1.若在倒数第一个节点,该节点是左孩子,child+1会导致越界
		//2.判断是否为最大孩子,若有右孩子,且右孩子大于左孩子,进入循环
        //该循环作用是将child变成左右孩子最大的那一个的下标
		if (child + 1 < n && a[child + 1] > a[child])
		{
			child++;
		}

        //调整过程
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = root * 2 + 1;
		}
		else
		{
			break;
		}
	}

}

step1:建大堆

        建大堆原因是堆顶数最大,移到堆尾后,刚好为升序的最后一个数。

         i 从最后一个节点的父节点开始,数组从0开始计数,最后一个节点的下标为 child = n - 1,其父节点的下标为parent  = ( child - 1 )/ 2,故从(n-1-1)/2开始。

for (int i = (n - 1 - 1) / 2;i > 0;i--)//i从最后一个节点的父节点开始
{
	AdjustDwon(a, n, i);
}

step2:将堆顶数移到堆尾,继续向下调整

int end = n - 1;//记录最后一个元素的位置,每一次循环都要end--,即将最后一个元素排除
while (end > 0)
{
	Swap(&a[0], &a[end]);
	AdjustDwon(a, end, 0);
	end--;
}

完整代码: 

void AdjustDwon(int* a, int n, int root)
{
	int child = root * 2 + 1;

	while (child < n)
	{
		//判断是否为最大孩子
		if (child + 1 < n && a[child + 1] > a[child])
		{
			child++;
		}

        //孩子与父节点比较
		if (a[child] > a[root])
		{
			Swap(&a[child], &a[root]);
			root = child;
			child = root * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	//建大堆
	for (int i = (n - 1 - 1) / 2;i > 0;i--)
	{
		AdjustDwon(a, n, i);
	}

    //交换堆顶和最后一个元素,并继续调整
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDwon(a, end, 0);
		end--;
	}
}
分析
时间复杂度

        O\left( NlogN \right)

        分析:向下调整算法时间复杂度是常数次

空间复杂度

        O\left( 1 \right),没有申请额外空间。

稳定性

        不稳定。

        原因:堆是一种树状结构,在调整的时候,左右两个子树互不干扰,但是把堆写成数组形式是左右两个子树的元素相互交叉组合。

        例子:在堆顶和最后一个元素交换时导致不稳定。

交换排序

冒泡排序

思路与实现

思路:每次从开始遍历将较大的元素向后移,这样第一次遍历就能将最大的元素移到最后的位置,

再遍历一遍就能将倒数第二个元素的位置归位,以此类推。

动图演示:(来源于网络)

实现

单趟排序

        将最大的元素移到最后,以下面这个为例:

for (int j = 0;j < n - 1;j++)
{
	if (a[j] > a[j + 1])
	{
		Swap(&a[j], &a[j + 1]);
	}
}
完整排序

        在每次单趟排序后,都去掉最后一个元素进入下一轮遍历。

for (int i = 0;i < n;i++)
{
	for (int j = 0;j < n - i - 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 - i - 1;j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
			}
		}
	}
}
改进

        进入一个flag变量,初始化为0,在交换处令其为1,如果一趟排序后flag仍然为0,说明这一趟排序中没有交换,该序列是顺序的,所以没有必要进行后续排序。

        代码如下:

void BubbleSort(int* a, int n)
{
	int flag = 0;//flag变量
	for (int i = 0;i < n;i++)
	{
		for (int j = 0;j < n - i - 1;j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
				flag = 1;
			}
		}
		if (flag == 0)//若第一趟排序没有交换说明是顺序的,直接跳出结束
		{
			break;
		}
	}
}
分析
时间复杂度

        O\left( N^{2} \right)

最坏情况:全部逆序,每一个都要比较交换。

\left( n-1 \right) +\left( n-2 \right) +\cdots +2+1 \\ =\frac{\left( n-1 \right) n}{2} \\ =\frac{1}{2}n^2-\frac{1}{2}n

        结果是O\left( N^{2} \right)

最好情况:全部顺序,改进的情况下,只需遍历一遍,时间复杂度是O\left( N \right)

        但是稍微有一点逆序,改进的情况失效,虽然很多地方不会进行交换,但是都要遍历,同最坏情况一样的计算过程,结果是O\left( N^{2} \right)

空间复杂度

        O\left( 1 \right),没有申请额外的空间。

稳定性

        稳定。

        这一步中,相等不交换,大于才交换,保证了稳定性。

if (a[j] > a[j + 1])//大于才交换
{
	Swap(&a[j], &a[j + 1]);
}

快速排序

思路与实现

思路:选择一个key,通过单趟排序直接让key到其准确位置上,则其左边是比它小的数,右边是比它大的数

实现

单趟排序

1.hoare版本

        动图演示:(来源于网络)

        设定left,key在开头,right指向末尾,右边right先走,遇到小于key的停下(不能小于等于,会导致死循环),然后左边再走,遇到大于key的停下,交换right和left指向的数。

        注意:在 right 找小,left 找大的过程中,必须要有 left < right 条件否则可能发生 left 与 right 错过的现象。

例如:

代码如下:

while (a[right] >= a[keyi] && left < right)//右边找大停止,注意要保证left始终小于right
{
	right--;
}
while (a[left] <= a[keyi] && left < right)//左边找小停止
{
	left++;
}
Swap(&a[left], &a[right]);

        再继续右边先走,找小停下,左边再走,找大停下,交换……以此类推,最终一定相遇,相遇时交换key指向的值和相遇时left与right指向的值。

 代码如下:

	while (left < right)
	{
		while (a[right] >= a[keyi] && left < right)//右边找大停止
		{
			right--;
		}
		while (a[left] <= a[keyi] && left < right)//左边找小停止
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
    //最后交换相遇位置left指向的值和keyi指向的值
	Swap(&a[left], &a[keyi]);

        这样,6的左边都是比它小的,右边都是比它大的,6到了正确的位置,以后都不用调整。 

一个问题:为什么左边作为key,让右边先走?

        答:保证相遇位置指向的值比key指向的值小,这样交换后,使得左边的值都比相遇位置指向的值小。

情况:这里不考虑中间来来回回的过程,只考虑最后相遇前的那一趟过程。

        1.right先走,right停下来,left去遇到right。相遇的位置就是right停下的位置,既然right是找小那自然相遇的位置指向的值比key指向的值小

        2.right先走,right没找到比key指向的值小的值,却遇到了left(由于left<right导致停止),相遇停止。

        这有有两种可能:一种是left是上一轮中停下的位置,此时Left指向的值已经与上一轮中right指向的比key小的值交换此时自然比key小

        另一种可能是该数组是顺序的或者key选到了最小的值,right从一开始就到了最左边与left相遇,这种情况单趟排序自然是排好的

        总结下来就是,左边做key,右边先走,右边做key,左边先走。 

hoare版本单趟排序的总代码如下:

int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
        //右边先走
		while (a[right] >= a[keyi] && left < right)//右边找大停止
		{
			right--;
		}
        //左边后走
		while (a[left] <= a[keyi] && left < right)//左边找小停止
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
    //最后交换相遇位置left指向的值和keyi指向的值
	Swap(&a[left], &a[keyi]);
    keyi = left;
	return keyi;//返回相遇位置的值
}

2.挖坑法 

动图演示:(来源于网络)

        与hoare版本类似,把key储存起来,原来的key位置为坑,右边先走,找到小后,将小的数填入坑中,这时右边这个位置变成坑,再左边走,找到大后,将大的数填入坑中,这时左边这个位置变成坑,以此类推,直到left与right相遇后停下,此时相遇位置是坑,再将储存的key值填入坑中,结束。

代码如下:

int PartSort2(int* a, int left, int right)
{
	int key = a[left];//用key保存值,而不用keyi保存索引,原因是keyi索引的值会由于填坑发生改变
	int hole = left;
	while (left < right)
	{
		while (a[right] >= key && left < right)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;
		while (a[left] <= key && left < right)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
	return hole;
}

 3.前后指针法

动图演示:(来源于网络)

        这个方法与前面的方法有明显不同。

        设定prev=left,cur=prev+1,cur找小于key指向的值,找到后停下,并且prev++,然后prev对应的值与cur交换。让这个过程循环起来,当cur越过数组(cur>right)时结束。

 剩下过程如下:

代码如下: 

int PartSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = prev + 1;

	int keyi = left;
	while (cur <= right)//cur越界时停止循环
	{
		if (a[cur] < a[keyi])//cur找小
		{
			prev++;
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}
完整排序 

        选取其中一个单趟排序,利用单趟排序后,返回相遇位置的值,这个值左边都比他小,右边都比他大,我把这个值记为keyi。

        实际上通过这个相遇位置keyi把数组分成了两个部分[ left , keyi-1 ] ,[ keyi+1 , right ],再对这两个区间进行单趟排序,以此类推,就可以排好整个数组。

void QuickSort(int* a, int left, int right)
{
	int keyi = PartSort1(a, left, right);
	//int keyi = PartSort2(a, left, right);
	//int keyi = PartSort3(a, left, right);    
	if (left>= right)//跳出条件
	{
		return;
	}
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
改进 

1.三数取中

        在单趟排序中,我选左边作为key,但是遇到一些极端情况,比如选取的key的最终位置就在开头或结尾,这样排序没有充分利用到递归折半的良好性质,这样排序的效率是很低的,我们更加希望选取的key的最终位置在中间。

例如:

        这样的复杂度达到 O\left( N^2 \right),自然不如key选的数在中间,这样折半递归来得快。

        所以选取key时用三数取中,即选取开头中间结尾三个数中,选取中间大小的数,让其与left最左边的数交换,这样选到的数就不会特别偏左或者偏右了。

三数取中代码如下:

int GetMidi(int* a, int left, int right)//三数取中
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])  // mid是最大值
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else // a[left] > a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] < a[right]) // mid是最小
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
int PartSort1(int* a, int left, int right)
{
    //三数取中
    int midi = GetMidi(a, left, right);
    Swap(&a[left], &a[midi]);
 
	int keyi = left;
	while (left < right)
	{
		while (a[right] >= a[keyi] && left < right)
		{
			right--;
		}
		while (a[left] <= a[keyi] && left < right)
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
    keyi = left;
	return keyi;
}

2.小区间优化

        对于大数据量的排序,递归次数多,递归深度深,可能导致栈溢出,或者导致时间开销和空间开销大。

        为了解决这个问题,我在递归到每个数组足够小时(数组长度小于等于10),不在用递归,而是用直接插入排序,这样可以减少大量的调用次数。

代码如下:

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	// 小区间优化,小区间不再递归分割区间进行排序,降低递归次数
	if ((right- left + 1) > 10)//区间长度 right - left + 1
	{
		int keyi = PartSort3(a, left , right);

		QuickSort1(a, left , keyi - 1);
		QuickSort1(a, keyi + 1, right);
	}
	else
	{
		InsertSort(a + left , right- left + 1);//区间够小时用直接插入排序
        //注意是a+left不是a,因为多次分割区间后区间,不一定是从a开始,要根据left确定
	}
}

总的改进后的快速排序代码:(hoare版本为例)

//三数取中
int GetMidi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right]) 
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else // a[left] > a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
//单趟排序
int PartSort1(int* a, int left, int right)
{
    //三数取中
    int midi = GetMidi(a, left, right);
    Swap(&a[left], &a[midi]);
 
	int keyi = left;
	while (left < right)
	{
		while (a[right] >= a[keyi] && left < right)
		{
			right--;
		}
		while (a[left] <= a[keyi] && left < right)
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
    keyi = left;
	return keyi;
}
//快速排序
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	if ((right- left + 1) > 10)
	{
		int keyi = PartSort3(a, left , right);

		QuickSort1(a, left , keyi - 1);
		QuickSort1(a, keyi + 1, right);
	}
	else
	{
		InsertSort(a + left , right- left + 1);
	}
}
快排非递归形式

        对于快速排序递归形式在极端条件下,可能会递归深度太深。为了解决这样的现象,可以将递归形式改成非递归形式。

        递归改非递归通常有两种方式:1.直接改成循环,比如求斐波那契数列第n项,归并排序非递归形式。2.用数据结构模拟递归过程。

        用栈模拟递归,注意先进后出,本质上就是把每一次递归中分割的节点装进栈里面,每次取出进行单趟排序。

代码如下:

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

		int right = STTop(&ps);
		STPop(&ps);
		int keyi = PartSort3(a, left, right);

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

		if (keyi - 1 > left)
		{
			STPush(&ps, keyi - 1);
			STPush(&ps, left);
		}
	}

	STDestroy(&ps);
}

        用队列模拟递归,注意先进先出。

void QuickSortNonR(int* a, int left, int right)
{
	Queue ps;
	QueueInit(&ps);
	QueuePush(&ps, right);
	QueuePush(&ps, left);
	while (!StackEmpty(&ps))
	{
		int left = QueueFront(&ps);
		QueuePop(&ps);

		int right = QueueFront(&ps);
		QueuePop(&ps);

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


		if (keyi - 1 > left)
		{
			QueuePush(&ps, left);
			QueuePush(&ps, keyi - 1);
		}

		if (keyi + 1 < right)
		{
			QueuePush(&ps,keyi + 1);
			QueuePush(&ps,right);
		}
	}

	QueueDestroy(&ps);
}
分析
时间复杂度

        O\left( NlogN \right)

最好情况:每次选取的key指向的值是刚好整组数据的中位数。

        每一层遍历的和是 O\left( N \right),又根据二分的特性,共有logN层,所以复杂度为O\left( NlogN \right)

最坏情况:每次选取的key是整组数据的最小值或者是最大值。

        递归n次,每一次遍历一遍数组。 

        ​​​​​​​n+\left( n-1 \right) +\cdots +2+1 \\ =\frac{n\left( n+1 \right)}{2} \\ =\frac{1}{2}n^2+\frac{1}{2}n 

        结果是O\left( N^{2} \right)

空间复杂度

        O\left( 1 \right)

稳定性

        不稳定

        left与right相遇时,相遇位置与key位置的值交换会导致不稳定。

归并排序

思路与实现

思路

step1:进行区间划分

        将n个区间对半分,分出来的区间继续对半分,直到划分至一个数, 不在分割。

step2:

将每次分割的数进行归并,返回,这使得每一次归并都是有序的。

动图演示:(来源于网络)

实现:与思路有不同,毕竟思路是比较理想化的做法。

大致实现过程:

举一个例子:

        tmp是用来归并时临时拷贝的数组 。

step1:

找到中间 mid=(begin+end)/2 ,将整个区间分成 [begin,mid] [mid+1,end] ,以此类推。

 step2:

从划分到最小的开始,进行归并,归并到tmp数组,再将tmp数组拷贝回原数组。

将上述过程写成代码:

void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	//分区间递归
	int mid = (begin + end) / 2;
	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);
	//归并
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int index = begin;
	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++];
	}
	memcpy(a + begin, tmp + begin, (end - begin + 1)*sizeof(int));
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc failed\n");
		return;
	}
	_MergeSort(a, tmp, 0, n - 1);
	free(tmp);
}
 归并排序非递归形式

        我是通过模拟循环过程解决,更像是忽略掉拆分过程,直接进行合并,11归并,22归并,44归并…

        通过设定一个gap来确定是几几归并,gap是几就是几几归并。然后就是在每一个归并中从哪里开始的问题,这里我是通过设定i,在一次归并中,归并完第一组,就跳过2倍gap个找到第二组再进行归并,以此类推。

        gap=8,只有一个数组,无法归并,结束。

代码如下:

	for (int gap = 1;gap < n;gap *= 2)//11归并,22归并,44归并……
	{
		for (int i = 0;i < n;i += 2 * gap)
		{
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + gap + gap - 1;
			int index = i;
			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++];
			}
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
	}

特殊情况:归并时出现奇数组

        遇到奇数组时最后一组不要理会,排着排着总会变成偶数组,这时最后一组与其他组可能长度不一样,只要修改一下最后一组结束的下标就好了(反正都是有序的都可以进行归并)。


void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc failed\n");
		return;
	}
	for (int gap = 1;gap < n;gap *= 2)
	{
		for (int i = 0;i < n;i += 2 * gap)
		{
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + gap + gap - 1;
			int index = i;

            //出现奇数组情况,该判断都在最后一组
			if (begin2 >= n)
			{
				break;
			}
            //若begin2<n,则此时匹配成两两一组(偶数组)
            //但若最后一组末尾下标越界,修正即可。
			if (end2 >= n)
			{
				end2 = n - 1;
			}

			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++];
			}
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
	}
}
分析
时间复杂度

        O\left( NlogN \right)

        原因:标准的O\left( NlogN \right),与快排最好情况类似。

        每一次归并都是对数组所有进行遍历,根据二分特性,长度为N的数组划分完需要logN次。 

 所以是O\left( NlogN \right)

s空间复杂度

        O\left( N \right)

        原因:申请一个与排序数组一样大小的tmp数组用于储存每次归并后的结果。

稳定性

        稳定。

        原因:没有大幅度的交换,主要看归并,每次归并时,相同的数始终在前面。

	if (a[begin1] <= a[begin2])//取等,不取不稳定
	{
		tmp[index++] = a[begin1++];
	}
	else
	{
		tmp[index++] = a[begin2++];
	}
//因为在begin1到end1中的元素在前面,若出现相同的元素,自然是begin1到end1中的元素优先

计数排序

思路与实现

思路:创建一个全为零的数组,遍历排序的数组,将数组的值作为全零数组的索引,每到一个数组的值,其对应全零数组的索引的值加一,最后遍历全零数组,将大于一的值的索引重新填回排序的数组。这是一种非比较排序,建立了一种数组值到索引的映射关系。

实现

step1:

        我们要开一个数组,要确定数组的范围,因为是排序数组的值映射到新开辟数组的索引,所以我希望数组的范围是从排序数组的最小值到最大值。

int max = a[0];
int min = a[0];
for (int i = 0;i < n;i++)
{
	if (max < a[i])//找到数组的最大值
	{
		max = a[i];
	}
	if (min > a[i])//找到数组的最小值
	{
		min = a[i];
	}
}
int size = max - min + 1;//数组大小
//根据范围创建一个全零的tmp数组
int* tmp = (int*)calloc(size,sizeof(int));

step2:

        将排序的数据填充到tmp数组。

for (int i = 0;i < n;i++)
{
	tmp[a[i] - min]++;
}

step3:

        遍历tmp数组填充数据。

int j = 0;
for (int i = 0;i < size;i++)
{
	while(tmp[i]--)
	{
		a[j++] = i + min;
	}
}

完整代码:

void CountSort(int* a, int n)
{
	//找最大元素最小元素,计算范围
	int max = a[0];
	int min = a[0];
	for (int i = 0;i < n;i++)
	{
		if (max < a[i])
		{
			max = a[i];
		}
		if (min > a[i])
		{
			min = a[i];
		}
	}
	int range = max - min + 1;

    //根据范围开辟数组
	int* tmp = (int*)calloc(size,sizeof(int));

	//已有数据填充数组
	for (int i = 0;i < n;i++)
	{
		tmp[a[i] - min]++;
	}

	//依次拷贝回排序数组
	int j = 0;
	for (int i = 0;i < range ;i++)
	{
		while(tmp[i]--)
		{
			a[j++] = i + min;
		}
	}
}
分析 
时间复杂度

O\left( max\left( N,range\right ) \right)

1.当开辟的数组tmp长度range小于排序数组长度N时,排序数组主导

	for (int i = 0;i < n;i++)
	{
		if (max < a[i])
		{
			max = a[i];
		}
		if (min > a[i])
		{
			min = a[i];
		}
	}

        最少要遍历一遍排序数组找最大最小值,所以是O\left( N \right)

2.当开辟的数组tmp长度range大于排序数组长度N时,tmp数组主导

	int j = 0;
	for (int i = 0;i < range ;i++)
	{
		while(tmp[i]--)
		{
			a[j++] = i + min;
		}
	}

        拷贝回排序数组需要根据tmp数组的长度来确定时间复杂度。

空间复杂度

O\left( range \right)

	int range = max - min + 1;
    //根据范围开辟数组
	int* tmp = (int*)calloc(size,sizeof(int));

        开辟 range = max - min + 1 个空间。

总结

排序时间复杂度空间复杂度稳定性
插入排序直接插入排序O\left( N^2 \right)O\left( 1 \right)稳定
希尔排序O\left( N^{1.3} \right)O\left( 1 \right)不稳定
选择排序选择排序O\left( N^2 \right)O\left( 1 \right)不稳定
堆排序O\left( NlogN \right)O\left( 1 \right)不稳定
交换排序冒泡排序O\left( N^2 \right)O\left( 1 \right)稳定
快速排序O\left( NlogN \right)O\left( logN\right)不稳定
归并排序归并排序O\left( NlogN \right)O\left( N \right)稳定
非交换排序计数排序O\left( max\left( N,range\right ) \right)O\left( range \right)×

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

橘子13

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

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

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

打赏作者

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

抵扣说明:

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

余额充值