您的位置:首页 > 编程语言 > Java开发

关于基本排序算法的简单研究总结(java 实现)

2017-09-11 10:46 302 查看
第一次写自己的技术博客还是很激动的,为了能更好的成为一名优秀的程序员,我决定将这个习惯一直养成下去,鉴于这几天一直研究基本排序算法,那么就将我本人的一些理解体会写下来,由于本人水平有限,错误之处请多多指教。

        一.冒泡排序

对于这个算法的基本思想不必我多说,两两交换迭代,是交换排序的一种,下面上代码。

public static void bubble(int[] arr){
int t=0;
for(int i=0;i<arr.length-1;i++){
for(int j=0;j<arr.length-i-1;j++){ //此行注意要-1,j<arr.length-i取到的是该次循环遍历未排序数组的最后一位,
if(arr[j]>arr[j+1]){	//与j+1比较所以要往前数一位,否则会出现ArrayIndexOutOfBoundsEception;
t=arr[j];
arr[j]=arr[j+1];
arr[j+1]=t;
}
}
}
}

这个是最基本的形式,算法的时间复杂度平均情况下O(n^2)量级,最坏最好情况也为O(n^2),效率较慢,我们来改进一下这个算法。

实际上,冒泡每遇到两个相邻元素的逆序对,都会进行交换,而在某一次扫描中,如果没有发生交换,那么就表示前面的元素已经有序,后面的元素本来就就位,所以可以设置一个标志位来记录这样的一种情况,来提前结束我们的算法,下面是优化后的代码。

public static void optiBubble(int[] arr){
int t = 0;
boolean sorted = false; 			//此处设置标志位为false,使得第一次扫描能够正常进行
for(int i=0;i<arr.length-1;i++){
if(sorted==true){		//如果标志位为true,代表刚进行的一次扫描所扫描的所有元素都已经有序,不必再进行下一次遍历
break;
}
sorted = true;					//默认标志位设为true
for(int j=0;j<arr.length-i-1;j++){
if(arr[j]>arr[j+1]){
t=arr[j];
arr[j]=arr[j+1];
arr[j+1]=t;
sorted=false;			//如果发生交换,则代表还未有序,设为false
}
}
}
}
下面这个图形象的显示了改进后的时间(上面的梯形)



然而,我们认为这个算法还存在改进的空间,试想,最后的一次交换表示的是后面的元素
4000
已经就位,那么我们可以记录这个位置,在下一次扫描时扫描

到这个位置即可,迭代进行,会一定程度提高算法的效率。下面是代码

public static void superOptiBubble(int[] arr){
int t = 0;
int low = 0;
int high = arr.length - 1;
int logo = high;			//logo两个作用,一用来记录最后一次交换low的位置,二代替上面sorted的作用

while(low!=logo){			       //low的值一定与上一次扫描的high值相等(循环退出条件),
for(low=0;low<high;low++){        //logo记录最后一次交换的位置,若相等,则一定没有发生交换
if(arr[low]>arr[low+1]){
t=arr[low];
arr[low]=arr[low+1];
arr[low+1]=t;
logo = low;			//记录最后一次扫描交换的位置
}
}
high = logo;		//将最后一次扫描交换的位置给high,下一次的扫描到high即可,后面元素已经有序
}
}




左边的这幅图形象的表示了改进后所用的时间。

总体来说,改进后的冒泡排序最好情况可以达到O(n),但是对于最坏的情况来说(所有元素逆序排列),依然是

O(n^2),其他情况介于两者之间,冒泡排序算法是稳定的排序算法。

二,归并排序

下面我们来谈谈归并排序。归并排序的主要思想是分而治之,即递归的排序两个子序列,然后再将排好序的数组合并,此合并的merge算法也是此算法

的核心。下面看代码。

public static void mergeSort(int[] arr,int left,int right,int[] temp){

if(left==right){                  //如果是递归基,则结束
return;
}

int center = (left + right)/2;
mergeSort(arr,left,center,temp);
mergeSort(arr,center+1,right,temp);
merge(arr,left,center,right,temp);	//这个函数是此算法的关键,将两个已经排序好的数组进行合并
}

