经典的排序算法原理和实现

第七章 排序


包括插入排序、希尔排序、堆排序、归并排序和快速排序的设计与实现

7.1 基础知识

被排序的对象属于Comparable类型。在这种条件下的排序叫做基于比较的排序。在默认排序不能满足需要求的情况下,可以用Comparator来重写排序算法。

7.2 插入排序

插入排序由N-1趟排序组成,对于p=1到N-1趟,插入排序保证从位置0到位置p上的元素为已排序状态。插入排序利用了如下事实:0到p-1位置元素处于已排序状态。

    /**
     * 插入排序例程
     * @param array
     * @param <T>
     */
    public <T extends Comparable<? super T>> void insertSort(T[] array){
        int j;
        for (int i=1;i<array.length;i++){
            T temp=array[i];
            for(j=i;j>0&&temp.compareTo(array[j-1])<0;j++){//当前值比前一个值大就不用调整了,反之,则把前一个值移到当前位置
                array[j]=array[j-1];
            }
            array[j]=temp;//移完了,把值填到那个位置
        }
    }

7.3 希尔排序

希尔排序通过比较相隔一定距离的元素来工作。各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一项为止。所以,希尔排序有时候也叫缩减增量排序。

希尔排序使用一个序列h1,h2,…hl,叫做增量序列。只要h1=1,任何增量序列都是可行的,不过一些增量序列可能比另一些增量序列好。在使用增量hk的一趟排序之后,对于每一个i我们都有a[i]<=a[i+hk] ;所有相隔hk的元素都被排序,此时称文件是hk排序的。希尔排序的一个重要性质是一个hk排序的文件保持他hk的排序性。

hk排序的一般做法是,对于hk,hk+1,…N-1中的每一个位置i,把其上的元素放到i,i-hk,i=2hk,中的正确位置上,一趟hk排序的作用就是对hk个独立的子数组进行一次插入排序。

增量序列的一个流行但是不好的选择是使用shell建议的序列ht=[N/2]和hk=[hk+1/2]

    /**
     * 希尔排序
     * @param array
     * @param <T>
     */
    public <T extends Comparable<? super T>> void shellSort(T[] array){
        int j;
        for(int gap=array.length>>>1;gap>0;gap=gap>>>1){
            //里面实际上用gap作为间隔做了插入排序
            for(int i=gap;i<array.length;i++){
                T temp=array[i];
                for(j=i;j>=gap&&temp.compareTo(array[j-gap])<0;j-=gap){
                    array[j]=array[j-gap];
                }
                array[j]=temp;
            }
        }
    }

目前对于希尔排序来说,Sedgewick增量序列是最好的。它的上限是O(N(4/3)),平均大概是O(N(7/6)),这个增量序列中的项是9* 4i-9*2i+1形式或者是4^i-3 *2^i+1的形式。{1,5,19,41,109,…}

7.4 堆排序

建立二叉堆的时间是O(N),执行每个deleteMin操作,按照顺序,最小的元素先离开堆,再把这个元素记录到第二个数组然后再将数组拷贝回来,得到N个元素的排序。单个deleteMin花费的时间是log(N),对N个元素进行操作,所以总时间为Nlog(N)

这个算法的问题是他使用了一个附加的数组。因此,存储需求容量增加一倍,主要问题是空间问题。回避使用附加数组的做法是利用以下事实:在每次delateMin之后,堆缩小1.因此位于堆中最后的单元可以用来存放刚刚删除的元素

    public void heapSort(T[] t){
        for(int i=t.length/2-1;i>=0;i--){//构建二叉堆
            percDown(t,i,t.length);
        }
        for(int i=t.length-1;i>=0;i--){//删除最大值
            swap(t,0,i);
            percDown(t,0,i);
        }

    }

    private void swap(T[] t, int i, int j) {
        T temp=t[i];
        t[i]=t[j];
        t[j]=temp;
    }

    /**
     * 下滤
     * @param t
     * @param i
     * @param n
     */
    private void percDown(T[] t, int i, int n) {
        int child;
        T temp;
        for (temp=t[i];getLeftChildIndex(i)<n;i=child){
            child=getLeftChildIndex(i);
            if(child!=n-1&&t[child].compareTo(t[child+1])<0){
                child++;
            }

            if(temp.compareTo(t[i])<0){
                t[i]=t[child];
            }else {
                break;
            }
        }
        t[i]=temp;
    }

    private int getLeftChildIndex(int i){
        return 2*i+1;
    }

7.5 归并排序(mergedort)

归并排序的最坏情形是Nlog(N),这个算法中的基本操作是合并两个已排序的表。因为这两个表是已排序的,所以将输出放在第三个表中,该算法可以通过对输入数据进行一趟排序来完成。基本的合并算法是取两个输入数组AB,一个输出数组C,以及三个计数器ACtr,BCtr,CCtr,他们初始置于对应数组的开始端,比较A[ACtr]和B[BCtr],把叫嚣着拷贝到C中的下一个位置上,相应的计数器向前推进一步。当有一个表用完之后,把另一个表剩余的一部分拷贝到C中。
合并两个表的时间是线性的,最多需要N-1比较,每次比较都把一个元素放到C上,但是最后一步除外,最后一步至少添加两个元素。

扫描二维码关注公众号,回复: 10255879 查看本文章

