排序算法总结(包含字符串排序算法)

动画排序算法:http://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html

排序算法真的好多,而且感觉都很好理解,写的时候才发现网上有很多资源,特别是发现了一个有趣的网站 ,可以动画模拟算法的过程,感觉看了之后能理解的更好。这次抽空总结下,可能图文比较少,第一次写博客纪录自己的学习笔记,希望能坚持下去。

动画演示地址:
http://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html
在正式开始各类排序算法之前,先mark下稳定排序,原地排序等概念,以及排序算法的不同分类方法。

稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;

不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;

原地排序:排序过程中不需要额外的辅助空间,只需要本身需要排序的数组。

非原地排序:排序的过程中需要额外的辅助空间来帮助施行排序过程。

在大多数的排序算法中,排序基本只用到了两个操作:比较、移动!大部分时候我们使用的排序算法都是基于比较的的,还有一类算法不是基于比较的排序算法,即计数排序、基数排序,桶排序;

排序分类:
按照排序结果是否稳定性分类:

1)稳定排序:插入排序,冒泡排序,归并排序,计数排序,基数排序,桶排序(如果桶内排序采用的是稳定性排序)
2)非稳定排序:选择排序,快速排序,堆排序。

按照排序过程中是否需要额外空间:
1)原地排序:插入排序,选择排序,冒泡排序,快速排序,堆排序。
2)非原地排序:归并排序,计数排序,基数排序,桶排序。

按照排序的主要操作分类:
1)交换类:冒泡排序、快速排序;此类的特点是通过不断的比较和交换进行排序;
2)插入类:简单插入排序、希尔排序;此类的特点是通过插入的手段进行排序;
3)选择类:简单选择排序、堆排序;此类的特点是看准了再移动;
4)归并类:归并排序;此类的特点是先分割后合并;

历史进程:一开始排序算法的复杂度都在O(n^2),希尔排序的出现打破了这个僵局;

**

初级排序算法:

**

一、选择排序

选择排序视频:http://v.youku.com/v_show/id_XMzMyODk5MDI0.html

思想:找到数组中最小的那个元素,然后将它和第一个位置所在的元素交换位置。接着下一轮,在剩下的元素中寻找最小的,然后将它和数组的第二个位置的元素交换位置。如此往复,直到整个数组排序,这种算法称为选择排序,因为它在不断选择剩余元素中最小者。(当然如果要递减排序的话,也可以每次都选择当前剩下元素中最大的元素)

选择排序运行轨迹图示例

选择排序的特点:
1、运行时间和输入状态无关:
每次为了找出当前剩余数组的最小值,都需要将剩余元素全部扫描一边(尽管有可能输入数组最开始就是有序的或者主键完全相等)每次的扫描都不会为下次扫描提供额外信息。也就是选择排序的效率和输入状态无关。
2、数据移动最少:
交换次数和数组的大小是线性关系,而其他大部分排序算法都不具备该特点。

选择排序对于长度为N的数组,大约需要N次交换和N*N/2次比较。

效率:
最好情况:O(n*n)
最坏情况:O(n*n)

实现代码(较为简单容易理解,原地排序,不需要额外辅助空间,不稳定的排序算法)

package BasicSort;

import java.util.Random;

public class Select {
    public static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static void sort(int[] a) {
        int min_index;
        for (int i = 0; i < a.length; i++) {
            min_index = i;
            for (int j = i; j < a.length; j++) {
                if (a[j] < a[min_index])
                    min_index = j;
            }
            exch(a, i, min_index);
        }
    }

