排序算法之希尔归并堆和基数排序

Posted haizai

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排序算法之希尔归并堆和基数排序相关的知识,希望对你有一定的参考价值。

//希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本.但希尔排序是非稳定排序算法.
    希尔排序是基于插入排序的以下两点性质而提出改进方法的  :
        1. 插入排序在对几乎已经排好序的数据操作时,效率高,既可以达到线性排序的效率
        2. 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
    
    希尔排序的基本思想是 : 先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,
    再堆全体记录进行依次直接插入排序.
    
    算法步骤 :
        1: 选择一个增量序列t1,t2,,,tk,其中ti > tj,tk = 1;
        2: 按增量序列个数k,对序列进行k趟排序.
        3: 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m的子序列,分别对各个表进行直接插入排序.仅增量因为为1时,
        整个序列作为一个表来处理,表长度度即为整个序列的长度.

    package com.baidurunnable;

    import java.util.Arrays;

    public class Demo5 {
        //希尔排序
        public static void main(String[] args) {
            int[] a = {1,4,3,5,2,7,6,8,9,0};
            quickSork(a);
            System.err.println(Arrays.toString(a));
        }

        private static void quickSork(int[] a) {
            int core = a.length/2;
            while(true) {
                for(int i = 0; i < core ;i++) {
                    for(int j = i;j+core < a.length  ; j+= core) {
                        int temp; //j = 0,core = 5  第二次 : j=5
                        if(a[j] > a[j+core]) {
                            temp = a[j];
                            a[j] = a[j+core];
                            a[j+core] = temp;
                        }
                        
                    }
                }
                if(core == 1) {
                    break;
                } 
                core--;
            }
        }
        
    }

归并排序(Mergo sort) : 是建立在归并操作上的一种有效的排序算法. 该算法是采用分治法(Divide and Conquer)的一个非常典型的应用.
    算法步骤 :
        1: 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列.
        2: 设定两个指针,最初位置分别为两个已经排序序列的起始位置.
        3: 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一个位置.
        4: 重复步骤3直到某一指针达到序列尾.
        5: 将另一序列剩下的元素直接复制到合并序列尾.
    
    package cn.baidu.client;

import java.util.Arrays;

public class DemoTest {
    
