基本排序算法总结

#include<iostream>
#include<stack>
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<deque>
using namespace std;

void show_arr(int* arr, int len)
{
	for(int index = 0x00; index < len; index++)
		std::cout<<arr[index]<<"  ";
	std::cout<<std::endl;
}


/*
思想:
假设待排序的记录存放在数组R[1..n]中。初始时,R[1]自成1个有序区,无序区为R[2..n]。
从i=2起直至i=n为止,依次将R[i]插入当前的有序区R[1..i-1]中,生成含n个记录的有序区。

BuildHeap的实现
  要将初始文件R[l..n]调整为一个大根堆,就必须将它所对应的完全二叉树中以
	每一结点为根的子树都调整为堆。
  显然只有一个结点的树是堆,而在完全二叉树中,所有序号 i > n/2的结点都是叶子,
	因此以这些结点为根的子树均已是堆。这样,我们只需依次将以序号为n/2,n/2-1,...,1的结点作为根的子树都调整为堆即可。

空间复杂度分析
算法所需的辅助空间是一个监视哨,辅助空间复杂度S(n)=O(1)。是一个就地排序。

稳定性
直接插入排序是稳定的排序方法。
*/
void insert_sort(int* arr, int len)
{
	for(int index = 0x01; index < len; index++)
	{
		int tmp = arr[index];
		int j = index - 0x01;
		while(j >= 0x00 && arr[j] > tmp)
		{
			arr[j+0x01] = arr[j];
			j--;
		}
		arr[j+0x01] = tmp;
	}

}

/*
基本思想:
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。
所有距离为dl的倍数的记录放在同一个组中。先在各组内进行直接插人排序;
然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),
即所有记录放在同一组中进行直接插入排序为止。
该方法实质上是一种分组插入方法。


算法分析

增量序列的选择
Shell排序的执行时间依赖于增量序列。
好的增量序列的共同特征:
  ① 最后一个增量必须为1;
  ② 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
有人通过大量的实验,给出了目前较好的结果:当n较大时,比较和移动的次数约在nl.25到1.6n1.25之间。

Shell排序的时间性能优于直接插入排序
希尔排序的时间性能优于直接插入排序的原因:
  ①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
  ②当n值较小时,n和n2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n2)差别不大。
  ③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,
	  后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,
	  使文件较接近于有序状态,所以新的一趟排序过程也较快。
因此,希尔排序在效率上较直接插人排序有较大的改进。

稳定性
希尔排序是不稳定的。参见上述实例,该例中两个相同关键字49在排序前后的相对次序发生了变化。
*/
void shell_pass(int* arr, int len, int step)
{
	for(int index = step + 0x01; index < len; index++)
	{
		int tmp = arr[index];
		int j = index - step;
		while(j >= 0x00 && arr[j] > tmp)
		{
			arr[j+step] = arr[j];
			j--;
		}
		arr[j+step] = tmp;
	}
}

void shell_sort(int* arr, int len)
{
	int step = len;
	while(step > 1)
	{
		step = step / 3 + 0x01;
		shell_pass(arr, len, step);
	}
}

/*
冒泡排序算法
  分析
  因为每一趟排序都使有序区增加了一个气泡,在经过n-1趟排序之后,
  有序区中就有n-1个气泡,而无序区中气泡的重量总是大于等于有序区中气泡的重量,
  所以整个冒泡排序过程至多需要进行n-1趟排序。
  若在某一趟排序中未发现气泡位置的交换,则说明待排序的无序区中所有气泡均满足
  轻者在上,重者在下的原则,因此,冒泡排序过程可在此趟排序后终止。为此,在下面给出的
  算法中,引入一个布尔量swap在每趟排序开始前,先将其置为FALSE。若排序过程中发生了交换,
  则将其置为TRUE。各趟排序结束时检查swap,若未曾发生过交换则终止算法,不再进行下一趟排序。

  算法的平均时间复杂度为O(n2)
  虽然冒泡排序不一定要进行n-1趟,但由于它的记录移动次数较多,故平均时间性能比直接插入排序要差得多。

  算法稳定性
  冒泡排序是就地排序,且它是稳定的。

*/