    public static void main(String[] args) {
        int N = 10;
        int[] a = new int[N];
        Random rondom = new Random();

        for (int i = 0; i < N; i++)
            a[i] = rondom.nextInt(20);
        System.out.println(" before sort ");
        show(a);
        System.out.println();
        sort(a);
        System.out.println(" after sort ");
        show(a);

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

二、插入排序

插入排序视频:http://v.youku.com/v_show/id_XMzMyODk3NjI4.html

思想:将当前索引指向的数字放入该索引左边其它已经有序的数字中的适当位置,也就是每次取没被排序的最左边一个和已排序的做比较,并插入到正确位置。在计算机的实现中,为了给要插入的元素腾出空间,我们需要将其余所有元素在插入动作前都向右移动一位,与选择排序的相同的是,当前索引的左边的所有元素都是有序的,但是它们的最终位置还不确定,为了给更小的元素腾出空间,它们也有可能被移动,但是当索引到达数组的最右端的时候,数组的排序就完成了。

插入排序轨迹实例图

特点:
运行时间和输入状态有关,对于原数组处于基本有序的状态时候的运行时间会比随机数组或者对大多处于逆序的数组快的多。

对于随机排列的数组长度为N,平均情况下插入排序需要n* n/4次比较和交换。最坏情况需要 n* n/2次比较和交换。最好情况下需要n-1次比较和0次交换。插入排序中比较和交换的次数一样多。

优化思想:要大幅度提高插入排序的速度并不难,只需要在内循环中将较大的元素都向右移动而不是交换这两个元素(这样访问数组的次数就能减半)。

效率:
最好情况:O(n)(待排序的数组初始化就是有序的)
最坏情况:O(n*n)

实现代码:(原地排序,稳定排序)

package BasicSort;

import java.util.Random;

public class Insertion {

    public static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static void sort(int[] a) { // 未优化的
        for (int i = 1; i < a.length; i++)
            for (int j = i; j > 0 && (a[j] < a[j - 1]); j--) {
                exch(a, j, j - 1);
            }
    }

    public static void sortOptimize(int[] a) {
        for (int i = 1; i < a.length; i++) {
            int j, temp = a[i];
            for (j = i; j > 0 && (temp < a[j - 1]); j--)
                ;
            for (int k = i; k > j; k--)
                a[k] = a[k - 1];
            a[j] = temp;
        }
    }

    public static void main(String[] args) {
        int N = 10;
        int[] a = new int[N];
        Random rondom = new Random();
        for (int i = 0; i < N; i++)
            a[i] = rondom.nextInt(20);
        System.out.println(" before sort ");
        show(a);
        System.out.println();
        // sort(a);
        sortOptimize(a);
        System.out.println(" after sort ");
        show(a);

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

总的来说,插入排序对于部分有序的小规模数组非常有效效果也比冒泡和选择排序大部分情况下效果好。

三、冒泡排序:

冒泡排序视频:http://v.youku.com/v_show/id_XMzMyOTAyMzQ0.html

思想:每次比较相邻的元素,如果有需要交换的时候,进行交换,接着向右比较相邻的元素,这样第一轮比较到数组的最右端,就将最大的元素移动了最后一位(也可以将当前序列“最小元素”冒泡“到达第一位”)。再次从头开始比较相邻的元素,对于需要交换的进行交换,直到上一轮的”最大“的位置,第二轮结束后就将第二大的元素移动到了倒数第二位,如此反复,就得到了有序的数组。由于每次都将一个‘当前’最大的元素移动到了它在最终排序应该在的位置,所以被称为冒泡排序。
冒泡排序轨迹部分实例图

效率:
最好情况:O(n)(改进后冒泡算法的最好效率,使用了标记,在整次循环中不发生交换的时候跳出循环)
最坏情况:O(n*n)

优化思想:建立一个boolean值,当当前序列没有发生交换的时候,也就是说当前序列已经处于有序状态了,跳出循环。

实现代码:(原地排序,稳定排序)

package BasicSort;

import java.util.Random;

public class Bubble {

    public static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static void sort(int[] a) { // 未优化
        for (int i = 0; i < a.length - 1; i++)
            for (int j = a.length - 1; j > i; j--) {
                if (a[j] < a[j - 1])
                    exch(a, j, j - 1);
            }
    }

    public static void sortOptimize(int[] a) {
        boolean flag = false;
        for (int i = 0; i < a.length - 1; i++) {
            flag = false;
            for (int j = a.length - 1; j > i; j--) {
                if (a[j] < a[j - 1]) {
                    exch(a, j, j - 1);
                    flag = true;
                }
            }
            if(!flag) break;
        }
    }

    public static void main(String[] args) {
        int N = 10;
        int[] a = new int[N];
        Random rondom = new Random();

        for (int i = 0; i < N; i++)
            a[i] = rondom.nextInt(20);
        System.out.println(" before sort ");
        show(a);
        System.out.println();
        sort(a);
        //sortOptimize(a);
        System.out.pr
intln(" after sort ");
        show(a);

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

高级排序算法

四、归并排序:

归并排序视频:http://v.youku.com/v_show/id_XMzMyODk5Njg4.html

思想:归并这个简单的操作,即将两个有序的数组归并成一个的更大有序数组。利用归并的思想,实现了一个简单的递归的排序算法。将一个要排序的数组,可以递归的将其分成两半,然后分别排序,最后将结果归并起来。实现归并直截了当的方式就是将两个不同的有序的数组归并到第三个数组,实现方法很简单就是创建一个适当大小的数组然后将两个输入数组中的元素一个个从小到大放入这个数组。但是当用归并给较大数组排序时候,这种简单的思想就需要很多辅助数组,在理想中,我们将希望采用一种原地归并的思想,就是先将前半部分排序,然后将后半部分排序,最后在数组中移动,而不是创建额外的辅助空间,但是这样构造太复杂。
归并排序示意图

原地(还是运用了辅助数组aux)归并的抽象方法:
实现代码:

    public static void merge(int[] a, int lo, int hi, int mid) {
        int i = lo;
        int j = mid + 1;
        for (int k = lo; k <= hi; k++)
            aux[k] = a[k];
        for (int k = lo; k <= hi; k++) {
            if (i > mid)
                a[k] = aux[j++];
            else if (j > hi)
                a[k] = aux[i++];
            else if (a[i] > a[j])
                a[k] = aux[j++];
            else
                a[k] = a[i++];
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

1)自顶向下的归并排序:

自顶向上归并排序递归示例图

对于长度为N的任意数组,自顶向下的归并需要1/2* N* log(N)~N * log(N)次比较,需要访问数组6* N * log(N)。

可以看出归并只需要遍历整个数组多个对数因子的时间就能将一个庞大的数组进行排序,这是前面所介绍的排序算法所不能做到的。但是归并排序需要的额外空间和N成正比。

算法优化思想:
1)用不同的方法处理小规模问题能改进大多数递归算法的性能,因为递归会使得小规模问题中的方法调用次数过于频繁。由于前面三种简单的排序方法对于小规模数组非常有效,因此在递归排序的小规模问题中使用插入排序可以极大的提高处理效率。
2)我们可以添加一个判断条件,如果a[mid]<=a[mid+1],我们就认为数组已经是有序的并跳过merge方法,这个改动不会影响排序的递归调用,但是任意有序的子数组算法的运行时间就会变为线性!
3)我们可以节省将数组复制到用于归并的辅助数组所用的时间(但是空间不可以)要做到这一点,我们需要调用两种排序方法,一种是将数据从输入数组排序到辅助数组,一种是将数据从辅助数组排序到输入数组。这种方法需要一些技巧,我们需要在递归调用的每一层交换输入数组和辅助数组的角色!

效率:
最好情况O(n*log(n));
最坏情况O(n*log(n));

这里写图片描述
实现代码:(非原地归并,稳定排序):

package BasicSort;

import java.util.Random;

public class Merge {
    private static int[] aux;
    private static final int M = 15; //小规模问题的切换阈值

    public static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public static void sortInsertion(int[] a, int lo, int hi) {
        for (int i = lo; i < hi; i++)
            for (int j = i; j > 0 && (a[j] < a[j - 1]); j--) {
                exch(a, j, j - 1);
            }
    }

    public static void merge(int[] a, int lo, int mid, int hi) {
        int i = lo;
        int j = mid + 1;
        for (int k = lo; k <= hi; k++)
            aux[k] = a[k];
        for (int k = lo; k <= hi; k++) {
            if (i > mid)
                a[k] = aux[j++];
            else if (j > hi)
                a[k] = aux[i++];
            else if (aux[i] > aux[j])
                a[k] = aux[j++];
            else
                a[k] = aux[i++];
        }
    }

    public static void sort(int[] a) {
        aux = new int[a.length];
        //sort(a, 0, a.length - 1);
        sortOptimize(a, 0, a.length - 1);
    }

    public static void sort(int[] a, int lo, int hi) {
 //未优化
        if (hi <= lo)
            return;
        int mid = lo + (hi - lo) / 2;
        sort(a, lo, mid);
        sort(a, mid + 1, hi);
        merge(a, lo, mid, hi);
    }

    public static void sortOptimize(int[] a, int lo, int hi) {

             //采用小规模问题时使用插入排序
        if (hi <= lo + M) {
            sortInsertion(a, lo, hi);
            return;
        }
        int mid = lo + (hi - lo) / 2;
        sort(a, lo, mid);
        sort(a, mid + 1, hi);
        merge(a, lo, mid, hi);
    }

    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static void main(String[] args) {
        int N = 1000;
        int[] a = new int[N];
        Random rondom = new Random();

        for (int i = 0; i < N; i++)
            a[i] = rondom.nextInt();
        System.out.println(" before sort ");
        show(a);
        System.out.println();
        sort(a);
        System.out.println(" after sort ");
        show(a);
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88

2)自底向上的归并排序:
思想:尽管我们考虑的问题是归并两个大叔组,但是实际上我们归并的数组都非常小。实现归并排序的另外一个方法就是先归并那些微型数组,然后再成对的归并得到的数组,如此这般直到我们将整个数组归并在一起。首先我们进行的是两两归并(把每个元素想成一个大小为1的数组),然后进行四四归并(将两个大小为2的数组归并成一个有4个元素的数组),然后是八八归并,一直下去,在每一轮归并中,最后一次归并的第二个子数组可能比第一个子数组要小,但是这对于归并并没有影响。

自底向上递归算法运行轨迹

public static void sortBU(int[] a){
        aux = new int[a.length];
        for(int size=1;size<a.length;size=size*2)
            for(int lo=0;lo<a.length-size;lo+=size+size){
                merge(a,lo,lo+size-1,Math.min(lo+size+size-1,a.length-1));
            }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

五、快速排序:

思想:快速排序也是一种分治的排序算法,它将整个数组分成两个小的数组,将这个两部分独立的排序,快速排序和归并排序是互补的,归并排序是将数组分成两个子数组分别排序,并且将有序的子数组归并从而将整个数组排序。而快速排序,将数组排序的方式则是当两个子数组都有序的时候,整个数组就有序了。在归并排序中递归调用发生在处理整个数组之前,而在快速排序中,递归调用发生在处理整个数组之后,在归并排序中一个数组被分成两半,而在快速排序中,切分的位置取决于数组的内容。该方法在于切分点元素,切分点元素的位置已经排定,在该位置前的元素都不大于切分点元素,在该位置右边的元素都不小于切分点元素。我们就是递归的调用切分来实现排序的。切分是快速排序的关键,一般的策略就是先随意的取a[lo]作为切分元素。然后从数组的左端开始扫描(指针i),直到找到大于等于它的元素,再从数组的右端开始扫描(指针j),直到遇到小于等于它的元素,这两个元素显然是没有排定的,我们交换他们的位置如此继续,我们就可以保证指针i左边的元素都小于切分元素,指针j右边的元素都大于切分元素。当两个指针相遇,我们只需要将切分元素和左边子数组的最右边的元素交换即可,并且纪录此时交换的位置。
切分示意图

效率:
当输入数组已排序时,时间为O(n^2)可以对数组进行随机化打乱,使得期望运行时间为O(nlgn)。
最佳运行时间:O(nlgn)
不需要额外的辅助空间。

算法优化:
1)切换到插入排序:和递归排序的优化一样,对于小规模问题的时候,切换成插入排序。
2)三取样切分:改进快排性能的另外一点,不是使用子数组的第一个元素作为切分元素而是使用子数组的中位数来切分数组。人们发现,取样大小设置为3的时候切分效果最好,我们还可以数组末尾作为哨兵,防止数组越界发生。

三向切分示意图

3)熵最优。在有大量数据重复的情况下,快速排序的递归性会使得元素全部重复的子数组经常出现,一个简单的想法就是,将数组切分成三个部分,大于,小于,等于切分元素三部分。
三向切分的快速排序(基于优化思想三):
维护一个指针lt,使得a[lo…lt-1]中的元素都小于切分元素,一个指针gt使得a[gt+1…hi]中的元素都大于切分元素,一个指针i使得a[lt…i-1]的元素都等于切分元素,而a[i…gt]中的元素的划分还未确定。

快排轨迹示意图

实现代码(原地排序,不稳定排序)

package BasicSort;

import java.util.Random;

public class QuickSort {

    private static final int M = 15; // 小规模问题的切换阈值

    public static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static void sortInsertion(int[] a, int lo, int hi) {
        for (int i = lo; i < hi; i++)
            for (int j = i; j > 0 && (a[j] < a[j - 1]); j--) {
                exch(a, j, j - 1);
            }
    }

    public static int partition(int[] a, int lo, int hi) {
        int u = a[lo];
        int i = lo, j = hi + 1;
        while (true) {
            while (a[++i] < u);
            while (a[--j] > u);
            if (i >= j)
                break;
            exch(a, i, j);
        }
        exch(a, lo, j);
        return j;
    }

    public static void sort(int[] a) {

        sort(a, 0, a.length - 1);
        // Quick3way(a, 0, a.length-1);
    }

    public static void sort(int[] a, int lo, int hi) {
        /*
         * 在添加小数组使用插入排序优化。
         * 三向切分优化这个版本下,只有对于重复数据较多的时候相对于,快排有优势
         * if (hi <= lo + M) { Insertion.sort(a, lo, hi); return; }
         */
        if (hi <= lo)
            return;
        int point = partition(a, lo, hi);
        sort(a, lo, point - 1);
        sort(a, point + 1, hi);

    }

    public static void Quick3way(int[] a, int lo, int hi) {
        /*
         * 添加小数组时候使用插入排序。
         * if (hi <= lo + M) { Insertion.sort(a, lo, hi); return; }
         */
        if (hi <= lo)
            return;
        int u = a[lo];
        int lt = lo, gt = hi, i = lo + 1;
        while (i <= gt) {
            if (a[i] > u)
                exch(a, i, gt--);
            else if (a[i] < u)
                exch(a, i++, lt++);
            else
                i++;
        }
        Quick3way(a, lo, lt - 1);
        Quick3way(a, gt + 1, hi);
    }

    public static void main(String[] args) {
        int N = 10;
        int[] a = new int[N];
        Random rondom = new Random();
        for (int i = 0; i < N; i++)
            a[i] = rondom.nextInt(20);
        System.out.println(" before sort ");
        show(a);
        System.out.println();
        sort(a);
        System.out.println(" after sort ");
        show(a);

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97

六、堆排序

堆排序算法之前讲下优先队列:
一个数据结构,能够删除最大元素和插入元素的数据类型。通过插入一列元素到优先队列中,然后一个个删除其中得到最小元素,来实现排序算法,而堆排序就来自于基于堆的优先队列的实现。
1)优先队列的初级实现:
a)数组实现(有序的和无序的):
b)链表表示法:
2)二叉堆表示法:(二叉堆:是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层次结构存储,不使用数组的第一个位置)
在二叉堆的数组中,每个元素都要保证大于等于另外两个特定位置的元素。相应的这些位置的元素又至少要大于等于数组中的另外两个元素。(当一棵二叉树的每个结点都大于等于它的两个子结点的时候,它被称为堆有序)。
二叉树表示法:如果我们使用指针来表示堆有序的二叉树,那么每个结点(除了根结点)都需要三个指针来表示它的上下结点。如果使用完全二叉树,表达就会变得简单。可以先定下跟结点,然后一层一层,从左向右在每个结点下方连接两个更小的结点。完全二叉树只用数组而不需要指针就能表示。具体方式,就是跟结点的位置在数组下标为1的位置。而它的子结点分别在位置2和3,而子结点的子结点的位置在4,5,6,7以此类推。位置在下标为K的结点的父结点位置为K/2向下取整。而它的两个子结点的所在的位置是2k和2k+1。使用这种方式实现的优先队列能够实现对顺级别内的插入元素和删除最大元素。

堆的数组表示

堆得有序化:堆的操作会首先进行一些简单的改动,打破堆的状态,然后再遍历堆并按照堆得要求将堆的状态恢复。这个过程被称为堆得有序化。
两种重要的堆操作:
1)由下至上的堆得有序化(上浮):
出现情景:如果堆得有序状态因为某个结点变得比它的父结点更大,而被打破,那么我们就需要来交换该结点和它的父节点来修复堆,交换后这个结点比它的两个结点都大,但是这个结点有可能比它现在的父结点大,仍然可能需要上述方法一遍遍的恢复秩序,将这个结点上移直到遇到了更大的结点。(代码实现关键点:位置k的父结点的位置是k/2)

