5

数据结构与算法——常用数据结构及其Java实现
经过前面文章的铺垫,我们巩固了基础数据结构的知识,接下来就可以进入算法的巩固阶段了。首先我们来看常见的排序算法。

冒泡排序

原理:依次比较相邻的两个数,将小数放在前面(左边),大数放在后面(右边),就像冒泡一样
具体操作:第一趟,首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后,这样第一趟下来最大的数就在最后一位了。然后还是从第一个数开始重复第一趟步骤比较,但是这次不比较最后一个数了,第二趟结束后第二大的数就在倒数第二位......以此类推,直至全部排序完成。
所有代码在这,关键代码如下:

private static void sort(Comparable[] a) throws IllegalAccessException, InstantiationException {
    Object tmp;
    boolean noChange = false;//用来标识输入序列的排序情况,
    for (int i = 0;i<a.length-1 && !noChange;i++){
        noChange = true;//如果某一趟没有交换,说明数据已经排好序无需再进行接下来的排序
        for (int j=0;j<a.length-1-i;j++){
            if(a[j].compareTo(a[j+1])>0){
                tmp =  a[j];
                a[j] = a[j+1];
                a[j+1] = (Comparable) tmp;
                noChange = false;//有交换
            }
        }
        System.out.println(noChange);//展示跑了多少趟,几个打印就对应几趟
    }
}

时间复杂度, 最好:正序O(n)、最坏:逆序O(n^2)、平均:O(n^2)
空间复杂度, O(1)
稳定性,因为相同的元素不会交换,所以是稳定的

选择排序

原理:每次选择未排序序列中的最小元素。
具体操作:首先在未排序序列中找到最小元素,放到序列的起始位置,然后从剩余未排序元素中寻找最小元素放到已排序序列的末尾,以此类推,直至排序完毕。
所有代码在这,关键代码如下:

public static void sort(Comparable[] a) {
    int n = a.length;
    for (int i = 0; i < n; i++) {
        int min = i;
        for (int j = i+1; j < n; j++) {
            if (less(a[j], a[min])) min = j;//找到剩下元素的最小值
        }
        exch(a, i, min);//将本次最小值与已经排好序的队列的最后一个元素交换
    }
}

时间复杂度, 都是:O(n^2)
空间复杂度, O(1)
稳定性,不稳定。举个例子,序列5 8 5 2 9, 第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了

插入排序

原理:将未排序的序列中的每一个数据依次按合理的顺序插入已排列的数据中。
具体操作:构建有序序列,对于未排序数据,在已排序序列中从头扫描,找到相应位置并插入。第一趟第一个就是有序数据,第二趟把第二个数据和第一个有序数据排序,第三趟把第三个数据和一、二个有序数据排序,以此类推直至排序完毕。
所有代码在,关键代码如下:


public static void sort(Comparable[] a) {
    int n = a.length;
    for (int i = 0; i < n; i++) {
        for (int j = i; j > 0 && less(a[j], a[j-1]); j--) {
            exch(a, j, j-1);//将未排序的第一个数据插入已排序的数据汇中的合适位置
        }
    }
}

时间复杂度, 最好:O(n)、最坏:O(n^2)、平均:O(n^2)。插入排序一般来说比选择排序快,因为插入排序每次都是在已排序的数据中找(插入点),而选择排序每次都是在未排序的数据中找(最小值),所以插入排序很好的利用了已有有序结果,当然更快。
空间复杂度, O(1)
稳定性,稳定,因为待插入元素和有序序列比较都是从最大值开始比较的,如果小于某个元素才放到该元素前面否则放该元素后面,也就是说,相同元素在有序队列中的顺序和其进入有序队列的先后(也就是原本相对位置)是一致的

希尔排序

原理:使数组中的任意间隔为 h 的元素都是有序的
具体操作:选择一个递增的增量序列t1,t2,…,tk,tk=1;按增量序列个数k,对序列进行k 趟排序;每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子序列进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
所有代码在这,关键代码如下:

public static void sort(Comparable[] a) {
    int n = a.length;

    // 3x+1 increment sequence:  1, 4, 13, 40, 121, 364, 1093, ... 
    int h = 1;
    while (h < n/3) h = 3*h + 1; 

    while (h >= 1) {
        // h-sort the array
        for (int i = h; i < n; i++) {
            for (int j = i; j >= h && less(a[j], a[j-h]); j -= h) {
                exch(a, j, j-h);
            }
        }
        h /= 3;
    }
}

时间复杂度, 具体取决于间隔 h,最好:O(nlogn)、最坏:O(n^2)、平均:无。希尔算法的性能与h有很大关系。只对特定的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数。想要弄清关键词比较次数和记录移动次数与h选择之间的关系,并给出完整的数学分析,至今仍然是数学难题。
空间复杂度, O(1)
稳定性,一次插入排序是稳定的,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,shell排序每个不同的增量都是插入排序,有多次,实际上是分组插入排序(又叫缩小增量排序),所以是不稳定的。

归并排序

