八大排序 -

八大排序 - ,第1张


目录

一.直接插入排序

1.思想

2.实现

3.特性总结

 二.希尔排序                                

1.思想

2.实现

3.特性总结 

 三.选择排序

         1.思想

2.实现

3.特性总结

 四.堆排序

1.思想

2.实现

3.特性分析

五.冒泡排序

1.思想

2.实现

3.特性总结

六.快速排序​​​​​​​

1.思想

2.实现

(1)递归版本

①Hoare版本 (左右指针法)

②挖坑法

③前后指针法

(2)快速排序非递归法

(3)快速排序的两个优化:

1.三数取中

2.小区间优化

七.归并排序

         1.思想

2.实现 

(1)递归实现

(2)非递归实现

3.特性总结

4.外排序

 八.计数排序

 1.思想

 2.实现

 3.特性总结


一.直接插入排序 1.思想       直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一 个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。


      实际中我们玩扑克牌时,就用了插入排序的思想 2.实现

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与 array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移

                                                                 

代码实现: 

void InsertSort(int* a, int n)
{
	 //多趟排序
	for (int i = 0; i < n - 1; ++i)
	{
		// 把tmp插入到数组的[0,end]有序区间中
		int end = i; //记录有序序列的最后一个元素的下标
		int tmp = a[end + 1]; //记录待插入元素
		while (end >= 0)
		{
			if (tmp < a[end]) //向后挪位置
			{
				a[end + 1] = a[end];
				--end;
			}
			else //找到了对应位置
			{
				break;
			}
		}
		a[end + 1] = tmp; 
        //代码执行到此位置有两种情况:
		//1.待插入元素找到对应的插入位置(break跳出循环)。


//2.待插入元素比当前有序序列中的所有元素都小(while循环结束)。


} }

                        

 3.特性总结

①元素集合越接近有序,直接插入排序算法效率的时间效率越高

②时间复杂度: 最好 O(N) 顺序有序

                          最坏 O(N^2)  接近逆序

③空间复杂度: O(1)

                                

 二.希尔排序                                 1.思想 希尔排序法又称缩小增量法。


希尔排序法的基本思想是: ①先选定一个整数gap < N(元素个数),把待排序文件中所有记录分成gap个组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行插入排序。


②重复上述分组和排序的工 作。


当到达gap = 1时,所有记录在同一组内的数据就排好序了。


                

 2.实现

代码实现:

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		//gap >1 是预排序
		//gap = 1 直接插入排序

        gap = gap / 2 + 1;

		//gap = gap / 3 + 1; 只是一种方法  +1 是为了最后一次一定为 1

		for (int i = 0; i < n - gap; ++i) //插入排序
		{
			int end = i;
			int temp = a[end + gap];
			while (end >= 0)
			{
				if (temp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = temp;
		}

		PrintArray(a, n); //将每一次插入排序的结果打印出来 (自己实现)
		printf("\n");
	}


}

                         

 3.特性总结

1.希尔排序是对直接插入排序的优化 (先分组 ,对分组的数据进行插入排序 )

2.当gap > 1的时候都是预排序 ,目的是让数组更接近于有序。


当gap == 1时,数组已经接近有序了,这样就会很快 。


整体而言 ,可以达到优化的效果。


3.gap越大,大的和小的数可以更快的挪到对应的方向去, gap越大,越不接近有序(针对接近逆序)
   gap越小,大的和小的数可以更慢的挪到对应的方向去, gap越小,就越接近有序
   gap =1直接就是插入排序。