上浮示意图

实现代码:

    private void swim(int[] a, int k) {
        while (k > 1 && a[k / 2] < a[k]) {
            exch(a, k / 2, k);
            k = k / 2;
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2)由上至下的堆有序化(下沉):
如果堆的有序状态因为某个结点变得比它的两个子结点或者其中一个子结点更加小了而被打破,那么我们可以通过将它和它的子结点中较大的交换位置来恢复堆,同样的道理,交换可能使得该结点仍然打破新位置的有序状态,因此我们需要重复不断的用同样的方式来恢复,将结点向下移动直至它的子结点都比它小或者是到达了堆的底部。
下沉示意图

实现代码:

    private void sink(int[] a, int k) {
        while (2 * k <= a.length) {
            int j = 2 * k;
            if (j < a.length && a[j] < a[j + 1])
                j++;
            exch(a, k, j);
            k = j;
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

插入元素:将新的元素插入到数组的尾部,增加堆的大小,并且让这个数组上浮到合适的位置。
删除最大元素:我们从数组的顶端删除最大的元素并将数组最末尾的元素放到顶端,减小堆的大小并且让这个元素下沉到合适的位置。

基于堆的优先队列的实现:
实现代码:

package BasicSort;

public class MaxPQ {
    private static int MIN;
    private int[] pq;
    private int N = 0;

    public MaxPQ(int max) {
        pq = new int[max + 1];
    }

    public boolean isEmpty() {
        return N == 0;
    }

    public int size() {
        return N;
    }

    public static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public void swim(int k) {
        while (k > 1 && pq[k / 2] < pq[k]) {
            exch(pq, k / 2, k);
            k = k / 2;
        }
    }

    private void sink(int k) {
        while (k * 2 < pq.length) {
            int j = 2 * k;
            if (j < pq.length && j < pq.length) {
                if (pq[j] < pq[j + 1])
                    j++;
                if (pq[k] < pq[j])
                    break;
                exch(pq, k, j);
                k = j;
            } else {
                if (j < pq.length && pq[k] > pq[j])
                    exch(pq, k, j);
                if (pq[k] < pq[j])
                    break;
                k = j;

            }
        }
    }

    public void insert(int key) {
        pq[++N] = key;
        swim(N);
    }

    public int delMax() {
        int max = pq[1];
        exch(pq, 1, N--);
        pq[N + 1] = MIN;
        sink(1);
        return max;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

思想:我们可以将任意的优先队列变成一种排序方法,将所有的元素插入一个寻找最大元素的优先队列(基于堆的优先队列)中,然后再重复的调用删除最大元素的操作来将他们按照顺序的删去。堆排序被分成两个阶段,第一阶段是堆的构造阶段,我们将原始数据重新组织安排进一个堆中,然后在下沉排序阶段,我们从堆中按照递减顺序取出所有的元素并得到排序结果,在排序的过程中,我们将需要排序的数组本身作为堆,因此不需要任何的辅助空间。
1)堆的构造:
思路一:用swim()指针保证扫描指针左侧的所有元素已经是一颗有序的完全树,就像往优先队列中插入元素一样,但是这样需要扫描所有的元素(除了跟结点外的)。
思路二:从右至左用sink()函数构造子堆。每个数组的位置都可以看成一个子堆的跟结点,sink()对于这些跟结点也有用。如果一个结点的两个子结点已经是有序堆的跟结点了,那么只需要调用sink()就能使它们变成一个堆,递归的使用是个方法直至到达整棵树的跟结点,我们就可以建立起来一个有序堆。由于叶子结点可以直接看成有序子堆,所以我们只用从编号最大的分支结点开始向上进行sink()操作,那么我们只用扫描一般的结点。
2)下沉排序:
堆排序的主要工作是在该阶段完成,在这里我们将堆的最大元素(也就是数组首位元素)删除并放入堆缩小后数组空出来的位置。这个过程和选择排序非常类似(按照降序的方式取出所有的元素),但是需要比较的次数大大地减少了。
堆排序的堆构造过程和下沉排序过程
将N个元素排序,堆排序只需要少于2*N* logN+2*N次比较以及一半次数的交换。
效率:
最好情况:O(nlgn)
最坏情况:O(nlgn)

实现代码:(原地排序,不稳定的排序)

package BasicSort;

import java.util.Random;

public class StackSort {
    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    private static void sink(int[] a, int k, int length) {
        while (2 * k < length) {
            int j = 2 * k;
            if (j + 1 < length) {
                if (j < length && a[j] < a[j + 1])
                    j = j + 1;
                if (a[k] >= a[j])
                    break;
                exch(a, k, j);
                k = j;
            }
            else{
                if (j < length && a[k] < a[j])
                    exch(a, k, j);
                if (a[k] >= a[j])
                    break;
                k = j;
            }
        }
    }

    public static void sort(int[] a) {

        int N = a.length;
        for (int k = N / 2; k > 0; k--)
            sink(a, k, N);
        while (N > 1) {
            exch(a, 1, --N);
            sink(a, 1, N);
        }

    }

    public static void main(String[] args) {
        int N = 10;
        int[] a = new int[N + 1];
        Random rondom = new Random();
        for (int i = 1; i < N; i++)
            // a[0]不用来存放数组
            a[i] = rondom.nextInt(20);
        System.out.println(" before sort ");
        show(a);
        System.out.println();
        sort(a);
        System.out.println(" after sort ");
        show(a);

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

也可以将比较和交换的时候的下标稍加改动,就可以实现a[0]~a[N-1]的排序。

七、计数排序

思想:适用于小整数键的简单排序,其优势是对于数值范围已知的数组进行排序,适用范围特别局限。但是如果满足该排序的使用条件,相对于任何基于比较的排序算法(基于比较的排序算法,最优效率N*logN)这种计数排序的效率都很高。该方法创建一个长度为待排序数组数值范围大小的数组C(即最大元素-最小元素+1),数组C中每个元素纪录待排序数组中对应的纪录出现的次数。

计数排序

这种方法有4个步骤:
1) 频率统计:每个种数值出现的频率count[ ]。(小技巧:如果键为r,则将count[r+1]加1);
2) 将频率count[ ]转换成索引count[ ]。将使用count[ ]来计算每个键在排序结果中的起始索引位置;
3) 数据分类:根据键对应的count[ ]值决定位置,在移动之后,count[ ]对应的元素总是+1,以保证count[r]总是下一个键为r的元素所对应的索引位置。
4) 回写。

效率:
最好情况:O(n+k)
最坏情况:O(n+k)

实现代码:(非原地排序,需要辅助数组,稳定的排序)

package BasicSort;

import java.util.Random;

public class CountSort {

    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static void sort(int[] a) {

        int N = a.length;
        int min = 0, max = 0;
        int[] aux = new int[N];
        for (int i = 0; i < N; i++) {
            if (a[i] < min)
                min = a[i];
            if (a[i] > max)
                max = a[i];
        }
        int R = max - min + 1;// 待排序数据组的范围。

        int[] count = new int[R + 1];
        for (int i = 0; i < N; i++) {
            count[a[i] - min + 1]++;

        }
        for (int i = 0; i < R; i++) {
            count[i + 1] += count[i];
        }
        for (int i = 0; i < N; i++) {
            aux[count[a[i] - min]++] = a[i];
        }
        for (int i = 0; i < N; i++) {
            a[i] = aux[i];
        }
    }

    public static void main(String[] args) {
        int N = 10;
        int[] a = new int[N];
        Random rondom = new Random();
        for (int i = 0; i < N; i++)
            a[i] = rondom.nextInt(20);
        System.out.println(" before sort ");
        show(a);
        System.out.println();
        sort(a);
        System.out.println(" after sort ");
        show(a);

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

八、桶排序

思想:首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]….B[M]中的全部内容即是一个有序序列。桶排序要求待排序的元素都属于一个固定的且有限的区间范围内。相对于其他两种不需要比较的线性排序算法(计数排序和基数排序)桶排序可以对浮点数进行排序。桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的映射值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。
桶排序
效率:
最好情况运行时间:O(n)
最坏情况运行时间:当分布不均匀时,全部元素都分到一个桶中,如果桶内元素排序使用的是(插入排序,选择排序,冒泡排序)则O(n^2),(堆排序、快速排序),这样最坏情况就是O(nlgn)。

实现代码:(非原地排序,稳定排序(如果桶内元素使用的是插入排序))

这里写图片描述

九、基数排序

思想:基数排序是将整数按照位来进行排列,从低位开始,对于所有待排序数据的相同的位置的每一位使用稳定的排序算法,例如计数排序,直到最高位排序完成,所有的排序完成。也就是说我们可以将整数的每一位看成一个关键字,基数排序就是将待排序数组中每组关键字进行桶分配。
演示动画:
http://www.cs.usfca.edu/~galles/visualization/RadixSort.html
效率:
最好情况:O((n+k)*d)
最坏情况:O((n+k)*d)

优化思想:对于整个待排序数组中的数字,我们不一定需要一位位的来排列,也可以多位多位的排列,例如,先对低几位进行排列,在对高几位进行排列。
实现代码:(稳定排序,非原地排序)

package BasicSort;

import java.util.Random;

public class Counting_Sort {
    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static int max_dig(int[] a) {
        int max = Integer.MIN_VALUE, N = a.length, d = 1;
        for (int i = 0; i < N; i++) {
            if (a[i] > max)
                max = a[i];
        }
        for (int i = 10;; i *= 10) {
            if (max > i)
                d++;
            else
                break;
        }
        return d;
    }

    public static void sort(int[] temp, int[] a, int k) {
        int[] count = new int[k + 1];
        int n = temp.length;
        int[] aux = new int[n];
        for (int i = 0; i < n; i++) {
            count[temp[i] + 1]++;
        }
        for (int i = 0; i < k; i++) {
            count[i + 1] += count[i];
        }
        for (int i = 0; i < n; i++) {
            aux[count[temp[i]]++] = a[i];
        }
        for (int i = 0; i < n; i++) {
            a[i] = aux[i];
        }
    }

    public static void main(String[] args) {
        int base = 1;
        int N = 10;
        int[] a = new int[N];
        Random rondom = new Random();
        for (int i = 0; i < N; i++)
            a[i] = rondom.nextInt(20);
        int d = max_dig(a);
        int[] temp = new int[N];
        System.out.println(" before sort ");
        show(a);
        while (d > 0) {
            base *= 10;
            for (int i = 0; i < N; i++) {
                temp[i] = a[i] % base;
                temp[i] /= (base / 10);
            }
            sort(temp, a, 10);
            d--;
        }
        System.out.println();
        System.out.println(" after sort ");
        show(a);
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

十、希尔排序

希尔排序视频: http://v.youku.com/v_show/id_XMzMyODk5MzI4.html
思想:对于插入排序的改进,插入排序需要交换相邻元素来实现将元素放到应该到的位置,假设恰好最小的元素在数组的最右端,就需要将它一点点的从数组的最右端移到最左端。希尔元素为了加快速度,简单地改进插入排序,交换不相邻的元素已对数组的局部进行排序,并最终将局部有序的数组排序。
希尔排序的思想是使数组中任意间隔为h的元素都是有序的,这样的数组被称为h有序数组。如果h很大,我们就能将元素移动到较远位置,为了实现更小的h有序创造方便。用这种方式,对于任意1开头h结尾的序列,我们都能将这个序列排序,这就是希尔排序。^_^(这个我也不是很理解)

排序算法真的好多,而且感觉都很好理解,写的时候才发现网上有很多资源,特别是发现了一个有趣的网站 ,可以动画模拟算法的过程,感觉看了之后能理解的更好。这次抽空总结下,可能图文比较少,第一次写博客纪录自己的学习笔记,希望能坚持下去。

动画演示地址:
http://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html
在正式开始各类排序算法之前,先mark下稳定排序,原地排序等概念,以及排序算法的不同分类方法。

稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;

不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;

原地排序:排序过程中不需要额外的辅助空间,只需要本身需要排序的数组。

非原地排序:排序的过程中需要额外的辅助空间来帮助施行排序过程。

在大多数的排序算法中,排序基本只用到了两个操作:比较、移动!大部分时候我们使用的排序算法都是基于比较的的,还有一类算法不是基于比较的排序算法,即计数排序、基数排序,桶排序;

排序分类:
按照排序结果是否稳定性分类:

1)稳定排序:插入排序,冒泡排序,归并排序,计数排序,基数排序,桶排序(如果桶内排序采用的是稳定性排序)
2)非稳定排序:选择排序,快速排序,堆排序。

按照排序过程中是否需要额外空间:
1)原地排序:插入排序,选择排序,冒泡排序,快速排序,堆排序。
2)非原地排序:归并排序,计数排序,基数排序,桶排序。