算法描述:如果N=1,那么只有一个元素需要排序,否则,递归的将前半部分数据和后半部分数据各自归并排序,得到排序后的两部分数据,然后使用上面描述的合并算法再将这两部分合并在一起,这就是经典的分治策略,他将问题"分"成一些小的问题然后递归求解,而"治"的阶段则将分的阶段得出的答案修复在一起。

    //Integer[] a={3,13,7,4,8,2,1,6};
    private void mergeSort(int[] a, int left, int right) {
        if(left<right){
            int center=(left+right)>>1;
            mergeSort(a,left,center);
            mergeSort(a,center+1,right);
            merge(a,left,center,right);
        }
    }

    private void merge(int a[],int left,int center,int right){
        int temp[]=new int[a.length];
        int p1=left,p2=center+1,p3=left;//p1指向前半部分,p2指向后半部分,p3指向临时数组。
        while (p1<=center&&p2<=right){
            if(a[p1]<=a[p2]){
                temp[p3++]=a[p1++];
            }else {
                temp[p3++]=a[p2++];
            }
        }

        while(p1<=center){
            temp[p3++]=a[p1++];
        }

        while (p2<=right){
            temp[p3++]=a[p2++];
        }
        for(int i=left;i<=right;i++){
            a[i]=temp[i];
        }
    }

7.6 快速排序

快速排序思路:

  • 如果数组中元素的个数是0或者1,那么返回
  • 在数组中选取任意一个元素v,作为枢纽元
  • 创建三个数组,第一个存放小于枢纽元的集合,第二个存放等于枢纽元的集合,第三个存放大于枢纽元的集合
  • 数组中各个元素与枢纽元比较,添加到相应的集合
  • 堆存放小于枢纽元的集合和大于枢纽元的集合进行递归。
  • 清除待排序数组的元素,按照顺序把上面三个临时集合的数据填充到待排序数组。
    /**
     * 完全按照快速排序思路
     * @param items
     */
    public void quickSort(List<Integer> items){
        if(items.size()>1){
            List<Integer> smallList=new ArrayList<>();
            List<Integer> sameList=new ArrayList<>();
            List<Integer> largeList=new ArrayList<>();
            Integer pivot=items.get(items.size()>>1);
            for (Integer i:items){
                if(i<pivot){
                    smallList.add(i);
                }else if(i>pivot){
                    largeList.add(i);
                }else {
                    sameList.add(i);
                }
            }

            quickSort(smallList);
            quickSort(largeList);
            items.clear();
            items.addAll(smallList);
            items.addAll(sameList);
            items.addAll(largeList);

        }
    }

枢纽元的选择:

1、一种错误的选择方法是将第一个元素作为枢纽元,这是因为假如输入是预排序的或者是反序的,所有的元素不是划分给集合1就是划分给集合2.

2、安全的做法是随机选择枢纽元,因为随机的枢纽元不可能总在接连不断的产生劣质的分割。但是由于随机数生成的开销很大,很难减少平均运行时间。

3、三数中值分割法:一组N个数的中值,也叫做中位数,是第[N/2]个最大的数。枢纽元的最好选择是数组的中值,但是者很难算出并且会明显的减慢快速排序的速度。这样的中值的估计量可以通过随机选择三个元素并用他们的中值作为枢纽元而得到,事实上,随机性并没有多大的帮助,因此一般的做法是选择左端,右端和中心位置的元素的中值。使用三数中值分割法消除了最坏情形,并且实际减少了14%的比较次数。

参考文献:图解排序算法(五)之快速排序——三数取中法

img


    //************************优化的快排******************************//

    /**
     * 快排驱动程序
     * @param a
     * @param <T>
     */
    public <T extends Comparable<? super T>> void quickSort(T[] a){
        quickSort(a,0,a.length-1);
    }

    /**
     * 快排核心程序
     * @param a
     * @param left
     * @param right
     * @param <T>
     */
    private <T extends Comparable<? super T>> void quickSort(T[] a,int left,int right){
        if(left<right){
            T pivot=getPivot(a,left,right);
            int i=left,j=right-1;
            while (true){
                while (a[++i].compareTo(pivot)<0);//从左向右找到第一个大于或者等于枢纽元的元素
                while (a[--j].compareTo(pivot)>0);//从右往左找到第一个小于枢纽元的元素。
                if(i<j){//交换
                    swap(a,i,j);//交换,如果i<j则没搜完,继续搜下一个交换
                }else {
                    break;//搜完了,这一次调整完成
                }
            }
            swap(a,i,right-1);//这时候i停放的位置的值是大于或者等于枢纽元的,所以,需要把枢纽元和这个值换回来。交换完成之后,i位置左侧小于等于a[i],右侧大于等于a[i]
            quickSort(a,left,i-1);
            quickSort(a,i+1,right);

        }
    }

    /**
     * 执行三数中值分割法,返回三个数的中值,也就是枢纽元,并且把三个中值放到三个数排序的位置上
     * @param a
     * @param left
     * @param right
     * @param <T>
     */
    private <T extends Comparable<? super T>> T getPivot(T[] a,int left,int right){
        int middle=(left+right)>>1;

        //对数组选的三个数排序
        if(a[left].compareTo(a[middle])>0){
            swap(a,left,middle);
        }
        if(a[left].compareTo(a[right])>0){
            swap(a,left,right);
        }
        if(a[middle].compareTo(a[right])>0){
            swap(a,middle,right);
        }

        swap(a,middle,right-1);//放到倒数第二个位置上去

        return a[right-1];

    }

    /**
     * 交换数组元素
     * @param a
     * @param i
     * @param j
     * @param <T>
     */
    private <T extends Comparable<? super T>> void swap(T[] a,int i,int j){
        T temp=a[i];
        a[i]=a[j];
        a[j]=temp;
    }


发布了28 篇原创文章 · 获赞 9 · 访问量 2426

猜你喜欢

转载自blog.csdn.net/qq_23594799/article/details/102643262