排序算法

算法的稳定性:

概念

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。

判断方法

对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性;而对于稳定的排序算法,必须对算法进行分析从而得到稳定的特性。需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。
再如,快速排序原本是不稳定的排序方法,但若待排序记录中只有一组具有相同关键码的记录,而选择的轴值恰好是这组相同关键码中的一个,此时的快速排序就是稳定的。

生成N个随机数:

在实际中,要测试各种排序算法,通常要输入大量的数据,为了简化程序,能快速看到排序算法的效果,编写一个随机生成整型数组来进行测试,下面的函数用来生成n个不相等的整数。
//创建一个随机数组,arr保存生成的数据,n为数组元素的数量,min为生成数据的最小值,max表示生成数据的最大值,数组中生成的数都不重复。
int CreateData(int arr[],int n,int min,int max) 
{
    int i,j,flag;
    srand(time(NULL));//取时间做随机数种子,这样种子酒不会重复,产生的随机数也就不会重复。
    if((max-min+1)<n) //最大数与最小数之差小于产生数组中元素的数量,生成数据不成功,因为数组中的元素都是不重复的。
		return 0; 
    for(i=0;i<n;i++)
    {
        do
        {
            arr[i]=(max-min+1)*rand()/(RAND_MAX+1)+min;
            flag=0;
            for(j=0;j<i;j++)
            {
                if(arr[i]==arr[j])
                    flag=1;
            }
        }while(flag);       
    }
    return 1;
}
上面的函数需要包含头文件:#include<stdlib.h>,#include <time.h>。

1.冒泡排序算法:

冒泡排序算法的思想:对待排序记录关键字从前往后进行多遍扫描,当发现相邻两个关键字的次序与排序要求的规则不符时,就将这两个记录进行交换。这样,关键字较小的记录将逐渐从后面向前面移动,就像气泡在水中向上浮一样,所以该算法称为冒泡排序算法。代码如下:
void Bubble(int a[],int length)
{
     int i,j,tmp;
     /*外层循环式控制循环次数,之所以i<length-1,是因为,
     当前length-1个数都排好序后,最后一个数不用在排序了*/
     for(i=0; i<length-1; i++)
     {
          /*内层循环是进行逐个比较,若满足大于或小于关系则交换,之所以j<length-1-i,
          是因为当前已经有i个数已经排好序了,故当前只需要比较前length-1-i个数就好了。*/
          for(j=0; j<length-1-i; j++)
          {
               //若满足关系,则交换。
               if(a[j] > a[j+1])
               {
                    tmp = a[j];
                    a[j] = a[j+1];
                    a[j+1] = tmp;
               }
          }
     }
}

从上面的程序可以看出,使用冒泡排序发对n个数据进行排序,一共需要进行n-1次的比较。如果本来就是有序的数据,也需要进行n-1次比较。这就造成了冒泡排序算法虽然简单,但效率较差。
     为了提升冒泡排序算法的效率,可对Bubble函数进行改进,当在某一遍扫描时,发现数据都已经按顺序排列了(就是在此遍扫面过程中没有出现数据之间的两两交换),就不再进行后面的扫描,而结束排序过程。
     具体实现是:可以设置一个标志变量flag,在每一遍扫描之前将其设置为0,在扫描数据的过程中,若有数据交换,则设置其值为1.在一遍扫描完成之后,判断flag的值,若其值为0,表示在这一遍扫遍中已经没有数据进行交换,数据已经按顺序排列,就不需要进行后续的扫描。
实现程序如下:
int bubbleSort(int arr[],int len)
{
     int i,j,tmp,flag=0;//增加标志变量flag判断本次扫描是否有交换。
     if(len < 0 )
          return -1;
     for(i=0;i<len-1;i++)
     {
          for(j=0;j<len-1-i;j++)
          {
               if(arr[j]<arr[j+1])
               {
                    tmp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = tmp;
                    flag=1;//本次扫描有交换,就将flag置1
               }
          }
          if(0 == flag)//如果本次扫描flag为0则本次扫描没有发生交换,那么数组已然有序,break退出,不用在扫描了
               break;
          else
               flag = 0;//否则将flag置为默认的0值。继续下一次扫描(冒泡排序操作)。
     }

     return 0;
}