4.时间复杂度:O(N^1.3) ~ O(N^2)      空间复杂度 : O(1)
 

 

                        

 三.选择排序 1.思想 每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始(最后)位置,直到全部待排序的 数据元素排完 。


                         2.实现 ① 在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素 ② 若它不是 这组元素中 的最后一个(第一个)元素,则将它与 这组元素中的 最后一个(第一个)元素交换 ③ 在剩余的array[i]--array[n-2]  or(array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素 优化:遍历一遍,每次选择一个最小的和最大的

 代码实现:

void SelectSort(int* a, int n)
{
	int left = 0, right = n - 1;
	while (left < right)
	{
		// 选出最大的值和最小的值
		int minIndex = left, maxIndex = left;
		for (int i = left; i <= right; ++i)
		{
			if (a[i] < a[minIndex])
				minIndex = i;

			if (a[i] > a[maxIndex])
				maxIndex = i;
		}

		Swap(&a[left], &a[minIndex]);

		// 如果max和left位置重叠,max被换走了,要修正一下max的位置  !!!
		if (left == maxIndex)
		{
			maxIndex = minIndex;
		}

		Swap(&a[right], &a[maxIndex]);
		++left;
		--right;


	}
}

                                                

3.特性总结

①直接选择排序思考非常好理解,但是效率不是很好。


实际中很少使用

②时间复杂度:O(N^2)   空间复杂度 : O(1)

                                

                                

 四.堆排序 1.思想

堆是一种数据结构,一种叫做完全二叉树的数据结构。


 

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。


它是通过堆来进行选择数据。


需要注意的是排升序要建大堆,排降序建小堆。


                         

 2.实现

 堆排序的实现有一个重要的算法: 向下调整算法( 用于建大堆 )

 从根结点处开始,选出左右孩子中值较大的孩子下标。



② 让值较大的孩子与其父亲进行比较。



③ 如果值孩子比父亲大,将父亲的值 和 孩子的值交换 ,并将原来值较大的孩子的位置当成父亲,循环到 ① 继续向下进行调整,直到调整到叶子结点为止。


如果值孩子比父亲小,则不需处理了,调整完成,整个树已经是大堆了。


 堆排序的前提是有一个堆。


 

(1)  如何建堆?      利用向下调整算法将原数组调整为一个大堆。


(2)  如何进行堆排序呢?
 1、将堆顶数据与堆的最后一个数据交换,此时产生一个新的堆,不包含交换到最后位置的那一个数据,然后从堆顶位置进行一次向下调整。



 2、经过步骤 1 堆中最大的数据又位于堆顶,循环执行步骤 1 ,每次把堆中的最大数据与堆的最后一个数据进行交换,以此类推就形成了一个有序的序列。




 

 代码实现:

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

	while (child < n) //一直向下调整
	{
		if (child + 1 < n && a[child + 1] > a[child]) //child防止越界 找最大child的下标
		{
			child++;
		}

		if (a[parent] < a[child]) 
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else //没有比parent大的结束
		{
			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;
	}
}

                                                                                                                

3.特性分析

堆的向下调整算法的时间复杂度:  最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1次(h为树的高度)。


而h = log2(N + 1) (N为树的总结点数)。


所以堆的向下调整算法的时间复杂度为:O(logN) 。


                                 

建堆的时间复杂度 :  假设高度为k,则从倒数第二层右边的节点开始,这一层的节点都要执行子节点比较然后交换(如果顺序是对的就不用交换);倒数第三层呢,则会选择其子节点进行比较和交换,如果没交换就可以不用再执行下去了。


如果交换了,那么又要选择一支子树进行比较和交换;高层也是这样逐渐递归。



  那么总的时间计算为:s = 2^( i - 1 ) * ( k - i );其中 i 表示第几层,2^( i - 1) 表示该层上有多少个元素,( k - i) 表示子树上要下调比较的次数。



  S = 2^(k-2) * 1 + 2^(k-3)2…..+2(k-2)+2^(0)*(k-1) ===> 因为叶子层不用交换,所以i从 k-1 开始到 1;
  S = 2^k -k -1;又因为k为完全二叉树的深度,而log(n) =k,把此式带入;
  得到:S = n - log(n) -1,所以时间复杂度为:O(n)

————————————————
版权声明:本文为CSDN博主「CavalryOuO」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。



原文链接:https://blog.csdn.net/qq_34228570/article/details/80024306

                                        

排序重建堆:  在取出堆顶点放到对应位置并把原堆的最后一个节点填充到堆顶点之后,需要对堆进行重建,只需要对堆的顶点调用AdjustDown() 函数。



  每次重建意味着有一个节点出堆,所以需要将堆的容量减一。


AdjustDown() 函数的时间复杂度k=log(n),k为堆的层数。


所以在每次重建时,随着堆的容量的减小,层数会下降,函数时间复杂度会变化。


重建堆一共需要n-1次循环,每次循环的比较次数为log(i),则相加为:log2+log3+…+log(n-1)+log(n)≈log(n!)。


可以证明log(n!)和nlog(n)是同阶函数:
∵(n/2)n/2≤n!≤nn,
∴n/4log(n)=n/2log(n1/2)≤n/2log(n/2)≤log(n!)≤nlog(n)
所以时间复杂度为O(nlogn)

————————————————
版权声明:本文为CSDN博主「CavalryOuO」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。



原文链接:https://blog.csdn.net/qq_34228570/article/details/80024306

                                        

 ④小结  : 初始化建堆的时间复杂度为O(n),排序重建堆的时间复杂度为nlog(n),所以总的时间复杂度为O(n+nlogn)=O(nlogn)。


另外堆排序的比较次数和序列的初始状态有关,但只是在序列初始状态为堆的情况下比较次数显著减少,在序列有序或逆序的情况下比较次数不会发生明显变化。


                                

⑤空间复杂度: O(1)

                

                        

五.冒泡排序 1.思想 基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。


                                                                         2.实现

                                                                        

代码实现:

void BubbleSort(int* a, int n)
{
	for (int i = 1; i < n; ++i) //多趟排序
	{
		int exchange = 0; //优化  单趟没有交换说明已经有序
		for (int j = 0; j < n - i; ++j) //单趟排序
		{
			if (a[j] > a[j + 1])
			{
				exchange = 1;
				Swap(&a[j], &a[j + 1]);
			}
		}

		if (exchange == 0)
		{
			return;
		}
	}
}

                                                                

 3.特性总结

① 时间复杂度: O(N^2)      空间复杂度: O(1)

② 冒泡和插入相比谁更好?    顺序有序一样好,接近有序插入好

                                                

六.快速排序 1.思想 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。


2.实现 (1)递归版本 ①Hoare版本 (左右指针法)

 

 Hoare版本单趟排序(左右指针法)实现思想:

1、选出一个key,一般是最左边或是最右边的值。



2、使key放到正确位置上去,使得 左边比key小 , 右边比key大 。



3、定义一个 left 从最左开始走找大 , 定义一个 right从最右开始找小。


(这里key在最左边 ,right先走 ,如果key在最右边 ,left 先走) 当left找到了 >=  key的数 , right 找到了 < key的值 ,交换left 和 right 位置的值 ,然后继续找 ,直到left 和 right 的位置相重合。


将 key 位置的值 和 left / right 位置的值交换 。


(     -----------------------------------------------------------------------------------------------------

这样经过一次单趟排序,最终使得key左边的数据全部都=key。


 然后我们再将key的左序列和右序列再次进行这种单趟排序,不断地递归分治下去 ,最终会排列成有序的一组数据

)----------------------------------------------------------------------------------------------------------
                                                                                                                 

为什么key在最左边 ,right先走 ,如果key在最右边 ,left 先走 ?

为了使最后相遇位置的值一定比key小.

① 相遇两种情况:  right 遇 left          left 遇 right
② right先走一定能找到比 key 小的,即使找不到 left 和 right 的位置重合,都指向key
③ 为了确保最后相遇时的a[left] < a[key],只要让右边的数先走,最后停下来时无论是“左边遇到右边”还是“右边遇到左边”,都满足a[left] < a[key]。


                                                

代码实现:

int PartSort1(int* a, int left, int right)
{
	int midIndex = GetMidIndex(a, left, right); //三数取中优化

	Swap(&a[left], &a[midIndex]);

	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[keyi], &a[left]); //每次 递归 keyi 和 left都会改变

	return left;
}

                                                                        

 ②挖坑法  

                                                

