各种排序算法的分析与实现

因为近期在找工作的过程中遇见了许多考察各种排序算法的问题,而我对此却不是很熟悉。所以花了一些时间做了一个很easy的总结。


一.直接插入排序(straight insertion sort

直接插入排序算法是稳定的排序算法。时间复杂度为O(n^2)

思想为:如果待排序的数组存放在a[1...n]中,初始时。a[1]自成一个有序区,无序区为a[2...n]。从i=2i=n为止,依次将a[i]插入到当前有序区a[1...i-1]中去,生成n个记录的有序区。

排序方法

1.简单方法

首先在当前有序区R[1..i-1]中查找R[i]的正确插入位置k(1ki-1)。然后将R[k..i-1]中的记录均后移一个位置。腾出k位置上的空间插入R[i]

注意:若R[i]的keyword大于等于R[1..i-1]中全部记录的keyword,则R[i]就是插入原位置。

2.改进的方法

一种查找比較操作和记录移动操作交替地进行的方法。详细做法:

将待插入记录R[i]的keyword从右向左依次与有序区中记录R[j](j=i-1i-2。…,1)的keyword进行比較:

① 若R[j]的keyword大于R[i]的keyword。则将R[j]后移一个位置;

②若R[j]的keyword小于或等于R[i]的keyword,则查找过程结束,j+1即为R[i]的插入位置。

keyword比R[i]的keyword大的记录均已后移,所以j+1的位置已经腾空。仅仅要将R[i]直接插入此位置就可以完毕一趟直接插入排序。

 

void insert_sort(int a[],int n)
{
    for(int i = 0 ; i < n ; i++)
    {
        int temp = a[i] ;
        int j ;
        for(j = i-1 ; j >= 0 && temp < a[j] ; j--)
        {
            a[j+1] = a[j] ;
        }
        a[j+1] = temp ;
    }
}

 

 

二.希尔排序(Shell Sort

希尔排序(Shell Sort)插入排序的一种。也称缩小增量排序。是直接插入排序算法的一种更高效的改进版本号。

希尔排序是非稳定排序算法。希尔排序时间复杂度的下界是n*log2n

 

希尔排序是依照不同步长对元素进行插入排序。当刚開始元素非常无序的时候,步长最大。所以插入排序的元素个数非常少。速度非常快;当元素基本有序了。步长非常小。插入排序对于有序的序列效率非常高。所以。希尔排序的时间复杂度会比o(n^2)好一些。

 

算法思想:先取一个小于n的整数d1作为第一个增量,把文件的所有记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序。然后,取第二个增量d2<d1反复上述的分组和排序,直至所取的增量dt = 1,即全部记录放在同一组中进行直接插入排序为止。

该方法实质上是一种分组插入方法。

一般的初次取序列的一半为增量,以后每次减半,直到增量为1

 

 

void shell_sort(int a[],int n)
{
    int h,i,j,temp ;
 
    for(h = n/2 ; h > 0 ; h/=2)  //控制增量
    {
        for(i = h ; h < n ; h++)  //这个for循环就是前面的直接插入排序
        {
            temp = a[i] ;
            for(j = i - h ; j >= 0 && temp < a[j] ; j-=h)
            {
                a[j+h] = a[j] ;
            }
            a[j+h] = temp ;
        }
    }
}


 

三.冒泡排序(Bubble Sort

冒泡排序(Bubble Sort)。是一种较简单的稳定的排序算法时间复杂度为O(n^2)

 

思想:(从小到大排序)存在n个不同大小的气泡。由底至上地把较轻的气泡逐步地向上升,这样经过遍历一次后。最轻的气泡就会被上升到顶(下标为0),然后再从底至上地这样升,循环直至n个气泡大小有序。

冒泡排序中,最重要的思想是两两比較。将两者较小的升上去。

 

void bubble_sort(int a[],int n)
{
    int exchange = 0 ;    //用于记录每次扫描时是否发生交换
 
    for(int i = 0 ; i < n-1 ; i++)  //进行n-1趟扫描
    {
        exchange = 0 ;
        for(int j = n-1 ; j >= i ; j--)  //从后往前交换,这样最小值每次都到开头的位置
        {
            if(a[j]>a[j+1])
            {
                int temp = a[j] ;
                a[j] = a[j+1] ;
                a[j+1] = temp ;
                exchange = 1 ;  //假如一趟扫描中至少有一组值发生了交换,那么就说明还没有序
            }
        }
        if(exchange != 1) break;
    }
}


 

四.高速排序(Quick Sort

高速排序(Quick sort)是对冒泡排序的一种改进。不稳定。它的平均时间复杂度为O(nlgn)

高速排序的时间主要耗费在划分操作上,对长度为k的区间进行划分。共需k-1次keyword的比較。

最坏情况是每次划分选取的基准都是当前无序区中keyword最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的还有一个非空的子区间中记录数目。只比划分前的无序区中记录个数降低一个。时间复杂度为O(n*n)

在最好情况下,每次划分所取的基准都是当前无序区的"中值"记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的keyword比較次数:O(nlgn)

虽然高速排序的最坏时间为O(n^2),但就平均性能而言,它是基于keyword比較的内部排序算法中速度最快者,高速排序亦因此而得名。

思想:设要排序的数组A[0]……A[N-1],首先随意选取一个数据(通常选用数组的第一个数)作为重要数据,然后将全部比它小的数都放到它前面。全部比它大的数都放到它后面,这个过程称为一趟高速排序。

一趟高速排序的算法是:

1)设置两个变量ij排序開始的时候:i=0j=N-1

2)以第一个数组元素作为重要数据。赋值给key,即key=A[0]

3)从j開始向前搜索,即由后開始向前搜索(j--),找到第一个小于key的值A[j],将A[j]A[i]互换;

4)从i開始向后搜索。即由前開始向后搜索(i++),找到第一个大于keyA[i],将A[i]A[j]互换。

