zoukankan      html  css  js  c++  java
  • 数据结构_排序算法总结



    作者丨fredal
    https://www.jianshu.com/p/28d0f65aa6a1


    所有内部排序算法的一个总结表格


    640?wx_fmt=other


    640?wx_fmt=gif简单选择排序


    首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。


    最差:O(n2)


    最优:O(n2)


    平均:O(n2)


    public void selectSort(int[] a){
        for(int i=0;i<a.length-1;i++){
            int min=i;//最小元素所在位置
            for(int j=i+1;j<a.length;j++){
                if(a[j]<a[min]) min=j;
            }
            {int temp=a[min];a[min]=a[i];a[i]=temp;}//交换元素,把最小元素放在头部
        }
     }


    640?wx_fmt=gif冒泡排序


    冒泡排序算法的运作如下:


    1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。

    2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

    3. 针对所有的元素重复以上的步骤,除了最后一个。

    4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

    最差:O(n2)

    最优:O(n)

    平均:O(n2)

    public static void bubbleSort(int[] a){
        for(int i=0;i<a.length-1;i++){
            for(int j=0;j<a.length-i-1;j++){
                if(a[j]>a[j+1]) {int temp=a[j];a[j]=a[j+1];a[j+1]=temp;}//交换元素位置 保证大的在后面
            }
        }
     }


    640?wx_fmt=gif插入排序


    一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:


    1. 从第一个元素开始,该元素可以认为已经被排序

    2. 取出下一个元素,在已经排序的元素序列中从后向前扫描

    3. 如果该元素(已排序)大于新元素,将该元素移到下一位置

    4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置

    5. 将新元素插入到该位置后

    6. 重复步骤2~5

    最差:O(n2)

    最优:O(n)

    平均:O(n2)

    640?wx_fmt=gif
    直接插入排序:


    public static void insertSort(int[] a){
        for(int i=1;i<a.length;i++){
            for(int j=0;j<=i;j++){
                if(a[j]>=a[i]){//找到了第一个比自己大的元素,插入到该元素之前
                    int temp=a[i];
                    for(int k=i-1;k>=j;k--) a[k+1]=a[k];//每个元素向后移动一格
                    a[j]=temp;
                    break;
                }
            }
        }
     }


    640?wx_fmt=gif折半插入排序: 


    就是寻找第一个比自己大的元素的时候用折半查找进行优化:


    public static void halfInsertSort(int[] a){
        for(int i=1;i<a.length;i++){
            int low=0;
            int high=i;
            while(low<=high){
                int half=(low+high)/2;
                if(a[half]<a[i]){//插入点在高半区
                    low=half+1;
                }else{//插入点在低半区
                    high=half-1;
                }
            }
            //low或者high指向的元素就是第一个比自己大的元素 插入到之前
            int temp=a[i];
            for(int k=i-1;k>=low;k--) a[k+1]=a[k];//每个元素向后移动一格
            a[low]=temp;
        }
     }


    640?wx_fmt=gif快速排序


    快速排序使用分治法策略来把一个序列(list)分为两个子序列(sub-lists)。


    步骤为:


    1. 从数列中挑出一个元素,称为"基准"(pivot)

    2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

    3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

    最差:O(n2)

    最优:O(n log n)

    平均:O(n log n)


    //这里left取0,right取a.length-1
     public static void quickSort(int[] a,int left,int right){
        if(left<right){//递归出口条件
            int i=left;//左指针
            int j=right;//右指针
            int x=a[left];//选择第一个元素作为标尺
            while(i<j){
                while(i<j && a[j]>=x) j--;//从右向左找第一个小于x的数
                if(i<j) a[i++]=a[j];
                while(i<j && a[i]<x) i++;//从左向右找第一个大于等于x的数
                if(i<j) a[j--]=a[i];
            }
            a[i]=x;//插入标尺
            quickSort(a,left,i-1);//递归左边
            quickSort(a, i+1, right);//递归右边
        }
     }


    当前选取第一个元素作为标尺是比较不好的,一种安全的做法是随记选取,但是也不好.我们一般采用三数中值分割法,就是使用左端,右端,中心位置上的三个元素的中位数作为标尺进行排序.


    public static void quickSortS(int[] a,int left,int right){
        if(left<right){//递归出口条件
                int i=left;//左指针
                int j=right;//右指针
                int center=(left+right)/2;
                //三数中值分割法选取标尺
                if((a[left]<=a[center]&&a[center]<=a[right])||(a[right]<=a[center]&&a[center]<=a[left])){
                    int tmp=a[left];
                    a[left]=a[center];
                    a[center]=tmp;//记得交换噢
                }else if((a[left]<=a[right]&&a[right]<=a[center])||(a[center]<=a[right]&&a[right]<=a[left])){
                    int tmp=a[left];
                    a[left]=a[right];
                    a[right]=tmp;
                }
                int x=a[left];//标尺
                while(i<j){
                    while(i<j && a[j]>=x) j--;//从右向左找第一个小于x的数
                    if(i<j) a[i++]=a[j];
                    while(i<j && a[i]<x) i++;//从左向右找第一个大于等于x的数
                    if(i<j) a[j--]=a[i];
                } 
                a[i]=x;//插入标尺
                quickSortS(a,left,i-1);//递归左边
                quickSortS(a, i+1, right);//递归右边
            }
      }


    640?wx_fmt=gif希尔排序


    希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。


    根据步长进行分组,对每一组进行插入排序.


    最差:O(因步长各异,最好的n log2 n)


    最优:O(n)


    平均:O(因步长各异)


    public static void shellSort(int[] a){
        int igap=a.length;//初始化步长,第一次步长为数组长度的一半
        for(int gap=igap/2;gap>0;gap/=2){//步长
            for(int i=0;i<gap;i++){//共做步长次插入排序
                for(int j=i+gap;j<a.length;j+=gap){//直接插入排序算法
                    for(int k=i;k<=j;k++){
                        if(a[k]>=a[j]){//找到了第一个比自己大的元素,插入到该元素之前
                            int temp=a[j];
                            for(int p=j-1;p>=k;p--) a[p+1]=a[p];//每个元素向后移动一格
                            a[k]=temp;
                            break;
                        }
                    }
                }
            }
        }
     }


    640?wx_fmt=gif归并排序


    原理如下(假设序列共有n个元素):


    1. 将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素

    2. 将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素

    3. 重复步骤2,直到所有元素排序完毕

    最差:O(n logn)


    最优:O(n)


    平均:O(n log n)


    public static void mergeSort(int[] a,int first,int last){
        if(first<last){
            int[] temp=new int[a.length];
            int middle=(first+last)/2;
            mergeSort(a, first, middle);//递归左边
            mergeSort(a, middle+1, last);//递归右边
            //将两个有序数列合并
            int i=first;
            int j=middle+1;
            int k=0;
            while(i<=middle&&j<=last){
                if(a[i]<=a[j]){
                    temp[k++]=a[i++];
                }else{
                    temp[k++]=a[j++];
                }
            }
            while(i<=middle){
                temp[k++]=a[i++];
            }
            while(j<=last){
                temp[k++]=a[j++];
            }
            //将排好的temp重新赋给a
            for(i=0;i<k;i++){
                a[first+i]=temp[i];
            }
        }
     }


    640?wx_fmt=gif树形选择排序


    树形选择排序(Tree Selection Sort),又称锦标赛排序(Tournament Sort),是一种按照锦标赛思想进行选择排序的方法。


    首先对n个记录的关键字进行两两比较,然后在其中[n/2](向上取整)个较小者之间再进行两两比较,如此重复,直至选出最小关键字的记录为止。


    这个过程可以用一颗有n个叶子节点的完全二叉树表示,每个非终端节点都是左右孩子中的较小值,这样根节点就是所有叶子节点中的最小值了.把这个值输出后,将叶子节点中的最小值改为"最大值",然后从该叶子节点开始,和其左右兄弟进行比较,修改从叶子节点到根节点的路径上各节点的值,则根节点的值为次小值.同理,可从小到大排出所有值.


    虽然说树,但是通常由数组实现,父节点i的左子节点在位置(2i+1);父节点i的右子节点在位置(2i+2);子节点i的父节点在位置floor((i-1)/2)


    时间复杂度为O(n log n)


    缺点是辅助存储空间太多,并且与"最大值"进行了多余的比较.


    public static int[] TreeSelectSort(int[] data){
            int dlong=data.length;
            int tlong=2*dlong-1;
            int low=0;
            int[] tree=new int[tlong];
            int[] ndata=new int[dlong];

            for(int i=0;i<dlong;i++){
                tree[tlong-i-1]=data[i];
            }

            for(int i=tlong-1;i>0;i-=2){
                tree[(i-1)/2]=(tree[i]<tree[i-1]?tree[i]:tree[i-1]);
            }

            int minIndex;
            while(low<dlong){
                int min=tree[0];
                ndata[low++]=min;
                minIndex=tlong-1;
                //找到最小值
                while(tree[minIndex]!=min){
                    minIndex--;
                }
                tree[minIndex]=Integer.MAX_VALUE;
                //找到其兄弟节点
                while(minIndex>0){//有父节点
                    if(minIndex%2==0){//是右节点
                        tree[(minIndex-1)/2]=(tree[minIndex]<tree[minIndex-1]?tree[minIndex]:tree[minIndex-1]);
                        minIndex=(minIndex-1)/2;
                    }else{//是左节点
                        tree[minIndex/2]=(tree[minIndex]<tree[minIndex+1]?tree[minIndex]:tree[minIndex+1]);
                        minIndex=minIndex/2;
                    }
                }
            }
            return ndata;

        }


    640?wx_fmt=gif堆排序


    堆排序是对树形选择排序的一种优化,建立在"二叉堆"这种数据结构上,二叉堆属于完全二叉树,除此之外还需满足所有父节点都要大于或小于左右子树.


    父节点大于左右孩子的叫最大二叉堆,父节点小于左右孩子的叫最小二叉堆.


    以最大二叉堆为基础进行排序,就是先构建成一个最大堆,接着取出根节点,然后将剩下的数组元素在建成一个最大二叉堆,再取出根节点,如此循环直到所有元素都被取光.


    和树形选择排序一样也是由数组实现,参考上一节.


    最坏,最优和平均的时间复杂度都是nlogn.


    public static void heapSort(int[] a){
            //建立最大堆
            int size=a.length;
            for(int i=(size-1-1)/2;i>=0;i--){
                maxHeap(i,a,size);
            }

            for(int i=a.length-1;i>0;i--){
                //将根节点上的最大值不断与最后一个交换,并将最后一个筛选出来,指向最后的指针向前推进
                int temp=a[i];
                a[i]=a[0];
                a[0]=temp;
                size--;
                //保证根节点最大特性,其他节点都已经保持了
                maxHeap(0, a,size);
            }
        }

        //保持堆的最大特性
        private static void maxHeap(int i,int[] a,int size) {
            int left=2*i+1;
            int right=2*i+2;
            int largest=i;
            //分别与左右子树比较,取最大值
            if(left<=size-1&&a[left]>a[i]){
                largest=left;
            }
            if(right<=size-1&&a[right]>a[largest]){
                largest=right;
            }
            if(largest!=i){
                //交换根节点与最大值
                int temp=a[i];
                a[i]=a[largest];
                a[largest]=temp;
                //递归
                maxHeap(largest, a,size);
            }

        }


    640?wx_fmt=gif桶排序


    桶排序(Bucket Sort)的原理很简单,它是将数组分到有限数量的桶子里。


    假设待排序的数组a中共有N个整数,并且已知数组a中数据的范围[0, MAX)。在桶排序时,创建容量为MAX的桶数组r,并将桶数组元素都初始化为0;将容量为MAX的桶数组中的每一个单元都看作一个"桶"。


    在排序时,逐个遍历数组a,将数组a的值,作为"桶数组r"的下标。当a中数据被读取时,就将桶的值加1。例如,读取到数组a[3]=5,则将r[5]的值+1。


    桶排序的时间复杂度为O(n+k),k为取值范围,在特殊情况下提供了排序的下界.


    public static void bucketSort(int[] a,int max){
            int[] buckets;
            if(a==null||max<1){
                return;
            }
            buckets=new int[max];//创建一个容量为max的数组 并将其数据都初始化为0

            //计数
            for(int i=0;i<a.length;i++){
                buckets[a[i]]++;
            }
            //排序
            for(int i=0,j=0;i<max;i++){
                while((buckets[i]--)>0){
                    a[j++]=i;
                }
            }

            buckets=null;
        }


    640?wx_fmt=gif基数排序(多关键字排序)


    基数排序已经不再是一种常规的排序方式,它更多地像一种排序方法的应用,基数排序必须依赖于另外的排序方法。基数排序的总体思路就是将待排序数据拆分成多个关键字进行排序,也就是说,基数排序的实质是多关键字排序。


    多关键字排序时有两种解决方案:


    最高位优先法(MSD)(Most Significant Digit first)


    最低位优先法(LSD)(Least Significant Digit first)


    这里我们采用LSD,对关键字的排序采用桶排序,那么实质上就变成了多次桶排序.


    时间复杂度为O(d(n+k),d为关键字个数,k为取值范围


    数字是四位数之内,所以四个关键字为个十百千位的值.


    //假定四位数字内比较,有四个关键字,个十百千位,同一关键字使用桶排序
        public static void radixSort(int[] a,int max,int d){
            int rate=1;//表示关键字层级
            int[] buckets=new int[max];//存放个位数,十位数,百位数....
            List<Integer>[] temp=new List[max];//存放缓存数字
            for(int i=0;i<d;i++){

                //清空操作
                 Arrays.fill(buckets, 0);
                 Arrays.fill(temp, null);

                 //计算每个待排序数据的子关键字
                for(int j=0;j<a.length;j++){
                    int subKey=(a[j]/rate)%max;//取得个位数,十位数,百位数...
                    if(temp[subKey]==null) temp[subKey]=new ArrayList<Integer>();//用list数组来存放缓存数字
                    temp[subKey].add(a[j]);
                    buckets[subKey]++;//计数+1
                }

                //进行排序,就是按顺序取
                for(int j=0,k=0;j<max;j++){
                    int t=0;
                    while((buckets[j]--)>0){
                        a[k++]=temp[j].get(t);
                        t++;
                    }
                }

                rate*=max;
            }
        }


    由于数组操作起来是比较麻烦,非要通过数组存储的话只能存储次数不能存储整个数字,那取出来的时候我还没想到好的方法把它还原成数字了.所以我就采取了很笨的方法,再用一个缓存list数组存数字...


    当然这很浪费空间,也很麻烦,直接用链表简单很多


    public static void linkedRadixSort(int[] a,int max,int d){
            ArrayList<ArrayList> list=new ArrayList<ArrayList>();
            int rate=1;

            for(int i=0;i<d;i++){    
                list.clear();
                for(int j=0;j<max;j++){
                    list.add(new ArrayList<Integer>());
                }
                for(int j=0;j<a.length;j++){
                    int num=a[j];
                    int subKey=(num/rate)%max;
                    list.get(subKey).add(num);
                }    
                for(int j=0,k=0;j<max;j++){
                    while(list.get(j).size()>0 && list.get(j)!=null){
                        a[k]=(Integer) list.get(j).remove(0);
                        k++;
                    }    
                }

                rate*=max;
            }
        }


    虽然说是用链表的,但是收集起来的时候仍然是放数组的,仍要开辟许多空间.于是我们采用链式基数排序


    所谓链式,就是用链表存储,前一个分组的尾指针指向下一个分组的头指针这样,这样链表很容易做到.


    public static void linkedRadixSortS(Integer[] a,int max,int d){
            List<Integer> slist=new ArrayList<Integer>();
            ArrayList<ArrayList> list=new ArrayList<ArrayList>();
            int rate=1;
            slist.addAll(Arrays.asList(a));

            for(int i=0;i<d;i++){    
                list.clear();
                for(int j=0;j<max;j++){
                    list.add(new ArrayList<Integer>());
                }
                while(slist.size()>0){
                    int num=slist.remove(0);
                    int subKey=(num/rate)%max;
                    list.get(subKey).add(num);
                }    
                for(int j=0;j<max;j++){
                    slist.addAll(list.get(j));    
                }
                rate*=max;
            }


            a=slist.toArray(a);
        }


    上面三种只是空间上的优化,对于时间复杂度是没有影响的.


    640?wx_fmt=gif外部排序


    之前所有的排序算法都属于内部排序,需要将输入数据装入内存中.然而在一些应用程序中,它们的输入数据量太大装不进内存,这时候就需要外部排序了.


    基本外部排序算法使用归并排序.设有四盘磁带,Ta1,Ta2,Tb1,Tb2,磁带a和磁带b或者用作输入磁带,或者用于输出磁带.从输入磁带一次读入M(我们取3)个记录,在内部将这些记录排序,然后再交替地写到Tb1或Tb2上,将每组排过序的记录叫做一个顺串.


    640?wx_fmt=other


    640?wx_fmt=other


    现在Tb1和Tb2都包含了一些顺串.我们将每个磁带的第一个顺串取出将两者合并,把结果写到Ta1上,该结果是一个二倍长的顺串.然后我们再从每盘磁带取出下一个顺串,合并然后写到Ta2上.继续这个过程直达Tb1或Tb2为空.继续这个过程直到得到长为N的一个顺串,这个算法需要log(N/M)趟工作,见下图


    640?wx_fmt=other


    640?wx_fmt=other


    如果我们有额外的磁带,那么我们可以减少将输入数据排序的趟数,将基本的(2-路)合并扩充为(k-路)合并就可以做到这一点.这就是所谓的多路合并


    640?wx_fmt=other


    640?wx_fmt=other


    640?wx_fmt=other


    上面讨论的k-路合并方案需要使用2k盘磁带,其实我们通过只使用k+1盘磁带也可以完成排序的工作,就是所谓的多相合并.以三盘磁带完成2-路合并为例:


    640?wx_fmt=other


    这儿顺串最初的分配是一个关键的问题,其实采用斐波那契数是最优的.
    对于顺串的构造还可以采用置换选择排序,采用堆的思想构建,具有特别的价值.


    640?wx_fmt=other


    接下来实现完整的外部排序


    package com.fredal.structure;

    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Iterator;
    import java.util.Random;

    public class ExternalSort {

        public static int BUFFER_SIZE = 10;

        public File sort(File file) throws IOException {
            ArrayList<File> files = split(file);
            return process(files);
        }

        // recursive method to merge the lists until we are left with a
        // single merged list
        private File process(ArrayList<File> list) throws IOException {
            if (list.size() == 1) {
                return list.get(0);
            }
            ArrayList<File> inter = new ArrayList<File>();
            for (Iterator<File> itr = list.iterator(); itr.hasNext();) {
                File one = itr.next();
                if (itr.hasNext()) {
                    File two = itr.next();
                    inter.add(merge(one, two));
                } else {
                    return one;
                }
            }
            return process(inter);
        }

        /**
         * Splits the original file into a number of sub files. 
         */

        private ArrayList<File>  split(File file) throws IOException {
            ArrayList<File> files = new ArrayList<File>();
            int[] buffer = new int[BUFFER_SIZE];
            FileInputStream fr = new FileInputStream(file);
            boolean fileComplete = false;
            while (!fileComplete) {
                int index = buffer.length;
                for (int i = 0; i < buffer.length && !fileComplete; i++) {
                    buffer[i] = readInt(fr);
                    if (buffer[i] == -1) {
                        fileComplete = true;
                        index = i;
                    }
                }
                if (buffer[0] > -1) {
                    Arrays.sort(buffer, 0, index);
                    File f = new File("set" + new Random().nextInt());
                    FileOutputStream writer = new FileOutputStream(f);
                    for (int j = 0; j < index; j++) {
                        writeInt(buffer[j], writer);
                    }
                    writer.close();
                    files.add(f);
                }

            }
            fr.close();
            return files;
        }

        /**
         * Merges two sorted files into a single file.
         * 
         * @param one
         * @param two
         * @return
         * @throws IOException
         */

        private File merge(File one, File two) throws IOException {
            FileInputStream fis1 = new FileInputStream(one);
            FileInputStream fis2 = new FileInputStream(two);
            File output = new File("merged" + new Random().nextInt());
            FileOutputStream os = new FileOutputStream(output);
            int a = readInt(fis1);
            int b = readInt(fis2);
            boolean finished = false;
            while (!finished) {
                if (a != -1 && b != -1) {
                    if (a < b) {
                        writeInt(a, os);
                        a = readInt(fis1);
                    } else {
                        writeInt(b, os);
                        b = readInt(fis2);
                    }
                } else {
                    finished = true;
                }

                if (a == -1 && b != -1) {
                    writeInt(b, os);
                    b = readInt(fis2);
                } else if (b == -1 && a != -1) {
                    writeInt(a, os);
                    a = readInt(fis1);
                }
            }
            os.close();
            return output;
        }

        private void writeInt(int value, FileOutputStream merged)
                throws IOException 
    {
            merged.write(value);
            merged.write(value >> 8);
            merged.write(value >> 16);
            merged.write(value >> 24);
            merged.flush();
        }

        private int readInt(FileInputStream fis) throws IOException {
            int buffer = fis.read();
            if (buffer == -1) {
                return -1;
            }
            buffer |= (fis.read() << 8);
            buffer |= (fis.read() << 16);
            buffer |= (fis.read() << 24);
            return buffer;
        }

        /**
         * @param args
         * @throws IOException
         */

        public static void main(String[] args) throws IOException {
            File file = new File("mainset");
            Random random = new Random(System.currentTimeMillis());
            FileOutputStream fw = new FileOutputStream(file);
            for (int i = 0; i < BUFFER_SIZE * 3; i++) {
                int ger = random.nextInt();
                ger = ger < 0 ? -ger : ger;
                fw.write(ger);
                fw.write(ger >> 8);
                fw.write(ger >> 16);
                fw.write(ger >> 24);
            }
            fw.close();
            ExternalSort sort = new ExternalSort();
            System.out.println("Original:");
            dumpFile(sort, file);
            File f = sort.sort(file);
            System.out.println("Sorted:");
            dumpFile(sort, f);

        }

        private static void dumpFile(ExternalSort sort, File f)
                throws FileNotFoundException, IOException 
    {
            FileInputStream fis = new FileInputStream(f);
            int i = sort.readInt(fis);
            while (i != -1) {
                System.out.println(Integer.toString(i));
                i = sort.readInt(fis);
            }
        }

    }
  • 相关阅读:
    批处理学习总结之常用命令1
    Delphi常用数据类型
    Delphi预编译指令总结
    Delphi同步互斥总结
    MyEclipse 环境配置总结
    倒排索引
    laravel 学习相关笔记
    elasticsearch倒排索引原理
    原生sql和 TP sql怎么关联?
    elastic
  • 原文地址:https://www.cnblogs.com/hgmyz/p/12351529.html
Copyright © 2011-2022 走看看