挖坑法单趟排序实现思想:

1、选出一个key,一般是最左边或是最右边的值,将key的值保存起来 ,这时key位置就是一个坑。



2、right从最右边开始找小 , 找到之后将 right下标位置的值放到 left 的坑位中,right现在就有一个坑位(我们选最左边的值为 key ,left 从最左边开始,自然有一个天然坑位);left 从左开始找大 ,找到之后将left 位置的值填到 right 位置的坑位中。


3.这样一直找下去之后一定会重合 ,重合的位置自然有一个坑位 ,将key的值放到坑位中。


                                                

代码实现:

int PartSort2(int* a, int left, int right)
{
	int midIndex = GetMidIndex(a, left, right); //三数取中优化

	Swap(&a[left], &a[midIndex]);

	int key = a[left];
	while (left < right)
	{
		//找小, 跳过大的
		while (left < right && a[right] >= key)
		{
			--right;
		}
		a[left] = a[right]; //放到左边的坑位中 , 右边就形成了新的坑

		//找大, 跳过小的
		while (left < right && a[left] < key)
		{
			++left;
		}
		a[right] = a[left];//放到右边的坑位中 , 左边就形成了新的坑

	}
	a[left] = key;

	return left;
}

                                        

③前后指针法

 

                                 

 前后指针法单趟排序思路:

1.在最左边或走右边选出一个key。


(例题选用最左边)

2.从最左边开始 prev 和 cur 一前一后 ,cur向右找比 key 小的值 ,找到后 ++prev ,然后交换 prev 和 cur 位置的值 ,一直循环执行下去,直到 cur 走到数组尾,指向最后一个数据的下一个位置。


3.当循环结束时,交换prev 和 key 位置的值。


                        

 代码实现:

int PartSort3(int* a, int left, int right)
{
	int midIndex = GetMidIndex(a, left, right); //三数取中

	Swap(&a[left], &a[midIndex]);

	int keyi = left; // 保存key 位置的下标
	int prev = left;
	int cur = prev + 1;

	while (cur <= right)
	{
		if (a[cur] < a[keyi]) //cur找小
		{
			++prev;

			if (cur != prev) // 相同位置不用交换
				Swap(&a[cur], &a[prev]);
		}

		++cur;
	}

	Swap(&a[keyi], &a[prev]);

	return prev;
}

                                        

 快速排序递归方式完整代码:

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end) //只剩一个数据返回
		return;

	    // 上述三种方法任选一个
		int keyi = PartSort1(a, begin, end);
        int keyi = PartSort2(a, begin, end);
        int keyi = PartSort3(a, begin, end);
        //


		//[begin ,keyi - 1] keyi [keyi+1 , end]  子区间   //相当于前序

		QuickSort(a, begin, keyi - 1); //左边序列
		QuickSort(a, keyi + 1, end); //右边序列

}

                        

三种单趟排序的时间复杂度均为 : O(N * logN)

                        

                         

 (2)快速排序非递归法

为什么要有非递归的方法?
递归 现代编译器优化很好,性能已经不是大问题
最大的问题->递归深度太深,程序本身没问题,但是栈空间不够,导致栈溢出
只能改成非递归,改成非递归有两种方式:
1、直接改循环
2、树遍历非递归和快排非递归等等,只能用Stack存储数据模拟递归过程

                                                 