原理:将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
具体操作:把长度为n的输入序列分成两个长度为n/2的子序列;对这两个子序列分别递归调用归并排序(终止条件是只有1个元素的最小子序列,两个最小子序列直接merge);将两个排序好的子序列合并成一个最终的排序序列。
所有代码在这,关键代码如下:

// stably merge a[lo .. mid] with a[mid+1 ..hi] using aux[lo .. hi]
private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi) {
    // precondition: a[lo .. mid] and a[mid+1 .. hi] are sorted subarrays
    assert isSorted(a, lo, mid);
    assert isSorted(a, mid+1, hi);

    // copy to aux[]
    for (int k = lo; k <= hi; k++) {
        aux[k] = a[k]; 
    }

    // merge back to a[]
    int i = lo, j = mid+1;
    for (int k = lo; k <= hi; k++) {
        if      (i > mid)              a[k] = aux[j++];
        else if (j > hi)               a[k] = aux[i++];
        else if (less(aux[j], aux[i])) a[k] = aux[j++];
        else                           a[k] = aux[i++];
    }

    // postcondition: a[lo .. hi] is sorted
    assert isSorted(a, lo, hi);
}

时间复杂度, 都是:O(nlogn)。通过使用插入排序来处理小规模子序列(如长度小于15)一般可以提升归并排序的效率10%~15%
空间复杂度, O(n)
稳定性,稳定

快速排序

原理:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,然后分别对这两部分记录进行排序,以达到整个序列有序
具体操作:从数列中挑出一个元素,称为 "基准"(pivot);重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
所有代码在这,关键代码如下:

public static void sort(Comparable[] a) {
    StdRandom.shuffle(a);//打乱数组,消除输入依赖
    sort(a, 0, a.length - 1);
    assert isSorted(a);
}

// quicksort the subarray from a[lo] to a[hi]
private static void sort(Comparable[] a, int lo, int hi) { 
    if (hi <= lo) return;
    int j = partition(a, lo, hi);
    sort(a, lo, j-1);
    sort(a, j+1, hi);
    assert isSorted(a, lo, hi);
}

// partition the subarray a[lo..hi] so that a[lo..j-1] <= a[j] <= a[j+1..hi]
// and return the index j.
private static int partition(Comparable[] a, int lo, int hi) {
    int i = lo;
    int j = hi + 1;
    Comparable v = a[lo];
    while (true) { 

        // find item on lo to swap
        while (less(a[++i], v))
            if (i == hi) break;

        // find item on hi to swap
        while (less(v, a[--j]))
            if (j == lo) break;      // redundant since a[lo] acts as sentinel

        // check if pointers cross
        if (i >= j) break;

        exch(a, i, j);
    }

    // put partitioning item v at a[j]
    exch(a, lo, j);

    // now, a[lo .. j-1] <= a[j] <= a[j+1 .. hi]
    return j;
}

时间复杂度, 最好:O(nlogn)、最坏:O(n^2)、平均:O(nlogn)。一般快于归并排序,虽然比较次数可能多些,但是移动数据次数更少。同样的小规模数据转换为插入排序会有效果提升。对于包含大量重复元素的数据,使用三向切分也能提高性能。
空间复杂度, 最好:每次划分都在中间O(logn)、最坏:退化为冒泡O(n)
稳定性,不稳定,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻

堆排序

原理:利用堆这种数据结构的一种排序算法,堆是一个近似完全二叉树的结构,满足堆的性质:即子结点的键总是小于(或者大于)它的父节点
具体操作:将初始待排序关键字序列(R1,R2....Rn)构建成大顶堆,此堆为初始的无序区; 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,......Rn-1)和新的有序区(Rn),且满足R[1,2...n-1]<=R[n]; 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,......Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2....Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
所有代码在,堆的相关代码,关键代码如下:

public static void sort(Comparable[] pq) {
    int n = pq.length;
    for (int k = n/2; k >= 1; k--)//构造堆,从最后一个有子节点的节点开始比较和下沉,直至根节点
        sink(pq, k, n);
    while (n > 1) {//堆排序
        exch(pq, 1, n--);//将最大值(根节点)和无序数组最后一元素交换,并将无序标志前移
        sink(pq, 1, n);//下沉交换后的根节点
    }
}

private static void sink(Comparable[] pq, int k, int n) {
    while (2*k <= n) {
        int j = 2*k;
        if (j < n && less(pq, j, j+1)) j++;//先比较左右子节点,找到较大的
        if (!less(pq, k, j)) break;//大于较大的子节点,无需下沉
        exch(pq, k, j);//否则下沉
        k = j;//继续比较以这个节点为根的子树
    }
}

时间复杂度, 都是:O(nlogn)
空间复杂度, O(1)
稳定性,比如:3 27 36 27,堆顶3先输出,则第三层的27(最后一个27)跑到堆顶,然后堆稳定,继续输出堆顶,是刚才那个27,这样说明后面的27先于第二个位置的27输出,不稳定

计数排序