public static void merge(int[] arr,int left,int center,int right,int[] temp){
int mid = center +1;

for(int k=left;k<=right;k++){		//将要排序的数组复制到一个同样大小的数组,便于后面的比较排序
temp[k] = arr[k];
}
for(int i=left;i<=right;i++){
if(left>center){
arr[i] = temp[mid++];		//如果左侧的标志超出,则只剩右侧,全部将其复制到相应的位置
}
else if(mid>right){
arr[i] = temp[left++];		//如果右侧的超出,则只剩左侧,将其全部复制到相应位置
}
else if(temp[left]<=temp[mid]){
arr[i] = temp[left++];    //如果左侧值小,复制左侧的值
}
else{
arr[i] = temp[mid++];	  //如果右侧值小,复制右侧
}
}
}


此算法的时间复杂度可有递推式数学导出:T(n)= 2*T(n/2) + O(n), 最后导出为 O(nlogn),仔细分析,归并排序无论最好还是最坏情况都按照上诉

的规则进行,故而时间复杂度都是O(nlogn),空间复杂度为O(n),综上,归并排序是比较高效率的算法,但是缺点是耗费内存空间,主要在外部排

序才会用归并排序,内部排序一般用快速排序。

三,简单选择排序

       算法的基本思想是每次扫描选择数组中的最大的(最小也行),并记录下来,当此次扫描过后,与该次最后一个交换位置。我们来仔细地研究这个算法,发现,其实冒泡排序与他是如出一辙的,只不过,冒泡排序是每次扫描需要进行O(n)的交换才能将最大的元素就位,而简单选择排序,每次扫描只需O(1)的交换就能让元素就位。下面是代码。

   

public static void selectionSort(int[] arr){
for(int i=0;i<arr.length-1;i++){	//进行arr.length-1次扫描,最后一次剩一个元素
int j = 1;				//每次将首位设为最大,从次位开始扫描即可
int max = 0;
int t = 0;
for(;j<arr.length-i;j++){	//注意边界,到最后一个未就位的元素,防止下标越界异常
if(arr[max]<=arr[j]){  //这里注意是<=,来保证排序的稳定性
max = j;
}
}
t = arr[j-1];
arr[j-1] = arr[max];
arr[max] = t;

}
}


此算法时间复杂度与冒泡排序一样是O(n^2),不过,在常数系数意义上,是比冒泡效率高的,上面已经分析过,最坏最好情况均为O(n^2)。

四,插入排序,算法的基本思想,从第二个元素开始一共需要扫描n-1趟,每次向前扫描每个元素,找到不大于(保持稳定且以从小到大排序为例)他的元素,将元素查到该位置后面。综合来看,插入排序就像我们平时玩的扑克,对于每个元素向前寻找不大于他的元素后确定其位置来保证前面的元素

都比他小。先看代码:

public static void insertSort(int[] arr){
int temp;				//定义一个变量来保存每次要考察的元素值

for(int i=1;i<arr.length;i++){  //除去第一位,一共需要进行length次扫描
temp = arr[i];
int j = i-1;			//从他前一位开始扫描
while(j>=0 && temp<arr[j]){  //终止条件:扫描到第一位或则找到不比他大的元素
arr[j+1] = arr[j];		//比他大的元素依次后移
j--;
}
arr[j+1] = temp;    //循环结束后j+1为其位置

}
}
我们来深入理解一下这个算法,很多人会认为其与选择排序如出一辙,但是我们说这两个是截然不同的两个算法,首先,对于选择排序而言,未就位的元素部分永远都是比就位元素部分小的,而插入排序则不一定。最重要的是,两者在性能上完全不同,对于插入排序而言,它是输入敏感性的算法,也就是说他的时间复杂度与输入的元素初始排列顺序有着密切的关系,这也使插入排序成为排序家族中非常特殊的一种算法。为了更好的说明这个问题,我们引入逆序对的概念,相信我们对这个词语都不陌生,为了更好的说明其时间复杂度,我们设定每个元素与他的后面的元素可以构成逆序对,可以发现,插入排序算法中,每一次的元素后移,都是消灭一个逆序对的过程,如果说,整个输入序列公有I对逆序对,那么第K次迭代,就会进行Ik次比较,基于用的数据结构的不同,交换的时间不会大于Ik,(比如对于链表而言,只需O(1)的交换)。

      所以对于插入排序,输入的逆序对情况排序的时间复杂度主要消耗,最好情况,所有的元素顺序,不需交换,每次一次比较,n-1次的扫描,复杂度O(n),最坏情况,所有元素都逆序,第K次需要K次比较,展开为级数,时间复杂度为O(n^2);平均性能而言,假设每个元素出现在每次扫描最后的位置机会相等(数学期望等知识的推导),可得出为O(n^2);