按照排序的主要操作分类:
1)交换类:冒泡排序、快速排序;此类的特点是通过不断的比较和交换进行排序;
2)插入类:简单插入排序、希尔排序;此类的特点是通过插入的手段进行排序;
3)选择类:简单选择排序、堆排序;此类的特点是看准了再移动;
4)归并类:归并排序;此类的特点是先分割后合并;

历史进程:一开始排序算法的复杂度都在O(n^2),希尔排序的出现打破了这个僵局;

**

初级排序算法:

**

一、选择排序

选择排序视频:http://v.youku.com/v_show/id_XMzMyODk5MDI0.html

思想:找到数组中最小的那个元素,然后将它和第一个位置所在的元素交换位置。接着下一轮,在剩下的元素中寻找最小的,然后将它和数组的第二个位置的元素交换位置。如此往复,直到整个数组排序,这种算法称为选择排序,因为它在不断选择剩余元素中最小者。(当然如果要递减排序的话,也可以每次都选择当前剩下元素中最大的元素)

选择排序运行轨迹图示例

选择排序的特点:
1、运行时间和输入状态无关:
每次为了找出当前剩余数组的最小值,都需要将剩余元素全部扫描一边(尽管有可能输入数组最开始就是有序的或者主键完全相等)每次的扫描都不会为下次扫描提供额外信息。也就是选择排序的效率和输入状态无关。
2、数据移动最少:
交换次数和数组的大小是线性关系,而其他大部分排序算法都不具备该特点。

