JAVA数据结构和算法:第八章(排序)

#排序

排序是我们程序中经常面对的问题,那么排序的严格定义是什么呢?

假设含有n个记录的序列为{r1,r2,r3…..,rn},其对应的关键字分别为{k1,k2,k3…..,kn},需确定1,2,…..,n的一种排列p1,p2,……..,pn,使其相应的关键字满足Kp1<=Kp2…….<=Kp2(非递减或非递增)关系,即使得序列成为一个按关键字有序的序列{rp1,rp2……rpn},这样的操作就称为排序。

内排序和外排序

根据在排序过程中待排序的记录是否全部被放置在内存中,将其分为:内排序和外排序。

内排序是在排序的整个过程中,待排序的所有记录全部被放置在内存中,外排序是由于需要排序的记录太多,不能同时放置咋子内存中,整个排序过程需要在内外存之间多次交换数据才能进行,

对于内排序来说,排序算法的性能主要受以下3个方面影响:

  • (1)时间性能,排序是数据处理中经常执行的操作,因此排序算法的时间性能是衡量其好坏的最重要的标志,内排序中主要进行比较和移动这两种操作。高效率的算法应该尽可能少的进行比较和移动。

  • (2)辅助空间,指的是除了存放待排序数据所占用的存储空间外,执行算法所需要的其他存储空间。

  • (3)算法的复杂性。

冒泡排序

冒泡排序(Bubble Sort)是一种交换排序,基本思想是两两比较相邻的元素,如果他们的顺序错误就把他们交换过来,直到排序完成。这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端

冒泡排序算法的过程如下:

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

  • (2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。

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

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

简单实现

 void sort(int[] a) {
        for(int i=0;i<a.length;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;
                }
            }
        }
    }

这应该是最简单的排序代码了,不过这个代码效率是非常低下的,所以我们需要进行改进。

冒泡排序优化

试想一下,有这么一个数组{2,1,3,4,5,6,7,8},也就是说,除了第一和第二个元素需要交换,其他的已经是正常的顺序了 ,如果我们用上面的算法,毫无疑问它会将每个循环再执行一次,这就耗费了大量的时间,所以我们可以设置一个标志位,当没有任何数据交换时说明已经有序,不需要进行后面的循环操作。

 void bestsort(int[] a) {
        boolean flag=true;
        for(int i=0;i<a.length&&flag;i++) { 
            flag=false;
            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;
                    flag=true;
                }
            }
        }
    }

冒泡排序时间复杂度分析

当最好的情况,也就是排序的数组本身是有序的,那么我们需要比较一轮,也就是n-1次,没有数据交换时间复杂度为O(n),最坏的情况,也就是排序的数组为逆序时,此时需要比较n-1+n-2+……2+1次,也就是n(n-1)/2,并且还需要移动,此时时间复杂度为O(n^2).这时候我们就知道了冒泡排序是一种效率多么低下的算法,尽管有很多人对它进行各种各样的优化,但是排序的特性在这里,性能依然大大相差于其他算法。

选择排序

选择排序的思想是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

简单选择排序

    public static void selectsort(int[] arr) {
        for (int i=0;i<arr.length;i++) {
            int minindex=i;
            for(int j=i+1;j<arr.length;j++) {
                if(arr[minindex]>arr[j]) {
                    minindex=j;
                }
            }
            if(i!=minindex) {
                int temp=arr[i];
                arr[i]=arr[minindex];
                arr[minindex]=temp;
            }
        }
    } 

简单选择排序时间复杂度分析

从算法上来看,选择排序交换移动次数相当少,分析时间复杂度,无论是最好还是最坏情况,比较次数都是一样的多,为n(n-1)/2次,而交换次数最好情况为0,最坏情况逆序为n-1次,因此总的来看,选择排序的时间复杂度为O(n^2),虽然说和冒泡排序同为O(n^2),但是选择排序的性能还是要优于冒泡排序。

插入排序

插入排序的思想是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数加一的有序表。

 void insertSort(int[] arr) {
        for(int i=1;i<arr.length;i++) {
            for(int j=i;j>0;j--) {
                if(arr[j]<arr[j-1]) {
                    int temp=arr[j];
                    arr[j]=arr[j-1];
                    arr[j-1]=temp;
                }else{
                    break;
                }
            }
        }
    }

插入排序时间复杂度分析