void mao_pao(int* arr, const int len)
{
	for(int index = 0x00; index < len; index++)
	{
		bool b_swap = false;
		for(int p = len - 0x02; p >= index; p--)
		{
			if(arr[p+0x01] < arr[p])
			{
				int tmp = arr[p];
				arr[p]  = arr[p+0x01];
				arr[p+0x01] = tmp;
				b_swap = true;
			}
		}
		if(!b_swap)
			break;
	}
}


/*
快速排序的基本思想
	设当前待排序的无序区为R[low..high],利用分治法可将快速排序的基本思想描述为:
①分解: 
	 在R[low..high]中任选一个记录作为基准(Pivot),以此基准将当前无序区划分为左、右两个较小的子区间R[low..pivotpos-1)
	和R[pivotpos+1..high],并使左边子区间中所有记录的关键字均小于等于基准记录(不妨记为pivot)的关键字pivot.key,
	右边的子区间中所有记录的关键字均大于等于pivot.key,而基准记录pivot则位于正确的位置(pivotpos)上,它无须参加后续的排序。
	注意:
	划分的关键是要求出基准记录所在的位置pivotpos。划分的结果可以简单地表示为(注意pivot=R[pivotpos]):
   R[low..pivotpos-1].keys≤R[pivotpos].key≤R[pivotpos+1..high].keys
    其中low≤pivotpos≤high。
②求解: 
   通过递归调用快速排序对左、右子区间R[low..pivotpos-1]和R[pivotpos+1..high]快速排序。
③组合: 
	因为当"求解"步骤中的两个递归调用结束时,其左、右两个子区间已有序。对快速排序而言,"组合"步骤无须做什么,可看作是空操作。

*/
int part_array(int* arr, int left, int right)
{
	int data = arr[left];
	while(left < right)
	{
		while(arr[right] > data && left < right)
			right--;
		if(left < right)
		{
			arr[left] = arr[right];
			left++;
		}
		
		while(arr[left] < data && left < right)
			left++;
		if(left < right)
		{
			arr[right] = arr[left];
			right--;
		}
	}
	//left == right
	arr[left] = data;
	return left;
}

//递归方法
void quick_sort(int* arr, int left, int right)
{
	if(left < right)
	{
		int p = part_array(arr,left, right);
		quick_sort(arr, left,p-1);
		quick_sort(arr, p+0x01,right);
	}
}

//非递归方法
//找到中间点之后,保留左半部和右半部的边界,入栈,待下次排序之用,如此反复
void q_sort(int* arr, int left, int right)
{
	std::stack<int> ve;
	if(left < right)
	{
		int mid = part_array(arr,left, right);
		if(left < mid - 1)
		{
			ve.push(left);
			ve.push(mid-1);
		}
		
		if(mid + 1 < right)
		{
			ve.push(mid+0x01);
			ve.push(right);
		}
		
		while(!ve.empty())
		{
			int tmp_right = ve.top();
			ve.pop();
			int tmp_left = ve.top();
			ve.pop();
			
			mid = part_array(arr,tmp_left, tmp_right);
			if(tmp_left < mid - 1)
			{
				ve.push(tmp_left);
				ve.push(mid-1);
			}
			
			if(mid + 1 < tmp_right)
			{
				ve.push(mid+1);
				ve.push(tmp_right);
			}
		}
	}
}