选择排序对于长度为N的数组,大约需要N次交换和N*N/2次比较。

效率:
最好情况:O(n*n)
最坏情况:O(n*n)

实现代码(较为简单容易理解,原地排序,不需要额外辅助空间,不稳定的排序算法)

package BasicSort;

import java.util.Random;

public class Select {
    public static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static void sort(int[] a) {
        int min_index;
        for (int i = 0; i < a.length; i++) {
            min_index = i;
            for (int j = i; j < a.length; j++) {
                if (a[j] < a[min_index])
                    min_index = j;
            }
            exch(a, i, min_index);
        }
    }

    public static void main(String[] args) {
        int N = 10;
        int[] a = new int[N];
        Random rondom = new Random();

        for (int i = 0; i < N; i++)
            a[i] = rondom.nextInt(20);
        System.out.println(" before sort ");
        show(a);
        System.out.println();
        sort(a);
        System.out.println(" after sort ");
        show(a);

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

二、插入排序

插入排序视频:http://v.youku.com/v_show/id_XMzMyODk3NjI4.html

思想:将当前索引指向的数字放入该索引左边其它已经有序的数字中的适当位置,也就是每次取没被排序的最左边一个和已排序的做比较,并插入到正确位置。在计算机的实现中,为了给要插入的元素腾出空间,我们需要将其余所有元素在插入动作前都向右移动一位,与选择排序的相同的是,当前索引的左边的所有元素都是有序的,但是它们的最终位置还不确定,为了给更小的元素腾出空间,它们也有可能被移动,但是当索引到达数组的最右端的时候,数组的排序就完成了。

插入排序轨迹实例图

特点:
运行时间和输入状态有关,对于原数组处于基本有序的状态时候的运行时间会比随机数组或者对大多处于逆序的数组快的多。

对于随机排列的数组长度为N,平均情况下插入排序需要n* n/4次比较和交换。最坏情况需要 n* n/2次比较和交换。最好情况下需要n-1次比较和0次交换。插入排序中比较和交换的次数一样多。

优化思想:要大幅度提高插入排序的速度并不难,只需要在内循环中将较大的元素都向右移动而不是交换这两个元素(这样访问数组的次数就能减半)。

效率:
最好情况:O(n)(待排序的数组初始化就是有序的)
最坏情况:O(n*n)

实现代码:(原地排序,稳定排序)

package BasicSort;

import java.util.Random;

public class Insertion {

    public static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static void sort(int[] a) { // 未优化的
        for (int i = 1; i < a.length; i++)
            for (int j = i; j > 0 && (a[j] < a[j - 1]); j--) {
                exch(a, j, j - 1);
            }
    }

    public static void sortOptimize(int[] a) {
        for (int i = 1; i < a.length; i++) {
            int j, temp = a[i];
            for (j = i; j > 0 && (temp < a[j - 1]); j--)
                ;
            for (int k = i; k > j; k--)
                a[k] = a[k - 1];
            a[j] = temp;
        }
    }

    public static void main(String[] args) {
        int N = 10;
        int[] a = new int[N];
        Random rondom = new Random();
        for (int i = 0; i < N; i++)
            a[i] = rondom.nextInt(20);
        System.out.println(" before sort ");
        show(a);
        System.out.println();
        // sort(a);
        sortOptimize(a);
        System.out.println(" after sort ");
        show(a);

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

总的来说,插入排序对于部分有序的小规模数组非常有效效果也比冒泡和选择排序大部分情况下效果好。

三、冒泡排序:

冒泡排序视频:http://v.youku.com/v_show/id_XMzMyOTAyMzQ0.html

思想:每次比较相邻的元素,如果有需要交换的时候,进行交换,接着向右比较相邻的元素,这样第一轮比较到数组的最右端,就将最大的元素移动了最后一位(也可以将当前序列“最小元素”冒泡“到达第一位”)。再次从头开始比较相邻的元素,对于需要交换的进行交换,直到上一轮的”最大“的位置,第二轮结束后就将第二大的元素移动到了倒数第二位,如此反复,就得到了有序的数组。由于每次都将一个‘当前’最大的元素移动到了它在最终排序应该在的位置,所以被称为冒泡排序。
冒泡排序轨迹部分实例图

效率:
最好情况:O(n)(改进后冒泡算法的最好效率,使用了标记,在整次循环中不发生交换的时候跳出循环)
最坏情况:O(n*n)

优化思想:建立一个boolean值,当当前序列没有发生交换的时候,也就是说当前序列已经处于有序状态了,跳出循环。

实现代码:(原地排序,稳定排序)

package BasicSort;

import java.util.Random;

public class Bubble {

    public static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static void sort(int[] a) { // 未优化
        for (int i = 0; i < a.length - 1; i++)
            for (int j = a.length - 1; j > i; j--) {
                if (a[j] < a[j - 1])
                    exch(a, j, j - 1);
            }
    }

    public static void sortOptimize(int[] a) {
        boolean flag = false;
        for (int i = 0; i < a.length - 1; i++) {
            flag = false;
            for (int j = a.length - 1; j > i; j--) {
                if (a[j] < a[j - 1]) {
                    exch(a, j, j - 1);
                    flag = true;
                }
            }
            if(!flag) break;
        }
    }

    public static void main(String[] args) {
        int N = 10;
        int[] a = new int[N];
        Random rondom = new Random();

        for (int i = 0; i < N; i++)
            a[i] = rondom.nextInt(20);
        System.out.println(" before sort ");
        show(a);
        System.out.println();
        sort(a);
        //sortOptimize(a);
        System.out.pr
intln(" after sort ");
        show(a);

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

高级排序算法

四、归并排序:

归并排序视频:http://v.youku.com/v_show/id_XMzMyODk5Njg4.html

思想:归并这个简单的操作,即将两个有序的数组归并成一个的更大有序数组。利用归并的思想,实现了一个简单的递归的排序算法。将一个要排序的数组,可以递归的将其分成两半,然后分别排序,最后将结果归并起来。实现归并直截了当的方式就是将两个不同的有序的数组归并到第三个数组,实现方法很简单就是创建一个适当大小的数组然后将两个输入数组中的元素一个个从小到大放入这个数组。但是当用归并给较大数组排序时候,这种简单的思想就需要很多辅助数组,在理想中,我们将希望采用一种原地归并的思想,就是先将前半部分排序,然后将后半部分排序,最后在数组中移动,而不是创建额外的辅助空间,但是这样构造太复杂。
归并排序示意图

原地(还是运用了辅助数组aux)归并的抽象方法:
实现代码:

    public static void merge(int[] a, int lo, int hi, int mid) {
        int i = lo;
        int j = mid + 1;
        for (int k = lo; k <= hi; k++)
            aux[k] = a[k];
        for (int k = lo; k <= hi; k++) {
            if (i > mid)
                a[k] = aux[j++];
            else if (j > hi)
                a[k] = aux[i++];
            else if (a[i] > a[j])
                a[k] = aux[j++];
            else
                a[k] = a[i++];
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

1)自顶向下的归并排序:

自顶向上归并排序递归示例图

对于长度为N的任意数组,自顶向下的归并需要1/2* N* log(N)~N * log(N)次比较,需要访问数组6* N * log(N)。

可以看出归并只需要遍历整个数组多个对数因子的时间就能将一个庞大的数组进行排序,这是前面所介绍的排序算法所不能做到的。但是归并排序需要的额外空间和N成正比。

算法优化思想:
1)用不同的方法处理小规模问题能改进大多数递归算法的性能,因为递归会使得小规模问题中的方法调用次数过于频繁。由于前面三种简单的排序方法对于小规模数组非常有效,因此在递归排序的小规模问题中使用插入排序可以极大的提高处理效率。
2)我们可以添加一个判断条件,如果a[mid]<=a[mid+1],我们就认为数组已经是有序的并跳过merge方法,这个改动不会影响排序的递归调用,但是任意有序的子数组算法的运行时间就会变为线性!
3)我们可以节省将数组复制到用于归并的辅助数组所用的时间(但是空间不可以)要做到这一点,我们需要调用两种排序方法,一种是将数据从输入数组排序到辅助数组,一种是将数据从辅助数组排序到输入数组。这种方法需要一些技巧,我们需要在递归调用的每一层交换输入数组和辅助数组的角色!

效率:
最好情况O(n*log(n));
最坏情况O(n*log(n));

这里写图片描述
实现代码:(非原地归并,稳定排序):

package BasicSort;

import java.util.Random;

public class Merge {
    private static int[] aux;
    private static final int M = 15; //小规模问题的切换阈值

    public static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public static void sortInsertion(int[] a, int lo, int hi) {
        for (int i = lo; i < hi; i++)
            for (int j = i; j > 0 && (a[j] < a[j - 1]); j--) {
                exch(a, j, j - 1);
            }
    }

    public static void merge(int[] a, int lo, int mid, int hi) {
        int i = lo;
        int j = mid + 1;
        for (int k = lo; k <= hi; k++)
            aux[k] = a[k];
        for (int k = lo; k <= hi; k++) {
            if (i > mid)
                a[k] = aux[j++];
            else if (j > hi)
                a[k] = aux[i++];
            else if (aux[i] > aux[j])
                a[k] = aux[j++];
            else
                a[k] = aux[i++];
        }
    }

    public static void sort(int[] a) {
        aux = new int[a.length];
        //sort(a, 0, a.length - 1);
        sortOptimize(a, 0, a.length - 1);
    }

    public static void sort(int[] a, int lo, int hi) {
 //未优化
        if (hi <= lo)
            return;
        int mid = lo + (hi - lo) / 2;
        sort(a, lo, mid);
        sort(a, mid + 1, hi);
        merge(a, lo, mid, hi);
    }

    public static void sortOptimize(int[] a, int lo, int hi) {

             //采用小规模问题时使用插入排序
        if (hi <= lo + M) {
            sortInsertion(a, lo, hi);
            return;
        }
        int mid = lo + (hi - lo) / 2;
        sort(a, lo, mid);
        sort(a, mid + 1, hi);
        merge(a, lo, mid, hi);
    }

    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static void main(String[] args) {
        int N = 1000;
        int[] a = new int[N];
        Random rondom = new Random();

        for (int i = 0; i < N; i++)
            a[i] = rondom.nextInt();
        System.out.println(" before sort ");
        show(a);
        System.out.println();
        sort(a);
        System.out.println(" after sort ");
        show(a);
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88

2)自底向上的归并排序:
思想:尽管我们考虑的问题是归并两个大叔组,但是实际上我们归并的数组都非常小。实现归并排序的另外一个方法就是先归并那些微型数组,然后再成对的归并得到的数组,如此这般直到我们将整个数组归并在一起。首先我们进行的是两两归并(把每个元素想成一个大小为1的数组),然后进行四四归并(将两个大小为2的数组归并成一个有4个元素的数组),然后是八八归并,一直下去,在每一轮归并中,最后一次归并的第二个子数组可能比第一个子数组要小,但是这对于归并并没有影响。

自底向上递归算法运行轨迹

public static void sortBU(int[] a){
        aux = new int[a.length];
        for(int size=1;size<a.length;size=size*2)
            for(int lo=0;lo<a.length-size;lo+=size+size){
                merge(a,lo,lo+size-1,Math.min(lo+size+size-1,a.length-1));
            }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

五、快速排序:

思想:快速排序也是一种分治的排序算法,它将整个数组分成两个小的数组,将这个两部分独立的排序,快速排序和归并排序是互补的,归并排序是将数组分成两个子数组分别排序,并且将有序的子数组归并从而将整个数组排序。而快速排序,将数组排序的方式则是当两个子数组都有序的时候,整个数组就有序了。在归并排序中递归调用发生在处理整个数组之前,而在快速排序中,递归调用发生在处理整个数组之后,在归并排序中一个数组被分成两半,而在快速排序中,切分的位置取决于数组的内容。该方法在于切分点元素,切分点元素的位置已经排定,在该位置前的元素都不大于切分点元素,在该位置右边的元素都不小于切分点元素。我们就是递归的调用切分来实现排序的。切分是快速排序的关键,一般的策略就是先随意的取a[lo]作为切分元素。然后从数组的左端开始扫描(指针i),直到找到大于等于它的元素,再从数组的右端开始扫描(指针j),直到遇到小于等于它的元素,这两个元素显然是没有排定的,我们交换他们的位置如此继续,我们就可以保证指针i左边的元素都小于切分元素,指针j右边的元素都大于切分元素。当两个指针相遇,我们只需要将切分元素和左边子数组的最右边的元素交换即可,并且纪录此时交换的位置。
切分示意图

效率:
当输入数组已排序时,时间为O(n^2)可以对数组进行随机化打乱,使得期望运行时间为O(nlgn)。
最佳运行时间:O(nlgn)
不需要额外的辅助空间。

算法优化:
1)切换到插入排序:和递归排序的优化一样,对于小规模问题的时候,切换成插入排序。
2)三取样切分:改进快排性能的另外一点,不是使用子数组的第一个元素作为切分元素而是使用子数组的中位数来切分数组。人们发现,取样大小设置为3的时候切分效果最好,我们还可以数组末尾作为哨兵,防止数组越界发生。

三向切分示意图

3)熵最优。在有大量数据重复的情况下,快速排序的递归性会使得元素全部重复的子数组经常出现,一个简单的想法就是,将数组切分成三个部分,大于,小于,等于切分元素三部分。
三向切分的快速排序(基于优化思想三):
维护一个指针lt,使得a[lo…lt-1]中的元素都小于切分元素,一个指针gt使得a[gt+1…hi]中的元素都大于切分元素,一个指针i使得a[lt…i-1]的元素都等于切分元素,而a[i…gt]中的元素的划分还未确定。

快排轨迹示意图

实现代码(原地排序,不稳定排序)

package BasicSort;

import java.util.Random;

public class QuickSort {

    private static final int M = 15; // 小规模问题的切换阈值

    public static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static void sortInsertion(int[] a, int lo, int hi) {
        for (int i = lo; i < hi; i++)
            for (int j = i; j > 0 && (a[j] < a[j - 1]); j--) {
                exch(a, j, j - 1);
            }
    }

    public static int partition(int[] a, int lo, int hi) {
        int u = a[lo];
        int i = lo, j = hi + 1;
        while (true) {
            while (a[++i] < u);
            while (a[--j] > u);
            if (i >= j)
                break;
            exch(a, i, j);
        }
        exch(a, lo, j);
        return j;
    }

    public static void sort(int[] a) {

        sort(a, 0, a.length - 1);
        // Quick3way(a, 0, a.length-1);
    }

    public static void sort(int[] a, int lo, int hi) {
        /*
         * 在添加小数组使用插入排序优化。
         * 三向切分优化这个版本下,只有对于重复数据较多的时候相对于,快排有优势
         * if (hi <= lo + M) { Insertion.sort(a, lo, hi); return; }
         */
        if (hi <= lo)
            return;
        int point = partition(a, lo, hi);
        sort(a, lo, point - 1);
        sort(a, point + 1, hi);

    }

    public static void Quick3way(int[] a, int lo, int hi) {
        /*
         * 添加小数组时候使用插入排序。
         * if (hi <= lo + M) { Insertion.sort(a, lo, hi); return; }
         */
        if (hi <= lo)
            return;
        int u = a[lo];
        int lt = lo, gt = hi, i = lo + 1;
        while (i <= gt) {
            if (a[i] > u)
                exch(a, i, gt--);
            else if (a[i] < u)
                exch(a, i++, lt++);
            else
                i++;
        }
        Quick3way(a, lo, lt - 1);
        Quick3way(a, gt + 1, hi);
    }

    public static void main(String[] args) {
        int N = 10;
        int[] a = new int[N];
        Random rondom = new Random();
        for (int i = 0; i < N; i++)
            a[i] = rondom.nextInt(20);
        System.out.println(" before sort ");
        show(a);
        System.out.println();
        sort(a);
        System.out.println(" after sort ");
        show(a);

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97

六、堆排序

堆排序算法之前讲下优先队列:
一个数据结构,能够删除最大元素和插入元素的数据类型。通过插入一列元素到优先队列中,然后一个个删除其中得到最小元素,来实现排序算法,而堆排序就来自于基于堆的优先队列的实现。
1)优先队列的初级实现:
a)数组实现(有序的和无序的):
b)链表表示法:
2)二叉堆表示法:(二叉堆:是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层次结构存储,不使用数组的第一个位置)
在二叉堆的数组中,每个元素都要保证大于等于另外两个特定位置的元素。相应的这些位置的元素又至少要大于等于数组中的另外两个元素。(当一棵二叉树的每个结点都大于等于它的两个子结点的时候,它被称为堆有序)。
二叉树表示法:如果我们使用指针来表示堆有序的二叉树,那么每个结点(除了根结点)都需要三个指针来表示它的上下结点。如果使用完全二叉树,表达就会变得简单。可以先定下跟结点,然后一层一层,从左向右在每个结点下方连接两个更小的结点。完全二叉树只用数组而不需要指针就能表示。具体方式,就是跟结点的位置在数组下标为1的位置。而它的子结点分别在位置2和3,而子结点的子结点的位置在4,5,6,7以此类推。位置在下标为K的结点的父结点位置为K/2向下取整。而它的两个子结点的所在的位置是2k和2k+1。使用这种方式实现的优先队列能够实现对顺级别内的插入元素和删除最大元素。

堆的数组表示

堆得有序化:堆的操作会首先进行一些简单的改动,打破堆的状态,然后再遍历堆并按照堆得要求将堆的状态恢复。这个过程被称为堆得有序化。
两种重要的堆操作:
1)由下至上的堆得有序化(上浮):
出现情景:如果堆得有序状态因为某个结点变得比它的父结点更大,而被打破,那么我们就需要来交换该结点和它的父节点来修复堆,交换后这个结点比它的两个结点都大,但是这个结点有可能比它现在的父结点大,仍然可能需要上述方法一遍遍的恢复秩序,将这个结点上移直到遇到了更大的结点。(代码实现关键点:位置k的父结点的位置是k/2)