快速排序的非递归思路:
 1、先将待排序列的左右边界的下标入栈。



 2、当栈不为空时,读取栈中的数据(分别读取到一段区间的 left ,right 边界),然后调用某一版本的单趟排序,排序之后返回 key 的下标,然后判断key的左右区间是否还有数据( 数据个数 > 1),如果还有数据则将相应区间的left 和 right 边界 入栈,否则就不需要排序了。



 3、循环执行执行步骤2,直到栈为空为止,此时整个区间就有序了。



                                        

void QuickSortNonR(int* a, int begin, int end)
{
	stack st; //辅助栈
	st.push(begin); //将一个范围的左右边界入栈
	st.push(end);

	while (!st.empty())
	{
		int left, right;
		right = st.top(); //获取左右边界
		st.pop();

		left = st.top();
		st.pop();

		int keyi = PartSort1(a, left, right); //对这个区间进行单趟排序
		if (left < keyi - 1)  //将排序完的数据再进行划分,划分为左右两个区间
		{                     //将左右两个区间的 ,左右边界分别入栈
			st.push(left);
			st.push(keyi - 1);
		}

		if (keyi + 1 < right)
		{
			st.push(keyi + 1);
			st.push(right);
		}
	}

}

                                                        

 (3)快速排序的两个优化: 1.三数取中

 分析:

最理想的情况下:如果每趟排序所选的key正好是该序列有序时的中间值,那么一趟排序之后key就位于序列正中间,此时的快速排序的时间复杂度就是O(NlogN)。


最差的情况下:当待排序列本就是一个有序的序列 或者 接近有序时,如果仍然选择最左边或者最右边的数作为key,那么快速排序的效率将达到最低 ,O(N^2).

为了防止这种极端情况对我们效率的影响,于是出现了三数取中来进行优化:
        三数取中指的是:最左边的数、最右边的数以及中间位置的数,我们取中三个数的中间值做key, 将这个值放到最左边 或者 最右边,这就确保了我们所选取的数不会是序列中的最大或是最小值了。


int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) >> 1;
	// left  mid  right
	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;
		}
	}
}

                                                                 

2.小区间优化

① 我们可以看到,即使是上面理想状态下的快速排序,也不能避免随着递归的深入,每一层的递归次数会以2倍的形式快速增长。


② 为了减少递归树的最后几层递归,我们可以设置一个判断语句,当序列的长度小于某个数的时候就不再进行快速排序,转而使用其他种类的排序。


小区间优化若是使用得当的话,会在一定程度上加快快速排序的效率,而且待排序列的长度越长,该效果越明显。


void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;

	// 1、如果这个子区间是数据较多,继续选key单趟,分割子区间分治递归
	// 2、如果这个子区间是数据较小,再去分治递归不太划算

	if ((end - begin) > 10) //个数 > 10 使用递归   一般给 10 ~ 20,现在编译器对递归的优化已经很好了
	{
		int keyi = PartSort3(a, begin, end);


		//[begin ,keyi - 1] keyi [keyi+1 , end]  子区间   //相当于前序
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
	else  //个数 <= 10 再使用递归消耗函数栈帧比较大
	{
		InsertSort(a + begin, end - begin + 1); //这里可以灵活修改,针对某个场景进行优化
	}

}
                          七.归并排序 1.思想 归并排序是建立在归并 *** 作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。


将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。


若将两个有序表合并成一个有序表,称为二路归并。


例如:

                                         

2.实现  (1)递归实现

 我们不断地将区间一半一半地划分,直到只剩下一个数据时开始往回合并 , 我们利用一个temp数组辅助我们将两段区间合并,最后合并为一个有序区间。


代码实现:

void _Merge(int* a, int* tmp, int begin1, int end1, int begin2, int end2) //使递归和非递归都能用  复用
{
	int i = begin1;
	int j = begin1;

	while (begin1 <= end1 && begin2 <= end2)
	{
        //对两段分组进行排序
		if (a[begin1] < a[begin2])
			tmp[i++] = a[begin1++];
		else
			tmp[i++] = a[begin2++];
	}

	//有一个分组先走完 , 把剩余的拷贝进tmp
	while (begin1 <= end1)
		tmp[i++] = a[begin1++];

	while (begin2 <= end2)
		tmp[i++] = a[begin2++];
	
	// 归并完成以后,拷贝回到原数组
	for (; j <= end2; ++j)
	{
		a[j] = tmp[j];
	}
}

//归并排序子函数
void _MergeSort(int* a, int left, int right, int* temp)
{
	if (left >= right) //递归结束条件
		return;

	int mid = (left + right) >> 1; // 移位运算效率高
	_MergeSort(a, left, mid, temp); //递归调用左
	_MergeSort(a, mid + 1, right, temp); // 递归调用右

	// 两段有序子区间归并tmp,并拷贝回去
	//int begin1 = left, end1 = mid; //左边区间的边界
	//int begin2 = mid + 1, end2 = right; //右边区间的边界

	_Merge(a, temp, left, mid, mid + 1, right);
}



//归并排序主函数
void MergeSort(int* a, int n)
{

	//开辟临时空间 用于暂存归并的结果
	int* temp = new int[n];
	if (temp == nullptr)
	{
		cout << "new falid !" << endl;
		exit(-1);
	}

	_MergeSort(a, 0, n - 1, temp);//归并开始

	delete[] temp;
}

                                                        

 (2)非递归实现

归并排序的非递归方式在这里我们使用循环的方式来进行合并,我们每次控制参与合并的元素个数,按照 2的次方方式 使 gap 逐渐增加 ,但是gap要满足  gap < n.

 图片中的例子只是一种理想的情况,在这里我们还会遇到三种需要特殊处理的情况:

情况1:当最后一个小组归并时,第一个小区间不够gap个,就不需要归并了。


                         

 情况2:当最后一个小组归并时,第二个小区间存在,第二个区间不够gap个,需要调整边界。


                

情况3:当最后一个小组归并时,第二个小区间不存在,不需要归并了。


 

                                        

代码实现:

void _Merge(int* a, int* tmp, int begin1, int end1, int begin2, int end2) //使递归和非递归都能用  复用
{
	int i = begin1;
	int j = begin1;

	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
			tmp[i++] = a[begin1++];
		else
			tmp[i++] = a[begin2++];
	}

	//有一个分组先走完 , 把剩余的拷贝进tmp
	while (begin1 <= end1)
		tmp[i++] = a[begin1++];

	while (begin2 <= end2)
		tmp[i++] = a[begin2++];
	
	// 归并完成以后,拷贝回到原数组
	for (; j <= end2; ++j)
	{
		a[j] = tmp[j];
	}
}



// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{

	//开辟临时空间 用于暂存归并的结果
	int* temp = new int[n];
	if (temp == nullptr)
	{
		cout << "new falid !" << endl;
		exit(-1);
	}

	int gap = 1;
	while (gap < n) //控制gap < n
	{
		for (int i = 0; i < n; i += 2 * gap) //gap以2的被数增长
		{
			//[i, i + gap - 1]  [ i + gap, i + 2 * gap - 1] 归并一个小组 //画图帮助理解
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			// 情况1,3:如果第二个小区间不存在就不需要归并了,结束本次循环
			if (begin2 >= n)
				break;

			// 情况2 :如果第二个小区间存在,但是第二个小区间不够gap个,结束位置越界了,需要修正一下
			if (end2 >= n)
				end2 = n - 1;

			_Merge(a, temp, begin1, end1, begin2, end2);
		}
		
		gap *= 2;
	}

	delete[] temp;

}

                        

3.特性总结                

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

2.归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题

                                        

4.外排序