    public static void main(String[] args) {
        int[] arr = {9,2,4,5,3,1,6,7,8};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    private static void sort(int[] arr) {
        //在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间.
        int[] temp = new int[arr.length];
        sort(arr,0,arr.length-1,temp);
    }

    private static void sort(int[] arr, int left, int right, int[] temp) {
        if(left < right) {
            int mid = (left + right)/2;
            //左边归并排序,使得左子序有序
            sort(arr,left,mid,temp);
            //右边归并排序,使得右子序列有序
            sort(arr,mid+1,right,temp);
            //将两个有序子数组合并操作
            merge(arr,left,mid,right,temp);
            
        }
    }

    private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        //左序列指针
        int i = left;
        //右序列指针
        int j = mid + 1;
        //临时数组指针
        int t = 0;
        while(i <= mid && j <= right) {
            if(arr[i] <= arr[j]) {
                temp[t++] = arr[i++];
            }else {
                temp[t++] = arr[j++];
            }
        }
        
        
        //将左边剩余元素填充进temp中
        while(i <= mid) {
            temp[t++] = arr[i++];
        }
        //将右序列剩余元素填充进temp中
        while(j <= right) {
            temp[t++] = arr[j++];
        }
        
        t=0;
        //将temp中的元素全部拷贝到原数组中
        while(left <= right) {
            arr[left++] = temp[t++];
        }
    }
    
}

        
        
    基本思想
    堆排序是一种树形选择排序,是对直接选择排序的改进。
     
    首先,我们来看看什么是堆(heap):
    (1)堆中某个节点的值总是不大于或不小于其父节点的值;
    (2)堆总是一棵完全二叉树(Complete Binary Tree)。
     完全二叉树是由满二叉树(Full Binary Tree)而引出来的。除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树称为满二叉树。
    如果除最后一层外,每一层上的节点数均达到最大值;在最后一层上只缺少右边的若干结点,这样的二叉树被称为完全二叉树。

    一棵完全二叉树,如果某个节点的值总是不小于其父节点的值,则根节点的关键字是所有节点关键字中最小的,称为小根堆(小顶堆);如果某个节点的值总是不大于其父节点的值,则根节点的关键字是所有节点关键字中最大的,称为大根堆(大顶堆)。
    从根节点开始,按照每层从左到右的顺序对堆的节点进行编号:

    可以发现,如果某个节点的编号为i,则它的子节点的编号分别为:2i、2i+1。据此,推出堆的数学定义:
    具有n个元素的序列(k1,k2,...,kn),当且仅当满足

    时称之为堆。
    需要注意的是,堆只对父子节点做了约束,并没有对兄弟节点做任何约束,左子节点与右子节点没有必然的大小关系。
     
    如果用数组存储堆中的数据,逻辑结构与存储结构如下:

     
    初始时把要排序的n个数看作是一棵顺序存储的完全二叉树,调整它们的存储顺序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依次类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。这个过程就称为堆排序。
     
    写代码之前,我们要解决一个问题:如何将一个不是堆的完全二叉树调整为堆。
    例如我们要将这样一个无序序列:
    49,38,65,97,76,13,27,49
    建成堆,将它直接映射成二叉树,结果如下图的(a):

    (a)是一个完全二叉树,但不是堆。我们将它调整为小顶堆。
    堆有一个性质是:堆的每个子树也是堆。
    调整的核心思想就是让树的每棵子树都成为堆,以某节点与它的左子节点、右子节点为操作单位,将三者中最小的元素置于子树的根上。
    (a)中最后一个元素是49,在树中的序号为8,对应的数组下标则为7,它的父节点对应的数组下标为3(如果一个元素对应的存储数组的下标为i,则它的父节点对应的存储数组的下标为(i-1)/2),49小于97,所以两者交换位置。
    此时,以第三层元素为根节点的所有子树都已是堆了,下一步继续调整以第二层元素为根节点的子树。
    先调整以65为根的子树,再调整以38为根的子树(满足堆的要求,实际上不用调整)。
    然后调整以第一层元素为根的子树,即以49为根,以38为左子节点,以13为右子节点的子树,交换13与49的位置。
    一旦交换位置,就有可能影响本来已经是堆的子树。13与49交换位置之后,破坏了右子树,将焦点转移到49上面来,继续调整以它为根节点的子树。如果此次调整又影响了下一层的子树,继续调整,直至叶子节点。
    以上就是由数组建堆的过程。
     
    堆建好之后开始排序,堆顶就是最小值,取出放入数组中的最后一个位置,将堆底(数组中的最后一个元素)放入堆顶。这一操作会破坏堆,需要将前n-1个元素调整成堆。
    然后再取出堆顶,放入数组的倒数第二个位置,堆底(数组中的倒数第二个元素)放入堆顶,再将前n-2个元素调整成堆。
    按照上面的思路循环操作,最终就会将数组中的元素按降序的顺序排列完毕。
     
    如果想要升序排列,利用大顶堆进行类似的操作即可。下面的java实现就是使用大顶堆完成的。

    堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
    堆排序的平均时间复杂度为Ο(nlogn) 。

    算法步骤:
    1)创建一个堆H[0..n-1]
    2)把堆首(最大值)和堆尾互换
    3)把堆的尺寸缩小1,并调用shift_down(0),目的是把新的数组顶端数据调整到相应位置
    4) 重复步骤2,直到堆的尺寸为1
    
    java实现
    [java] view plain copy print?
    //堆排序  
          public void heapSort(){  
                  
                 buildHeap();  
                 System.out.println("建堆:");  
                 printTree(array.length);  
                  
                 int lastIndex = array.length-1;  
                 while(lastIndex>0){  
                        swap(0,lastIndex);  //取出堆顶元素,将堆底放入堆顶。其实就是交换下标为0与lastIndex的数据  
                        if(--lastIndex == 0) break;  //只有一个元素时就不用调整堆了,排序结束  
                        adjustHeap(0,lastIndex);  //调整堆  
                         
                        System.out.println("调整堆:");  
                        printTree(lastIndex+1);  
                 }  
                  
          }  
           
          /** 
           * 用数组中的元素建堆 
           */  
          private void buildHeap(){  
                 int lastIndex = array.length-1;  
                 for(inti= (lastIndex-1)/2;i>=0;i--){ //(lastIndex-1)/2就是最后一个元素的根节点的下标,依次调整每棵子树  
                        adjustHeap(i,lastIndex);  //调整以下标i的元素为根的子树                    
                 }  
          }  
           
          /** 
           * 调整以下标是rootIndex的元素为根的子树 
           *@param rootIndex 根的下标 
           *@param lastIndex 堆中最后一个元素的下标 
           */  
          private void adjustHeap(int rootIndex,intlastIndex){  
                  
                 int biggerIndex = rootIndex;   
                 int leftChildIndex = 2*rootIndex+1;  
                 int rightChildIndex = 2*rootIndex+2;  
                  
                 if(rightChildIndex<=lastIndex){  //存在右子节点,则必存在左子节点  
                         
                        if(array[rootIndex]<array[leftChildIndex] || array[rootIndex]<array[rightChildIndex]){ //子节点中存在比根更大的元素  
                         biggerIndex = array[leftChildIndex]<array[rightChildIndex] ? rightChildIndex :leftChildIndex;   
                        }  
                         
                 }else if(leftChildIndex<=lastIndex){  //只存在左子节点  
                         
                        if(array[leftChildIndex]>array[rootIndex]){  //左子节点更大  
                               biggerIndex = leftChildIndex;  
                        }  
                 }  
                  
                 if(biggerIndex != rootIndex){  //找到了比根更大的子节点  
                         
                        swap(rootIndex,biggerIndex);  
                         
                        //交换位置后可能会破坏子树,将焦点转向交换了位置的子节点,调整以它为根的子树  
                        adjustHeap(biggerIndex,lastIndex);  
                 }  
          }  
           
          /** 
           * 将数组按照完全二叉树的形式打印出来 
           */  
          private void printTree(int len){  
       
                 int layers = (int)Math.floor(Math.log((double)len)/Math.log((double)2))+1;  //树的层数  
                 int maxWidth = (int)Math.pow(2,layers)-1;  //树的最大宽度  
                 int endSpacing = maxWidth;  
                 int spacing;  
                 int numberOfThisLayer;  
                 for(int i=1;i<=layers;i++){  //从第一层开始,逐层打印  
                        endSpacing = endSpacing/2;  //每层打印之前需要打印的空格数  
                        spacing = 2*endSpacing+1;  //元素之间应该打印的空格数  
                        numberOfThisLayer = (int)Math.pow(2, i-1);  //该层要打印的元素总数  
                         
                        int j;  
                        for(j=0;j<endSpacing;j++){  
                               System.out.print("  ");  
                        }  
                         
                        int beginIndex = (int)Math.pow(2,i-1)-1;  //该层第一个元素对应的数组下标  
                        for(j=1;j<=numberOfThisLayer;j++){  
                               System.out.print(array[beginIndex++]+"");  
                               for(intk=0;k<spacing;k++){  //打印元素之间的空格  
                                      System.out.print("  ");  
                               }  
                               if(beginIndex == len){  //已打印到最后一个元素  
                                      break;  
                               }  
                        }  
                         
                        System.out.println();  
                 }  
                 System.out.println();   
          }      
            
    第二种实现方式 :
            package com.dn.sort;


        public class HeapSort {
        //堆排序
            public static void main(String[] args){
                int[] array = {39,44,1,0,8,66,23,67,9,15,100,70,22,3,6,54};
                HeapSort heapSort = new HeapSort();
                heapSort.heapSort(array);
                for(int i = 0;i<array.length;i++){
                    System.out.println(" "+array[i]);
                }
            }
            
            public void heapSort(int [] a){
                if(a == null||a.length<=1){
                    return;
                }
                //创建大堆
                buildMaxHeap(a);
                for(int i = a.length-1;i>=1;i--){
                    //最大元素已经排在了下标为0的地方
                    exchangeElements(a, 0, i);//每交换换一次,就沉淀一个大元素
                    maxHeap(a, i, 0);
                }
            }

            
            private void buildMaxHeap(int[] a) {
                int half = (a.length -1)/2;//假设长度为9
                for(int i = half;i>=0;i--){
                    //只需遍历43210
                    maxHeap(a,a.length,i);
                }
            }

            //length表示用于构造大堆的数组长度元素数量
            private void maxHeap(int[] a, int length, int i) {
                int left = i*2+1;
                int right = i*2+2;
                int largest = i;
                if(left<length&&a[left]>a[i]){
                    largest = left;
                }
                if(right<length&&a[right]>a[largest]){
                    largest = right;
                }
                if(i!=largest){
                    //进行数据交换
                    exchangeElements(a,i,largest);
                    maxHeap(a, length, largest);
                }
            }

            //在数组a里进行两个下标元素交换
            private void exchangeElements(int[] a, int i, int largest) {
                int temp = a[i];
                a[i] = a[largest];
                a[largest] = temp;
            }
        }

    基数排序
    基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
    说基数排序之前,我们简单介绍桶排序:

    算法思想:是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。

    简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。

    例如要对大小为[1..1000]范围内的n个整数A[1..n]排序
    首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集合B[2]存储 (10..20]的整数,……集合B存储( (i-1)*10, i*10]的整数,i = 1,2,..100。总共有 100个桶。

    然后,对A[1..n]从头到尾扫描一遍,把每个A放入对应的桶B[j]中。 再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任 何排序法都可以。

    最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这 样就得到所有数字排好序的一个序列了。

    假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果对每个桶中的数字采用快速排序,那么整个算法的复杂度是
    O(n + m * n/m*log(n/m)) = O(n + nlogn – nlogm)

    从上式看出,当m接近n的时候,桶排序复杂度接近O(n)。当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的 ,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。

    前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是:
    1)首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。
    2)其次待排序的元素都要在一定的范围内等等。

 

以上是关于排序算法之希尔归并堆和基数排序的主要内容,如果未能解决你的问题,请参考以下文章

排序---内部排序算法(快排希尔排序归并排序基数排序冒泡选择排序)比较

Python八大算法的实现,插入排序希尔排序冒泡排序快速排序直接选择排序堆排序归并排序基数排序。

基本排序算法(冒泡排序 选择排序 插入排序 快速排序 归并排序 基数排序 希尔排序)

算法排序算法之希尔排序

算法排序算法之基数排序

插入排序(直接插入排序希尔排序);交换排序(冒泡排序快速排序);选择排序(简单选择排序堆排序);归并排序和基数排序;基于关键词比较的排序算法下界分析