当最好的情况,也就是要排序的表本身就有序时,那么只需要比较n-1次,此时时间复杂度为O(n),当最坏情况发生时,即待排序的数组为逆序,此时需要比较1+2+3+。。。(n-1)次,时间复杂度为O(n^2),但是同样为O(n^2),插入排序的性能要比冒泡和选择排序性能要好。

希尔排序

希尔排序是D.L.Shell发明的一种排序算法,在这之前的排序算法的时间复杂度基本都是O(n^2),希尔排序是突破这个时间复杂度的第一批算法之一,也被称为缩减增量排序。

基本思想是先将整个待排序的记录序列分割成为若干子序列(由相隔某个“增量”的元素组成)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。所谓的基本有序,就是小的关键字基本在前面,大的基本在后面,例如{2,1,3,6,4,7,5,8,9}就可以称为基本有序,但{1,5,9,3,7,8,2,4,6}这样的9在第三位,2很靠后,就谈不上基本有序。

我们来举一个例子来更好的理解一下希尔排序。

(1)我们有一个数组为{49,38,65,97,26,13,27,49,55,4},长度为10,我们设增量为数组长度/2,所以这里增量为5

(2) 然后按照增量将整个元素序列分为子序列,这里分为{49,13},{38,27},{65,49},{97,55},{26,4},然后对其进行直接排序为{13,49},{27,38},{49,65},{55,97},{4,26},第一次排序后即为13 ,27, 49 ,55 , 4, 49,38,65,97 ,26

(3)然后我们缩减增量,缩减规律为当前增量/2,即5/2=2,然后再分为子序列{13,49,4,38,97},{27,55,49,65,26},然后继续直接插入排序,以此类推

(4)当增量缩减到0时,则排序完成得到数组。

代码实现

   void shellSort(int[] arr) { 
            //确定增量,这里使用(数组长度/2),并且每次/2
            for(int gap=arr.length/2;gap>0;gap/=2) { 
                //分组进行交换
                for(int i=gap;i<arr.length;i++) {
                    for(int j=i-gap;j>=0&&arr[j]>arr[j+gap];j-=gap) {
                        int temp=arr[j];
                        arr[j]=arr[j+gap];
                        arr[j+gap]=temp;
                    }
                }
            }
        }

通过我们的分析,大家应该明白,希尔排序的关键就是增量,将相隔某个增量的数据组成一个子序列,形成跳跃式的移动,使得移动次数变少,效率变高。这里的增量的选取非常关键,可究竟选取什么增量才是最好?目前还是一个数学难题,到现在位置还没有找到一种最好的增量,不过大量的研究表明,当增量为 dlta[k]=2^(t-k+1)-1(0<=k<=t<=[log2(n+1)])时,可以有很不错的效率,时间复杂度为O(n^(3/2)),效率相比前面几种有了大大的提高,不过因为是跳跃式移动,希尔排序并不是一种稳定的排序算法。

堆排序

我们先来了解一下堆这种数据结构,堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。下图左为大顶堆,右为小顶堆

这里写图片描述

如果给结点按照层序遍历方式编号,则结点之间满足如下关系

这里写图片描述

堆排序就是利用堆进行排序的方法。基本思想是,将待排序的序列构造成一个大顶堆(或小顶堆),此时整个序列的最大值就是根结点,将其移除输出,然后将剩下的元素再构成一个大顶堆,继续输出最大的元素,反复执行,便能得到一个有序序列。

这里我们就需要解决两个最关键的问题

  • 1.如何将n个待排序元素构建成堆?
  • 2.输出堆顶元素后,如何调整剩余元素使其成为一个新的堆?

我们先来看第一个问题,我们曾经学习过一个结论,对于完全二叉树而言,第n个元素的双亲节点是n/2,也就是说最后一个结点n是n/2的子树,然后我们就从n/2结点开始排序,使其子树成为堆,然后从n/2以此向前对每一个有子树的根结点进行排序,使其成为堆,直到根结点。

然后我们来看第二个问题,如果有n个元素的堆,输出堆顶元素后,我们将堆底元素放在堆顶,然后再与其左右子树进行比较交换构建堆,这样到比较完排序过程也就完成了。

代码实现

public class HeapSort {


    public static void main(String[] args) {
        int[] sort = new int[] { 1, 0, 10, 20, 3, 5, 6, 4, 9, 8, 12, 17, 34, 11 };
        heapSort(sort);
        for(int i:sort) {
            System.out.print(i+" ");
        }
    }