首先,我先说明一下什么是内排序,什么是外排序:
 内排序:数据量相对少一些,可以放到内存中进行排序。



 外排序:数据量较大,内存中放不下,数据只能放到磁盘文件中,需要排序。


 上面介绍的排序算法均是在内存中进行的,对于数据量庞大的序列,上面介绍的排序算法都束手无策,而归并排序却能胜任这种海量数据的排序。


 假设现在有10亿个整数(4GB)存放在文件A中,需要我们进行排序,而内存一次只能提供512MB空间,归并排序解决该问题的基本思路如下:
 1、每次从文件A中读取八分之一,即512MB到内存中进行排序(内排序),并将排序结果写入到一个文件中,然后再读取八分之一,重复这个过程。


那么最终会生成8个各自有序的小文件(A1~A8)。



 2、对生成的8个小文件进行11合并,最终8个文件被合成为4个,然后再11合并,就变成2个文件了,最后再进行一次11合并,就变成1个有序文件了。


注意:这里将两个文件进行11合并,并不是先将两个文件读入内存然后进行合并,因为内存装不下。


这里的11合并是利用文件输入输出函数,从两个文件中各自读取一个数据,然后进行比较,将较小的数据写入到一个新文件中去,然后再读取,再比较,再写入,最终将两个文件中的数据全部写入到另一个文件中去,那么此时这个文件又是一个有序的文件了。


        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        

                                          

 当然,你也可以这样合并文件:

                                                                 

外排序代码示例:

//将file1文件和file2文件中的数据归并到mfile文件中
void _MergeFile(const char* file1, const char* file2, const char* mfile)
{
	FILE* fout1 = fopen(file1, "r");//打开file1文件
	if (fout1 == NULL)
	{
		printf("打开文件失败\n");
		exit(-1);
	}

	FILE* fout2 = fopen(file2, "r");//打开file2文件
	if (fout2 == NULL)
	{
		printf("打开文件失败\n");
		exit(-1);
	}

	FILE* fin = fopen(mfile, "w");//打开mfile文件
	if (fin == NULL)
	{
		printf("打开文件失败\n");
		exit(-1);
	}

	int num1, num2;
	int ret1 = fscanf(fout1, "%d\n", &num1);//读取file1文件中的数据
	int ret2 = fscanf(fout2, "%d\n", &num2);//读取file2文件中的数据
	while (ret1 != EOF && ret2 != EOF)
	{
		//将读取到的较小值写入到mfile文件中,继续从file1和file2中读取数据进行比较
		if (num1 < num2)
		{
			fprintf(fin, "%d\n", num1);
			ret1 = fscanf(fout1, "%d\n", &num1);
		}
		else
		{
			fprintf(fin, "%d\n", num2);
			ret2 = fscanf(fout2, "%d\n", &num2);
		}
	}
	//将file1文件中未读取完的数据写入文件mfile中
	while (ret1 != EOF)
	{
		fprintf(fin, "%d\n", num1);
		ret1 = fscanf(fout1, "%d\n", &num1);
	}
	//将file2文件中未读取完的数据写入文件mfile中
	while (ret2 != EOF)
	{
		fprintf(fin, "%d\n", num2);
		ret2 = fscanf(fout2, "%d\n", &num2);
	}
	fclose(fout1);//关闭file1文件
	fclose(fout2);//关闭file2文件
	fclose(fin);//关闭mfile文件
}
//将文件中的数据进行排序
void MergeSortFile(const char* file)
{
	FILE* fout = fopen(file, "r");//打开文件
	if (fout == NULL)
	{
		printf("打开文件失败\n");
		exit(-1);
	}
	// 分割成一段一段数据,内存排序后写到小文件中
	int n = 10;//一次读取10个数据进行内排序
	int a[10];//读取数据放到数组中,准备进行内排序
	int i = 0;
	int num = 0;
	char subfile[20];//文件名字符串
	int filei = 1;//储存第filei组内排序后的数据的文件的文件名

	memset(a, 0, sizeof(int)*n);//将数组a的空间置0
	while (fscanf(fout, "%d\n", &num) != EOF)//从文件中读取数据
	{
		if (i < n - 1)//读取前9个数据
		{
			a[i++] = num;
		}
		else//读取第10个数据
		{
			a[i] = num;
			QuickSort(a, 0, n - 1);//将这10个数据进行快速排序
			sprintf(subfile, "%d", filei++);//将储存第filei组内排序后的数据的文件的文件名命名为filei
			FILE* fin = fopen(subfile, "w");//创建一个以字符串subfile[20]为名字的文件并打开
			if (fin == NULL)
			{
				printf("打开文件失败\n");
				exit(-1);
			}
			//将内排序排好的数据写入到subfile文件中
			for (int i = 0; i < n; i++)
			{
				fprintf(fin, "%d\n", a[i]);
			}
			fclose(fin);//关闭subfile文件

			i = 0;
			memset(a, 0, sizeof(int)*n);//将a数组内存置0,准备再次读取10个数据进行内排序
		}
	}
	// 利用互相归并到文件,实现整体有序
	char mfile[100] = "12";//归并后文件的文件名
	char file1[100] = "1";//待归并的第一个文件的文件名
	char file2[100] = "2";//待归并的第二个文件的文件名
	for (int i = 2; i <= n; ++i)
	{
		//将file1文件和file2文件中的数据归并到mfile文件中
		_MergeFile(file1, file2, mfile);

		strcpy(file1, mfile);//下一次待归并的第一个文件就是上一次归并好的文件
		sprintf(file2, "%d", i + 1);//上一次待归并的第二个文件的文件名加一,就是下一次待归并的第二个文件的文件名
		sprintf(mfile, "%s%d", mfile, i + 1);//下一次归并后文件的文件名
	}

	fclose(fout);//关闭文件
}
//主函数
int main()
{
	MergeSortFile("Sort.txt");//将Sort.txt文件中的数据进行排序
	return 0;
}

 注:代码中使用的是第二种文件合并的方式。