5)反复第34步。直到i = j

 

 

void quick_sort(int a[],int low,int high)
{
	int i,j,pivot;           //pivot基准
	if(low < high)    //传进来进行一次推断
	{
		pivot = a[low] ; 
		i = low ;
		j = high ;
		while(i<j)
		{
			while(i<j && a[j] >= pivot) j--;
			if(i<j) a[i++] = a[j] ;     //将比pivot小的元素移到低端
			while(i<j && a[i] <= pivot) i++;
			if(i<j) a[j--] = a[i] ;    //将比pivot大的元素移到高端
		}
		a[i] = pivot ;                   //pivot移到终于位置
		quick_sort(a,low,i-1);           //左区间递归排序
		quick_sort(a,i+1,high);          //右区间递归排序
	}
}

 

五.直接选择排序(Straight Select Sorting )

直接选择排序(Straight Select Sorting)是一种较简单的不稳定的排序算法时间复杂度为O(n^2)

思想:第一次从R[0]~R[n-1]中选取最小值,与R[0]交换,第二次从R[1]~R[n-1]中选取最小值,与R[1]交换。....,第i次从R[i-1]~R[n-1]中选取最小值,与R[i-1]交换。.....。第n-1次从R[n-2]~R[n-1]中选取最小值。与R[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列

 

void select_sort(int a[],int n)
{
	for(int i = 0 ; i < n ; i++)    //进行n-1次遍历
	{
		int x = a[i] ;     //每次遍历前x和l的初始设置
		int l = i ;    
		for(int j = i ; j < n ; j++)   //遍历从i位置到数组的尾部
		{
			if (a[j]<x)
			{
				x = a[j] ; //x记录最小值
				l = j ;    //l记录最小值的位置
			}
		}
		a[l] = a[i] ;    //把最小元素与a[i]进行交换
		a[i] = x ;
	}
}


 

 

六.堆排序(Heap Sort

平均性能 :O(N*logN)它是不稳定的排序方法。


其它性能 :因为建初始堆所需的比較次数较多,所以堆排序不适宜于记录数较少的文件。  堆排序是就地排序,辅助空间为O(1.

 

小顶堆:全部子节点都大于等于其父节点。

大顶堆:全部子节点都小于等于其父节点。

若将此序列所存储的向量A[1...n]看为一颗全然二叉树的存储结构,那么堆实质上是满足一下性质的全然二叉树:树中任一非叶子结点的keyword均不大于(或不小于)其左右子节点的keyword。

因此堆排序是一种树形选择排序。

 

堆排序利用了大根堆(或小根堆)堆顶记录的keyword最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)keyword的记录变得简单。

用大根堆排序的基本思想

① 先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区

② 再将keyword最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keysR[n].key

③因为交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。

然后再次将R[1..n-1]中keyword最大的记录R[1]和该区间的最后一个记录R[n-1]交换。由此得到新的无序区R[1..n-2]和有序区R[n-1..n]。且仍满足关系R[1..n-2].keysR[n-1..n].keys,相同要将R[1..n-2]调整为堆。

……

直到无序区仅仅有一个元素为止。

2)大根堆排序算法的基本操作:

建堆。建堆是不断调整堆的过程,从len/2处開始调整,一直到第一个节点。此处len是堆中元素的个数。建堆的过程是线性的过程,从len/20处一直调用调整堆的过程,相当于o(h1)+o(h2)…+o(hlen/2)当中h表示节点的深度,len/2表示节点的个数,这是一个求和的过程,结果是线性的O(n)

PS:从len/2開始调整的原因是,从最后一个非叶子节点处開始调整,它的左右叶子节点已经是堆了。

每次调整一个节点之前。保证它的左右叶子一定是一个堆。

调整堆:调整堆在构建堆的过程中会用到,并且在堆排序过程中也会用到。

利用的思想是比較节点i和它的孩子节点left(i),right(i),选出三者最大(或者最小)者,假设最大(小)值不是节点i而是它的一个孩子节点。那便交换节点i和该节点,然后再调用调整堆过程。这是一个递归的过程。调整堆的过程时间复杂度与堆的深度有关系,是lgn的操作,由于是沿着深度方向进行调整的。

堆排序:堆排序是利用上面的两个过程来进行的。

首先是依据元素构建堆。然后将堆的根节点取出(通常是与最后一个节点进行交换),将前面len-1个节点继续进行堆调整的过程。然后再将根节点取出。这样一直到全部节点都取出。堆排序过程的时间复杂度是O(nlgn)。由于建堆的时间复杂度是O(n)(调用一次);调整堆的时间复杂度是lgn。调用了n-1次,所以堆排序的时间复杂度是O(nlgn) 。

调整堆(Heapify)函数思想方法:

每趟排序開始前,A[1...i]是以A[1]为根的堆。在A[1]A[i]交换之后。新的无序区A[1...i-1]中仅仅有A[1]的值发生了变化,故除A[1]可能违反堆的性质外,其余不论什么节点为根的子树均是堆。

因此调整区间A[low...high]时,仅仅须调整以A[low]为根的树就能够了。建立堆也是这种一个过程。


int heapSize = 0 ;  //堆大小。值为数组的长度
//返回右子节点的索引
int Left(int index){ return ((index<<1) + 1 ) ; }

//返回左子节点的索引
int Right(int index){ return ((index<<1) + 2 ) ; }

//交换a,b的值,指针操作
void swap(int *a,int *b){int temp = *a ; *a = *b ; *b = temp ;}

//a[index]与其左右子树进行递归对照
void maxHeapify(int a[],int index)
{
	int largest = index ;   //最大数
	int left = Left(index) ; //左子树索引
	int right = Right(index) ; //右子树索引
	if((left<=heapSize) && (a[left]>a[largest])) largest = left ;
	if((right<=heapSize) && (a[right]>a[largest])) largest = right ;
	//把largest与堆顶的左右子节点比較。取最大值

	//此时largest为堆顶。左子节点,右子节点的最大值
	if(largest!=index)
	{
		//假设堆顶不是最大值。那么交换并递归调整堆
		swap(&a[index],&a[largest]);
		maxHeapify(a,largest);
	}
}

//初始化堆。将数组中的每个元素放到合适的位置
//完毕之后。堆顶元素为数组的最大值
void buildMaxheap(int a[],int n)
{
	heapSize = n ;   //堆大小为数组长度
	for (int i = (n>>1) ; i>= 0 ; i--)
	{    
		//从最后一个非叶子节点開始调整。由于全部的叶子结点已经为堆了
		maxHeapify(a,i);
	}
}

void heap_sort(int a[],int n)
{
	//初始化堆
	buildMaxheap(a,n-1);  //传进去的是数组长度
	for(int i = (n-1) ; i>= 1; i--)
	{
		//堆顶元素a[0](数组的最大值)被置换到数组的尾部a[i]
		swap(&a[0],&a[i]);
		heapSize--;  //从堆中移除该元素
		maxHeapify(a,0);//每次调整都是从堆顶開始的。由于除堆顶外其它的节点都满足堆的性质
	}
}


 

七.基数排序(Radix Sort)

基数排序法是属于稳定性的排序,其时间复杂度O (nlog(r)m)。当中r为所採取的基数,而m为堆数,在某些时候,基数排序法的效率高于其他的稳定性排序法。

原理类似桶排序,这里总是须要10个桶,多次使用

首先以个位数的值进行装桶,即个位数为1则放入1号桶,9则放入9号桶,临时忽视十位数

比如

待排序数组[62,14,59,88,16]简单点五个数字

分配10个桶,桶编号为0-9,以个位数数字为桶编号依次入桶,变成下边这样

|  0  |  0  | 62 |  0  | 14 |  0  | 16 |  0  |  88 | 59 |

|  0  |  1  |  2  |  3  |  4 |  5  |  6  |  7  |  8  |  9  |桶编号

将桶里的数字顺序取出来,

输出结果:[62,14,16,88,59]

再次入桶,只是这次以十位数的数字为准,进入对应的桶,变成下边这样:

因为前边做了个位数的排序,所以当十位数相等时,个位数字是由小到大的顺序入桶的,就是说,入完桶还是有序

|  0  | 14,16 |  0  |  0  |  0  | 59 | 62  | 0  | 88  |  0  |

|  0  |  1      |  2  |  3  |  4  |  5  |  6  |  7  |  8  |  9  |桶编号

 

由于没有大过100的数字,没有百位数,所以到这排序完成,顺序取出就可以

最后输出结果:[14,16,59,62,88].

 

既然我们能够从最低位到最高位进行如此的分配收集。那么能否够由最高位到最低位依次操作呢? 答案是全然能够的。

基于两种不同的排序顺序,我们将基数排序分为最低位优先LSDLeast significant digital)或最高位优先MSDMost significant digital),