    //堆排序
    private static void heapSort(int[] data) { 
        //先将当前数组转换为最大堆
        buildMaxHeapify(data);
        // 末尾与头交换,然后将剩下的元素构建最大堆
        for (int i = data.length - 1; i > 0; i--) {
            int temp = data[0];
            data[0] = data[i];
            data[i] = temp;
            maxHeapify(data, i, 0);
        }
    }

    //构建最大堆
    private static void buildMaxHeapify(int[] data) { 
        //从最后一个具有子树的结点开始构建
        for (int i = data.length/2; i >= 0; i--) {
            maxHeapify(data, data.length, i);
        }
    }

    /**
    *创建最大堆
    * data为数组,heapsize为数组大小,index为当前根结点
    **/
    private static void maxHeapify(int[] data, int heapSize, int index) {
        // 获取当前结点的左右孩子结点
        int left =index*2;
        int right = index*2+1; 

        //与左右结点判断,获取最大的
        int largest = index;
        if (left < heapSize && data[index] < data[left]) {
            largest = left;
        }
        if (right < heapSize && data[largest] < data[right]) {
            largest = right;
        }
        // 得到最大值后可能需要交换,如果交换了,其子节点可能就不是最大堆了,所以还需要递归调整其子结点
        if (largest != index) {
            int temp = data[index];
            data[index] = data[largest];
            data[largest] = temp;
            maxHeapify(data, heapSize, largest);
        }
    }
}

堆排序的运行时间主要用在初始化构建堆和重建堆的反复筛选上,堆排序的时间复杂度为O(nlogn),这在性能上显然要远远好于冒泡、简单选择、直接插入的O(n^2)的时间复杂度。有兴趣的可以去看一下堆排序的数学计算时间复杂度的过程

归并排序

归并排序就是利用归并的思想实现排序。基本思想是假设初始序列有n个记录,可以将其看成n个子序列,然后两两归并,得到n/2个有序子序列,然后继续两两归并,直到最后得到一个长度为n的有序序列。这种排序方法称为2路归并排序。

这里写图片描述

对于给定一个无序的数组,我们需要先将其拆分为一个个有序子序列,然后再进行合并。

这里写图片描述

我们先来看看如何将两个有序序列进行合并,这个非常简单,只要以此比较两个序列中的数,谁小就取谁,如果有一个序列空了,那么就依次把剩下序列中数放入即可。那么如何使一个无序序列分解为有序子序列呢,我们可以将当前序列不断的进行分解,当分出来的子序列只有一个数据时,可以认为这个子序列已经达到了有序,然后再合并相邻的二个子序列就可以了

代码实现

public class MergeSort {

      public static void main(String[] args) {
            int []array= {9,8,7,6,5,4,23,2,1,0};
            mergeSort(array);
            for (int i = 0; i < array.length; ++i) {
                System.out.print(array[i] + " ");
            }
        } 

    public static void mergeSort(int[] array) { 
        //创建一个辅助数组来方便我们进行合并
        int[] temp=new int[array.length]; 
        //将整个数组分解
        mergesort(array,0,array.length-1,temp);
    }

    //将整个无序数组分解为一个个的单元素,然后合并,参数为无序数组、数组的起始位置、数组的结束位置、辅助数组
    static void mergesort(int a[], int first, int last, int temp[])  
    {  
        //如果起始位置小于结束位置,说明未完全分解,则继续递归
        if (first < last)  
        {  
            //获取中间位置,进行分解
            int mid = (first + last) / 2;  
            //对左边数组进行分解
            mergesort(a, first, mid, temp);    
            //对右边数组进行分解
            mergesort(a, mid + 1, last, temp); 
            //分解完毕后,进行合并
            mergearray(a, first, mid+1, last, temp); //再将二个有序数列合并  
        }  
    }  

    //firstindex为第一个数组起始位置,secondindex为第二个数组起始位置,last为第二个数组结束位置
    static void mergearray(int a[], int firstindex, int secondindex, int last, int temp[])  
    {  
        //获取第一个数组的结束位置
        int firstend=secondindex-1; 
        int tmppos=firstindex;
        //获取当前合并的元素个数
        int numbers=last-firstindex+1;

        //进行比较
        while (firstindex <= firstend && secondindex <= last)  
        {  
            if (a[firstindex] <= a[secondindex])  
                temp[tmppos++] = a[firstindex++];  
            else  
                temp[tmppos++] = a[secondindex++];  
        }  

        //如果某个数组为空了,则将另一个数组剩下的元素依次放入数组  
        while (firstindex <= firstend)  
            temp[tmppos++] = a[firstindex++];  

        while (secondindex <= last)  
            temp[tmppos++] = a[secondindex++];  


         //将我们的辅助数组复制到原数组中,要从后向前复制
        for (int i = 0; i < numbers; i++,last--)  
            a[last] = temp[last];  
    }   
}