原理:使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。不是比较排序,排序的速度快于任何比较排序算法
具体操作:找出待排序的数组中最大和最小的元素;统计数组中每个值为i的元素出现的次数,存入数组C的第i项;对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
所有代码在这,关键代码如下:

private static int[] countSort(int[] A,int k){
    int[] C=new int[k+1];//构造C数组
    int length=A.length,sum=0;//获取A数组大小用于构造B数组
    for (int anArray : A) { C[anArray] += 1;}// 统计A中各元素个数,存入C数组
    for(int i=0;i<k+1;i++){ //修改C数组,使得A中小于等于元素i的元素有C[i]个,亦即i在B中的自然序号(减去1得到数组序号)
        sum+=C[i];
        C[i]=sum;
    }
    int[] B=new int[length];//构造B数组
    for(int i=length-1;i>=0;i--){//倒序遍历A数组(保证稳定性,因为相同的元素中靠后的个体的序号也相对较大),构造B数组
        B[C[A[i]]-1]=A[i];//将A中该元素放到排序后数组B中指定的位置
        C[A[i]]--;//将C中该元素-1,方便存放下一个同样大小的元素
    }
    return B;//将排序好的数组返回,完成排序
}

时间复杂度, 都是:O(n+k),(输入的元素是n 个0到k之间的整数)
空间复杂度, O(k)
稳定性,稳定

桶排序

原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)
具体操作: 设置一个定量的数组当作空桶;遍历输入数据,并且把数据一个一个放到对应的桶里去;对每个不是空的桶进行排序;从不是空的桶里把排好序的数据拼接起来。
所有代码在这,关键代码如下:

private static void bucketSort(int[] arr){
    int max = Integer.MIN_VALUE,min = Integer.MAX_VALUE;
    for (int anArr : arr) {
        max = Math.max(max, anArr);
        min = Math.min(min, anArr);
    }
    //桶数
    int bucketNum = (max - min) / arr.length + 1;
    ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
    for(int i = 0; i < bucketNum; i++){
        bucketArr.add(new ArrayList<Integer>());
    }
    //将每个元素放入桶
    for (int anArr : arr) {
        int num = (anArr - min) / (arr.length);
        bucketArr.get(num).add(anArr);
    }
    //对每个桶进行排序,调用自带的排序
    for (ArrayList<Integer> aBucketArr : bucketArr) {
        Collections.sort(aBucketArr);
    }
    //打印结果
    for (ArrayList<Integer> anA : bucketArr) {StdOut.print(anA + "\t"); }
    StdOut.println();
}

时间复杂度, 最好:O(n+k)、最坏:O(n^2)、平均:O(n+k)
空间复杂度, O(n+k)
稳定性,稳定,因为相同的元素肯定在同一个桶里,并且加入桶的顺序和原顺序一致

基数排序

原理:将待排序数据拆分成多个关键字进行排序,基数排序的实质是多关键字排序,将待排数据里的关键字拆分成多个排序关键字,第1个排序关键字,第2个排序关键字,......,第k个排序关键字,然后根据子关键字对待排序数据进行排序(必须借助于另一种排序方法,而且这种排序方法必须是稳定的)
具体操作:取得数组中的最大数,并取得位数;arr为原始数组,从最低位开始取每个位组成radix数组;对radix进行排序;换句话说,第一轮下来,数组按照个位有序,第二轮下来数组按照十位有序,依次类推,由于子关键字排序稳定所以最终的数组是有序的
所有代码在这,关键代码如下:

private static void radixSort(int[] array,int d){
    int n=1;//代表位数对应的数:1,10,100...
    int k=0;//保存每一位排序后的结果用于下一位的排序输入
    int length=array.length;
    int[][] bucket=new int[10][length];//二维数组排序桶,用于保存每次排序后的结果,这一位上排序结果相同的数字放在同一个桶里
    int[] order=new int[length];//用于保存每个桶里有多少个数字,默认初始化为0
    while(n<d){
        for(int num:array) {//将数组array里的每个数字放在相应的桶里
            int digit=(num/n)%10;
            bucket[digit][order[digit]]=num;
            order[digit]++;
        }
        for(int i=0;i<length;i++){//将前一个循环生成的桶里的数据覆盖到原数组中用于保存这一位的排序结果
            if(order[i]!=0){//这个桶里有数据,从上到下遍历这个桶并将数据保存到原数组中
                for(int j=0;j<order[i];j++){
                    array[k]=bucket[i][j];
                    k++;
                }
            }
            order[i]=0;//将桶里计数器置0,用于下一次位排序
        }
        n*=10;
        k=0;//将k置0,用于下一轮保存位排序结果
    }
}

时间复杂度, 都是:O(nxk)
空间复杂度, O(nxk)
稳定性,稳定

参考

本书大部分代码来自于算法第四版
http://www.cnblogs.com/jztan/...
https://www.cnblogs.com/devel...
百度百科
http://blog.csdn.net/apei830/...
https://www.cnblogs.com/devel...

访问原文。算法博大精深,这里把各种排序算法总结,如有错误请轻拍~


MageekChiu
4.4k 声望1.7k 粉丝

T