上浮示意图

实现代码:

    private void swim(int[] a, int k) {
        while (k > 1 && a[k / 2] < a[k]) {
            exch(a, k / 2, k);
            k = k / 2;
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2)由上至下的堆有序化(下沉):
如果堆的有序状态因为某个结点变得比它的两个子结点或者其中一个子结点更加小了而被打破,那么我们可以通过将它和它的子结点中较大的交换位置来恢复堆,同样的道理,交换可能使得该结点仍然打破新位置的有序状态,因此我们需要重复不断的用同样的方式来恢复,将结点向下移动直至它的子结点都比它小或者是到达了堆的底部。
下沉示意图

实现代码:

    private void sink(int[] a, int k) {
        while (2 * k <= a.length) {
            int j = 2 * k;
            if (j < a.length && a[j] < a[j + 1])
                j++;
            exch(a, k, j);
            k = j;
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

插入元素:将新的元素插入到数组的尾部,增加堆的大小,并且让这个数组上浮到合适的位置。
删除最大元素:我们从数组的顶端删除最大的元素并将数组最末尾的元素放到顶端,减小堆的大小并且让这个元素下沉到合适的位置。

基于堆的优先队列的实现:
实现代码:

package BasicSort;

public class MaxPQ {
    private static int MIN;
    private int[] pq;
    private int N = 0;

    public MaxPQ(int max) {
        pq = new int[max + 1];
    }

    public boolean isEmpty() {
        return N == 0;
    }

    public int size() {
        return N;
    }

    public static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public void swim(int k) {
        while (k > 1 && pq[k / 2] < pq[k]) {
            exch(pq, k / 2, k);
            k = k / 2;
        }
    }

    private void sink(int k) {
        while (k * 2 < pq.length) {
            int j = 2 * k;
            if (j < pq.length && j < pq.length) {
                if (pq[j] < pq[j + 1])
                    j++;
                if (pq[k] < pq[j])
                    break;
                exch(pq, k, j);
                k = j;
            } else {
                if (j < pq.length && pq[k] > pq[j])
                    exch(pq, k, j);
                if (pq[k] < pq[j])
                    break;
                k = j;

            }
        }
    }

    public void insert(int key) {
        pq[++N] = key;
        swim(N);
    }

    public int delMax() {
        int max = pq[1];
        exch(pq, 1, N--);
        pq[N + 1] = MIN;
        sink(1);
        return max;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

思想:我们可以将任意的优先队列变成一种排序方法,将所有的元素插入一个寻找最大元素的优先队列(基于堆的优先队列)中,然后再重复的调用删除最大元素的操作来将他们按照顺序的删去。堆排序被分成两个阶段,第一阶段是堆的构造阶段,我们将原始数据重新组织安排进一个堆中,然后在下沉排序阶段,我们从堆中按照递减顺序取出所有的元素并得到排序结果,在排序的过程中,我们将需要排序的数组本身作为堆,因此不需要任何的辅助空间。
1)堆的构造:
思路一:用swim()指针保证扫描指针左侧的所有元素已经是一颗有序的完全树,就像往优先队列中插入元素一样,但是这样需要扫描所有的元素(除了跟结点外的)。
思路二:从右至左用sink()函数构造子堆。每个数组的位置都可以看成一个子堆的跟结点,sink()对于这些跟结点也有用。如果一个结点的两个子结点已经是有序堆的跟结点了,那么只需要调用sink()就能使它们变成一个堆,递归的使用是个方法直至到达整棵树的跟结点,我们就可以建立起来一个有序堆。由于叶子结点可以直接看成有序子堆,所以我们只用从编号最大的分支结点开始向上进行sink()操作,那么我们只用扫描一般的结点。
2)下沉排序:
堆排序的主要工作是在该阶段完成,在这里我们将堆的最大元素(也就是数组首位元素)删除并放入堆缩小后数组空出来的位置。这个过程和选择排序非常类似(按照降序的方式取出所有的元素),但是需要比较的次数大大地减少了。
堆排序的堆构造过程和下沉排序过程
将N个元素排序,堆排序只需要少于2*N* logN+2*N次比较以及一半次数的交换。
效率:
最好情况:O(nlgn)
最坏情况:O(nlgn)

实现代码:(原地排序,不稳定的排序)

package BasicSort;

import java.util.Random;

public class StackSort {
    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    private static void sink(int[] a, int k, int length) {
        while (2 * k < length) {
            int j = 2 * k;
            if (j + 1 < length) {
                if (j < length && a[j] < a[j + 1])
                    j = j + 1;
                if (a[k] >= a[j])
                    break;
                exch(a, k, j);
                k = j;
            }
            else{
                if (j < length && a[k] < a[j])
                    exch(a, k, j);
                if (a[k] >= a[j])
                    break;
                k = j;
            }
        }
    }

    public static void sort(int[] a) {

        int N = a.length;
        for (int k = N / 2; k > 0; k--)
            sink(a, k, N);
        while (N > 1) {
            exch(a, 1, --N);
            sink(a, 1, N);
        }

    }

    public static void main(String[] args) {
        int N = 10;
        int[] a = new int[N + 1];
        Random rondom = new Random();
        for (int i = 1; i < N; i++)
            // a[0]不用来存放数组
            a[i] = rondom.nextInt(20);
        System.out.println(" before sort ");
        show(a);
        System.out.println();
        sort(a);
        System.out.println(" after sort ");
        show(a);

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

也可以将比较和交换的时候的下标稍加改动,就可以实现a[0]~a[N-1]的排序。

七、计数排序

思想:适用于小整数键的简单排序,其优势是对于数值范围已知的数组进行排序,适用范围特别局限。但是如果满足该排序的使用条件,相对于任何基于比较的排序算法(基于比较的排序算法,最优效率N*logN)这种计数排序的效率都很高。该方法创建一个长度为待排序数组数值范围大小的数组C(即最大元素-最小元素+1),数组C中每个元素纪录待排序数组中对应的纪录出现的次数。

计数排序

这种方法有4个步骤:
1) 频率统计:每个种数值出现的频率count[ ]。(小技巧:如果键为r,则将count[r+1]加1);
2) 将频率count[ ]转换成索引count[ ]。将使用count[ ]来计算每个键在排序结果中的起始索引位置;
3) 数据分类:根据键对应的count[ ]值决定位置,在移动之后,count[ ]对应的元素总是+1,以保证count[r]总是下一个键为r的元素所对应的索引位置。
4) 回写。

效率:
最好情况:O(n+k)
最坏情况:O(n+k)

实现代码:(非原地排序,需要辅助数组,稳定的排序)

package BasicSort;

import java.util.Random;

public class CountSort {

    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static void sort(int[] a) {

        int N = a.length;
        int min = 0, max = 0;
        int[] aux = new int[N];
        for (int i = 0; i < N; i++) {
            if (a[i] < min)
                min = a[i];
            if (a[i] > max)
                max = a[i];
        }
        int R = max - min + 1;// 待排序数据组的范围。

        int[] count = new int[R + 1];
        for (int i = 0; i < N; i++) {
            count[a[i] - min + 1]++;

        }
        for (int i = 0; i < R; i++) {
            count[i + 1] += count[i];
        }
        for (int i = 0; i < N; i++) {
            aux[count[a[i] - min]++] = a[i];
        }
        for (int i = 0; i < N; i++) {
            a[i] = aux[i];
        }
    }

    public static void main(String[] args) {
        int N = 10;
        int[] a = new int[N];
        Random rondom = new Random();
        for (int i = 0; i < N; i++)
            a[i] = rondom.nextInt(20);
        System.out.println(" before sort ");
        show(a);
        System.out.println();
        sort(a);
        System.out.println(" after sort ");
        show(a);

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

八、桶排序

思想:首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]….B[M]中的全部内容即是一个有序序列。桶排序要求待排序的元素都属于一个固定的且有限的区间范围内。相对于其他两种不需要比较的线性排序算法(计数排序和基数排序)桶排序可以对浮点数进行排序。桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的映射值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。
桶排序
效率:
最好情况运行时间:O(n)
最坏情况运行时间:当分布不均匀时,全部元素都分到一个桶中,如果桶内元素排序使用的是(插入排序,选择排序,冒泡排序)则O(n^2),(堆排序、快速排序),这样最坏情况就是O(nlgn)。

实现代码:(非原地排序,稳定排序(如果桶内元素使用的是插入排序))

这里写图片描述

九、基数排序

思想:基数排序是将整数按照位来进行排列,从低位开始,对于所有待排序数据的相同的位置的每一位使用稳定的排序算法,例如计数排序,直到最高位排序完成,所有的排序完成。也就是说我们可以将整数的每一位看成一个关键字,基数排序就是将待排序数组中每组关键字进行桶分配。
演示动画:
http://www.cs.usfca.edu/~galles/visualization/RadixSort.html
效率:
最好情况:O((n+k)*d)
最坏情况:O((n+k)*d)

优化思想:对于整个待排序数组中的数字,我们不一定需要一位位的来排列,也可以多位多位的排列,例如,先对低几位进行排列,在对高几位进行排列。
实现代码:(稳定排序,非原地排序)

package BasicSort;

import java.util.Random;

public class Counting_Sort {
    public static void show(int[] a) {
        for (int temp : a)
            System.out.print(temp + " ");
    }

    public static int max_dig(int[] a) {
        int max = Integer.MIN_VALUE, N = a.length, d = 1;
        for (int i = 0; i < N; i++) {
            if (a[i] > max)
                max = a[i];
        }
        for (int i = 10;; i *= 10) {
            if (max > i)
                d++;
            else
                break;
        }
        return d;
    }

    public static void sort(int[] temp, int[] a, int k) {
        int[] count = new int[k + 1];
        int n = temp.length;
        int[] aux = new int[n];
        for (int i = 0; i < n; i++) {
            count[temp[i] + 1]++;
        }
        for (int i = 0; i < k; i++) {
            count[i + 1] += count[i];
        }
        for (int i = 0; i < n; i++) {
            aux[count[temp[i]]++] = a[i];
        }
        for (int i = 0; i < n; i++) {
            a[i] = aux[i];
        }
    }

    public static void main(String[] args) {
        int base = 1;
        int N = 10;
        int[] a = new int[N];
        Random rondom = new Random();
        for (int i = 0; i < N; i++)
            a[i] = rondom.nextInt(20);
        int d = max_dig(a);
        int[] temp = new int[N];
        System.out.println(" before sort ");
        show(a);
        while (d > 0) {
            base *= 10;
            for (int i = 0; i < N; i++) {
                temp[i] = a[i] % base;
                temp[i] /= (base / 10);
            }
            sort(temp, a, 10);
            d--;
        }
        System.out.println();
        System.out.println(" after sort ");
        show(a);
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

十、希尔排序

希尔排序视频: http://v.youku.com/v_show/id_XMzMyODk5MzI4.html
思想:对于插入排序的改进,插入排序需要交换相邻元素来实现将元素放到应该到的位置,假设恰好最小的元素在数组的最右端,就需要将它一点点的从数组的最右端移到最左端。希尔元素为了加快速度,简单地改进插入排序,交换不相邻的元素已对数组的局部进行排序,并最终将局部有序的数组排序。
希尔排序的思想是使数组中任意间隔为h的元素都是有序的,这样的数组被称为h有序数组。如果h很大,我们就能将元素移动到较远位置,为了实现更小的h有序创造方便。用这种方式,对于任意1开头h结尾的序列,我们都能将这个序列排序,这就是希尔排序。^_^(这个我也不是很理解)
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值