//直接选择排序
/*
直接选择排序的基本思想
 n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果:
①初始状态:无序区为R[1..n],有序区为空。
②第1趟排序
在无序区R[1..n]中选出关键字最小的记录R[k],将它与无序区的第1个记录R[1]交换,
使R[1..1]和R[2..n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。
  ……
③第i趟排序
  第i趟排序开始时,当前有序区和无序区分别为R[1..i-1]和R[i..n](1≤i≤n-1)。
该趟排序从当前无序区中选出关键字最小的记录R[k],将它与无序区的第1个记录R[i]交换,
使R[1..i]和R[i+1..n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。
   这样,n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果。
   
   算法分析
(1)关键字比较次数
	无论文件初始状态如何,在第i趟排序中选出最小关键字的记录,需做n-i次比较,因此,总的比较次数为:
	n(n-1)/2=0(n2)
(2)记录的移动次数
	当初始文件为正序时,移动次数为0
	文件初态为反序时,每趟排序均要执行交换操作,总的移动次数取最大值3(n-1)。
	直接选择排序的平均时间复杂度为O(n2)。
(3)直接选择排序是一个就地排序
(4)稳定性分析
	直接选择排序是不稳定的

*/
void select_sort(int* arr, int len)
{
	for(int index = 0x00; index < len; index++)
	{
		int data = arr[index];
		int min_index = index;
		for(int k = index + 0x01; k < len; k++)
		{
			if(arr[k] < data)
			{
				min_index = k;
				data = arr[k];
			}
		}
		if(min_index != index)
		{
			int tmp = arr[min_index];
			arr[min_index] = arr[index];
			arr[index] = tmp;
		}
	}
}

//改进型直接选择排序
void select_sort_ex(int* arr, int len)
{
	int left = 0x00;
	int right = len - 0x01;
	while(left < right)
	{
		int min = arr[left];
		int max = arr[right];
		int min_index = left;
		int max_index = right;
		for(int index = left ; index <= right; index++)
		{
			if(arr[index] < min)
			{
				min_index = index;
				min = arr[index];
			}
			
			if(arr[index] > max)
			{
				max_index = index;
				max = arr[index];
			}
		}
		
		if(min_index != left)
		{
			int tmp = arr[min_index];
			arr[min_index] = arr[left];
			arr[left] = tmp;
		}
		
		if(max_index != right)
		{
			int tmp = arr[max_index];
			arr[max_index] = arr[right];
			arr[right] = tmp;
		}
		
		left++;
		right--;
	}
}

/*
堆排序---详见:“堆排序/插入/删除 整理”
排序思想:
每趟排序开始前R[l..i]是以R[1]为根的堆,在R[1]与R[i]交换后,
新的无序区R[1..i-1]中只有R[1]的值发生了变化,故除R[1]可能违反
堆性质外,其余任何结点为根的子树均是堆。因此,当被调整区间是R[low..high]时,
只须调整以R[low]为根的树即可。

算法分析:
堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成,它们均是通过调用Heapify实现的。
堆排序的最坏时间复杂度为O(nlgn)。堆排序的平均性能较接近于最坏性能。
由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。
堆排序是就地排序,辅助空间为O(1),
它是不稳定的排序方法。
*/


/*
	二路归并排序算法
	算法基本思路
	设两个有序的子文件(相当于输入堆)放在同一向量中相邻的位置上:
	R[low..m],R[m+1..high],先将它们合并到一个局部的暂存向量R1
	(相当于输出堆)中,待合并完成后将R1复制回R[low..high]中。
	
	(1)合并过程
	合并过程中,设置i,j和p三个指针,其初值分别指向这三个记录区的起始位置。
	合并时依次比较R[i]和R[j]的关键字,取关键字较小的记录复制到R1[p]中,
	然后将被复制记录的指针i或j加1,以及指向复制位置的指针p加1。
	重复这一过程直至两个输入的子文件有一个已全部复制完毕(不妨称其为空),
	此时将另一非空的子文件中剩余记录依次复制到R1中即可。

	(2)动态申请R1
	实现时,R1是动态申请的,因为申请的空间可能很大,故须加入申请空间是否成功的处理。
*/

