常用排序算法全解析(Java实现)

这篇博客是我之前做的一些零散笔记的重新整理,有些想不起来了,有新的内容我会更新进来。 

为简单起见,假设我们讨论目标只包含整数,当然我们的程序也允许更一般的对象(实现Comparable接口中的compareTo方法)。我们还假设整个排序工作能够在主存中完成,不能在主存中完成的排序叫作外部排序,我们单独讨论。

接下来我们会分析一些排序算法的实现原理,时间复杂度,针对特定待排序目标下性能优劣以及改进的空间,设计的排序算法有:

插入排序

冒泡排序

简单选择排序

希尔排序

快速排序

归并排序

堆排序

桶排序

基数排序

————————————————————————————————————————————————————

最简单的排序算法之一是插入排序。插入排序由N-1趟排序组成。对于p=1到N-1趟,插入排序保证从位置0到位置p上的元素为已排序状态。说白了就是第二个数插入到第一个数,保证这两个数有序,第三个数插入到前两个数中去,保证这三个数有序,以此类推。

插入排序的实现例程

//插入排序
public static void insertionSort(int[] nums){
    int j;
    for(int i=1;i<nums.length;i++){
        int tmp = nums[i];
        for(j=i;j>0&&tmp<nums[j-1];j--){
            nums[j] = nums[j-1];
        }
        nums[j] = tmp;
    }
}

由于嵌套循环的每一个都花费N次迭代,因此插入排序为O(N平方),而且这个界是精确的,当输入的序列是反序的时候可以达到该界。另外如果输入数据已预先排序,那么运行时间为O(N),此时内存循环总是监测判定不成立而终止。事实上,如果输入序列几乎被排序,那么插入排序将运行得很快。插入排序的平均时间复杂度为O(N平方)。

————————————————————————————————————————————————————

冒泡排序就是通过比较相邻的两个数,将大的放后边,小的放左边(增序的话是这样),这样一轮遍历下来,最大的数就会出现在序列的最后,如此重复不停的将较大的数放到最后,实现排序。

冒泡排序的实现例程

public static void bubbleSort(int[] nums){
    for(int i=0;i<nums.length;i++){
        for(int j=1;j<nums.length-i;j++){
            if(nums[j]<nums[j-1]){
                int tmp = nums[j];
                nums[j] = nums[j-1];
                nums[j-1] = tmp;
            }
        }
    }
}

冒泡排序通过交换相邻元素,其平均时间复杂度为O(N平方),最差也是O(N平方)

如果输入的序列有序,本来走完一趟便可完成排序,这里却需要重复的判断,这里还有优化的空间。对于本生有序或者部分有序的序列,我们可以在算法中加一个区间来记录从哪里到哪里没有发生交换。进而只在剩下无序的空间中排序。

————————————————————————————————————————————————————

选择排序就是每一趟从待排序的序列中选出最小的,依次摆放最小的形成排序序列

简单选择排序的实现例程

public static void selectionSort(int[] nums){
    for(int i=0;i<nums.length-1;i++){
        int k =i;
        for(int j=k+1;j<nums.length;j++){
            if(nums[j]<nums[k]){
                k = j;
            }
        }
        if(i != k){
            int tmp = nums[i];
            nums[i] = nums[k];
            nums[k] = tmp;
        }
    }
}

简单选择排序的时间复杂度为O(N平方)

————————————————————————————————————————————————————

希尔排序的名称来自他的发明者Donald Shell,它通过比较相距一定间隔的元素的来工作,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。有间隔的增长序列有多种选择,性能也会不一样,Shell建议的是N/2,其例程如下

//希尔排序
public static void shellSort(int[] nums){
    int j;
    for(int gap = nums.length/2;gap>0;gap/=2)
        for(int i = gap;i<nums.length;i++){
            int tmp = nums[i];
            for(j = i;j>=gap&&tmp<nums[j-gap];j-=gap){
                nums[j] = nums[j-gap];
            }
            nums[j] = tmp;
        }
}

使用希尔增量时希尔排序的最坏情形运行时间为O(N平方),使用Hibbard增量的希尔排序的最坏情形运行时间为O(N的1.5次方)

————————————————————————————————————————————————————

快速排序,顾名思义就是实践中的一种很快的排序算法,在C++或对Java基本类型的排序中特别有用。该算法之所以特别快,主要是由于非常精炼和高度优化的内部循环。快速排序跟后面说到的归并排序都是一种分治的递归算法。经典快速排序的思想也很简单,选待排序列中的一个数,用这个数将序列划分为两部分,一个比它小一个比它大,这样就形成了左中右三节,然后递归重复这个过程。

快速排序的实现例程

