2

常见排序算法

插入排序

复杂度

最优 O(N)
最差 O(N^2)
平均 O(N^2)
空间 O(1)
稳定排序

思路

插排的思路是保证遍历到每个元素时,该元素前面的序列都是有序的。基于这个前提,我们将遍历到的新元素和它前面的序列相比对,然后将这个元素插入到适当的位置,使得这个包含新元素的序列也是有序的。虽然外层遍历只要O(N)时间,但是因为找到新元素的适当位置后要将之后的所有元素后移,所以最差时间是O(N^2)。

示意图

Insertion_sort_animation.gif

代码

public class InsertionSort<T extends Comparable> {
    public T[] sort(T[] a){
        T[] array=a;
        T tmp;
        for(int i = 1; i<a.length; i++){
            tmp=array[i];
            int j;
            // 从后向前找到插入新元素的适当位置,比新元素大的都后移一位
            for(j=i;j>0;j--){
                if(array[j-1].compareTo(tmp)<0) break;
                array[j]=array[j-1];
            }
            // 较大的元素都后移了一位,这里只要插入这个新元素,则新序列还是有序的
            array[j]=tmp;
        }
        return array;
    }
}

希尔排序

复杂度

最优 O(N)
最差 O(N^2)
空间 O(1)
不稳定排序

思路

插排的一个优化是希尔排序,在原本的插入排序中,我们选择以一个一个往后看等待插入的元素。然而插入排序有这么一个性质,即如果序列有序程度越高,则时间复杂度越低。所以希尔排序利用了这个性质,先通过较大的步长进行一次插入排序(不再是一个一个往后找待插元素,而是隔一个或者隔n/2个元素来找),这样每次子插排都能将序列变得更有序一点,而且因为步长一开始很大,所以要遍历的数少,时间复杂度也很低。后来步长变小了,要遍历的数变多了,但数组也更有序了,时间复杂度也降低了。当然n/2^i这个步长序列不是最好的,最好的步长序列能使最差复杂度降低至O(N(logN)^2)

示意图

Sorting_shellsort_anim.gif

代码

public class Shellsort {
    public static <T extends Comparable<? super T>> T[] sort(T[] a) {
        int j = 0;
        // 从n/2开始选择步长,每轮插排后减少步长,直到步长为1是最后一次
        for (int gap = a.length / 2; gap > 0; gap /= 2) {
            T tmp = null;
            // 步长为gap的插入排序
            for(int i = gap; i<a.length; i++){
                tmp = a[i];
                // 找到合适的插入点
                for(j=i;j>=gap&&tmp.compareTo(a[j-gap])<0;j-=gap){
                    a[j]=a[j-gap];
                }
                a[j]=tmp;
            }
        }
        return a;
    }
}

选择排序

复杂度

最优 O(N^2)
最差 O(N^2)
平均 O(N^2)
空间 O(1)
不稳定排序

思路

选排的思路和插排相反,插排是搜索有序序列找到合适的位置,选排则是搜索剩余的无序序列选择出其中最大(或最小)的,并将这个被选择的数放到无序序列的首部。需要注意的是。将这个数放到首部时,用的是交换的方法。因为无论如何都要遍历完子序列,所以时间复杂度至少是O(N^2)

示意图

Selection_sort_animation.gif

代码

public class SelectionSort<T extends Comparable> {
    public T[] sort(T[] array) {
        T[] a = array;
        T tmp;
        int minIndex;
        for (int i = 0; i < a.length; i++) {
            minIndex = i;
            // 在剩余无序序列中找到一个最小的
            for (int j = i; j < a.length; j++) {
                if (a[minIndex].compareTo(a[j]) > 0)
                    minIndex = j;
            }
            // 将这个最小的交换到无序序列的首部
            if (minIndex != i) {
                tmp = a[i];
                a[i] = a[minIndex];
                a[minIndex] = tmp;
            }
        }
        return a;
    }
}

归并排序

复杂度

最优 O(NlogN)
最差 O(NlogN)
平均 O(NlogN)
空间 O(N)
稳定排序

思路

归并排序是典型的分治法。基本思路是将一个较长数组一分为二,我们将它的左右两半部分分别排好序,然后将这有序的两部分合并起来,这样较长数组也是有序的了。因为两半部分都是有序的,所以合并只需要同时遍历一下两半部分,看谁较小(大)就把谁先塞进数组就可以了。根据这个思路,我们不停的二分、二分,这样二分到尽头肯定只有一个数的数组,那它肯定是有序的,然后我们将两个只有一个数的数组合并,只要比较下这两个数就行了。这样就返回了一个有两个数的有序数组,然后再一直合并、合并、直到合并出整个数组。不过,在合并两个子数组时,要先用一个临时数组存放,否则会修改两个子数组的数据(因为两个子数组实际上是原数组的两个子部分)。

示意图

Merge_sort_animation2.gif

代码

public class Mergesort<T extends Comparable<? super T>> {