void merge_sort(int* arr, int left, int mid, int right)
{
	int* new_space = new int[right - left + 0x01];
	if(new_space == NULL)
		exit(0x01);
	memset(new_space, 0x00, (right - left + 0x01) * sizeof(int));
	int low = left;
	int high = mid + 0x01;
	int cur_index = 0x00;
	while(low <= mid && high <= right)
	{
		if(arr[low] == arr[high])
		{
			new_space[cur_index++] = arr[low++];
			new_space[cur_index++] = arr[high++];
		}
		else if(arr[low] > arr[high])
			new_space[cur_index++] = arr[high++];
		else
			new_space[cur_index++] = arr[low++];
	}
	while(low <= mid)
		new_space[cur_index++] = arr[low++];
	while(high <= right)
		new_space[cur_index++] = arr[high++];
	
	for(int index = left, np = 0x00; index <= right; index++, np++)
		arr[index] = new_space[np];
	delete [] new_space;
}

/*
	箱排序的基本思想
	箱排序也称桶排序(Bucket Sort),其基本思想是:设置若干个箱子,依次扫描待
	排序的记录R[0],R[1],…,R[n-1],把关键字等于k的记录全都装入到第k个箱子里(分配),
	然后按序号依次将各非空的箱子首尾连接起来(收集)。
	【例】要将一副混洗的52张扑克牌按点数A<2<…<J<Q<K排序,需设置13个"箱子",排序时依次将
	每张牌按点数放入相应的箱子里,然后依次将这些箱子首尾相接,就得到了按点数递增序排列的一副牌。

	箱排序中,箱子的个数取决于关键字的取值范围。
	若R[0..n-1]中关键字的取值范围是0到m-1的整数,则必须设置m个箱子。因此箱排序要求关键字
	的类型是有限类型,否则可能要无限个箱子。

	箱子的类型应设计成链表为宜
	一般情况下每个箱子中存放多少个关键字相同的记录是无法预料的,故箱子的类型应设计成链表为宜。

	算法简析
	分配过程的时间是O(n);收集过程的时间为O(m) (采用链表来存储输入的待排序记录)或O(m+n)。
	因此,箱排序的时间为O(m+n)。若箱子个数m的数量级为O(n),则箱排序的时间是线性的,即O(n)。
	注意:箱排序实用价值不大,仅适用于作为基数排序的一个中间步骤。
*/


/*
基数排序:
基数排序的基本思想是:从低位到高位依次对Kj(j=d-1,d-2,…,0)进行箱排序。
在d趟箱排序中,所需的箱子数就是基数rd,这就是"基数排序"名称的由来。

基数排序的类型说明和算法描述
要保证基数排序是正确的,就必须保证除第一趟外各趟箱排序是稳定的。相应的类型说明及算法描述

算法分析
若排序文件不是以数组R形式给出,而是以单链表形式给出(此时称为链式的基数排序),
则可通过修改出队和人队函数使表示箱子的链队列无须分配结点空间,而使用原链表的结点空间。
人队出队操作亦无需移动记录而仅需修改指针。虽然这样一来节省了一定的时间和空间,
但算法要复杂得多,且时空复杂度就其数量级而言并未得到改观。 有关链式的基数排序可。
	   
	基数排序的时间是线性的(即O(n))。
	基数排序所需的辅助存储空间为O(n+rd)。
	基数排序是稳定的。
*/


int main()
{
	int arr[] = {2321,434,2,32,1232,-123,4,54,62,6567};
	int len = sizeof(arr)/sizeof(arr[0]);
	
	show_arr(arr,len);
	std::cout<<"len is :"<<len<<std::endl;
	
	insert_sort(arr,len);
	select_sort(arr, len);
	select_sort_ex(arr, len);
	shell_sort(arr,len);
	mao_pao(arr,len);
	quick_sort(arr,0x00,len);
	q_sort(arr,0x00,len);
	select_sort(arr,len);
	select_sort_ex(arr,len);
	
	int sarr[] =  {1,2,3,4,5,6,7,8,3,4,5,6,7,8,9,10,11,23,43,54};
	int sum_len = sizeof(sarr)/sizeof(sarr[0]);
	merge_sort(sarr, 0x00, 7,sum_len-0x01);
	show_arr(sarr,sum_len);
	return 0x00;
}