LSD的排序方式由数值的最右边(低位)開始,而MSD则相反。由数值的最左边(高位)開始。

注意一点:LSD的基数排序适用于位数少的数列,假设位数多的话,使用MSD的效率会比較好。

MSD的方式与LSD相反,是由高位数为基底開始进行分配。但在分配之后并不立即合并回一个数组中。而是在每一个桶子中建立子桶,将每一个桶子中的数值依照下一数位的值分配到子桶中。

在进行完最低位数的分配后再合并回单一的数组中。

 

int find_max(int a[],int n)
{
	int maxx = a[0] ; 
	for(int i = 1 ; i < n ; i++)
	{
		if(a[i]>maxx)maxx = a[i] ;
	}
	return maxx ; 
}

//返回数字n的位数
int digit_num(int n)
{
	int digit = 0 ; 
	do 
	{
		++digit ;
		n/=10 ;
	} while (n!=0);
	return digit ; 
}

//返回数组num的第k位数字
int kth_digit(int num,int k)
{
	num/=pow(10,k) ;
	return num%10 ; 
}

//对长度为n的数组进行基数排序
void radix_sort(int a[],int n)
{
	int *temp[10] ; //指针数组。每个数组表示一个箱子
	int count[10] = {0,0,0,0,0,0,0,0,0,0} ; //用于存储每个箱子装的元素个数
	int maxx = find_max(a,n);   //取得数组中的最大值
	int maxDigit = digit_num(maxx) ; //取得maxx的位数
	for(int i = 0 ; i < 10  ; i++)
	{
		temp[i] = new int[n] ; //使得每个箱子能够装下n的元素
		memset(temp[i],0,sizeof(int)*n) ;
	}
	for(int i = 0 ; i < maxDigit ; i++)
	{
		memset(count,0,sizeof(count)) ; // 将count清空
		for(int j = 0 ; j < n ; j++)
		{
			int x = kth_digit(a[j],i) ;    //将数据依照位数的值放入数组中
			temp[x][count[x]] = a[j] ; 
			count[x]++ ;  //此箱子里面装入的数字加1
		}
		int num = 0 ; 
		for(int j = 0 ; j < 10 ; j++)
		{
			for(int k = 0 ; k < count[j] ; k++)
			{
				a[num++] = temp[j][k] ; 
			}
		}
	}
}


 