//快速排序
public static void fastSort(List<Integer> nums){
    if(nums.size() > 1){
        List<Integer> smaller = new ArrayList<>();
        List<Integer> same = new ArrayList<>();
        List<Integer> larger = new ArrayList<>();

        Integer chosenNum = nums.get(nums.size() / 2);
        for(Integer i : nums){
            if(i < chosenNum){
                smaller.add(i);
            }else if(i > chosenNum){
                larger.add(i);
            }else{
                same.add(i);
            }
        }
        fastSort(smaller);//递归调用
        fastSort(larger);//递归调用

        nums.clear();
        nums.addAll(smaller);
        nums.addAll(same);
        nums.addAll(larger);
    }
}

它的平均运行时间是O(NlogN),最坏情形性能为O(N平方)。

————————————————————————————————————————————————————

归并排序,这个算法中的基本操作是合并两个已排序的表。因为这两个表是已排序的,所以若将输出放到第三个表中,一趟遍历几个完成合并排序。归并排序也是一种分治思想的体现。

归并排序的实现例程

//归并排序
private static void mergeSort(int[] nums,int[] tmp,int left,int right){
    if(left<right){
        int center = (left+right)/2;
        mergeSort(int[] nums,int[] tmp,int left,int center);
        mergeSort(int[] nums,int[] tmp,int lcenter+1,int right);
        merge(nums,tmp,left,center+1,right);
    }
}
private static void merge(int[] nums,int tmp,int leftPos, int rightPos, int rightEnd){
    int leftEnd = rightPos-1;
    int tmpPos = leftPos;
    int numElements = rightEnd - leftPos + 1;

    while(leftPos<=leftEnd&&rightPos<=rightEnd){
        if(nums[leftPos]<nums[rightPos])
            tmp[tmpPos++]=nums[leftPos++];
        else 
            tmp[tmpPos++]=nums[rightPos++];
    }
    while(leftPos<=leftEnd)
        tmp[tmpPos++]=nums[leftPos++];
    
    while(rightPos<=rightEnd)
        tmp[tmpPos++]=nums[rightPos++];

    for(int i = 0;i<numElements;i++,rightEnd--)
        nums[rightEnd]=tmp[rightEnd];
}
public static void mergeSort(int[] nums){
    int[] tmp;
    mergeSort(nums,tmp,0,nums.length-1);
}

归并排序最坏情形的运行时间是O(NlogN)。

————————————————————————————————————————————————————

堆排序是优先队列数据结构的使用,建立N的元素的二叉堆花费O(N)时间,而执行deleteMin操作只花费O(logN)时间,因此总的运行时间是O(NlogN)。

堆排序的实现例程

//堆排序
private static int leftChild(int i){
    return 2 * i + 1;
}
private static void percDown(int [] a ,int i, int n){
    int child;
    int tmp;
    for(tmp = a[i];leftChild(i)<n;i =child){
        child = leftChild(i);
        if(child != n-1 && a[child] < a[child+1]) child++;
        if(tmp < a[child]) a[i] = a[child];
        else break;
    }
    a[i] = tmp;
}
private static void swapReferences(int[] a, int i, int j){
    int tmp = a[i];
    a[i] = a[j];
    a[j] = tmp;
}
public static void heapSort(int[] a){
    for(int i = a.length/2 -1;i >= 0;i--) percDown(a, i, a.length);
    for(int i = a.length-1;i>0;i--){
        swapReferences(a, 0 ,i);//将堆顶元素与末尾元素进行交换
        percDown(a, 0, i);
    }
}

————————————————————————————————————————————————————

桶排序和基数排序都是线性时间的排序,不过是在某些特殊的情况下才能实现。对于桶排序,要使桶排序能够正常工作,输入数据必须仅由小于M的正整数组成。这时我们可以使用一个大小为M的称为count的数组,初试化为全0。于是,count有M个单元(称为桶),当读入一个数i时,count[i]增加1。读入所有的输入数据后,扫描count数组,打印出排序后的表。

//桶排序
public static void bucketSort(int[] nums, int m){
    int[] sorted = new int[m];
    for(int i=0;i<m;i++){
        sorted[i]=0;
    }
    for(int i : nums){
        sorted[i]++;
    }
    int j = 0;
    for(int i = 0;i<m;i++){
        while(sorted[i]>0){
            nums[j] = sorted[i];
            j++;
            sorted[i]--;
        }
    }
}

基数排序的原理是将数值按照位数切分为不同数字,然后对每位数分别进行比较,从而达到排序的目的。比如可以实现字符串的排序。


猜你喜欢

转载自blog.csdn.net/whut2010hj/article/details/80786831