    private static <T extends Comparable<? super T>> T[] sort(T[] array, T[] tmp, int left, int right){
        if(left<right){
            // 计算二分中点位置
            int center = (left+right)/2;
            // 将左半部分排序 
            sort(array,tmp,left,center);
            // 将右半部分排序
            sort(array,tmp,center+1,right);
            // 将左右两半部分合并
            merge(array,tmp,left,center+1,right);
        }
        return array;
    }
    
    public static <T extends Comparable<? super T>> T[] sort(T[] a){
        T[] tmp = (T[]) new Comparable[a.length];
        return sort(a,tmp,0,a.length-1);
    }
    
    private static <T extends Comparable<? super T>> void merge(T[] a, T[] tmpArray, int leftPos, int rightPos, int rightEnd){
        int leftEnd=rightPos-1;
        int tmpPos = leftPos;
        int numElements = rightEnd - leftPos +1;
        // 将两个有序数组的元素比较大小后合并成一个数组,存入一个临时数组中
        while(leftPos<=leftEnd && rightPos<=rightEnd){
            if(a[leftPos].compareTo(a[rightPos])<0)
                tmpArray[tmpPos++]=a[leftPos++];
            else
                tmpArray[tmpPos++]=a[rightPos++];
        }
        // 如果左半部分没有合并完(说明右半部分的数较小),将其合并进去
        while(leftPos<=leftEnd){
            tmpArray[tmpPos++]=a[leftPos++];
        }
        // 如果右半部分没有合并完(说明左半部分的数较小),将其合并进去
        while(rightPos<=rightEnd){
            tmpArray[tmpPos++]=a[rightPos++];
        }
        // 将这个临时数组拷贝回原数组
        for(int i = 0;i<numElements;i++,rightEnd--){
            a[rightEnd]=tmpArray[rightEnd];
        }
    }
}

快速排序

复杂度

最优 O(NlogN)
最差 O(N^2)
平均 O(NlogN)
空间 O(logN)
不稳定排序

思路

快速排序也是分治的思想,它先选取一个枢纽值pivot,将整个数组按照枢纽值的大小重新排列,比枢纽值小的放在左边,比枢纽值大的放在右边。然后,我们再分别对这左半部分和右半部分进行同样的操作,这里半个部分不一定有半个数组的元素,要看具体有多少元素大于或小于枢纽值。一般情况下我们都选择子数组的第一个值作为枢纽,简化实现。最好还是能写个随机选择枢纽值的函数。最差的情况下,每次选到的枢纽值都是最小(最大)的,所以每次分割出来的子数组都只比当前整个数组小1的长度,就会导致O(N^2)的复杂度。

示意图

Sorting_quicksort_anim.gif

代码

public class Quicksort {

    public <T extends Comparable<? super T>> T[] sort(T[] a){
        return sort(a,0,a.length);
    }
    
    private <T extends Comparable<? super T>> T[] sort(T[] a, int left, int right){
        if(left==right-1) return null;
        //Partition into two part first
        int k=partition(a,left,right);
        //Recurse on front part
        sort(a,left,k);
        //Recurse on rear part
        sort(a,k,right);
        return a;
    }
    
    private <T extends Comparable<? super T>> int partition(T[] a, int left, int right){
        T p = a[left];
        int i = left, j =right;
        // 先找到右边第一个小于枢纽值的数
        do{
            j--;    
        }while(a[j].compareTo(p)>0 && j>i);
        //Swap a[i] and a[j] to partition
        while(i<j){
            // 将右边那个小于枢纽值的数和左边那个大于枢纽值的数交换(第一次交换的是枢纽值本身)
            swap(a,i,j);
            // 找到下一对可以交换的元素
            do{
                i++;
            }while(a[i].compareTo(p)<0);
            do{
                j--;
            }while(a[j].compareTo(p)>0);
        }
        //Elements before j+1 are partitioned, so we need to partition next part start from j+1
        return j+1;
    }
    
    private <T extends Comparable<? super T>> void swap(T[] a, int i, int j){
        T tmp = a[j];
        a[j] = a[i];
        a[i] = tmp;
    }
}

外部排序

例题

假设我们有1TB的数据需要排序,但是只有一台1GB内存的机器,请问如何排序?

思路

外部排序基于归并排序的理念,以该题为例,为了计算方便我们假设TB/GB/MB是以1000换算的,而且内存也有1.001G空间:

  1. 将1TB数据分1000次读入内存中,每次对1GB的数据进行排序,每次排序完将其作为一个区块结果存入磁盘中。

  2. 在内存中划分1000个通道,每个通道读入每个区块前1MB的数据,可知每个1MB都是有序的。

  3. 对这1000个有序的1MB数据进行归并。将结果存入一个1MB的缓存当中,每当这1MB缓存满的时候,将这1MB缓存存入磁盘并清空。每当任何一个1MB通道空时,我们将对应区块的下1MB数据读入这个通道。

  4. 持续执行步骤3直到所有区块的数据都被读完,这时候磁盘中就是1TB有序的数据了。

载入中...