八.归并排序(Merge Sort

时间复杂度O(nlogn)这是该算法中最好、最坏和平均的时间性能。空间复杂度O(n)归并排序比較占用内存。但却是一种效率高且稳定的算法。

归并排序是建立在归并操作上的一种有效的排序算法,该算法是採用分治法(Divide and Conquer)的一个很典型的应用。将已有序的子序列合并。得到全然有序的序列;即先使每一个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并

首先考虑下怎样将将二个有序数列合并。

这个很easy。仅仅要从比較二个数列的第一个数,谁小就先取谁。取了后就在相应数列中删除这个数。然后再进行比較,假设有数列为空。那直接将还有一个数列的数据依次取出就可以。

一次归并操作的代码

//将有序数组a[]和b[]合并到c[]中
void MemeryArray(int a[], int n, int b[], int m, int c[])
{
	int i, j, k;
	i = j = k = 0;
	while (i < n && j < m)
	{
		if (a[i] < b[j])
			c[k++] = a[i++];
		else
			c[k++] = b[j++]; 
	}
	while (i < n)
		c[k++] = a[i++];
	while (j < m)
		c[k++] = b[j++];
}


 

能够看出合并有序数列的效率是比較高的,能够达到O(n)

攻克了上面的合并有序数列问题,再来看归并排序。其的基本思路就是将数组分成二组AB。假设这二组组内的数据都是有序的,那么就能够非常方便的将这二组数据进行排序。怎样让这二组组内数据有序了?

能够将AB组各自再分成二组。依次类推,当分出来的小组仅仅有一个数据时,能够觉得这个小组组内已经达到了有序,然后再合并相邻的二个小组就能够了。

这样通过先递归的分解数列,再合并数列就完毕了归并排序。

//将结果填入暂时数组然后将暂时数组复制到原始数组
//将有二个有序数列a[first...mid]和a[mid...last]合并。  
void Merge(int a[],int temp[],int first,int middle,int last)
{
	int i = first , j = middle+1 ;
	int n = middle , m = last ;
	int k = 0 ; 
	while(i<=n&&j<=m)
	{
		if(a[i]<=a[j])
		{
			temp[k++]=a[i++];
		}
		else temp[k++]=a[j++];
	}
	while(i<=n) temp[k++]=a[i++];
	while(j<=m) temp[k++]=a[j++];

	for(i = 0 ; i < k ; i++)
	{
		a[first+i]=temp[i] ;
	}
}

void msort(int a[],int temp[],int low,int high)
{
	if(low>=high) return ;   //结束条件
	int middle = (low+high)/2;   //分裂点
	msort(a,temp,low,middle);
	msort(a,temp,middle+1,high);
	Merge(a,temp,low,middle,high); //组合,将两个有序区间组合成一个
}

void merge_sort(int a[],int n)
{
	int *temp = new int[n] ; // 分配暂时数组空间
	if(temp!=NULL)
	{
		msort(a,temp,0,n-1) ; //调用msort归并排序
		delete []temp ;    //释放暂时空间的地址
	}
}


posted @ 2017-07-25 15:40  brucemengbm  阅读(302)  评论(0编辑  收藏  举报