五,快速排序

     快速排序的基本思想是分而治之,将整个序列递归的分为两个子序列求解,再合并,而最重要的是,分成的这两个子序列有一个重要的特点,左边的最大的小于右边最小的,那么当子序列有序,两个子序列又是相互独立的,合并后整体序列自然有序。那么,要让右边大于左边,即怎样分,成为快速排序的关键。同归并排序一样,他们都是分而治之递归,不过,归并排序是主要的难点在于怎样和并,而快速排序在于怎么分。下面是我自己实现的代码

public static void quickSort(int[] arr,int low,int high){
if(high-low<=0){		//单元素一下,结束递归
return;
}
int tlow = low;
int thigh = high;
int temp = arr[low];	//以首节点为轴点
while(low<high){		//迭代结束时low与high相等
while(arr[high]>=temp && high>low){ //从high端找小于轴点的元素,注意要满足迭代条件
high--;
}
arr[low] = arr[high];  //找到小于轴点的节点,或则low等于high,注意不要low++
while(arr[low]<=temp && high>low){ //从low端开始找
low++;
}
arr[high] = arr[low];
}
arr[low] = temp;	//最终找到轴点的位置
quickSort(arr,tlow,low-1);  //递归求解
quickSort(arr,low+1,thigh);
}


    

       这个算法的时间复杂度在最好的情况下是O(nlogn),即每次轴点的就位位置都在接近中间,被分的两边数量级都是n/2,一次迭代扫描一次需要O(n),那么递推公式T(n)=2*T(n/2)+O(n),经过数据推导,可以得出时间复杂度为O(nlogn),在最坏情况下,即每次的轴点都是在两边,划分极不均衡,那么这个递归就会退化,递推T(n)=T(n-1)+O(n),一共需要扫描n次,那么就会退化为跟冒泡排序一样,为O(n^2),在一般情况下,我们以轴点位置均匀独立分布为例,那么经过递推公式,数学推导,最后得出复杂度也为O(nlogn).可以看出,快速排序的时间复杂度与轴点的位置有着很大的关系,应为他决定着下一次递归规模的划分,为了使排序时间复杂度更接近O(nlogn),可以采用每次轴点随机采取等策略。

     这个算法还有一个变形,即将整个序列分为四部分,开始位的轴点,L区(比轴点小的),H区(比轴点大的),U区(未扫描的)。下面看代码

public static void quickSort2(int[] arr,int low,int high){
if(high-low<=0){		//只有一个元素以下,结束递归
return;
}
int t;
int mid = low ;		//low与mid之间是L部分,左开右闭,mid与high之间是H部分,左开右闭
for(int k=low+1;k<=high;k++){ //从轴点下个元素开始依次扫描
if(arr[k]<arr[low]){		//如果小于轴点,与H区第一个元素,换位置,同时L区拓展一个单位

t = arr[mid+1];
arr[mid+1] = arr[k];
arr[k] = t;
mid++;
}
}							//大于等于轴点,H拓展一个单位,k向下一个单位扫描

t = arr[mid];
arr[mid] = arr[low];		//最后将L1的最后一个元素与第一个元素换位,实现轴点就位
arr[low] = t;
quickSort2(arr,low,mid-1);	//递归求解
quickSort2(arr,mid+1,high);
}


     经典的快速排序,是每次扫描从两边进行,通过数列本身的特点自然分区,而变形的快排是从一端扫描,要想分区所以需要加一个标志位。再有,我们可以发现快排是不稳定的算法。在这里不再说明。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: