算法的稳定性:
概念
判断方法
生成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;
}
}
}
}
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(n-1)/2=O(n^2)
移动:3n(n-1)/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;
}
3.直接插入排序:
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;
}
4.快速排序:
#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)。
基准关键字的选取:在当前无序区中选取划分的基准关键字是决定算法性能的关键。 ①“三者取中”的规则,即在当前区间里,将该区间首、尾和中间位置上的关键字比较,以三者之中值所对应的记录作为基准,在划分开始前将该基准记录和该区的第1 个记录进行交换,此后的划分过程与上面所给的 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,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?
//将有序的两个序列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 ;
}
1、稳定性
归并排序是一种稳定的排序。
2、存储结构要求
可用顺序存储结构。也易于在链表上实现。
3、时间复杂度
对长度为n的文件,需进行 趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。
4、空间复杂度
需要一个辅助向量来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n),显然它不是就地排序。
注意:
若用单链表做存储结构,很容易给出就地的归并排序
归并算法将两个有序的数组合并到一个数组中并使之有序,这两个数组并不一定相同大小,但需要一个额外的数组存放归并结果。算法比较两个数组相同位置的元素,将小的放入结果数组中,如此往复,如果其中一个先到达末尾,则将另外一个剩下部分放入结果数组中。
归并排序将数组不断划分, 第一次分成两半, 第二次分成四份, 如此直到得到只有一个元素的数组返回, 假定一个元素是有序的, 然后将两个数据项归并到两个元素的有序数组中, 再次返回, 将这一对两个元素的数组归并到一个四个元素的数组中, 返回最外层的时候, 这个数组将会有两个分别有序的子数组, 再次归并则完成排序.