算法分析

 若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数和记录移动次数均达到最小值n-1;
所以,冒泡排序最好的时间复杂度为O(n)。
初始文件是反序的,需要进行n-1趟排序。每趟排序要进行n-i次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:
比较:n(n-1)/2=O(n^2)
移动:3n(n-1)/2=O(n^2)

冒泡排序的最坏时间复杂度为O(n^2)。
综上,因此冒泡排序总的平均时间复杂度为O(n^2)。

冒泡排序毕竟是一种效率低下的排序方法,在数据规模很小时,可以采用。数据规模比较大时,最好用其它排序方法。

算法稳定性

冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。


2.选择排序:

选择排序法就是在当前的集合中选择最小的数与第一个数进行交换,然后将集合缩小一个,即是将已经选出的最小数排除。然后再在剩下的集合中重复上面步骤,直到所有的都排好序。注意只剩最后一个数的时候不需比较。
还有一种解释就是每一趟从待排序的数据元素中选出最小的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完直接选择排序和直接插入排序类似,都将数据分为有序区和无序区,所不同的是直接插入排序是将无序区的第一个元素直接插入到有序区以形成一个更大的有序区,而直接选择排序是从无序区选一个最小的元素直接放到有序区的最后。
代码如下:
bool SelectSort(int arr[],int len)
{
	if(NULL == arr || len<=0)
		return false;
	int i,j,tmp,minIndex;
	for(i=0; i<len-1; i++)
	{
		minIndex = i;
		for(j=i+1; j<len; j++)
		{
			if(arr[j]<arr[minIndex])
				minIndex = j;
		}
		if(minIndex != i)
		{
			tmp = arr[i];
			arr[i] = arr[minIndex];
			arr[minIndex] = tmp;
		}
	}
	return true;
}
性能分析:
比较次数O(n^2),比较次数与关键字的初始状态无关,总的比较次数N=(n-1)+(n-2)+...+1=n*(n-1)/2。交换次数O(n),最好情况是,已经有序,交换0次;最坏情况是,逆序,交换n-1次。交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。选择排序的赋值操作介于 0 和 3 (n - 1) 次之间。以为每次交换两个数都要进行3次交换操作。由于选择排序会改变数的相对位置,所以选择排序不是一个稳定的排序算法,例如: { 2, 2, 1}, 第一次选的时候变成 { 1, 2, 2 }, 两个2的次序就变了
直接选择排序的平均时间复杂度为O(n2)。

3.直接插入排序:

插入排序:首先将第一个元素视为一个集合,然后将后面的一个元素插入此集合并使集合有序,然后依次将后面的元素按照同样的方法插入到集合中,注意每次插入完成后集合都是有序的。插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。每次处理就是将无序数列的第一个元素与有序数列的元素从后往前逐个进行比较,找出插入位置,将该元素插入到有序数列的合适位置中。基本插入排序的时间复杂度为O(n的平方),属于稳定排序的一种(通俗地讲,就是两个相等的数不会交换位置) 。

bool InsertSort(int arr[],int len)
{
	if(NULL == arr || len<=0)
		return false;
	int i,j,currentInsertValue;
	for(i=1; i<len; i++)
	{
		currentInsertValue = arr[i];
		for(j=i-1; j>=0; j--)
		{
			if(currentInsertValue < arr[j])
			{
				arr[j+1] = arr[j]
			}
			else
				break;
		}
		arr[j+1] = currentInsertValue;
	}
	return true;
}

算法的时间复杂度
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数加上 (n-1)次。平均来说插入排序算法的时间复杂度为O(n^2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。
 直接插入排序属于稳定的排序,时间复杂性为o(n^2),空间复杂度为O(1)。

4.快速排序:

快速排序(Quicksort)是对冒泡排序的一种改进。由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。设要排序数组是A[0]……A[N-1],首先任意选取一个数据(通常选用第1个数据)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。
#include <iostream>
using namespace std;

/*进行一趟快速排序,将第一个元素作为基准。返回此趟拍完序的基准应该处的位置
此函数还进行一趟排序处理,将大于基准的元素都放入基准元素的右方,小于基准的元素放入基准的左方。*/
int Position(int arr[],int left,int right)
{
	//取最左边的元素为基准元素
	int base = arr[left];
	while(left<right)
	{
		//找到右边第一个小于基准的数将他放入左边
		while(left<right && base<=arr[right])
			right--;
		arr[left] = arr[right];
		//找到左边第一个大于基准的数,将它放入右边
		while(left<right && base>=arr[left])
			left++;
		arr[right] = arr[left];
	}
	//最后将基准值放入该插入的位置,并返回此位置
	arr[left] = base;
	return left;
}

bool QuickSort(int arr[],int left,int right)
{
	if(NULL==arr || right<=left)
		return false;
	int pos;
	if(left<right)
	{
		//以基准元素将数组分为两部分,获得基准元素在数组中的位置
		pos = Position(arr,left,right);
		//递归排序数组左部分
		QuickSort(arr,left,pos-1);
		//递归排序数组右部分
		QuickSort(arr,pos+1,right);
	}
	return true;
}

int main()
{
	int arr[] = {5};
	int len = sizeof(arr)/sizeof(int);
	int i;
	for(i=0; i<len; i++)
		cout<<arr[i]<<" ";
	cout<<endl;
	if(QuickSort(arr,0,len-1))
	{
		for(i=0; i<len; i++)
			cout<<arr[i]<<" ";
		cout<<endl;
	}
	return 0; 
}

快速排序算法分析:

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

 

最坏时间复杂度:最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。因此,快速排序必须做 n-1 次划分,第 i 次划分开始时区间长度为 n-i-1, 所需的比较次数为 n-i(1<=i<=n-1), 故总的比较次数达到最大值 Cmax =n(n-1)/2=O(n^2) 。如果按上面给出的划分算法,每次取当前无序区的第 1 个记录为基准,那么当文件的记录已按递增序(或递减序)排列时,每次划分所取的基准就是当前无序区中关键字最小(或最大)的记录,则快速排序所需的比较次数反而最多。

 

最好时间复杂度:在最好情况下,每次划分所取的基准都是当前无序区的“中值”记录,划分的结果与基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数为 O(n×lgn)

 

用递归树来分析最好情况下的比较次数更简单。因为每次划分后左、右子区间长度大致相等,故递归树的高度为 O(lgn), 而递归树每一层上各结点所对应的划分过程中所需要的关键字比较次数总和不超过 n,故整个排序过程所需要的关键字比较总次数C(n)=O(n×lgn) 。因为快速排序的记录移动次数不大于比较的次数,所以快速排序的最坏时间复杂度应为 O(n^2 ),最好时间复杂度为 O(n×lgn)

 

基准关键字的选取:在当前无序区中选取划分的基准关键字是决定算法性能的关键。 ①“三者取中”的规则,即在当前区间里,将该区间首、尾和中间位置上的关键字比较,以三者之中值所对应的记录作为基准,在划分开始前将该基准记录和该区的第个记录进行交换,此后的划分过程与上面所给的 Partition 算法完全相同。 ② 取位于 low  high 之间的随机数k(low<=k<=high),  R[k] 作为基准;选取基准最好的方法是用一个随机函数产生一个位于 low  high 之间的随机数k(low<=k<=high),  R[k] 作为基准 , 这相当于强迫 R[low..high] 中的记录是随机分布的。用此方法所得到的快速排序一般称为随机的快速排序。随机的快速排序与一般的快速排序算法差别很小。但随机化后,算法的性能大大提高了,尤其是对初始有序的文件,一般不可能导致最坏情况的发生。算法的随机化不仅仅适用于快速排序,也适用于其他需要数据随机分布的算法。

 

平均时间复杂度:尽管快速排序的最坏时间为 O(n^2 ), 但就平均性能而言,它是基于关键字比较的内部排序算法中速度最快的,快速排序亦因此而得名。它的平均时间复杂度为 O(n×lgn)

 

空间复杂度:快速排序在系统内部需要一个栈来实现递归。若每次划分较为均匀,则其递归树的高度为 O(lgn), 故递归后所需栈空间为 O(lgn) 。最坏情况下,递归树的高度为 O(n), 所需的栈空间为 O(n) 

 

稳定性:

快速排序是不稳定的


5.归并排序:

来看归并排序,其的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?

可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递的分解数列,再合数列就完成了归并排序。

使用二路合并排序法进行排序时,需要占用较大的辅助空间,辅助空间的大小与待排序列一样多。

程序代码如下:
//将有序的两个序列first到mid 和mid到last 合并
void MergeArray(int arr[],int first, int mid,int last,int tmp[])
{
         int i = first;
         int j = mid + 1;
         int k = 0;

         while(i<=mid && j<=last)
        {
                 if(arr[i] < arr[j])
                        tmp[k++] = arr[i++];
                 else
                        tmp[k++] = arr[j++];
        }

         while(i<=mid)
                tmp[k++] = arr[i++];
         while(j<=last)
                tmp[k++] = arr[j++];
         for(i=0; i<k; i++)
                arr[first+i] = tmp[i]; //注意这里是 arr[first+i] = tmp[i];不是arr[i] = tmp[i];
}
//合并平排序的核心函数
void MergeSortCore(int arr[],int first, int last,int tmp[])
{
         int mid;
         if(first < last)
        {
                mid = (first+last)/2;
                MergeSortCore(arr,first,mid,tmp);//排序前半序列
                MergeSortCore(arr,mid+1,last,tmp);//排序后半序列
                MergeArray(arr,first,mid,last,tmp);//合并前半和后半序列
        }
}

//在此函数中调用MergeSortCore函数
bool MergeSort(int arr[],int len)
{
         if(NULL == arr || len<=0)
                 return false ;
         int* tmp = new int[len];
         if(!tmp)
                 return false ;
        MergeSortCore(arr,0,len-1,tmp);
         delete[] tmp;
         return true ;
}

有的书上是在mergearray()合并有序数列时分配临时数组,但是过多的new操作会非常费时。因此作了下小小的变化。只在MergeSort()中new一个临时数组。后面的操作都共用这一个临时数组。
对于N个元素的数组来说, 如此划分需要的层数是以2为底N的对数, 每一层中, 每一个元素都要复制到结果数组中, 并复制回来, 所以复制2N次, 那么对于归并排序,它的时间复杂度为O(N*logN), 而比较次数会少得多, 最少需要N/2次,最多为N-1次, 所以平均比较次数在两者之间. 它的主要问题还是在于在内存中需要双倍的空间.
二、算法分析
1、稳定性
       归并排序是一种稳定的排序。
2、存储结构要求
      可用顺序存储结构。也易于在链表上实现。
3、时间复杂度
      对长度为n的文件,需进行 趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。
4、空间复杂度
      需要一个辅助向量来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n),显然它不是就地排序。
  注意:
      若用单链表做存储结构,很容易给出就地的归并排序
归并算法将两个有序的数组合并到一个数组中并使之有序,这两个数组并不一定相同大小,但需要一个额外的数组存放归并结果。算法比较两个数组相同位置的元素,将小的放入结果数组中,如此往复,如果其中一个先到达末尾,则将另外一个剩下部分放入结果数组中。 
   归并排序将数组不断划分, 第一次分成两半, 第二次分成四份, 如此直到得到只有一个元素的数组返回, 假定一个元素是有序的, 然后将两个数据项归并到两个元素的有序数组中, 再次返回, 将这一对两个元素的数组归并到一个四个元素的数组中, 返回最外层的时候, 这个数组将会有两个分别有序的子数组, 再次归并则完成排序.


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章