归并排序时间复杂度分析

归并排序的时间复杂度为O(nlogn),因为归并排序每次都是在相邻的数据中进行操作,所以归并排序在O(N*logN)的几种排序方法(快速排序,归并排序,希尔排序,堆排序)也是效率比较高的。

快速排序

快速排序的基本思想是:选择一个基准元素(称为枢纽元),通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比基准元素小,另一部分元素都比基准元素大,这时候枢纽元元素的位置就已经确定了,然后再分别对这两部分记录继续进行排序,直到左右两部分只有一个数时结束,以达到整个序列有序的目的。

public class QuickSort {

        public static void main(String[] args) {
            int [] arr= {74,200,200,74,57,1023,1,85,32,57,99}; 
            //调用快速排序
            quickSort(arr);
            for(int a:arr) {
                System.out.print(a+"  ");
            }
        }

        public static void quickSort(int[] array) {
            quickSort(array,0,array.length-1);
        }

        public static void quickSort(int[] array,int low,int high) {
            int pivot;
            if(low<high) { 
                //得到枢纽元的位置,这时候枢纽元已经固定了在数组中的位置
                pivot=partition(array,low,high);
                //对左侧子序列进行快排
                quickSort(array,low,pivot-1);
                //对右侧子序列进行快排
                quickSort(array,pivot+1,high);
            }
        }

        //选取枢纽元,然后把它放入固定的位置,使其左侧元素都小于它,右侧元素都大于它
        public static int partition(int[] array,int low, int high) {
            //选取第一个元素为枢纽元
            int pivotkey=array[low];
            while(low<high) { 
                //当右边元素小于枢纽元时跳出循环,与左边元素交换位置
                while(low<high&&array[high]>=pivotkey) {
                    high--;
                }
                swap(array,low,high);
                //当左边元素大于枢纽元时跳出循环,与右边元素交换位置
                while(low<high&&array[low]<=pivotkey) {
                    low++;
                }
                swap(array,low,high);
            }
            //返回当前枢纽元确定的位置
            return low;
        }

        public static void swap(int[] array,int low,int high) {
            int temp=array[low];
            array[low]=array[high];
            array[high]=temp;
        }


}

枢纽元优化

我们前面的枢纽元直接选取了第一个元素,这是一种非常蠢的做法,如果我们的数组是反序的,那么就会产生一个特别差的分割效果,所有元素都被分割到一侧,时间效率是二次的,极其差劲,所以我们需要改变一下。有人提出了一种方法就是随机选取枢纽元,但是也不是最好的选择,因为随机数生成也花费不少的时间性能。于是就有了我们要使用的三数取中法。

三数取中法即取三个关键字先进行排序,将中间数作为枢纽元。一般是选左端、右端和中间三个数。这样这个中间数一定不会是最小或者最大的数。

我们只需要在取枢纽元的前面加上如下代码 

    public static int partition(int[] array,int low, int high) { 
            //交换之后,array[low]位置上就是我们的中间值
            int pivotkey;
            int mid=low+(high-low)/2;
            if(array[low]>array[high]) {
                swap(array,low,high);
            }

            if(array[mid]>array[high] ) {
                swap(array,high,mid);
            }
            if(array[mid]>array[low] ) {
                swap(array,mid,low);
            }
            pivotkey=array[low];
            while(low<high) {
                while(low<high&&array[high]>=pivotkey) {
                    high--;
                }
                swap(array,low,high);
                while(low<high&&array[low]<=pivotkey) {
                    low++;
                }
                swap(array,low,high);
            }
            return low;
        }

小数组优化
对于很小的数组,快速排序的效率是不如插入排序的。所以我们增加一个判断,当high-low不大于某个常数,有研究说是7比较合适,就使用插入排序。

        public static void quickSort(int[] array,int low,int high) {
            if(high-low>7) {
                int pivot;

                    pivot=partition(array,low,high);
                    quickSort(array,low,pivot-1);
                    quickSort(array,pivot+1,high);

            }else {
                InsertSort.insertSort(array);
            }
        }

当然快速排序还有很多种的更加细致的优化,有兴趣可以去了解一下。

这里写图片描述

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值