//这里的外排序使用 2021dragon 的内容,写的非常仔细,图也画的整齐好看。



版权声明:本文为CSDN博主「2021dragon」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。



原文链接:https://blog.csdn.net/chenlong_cxy/article/details/116563972

                                        

                                        

 八.计数排序 1.思想 计数排序,顾名思义,就是对每一个数据进行统计,计算相同数据出现的次数,然后将统计的结果按序放回原数组中。


计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用

                         

                                                                        

 2.实现

这里我们使用相对映射的方式进行排序。


代码实现:

void CountSort(int* a, int n)
{
	int Max = a[0]; //找到最大和最小确定边界
	int Min = a[0];

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

		if (a[i] < Min)
			Min = a[i];
	}

	int range = Max - Min + 1;  //实际存储的个数
	int* count = new int[range]; // 开辟空间
	memset(count, 0, sizeof(int) * range); //将这段空间初始化为0

	for (int i = 0; i < n; ++i) //使用相对映射,统计相同数字出现的次数
	{
		count[a[i] - Min]++;
	}

	int i = 0; 
	for (int j = 0; j < range; ++j) //从映射的空间中输出到 a 中
	{
		while (count[j]--)
		{
			a[i++] = j + Min;
		}
	}

	delete[] count;

}

                                                                                 

3.特性总结

①绝对映射的方式可能会对空间造成大量浪费,例如: 只有两个数据 100,99,我们只需要排两个数据,但是需要开辟101 个int类型空间; 而相对映射的方式就比较合理了,我们尽可能使用相对映射的方式进行排序。


②计数排序只适合一组数据,数据的范围比较集中. 如果范围集中,效率是很高的,但是局限性也在这里,并且只适合整数,如果是浮点数、字符串等等就不行了。


③时间复杂度 O(N+range)   空间复杂度: O(range)

                                                                

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/568158.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-09
下一篇 2022-04-09

发表评论

登录后才能评论

评论列表(0条)

保存