/*

按平均时间将排序分为四类:

(1)平方阶(O(n2))排序
  一般称为简单排序,例如直接插入、直接选择和冒泡排序;

(2)线性对数阶(O(nlgn))排序
  如快速、堆和归并排序;

(3)O(n1+£)阶排序
  £是介于0和1之间的常数,即0<£<1,如希尔排序;

(4)线性阶(O(n))排序
  如桶、箱和基数排序。

各种排序方法比较

 简单排序中直接插入最好,快速排序最快,当文件为正序时,直接插入和冒泡均最佳。

影响排序效果的因素

  因为不同的排序方法适应不同的应用环境和要求,所以选择合适的排序方法应综合考虑下列因素:
  ①待排序的记录数目n;
  ②记录的大小(规模);
  ③关键字的结构及其初始状态;
  ④对稳定性的要求;
  ⑤语言工具的条件;
  ⑥存储结构;
  ⑦时间和辅助空间复杂度等。

不同条件下,排序方法的选择

(1)若n较小(如n≤50),可采用直接插入或直接选择排序。
  当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
(2)若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜;
(3)若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
  快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
  堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
  若要求排序稳定,则可选用归并排序。但本章介绍的从单个记录起进行两两归并的 排序算法并不值得提倡,
 通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。
 因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。
 (4)在基于比较的排序方法中,每次比较两个关键字的大小之后,仅仅出现两种可能的转移,因此可以用一棵二叉树来描述比较判定过程。
  当文件的n个关键字随机分布时,任何借助于"比较"的排序算法,至少需要O(nlgn)的时间。
  箱排序和基数排序只需一步就会引起m种可能的转移,即把一个记录装入m个箱子之一,因此在一般情况下,
   箱排序和基数排序可能在O(n)时间内完成对n个记录的排序。但是,箱排序和基数排序只适用于像字符串和整数
   这类有明显结构特征的关键字,而当关键字的取值范围属于某个无穷集合(例如实数型关键字)时,无法使用箱排序和基数排序,
   这时只有借助于"比较"的方法来排序。
  若n很大,记录的关键字位数较少且可以分解时,采用基数排序较好。虽然桶排序对关键字的结构无要求,但它也只有在关键字是
	随机分布时才能使平均时间达到线性阶,否则为平方阶。同时要注意,箱、桶、基数这三种分配排序均假定了关键字若为数字时,
	则其值均是非负的,否则将其映射到箱(桶)号时,又要增加相应的时间。
(5)有的语言(如Fortran,Cobol或Basic等)没有提供指针及递归,导致实现归并、快速(它们用递归实现较简单)和基数(使用了指针)
	等排序算法变得复杂。此时可考虑用其它排序。
(6)本章给出的排序算法,输人数据均是存储在一个向量中。当记录的规模较大时,为避免耗费大量的时间去移动记录,
	可以用链表作为存储结构。譬如插入排序、归并排序、基数排序都易于在链表上实现,使之减少记录的移动次数。
	但有的排序方法,如快速排序和堆排序,在链表上却难于实现,在这种情况下,可以提取关键字建立索引表,
	然后对索引表进行排序。然而更为简单的方法是:引人一个整型向量t作为辅助表,排序前令t[i]=i(0≤i<n),
	若排序算法中要求交换R[i]和R[j],则只需交换t[i]和t[j]即可;排序结束后,向量t就指示了记录之间的顺序关系:
 R[t[0]].key≤R[t[1]].key≤…≤R[t[n-1]].key
 若要求最终结果是:
 R[0].key≤R[1].key≤…≤R[n-1].key
则可以在排序结束后,再按辅助表所规定的次序重排各记录,完成这种重排的时间是O(n)。
 
*/











评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值