算法与数据结构(JAVA)-排序算法总结

来源:互联网 发布:下载51vv软件 编辑:程序博客网 时间:2024/05/22 10:32

冒泡排序

排序思想:通过与相邻元素的比较和交换来把小的数交换到最前面。这个过程类似于水泡向上升一样,因此而得名。

例如,对5,3,8,6,4这个无序序列进行冒泡排序。首先从后向前冒泡,4和6比较,把4交换到前面,序列变成5,3,8,4,6。同理4和8交换,变成5,3,4,8,6,3和4无需交换。5和3交换,变成3,5,4,8,6,3.这样一次冒泡就完了,把最小的数3排到最前面了。对剩下的序列依次冒泡就会得到一个有序序列。

冒泡排序的时间复杂度为O(n^2)。

/** *@Description:<p>冒泡排序算法实现</p> *@author FinMo *@time 2016-4-25 下午20:00 */public class BubbleSort{    public static void bubbleSort(int[] arr){        if(arr == NULL||arr.length == 0){            return ;        }        for(int i = 0;i < arr.length - 1;i++){            for(int j = arr.length - 1;j > i;j--){                if(arr[j] < arr[j - 1]){                    swap(arr,j - 1,j);                }            }        }    }    public static void swap(int[] arr, int i, int j){        int temp = arr[i];        arr[i] = arr[j];        arr[j] = temp;    }}

选择排序

排序思想:和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择。

例如,对5,3,8,6,4这个无序序列进行简单选择排序,首先要选择5以外的最小数来和5交换,也就是选择3和5交换,一次排序后就变成了3,5,8,6,4.对剩下的序列一次进行选择和交换,最终就会得到一个有序序列。其实选择排序可以看成冒泡排序的优化,因为其目的相同,只是选择排序只有在确定了最小数的前提下才进行交换,大大减少了交换的次数。

选择排序的时间复杂度为O(n^2)。

/** *@Description:<p>冒泡排序算法实现</p> *@author MoFa *@time 2016-4-25 下午20:00 */public class SelectSort{    public static void selectSort(int[] arr){        if(arr == NULL||arr.length == 0){            return ;        }        int minIndex = 0;        for(int i = 0;i < arr.length - 1;i++){ //只需要比较n-1次            minIndex = i;            for(int j = i + 1;j < arr.length;j++){//从i+1开始比较,因为minIndex默认为i了,i就没有必要比了                if(arr[j] < arr[minIndex]){                    minIndex = j;                }            }            if(minIndex != i){ //如果minIndex不为i,说明找到了更小的值,交换之。                swap(arr, i, minIndex);            }        }    }    public static void swap(int[] arr, int i,int j){        int temp = arr[i];        arr[i] = arr[j];        arr[j] = temp;    }}

插入排序

算法思想:插入排序不是通过交换位置而是通过比较找到合适的位置插入元素来达到排序的目的的。相信大家都有过打扑克牌的经历,特别是牌数较大的。在分牌时可能要整理自己的牌,牌多的时候怎么整理呢?就是拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是一样的。

例如,对5,3,8,6,4这个无序序列进行简单插入排序,首先假设第一个数的位置时正确的,想一下在拿到第一张牌的时候,没必要整理。然后3要插到5前面,把5后移一位,变成3,5,8,6,4.想一下整理牌的时候应该也是这样吧。然后8不用动,6插在8前面,8后移一位,4插在5前面,从5开始都向后移一位。注意在插入一个数的时候要保证这个数前面的数已经有序。

插入排序的时间复杂度也是O(n^2)。

/** *@Description:<p>冒泡排序算法实现</p> *@author MoFa *@time 2016-4-25 下午20:00 */public class InsertSort{    public static void insertSort(int[] arr){        if(arr == NULL||arr.length == 0){            return ;        }        for(int i = 1;i < arr.length;i++){//假设第一个数位置正确            int j = i;            int target = arr[i];//待插入的数            //后移                while(j > 0&&target < arr[j - 1]){                arr[j] = arr[j - 1];                j--;            }            //插入            arr[j] = target;        }    }}

快速排序

算法思想:在实际应用中快速排序是表现最好的排序算法。其思想来自冒泡排序,排序是通过相邻元素的比较和交换把最小的冒泡到最顶端,而快速排序是比较和交换小数和小数,这样一来不仅把小数冒泡到上面同时也把大数沉到下面。

例如:对5,3,8,6,4这个无序序列进行快速排序,思路是右指针找比基数小的,左指针找比基准数大的,交换之。

5,3,8,6,4 用5作为比较的基准,最终会把5小的移动到5的左边,比5大的移动到5的右边。

5,3,8,6,4 首先设置i,j两个指针分别指向两端,j指针先扫描(*)4比5小停止。然后i扫描,8比5大停止。交换i,j位置。

5,3,4,6,8 然后j指针再扫描,这时j扫描4时两指针相遇。停止。然后交换4和基准数。

4,3,5,6,8 一次划分后达到了左边比5小,右边比5大的目的。之后对左右子序列递归排序,最终得到有序序列。

(*)为什么一定要j指针先动呢?首先这也不是绝对的,这取决于基准数的位置,因为在最后两个指针相遇的时候,要交换基准数到相遇的位置。一般选取第一个数作为基准数,那么就在左边,所有最后相遇的数和基准数交换,那么相遇的数一定要比基准数小。所以j指针先移动才能先找到比基准数小的数。

快速排序是不稳定的,其时间平均时间复杂度是O(nlgn)。

/** *@Description:<p>冒泡排序算法实现</p> *@author MoFa *@time 2016-4-25 下午20:00 */public class QuickSort{    //一次划分    public static int partition(int[] arr, int left, int right){        int pivotKey = arr[left];        int pivorPointer = left;        while(left < right){            while(left < right&&arr[right] >= pivotKey){                right--;            }            while(left < right&&arr[left] <=pivotKey){                left++;            }            swap(arr, left, right); //把大的交换到右边,把小的交换到左边        }        swap(arr, pivotPointer, left); //最后把pivot交换到中间        return left;    }    public static void quickSort(int[] arr, int left, int right){        if(left >= right){            return ;        }        int pivotPos = partiton(arr, left, right);        quickSort(arr, left, pivotPos - 1);        quickSort(arr, pivotPos + 1, right);    }    public static void swap(int[] arr, int left, int right){        int temp = arr[i];        arr[i] = arr[j];        arr[j] = temp;    }}

其实上面的代码还可以再优化,上面代码中基准数已经在pivotKey中保存了,所以不需要每次交换都设置一个temp变量,在交换左右指针的时候只需要先后覆盖就可以了。这样既能减少空间的使用还能降低赋值运算的次数。

优化代码如下:

/** *@Description:<p>冒泡排序算法实现</p> *@author MoFa *@time 2016-4-25 下午20:00 */public class QuickSort{    /**     * 划分     * @param arr     * @param left     * @param right     * @return     */     public static int partition(int[] arr, int left, int right){         int pivotKey = arr[left];         while(left < right){             while(left < right&&arr[right] >= pivotKey){                 right--;             }             arr[left] = arr[right]; //把小的移动到左边             while(left < right&&arr[left] <=pivotKey){                 left++;             }             arr[right] = arr[left]; //把大的移动到右边         }         arr[left] = pivotKey; //最后把pivot赋值到中间         return left;     }    /**     * 递归划分子序列     * @param arr     * @param left     * @param right     */     public static void quickSort(int[] arr, int left, int right){        if(left >= right){            return ;        }        int pivotPos = partiton(arr, left, right);        quickSort(arr, left, pivotPos - 1);        quickSort(arr, pivotPos + 1, right);    }    public static void sort(int[] arr){        if(arr == null||arr.lengh == 0){            return ;        }        quickSort(arr, 0, arr.length - 1);    } }

总结:快速排序的思想:冒泡+二分+递归分治。

堆排序

排序思想:借助堆来实现的选择排序,思想同选择排序。

(以下以大顶堆为例)

注意:如果想升序排序就使用大顶堆,反之使用小顶堆。原因是堆顶元素需要交换到序列尾部。

首先,实现堆排序需要解决两个问题:

希尔排序

基本思想是:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。

这里写图片描述

希尔排序的特点是,子序列的构成不是简单的逐段分割,而是将某个相隔某个增量的记录组成一个子序列。如上面的例子,第一堂排序时的增量为5,第二趟排序的增量为3。由于前两趟的插入排序中记录的关键字是和同一子序列中的前一个记录的关键字进行比较,因此关键字较小的记录就不是一步一步地向前挪动,而是跳跃式地往前移,从而使得进行最后一趟排序时,整个序列已经做到基本有序,只要作记录的少量比较和移动即可。因此希尔排序的效率要比直接插入排序高。

希尔排序的分析是复杂的,时间复杂度是所取增量的函数,这涉及一些数学上的难题。但是在大量实验的基础上推出当n在某个范围内时,时间复杂度可以达到O(n^1.3)。

/** *@Description:<p>希尔排序算法实现</p> *@author MoFa *@time 2016-5-6 上午10:53:55 */public class ShellSort {    /**     * 希尔排序的一趟插入     * @param arr 待排数组     * @param d 增量     */    public static void shellInsert(int[] arr, int d) {        for(int i=d; i<arr.length; i++) {            int j = i - d;            int temp = arr[i];    //记录要插入的数据              while (j>=0 && arr[j]>temp) {  //从后向前,找到比其小的数的位置                   arr[j+d] = arr[j];    //向后挪动                  j -= d;              }              if (j != i - d)    //存在比其小的数                 arr[j+d] = temp;                    }    }    public static void shellSort(int[] arr) {        if(arr == null || arr.length == 0)            return ;        int d = arr.length / 2;        while(d >= 1) {            shellInsert(arr, d);            d /= 2;        }    }}  

归并排序

归并排序是另一种不同的排序方法,因为归并排序使用了递归分治的思想,所以理解起来比较容易。其基本思想是,先递归划分子问题,然后合并结果。把待排序列看成由两个有序的子序列,然后合并两个子序列,然后把子序列看成由两个有序序列。倒着来看,其实就是先两两合并,然后四四合并。最终形成有序序列。

空间复杂度为O(n),时间复杂度为O(nlogn)。

这里写图片描述

/** *@Description:<p>归并排序算法的实现</p> *@author MoFa *@time 2016-5-6 上午10:53:55 */public class MergeSort {    public static void mergeSort(int[] arr) {        mSort(arr, 0, arr.length-1);    }    /**     * 递归分治     * @param arr 待排数组     * @param left 左指针     * @param right 右指针     */    public static void mSort(int[] arr, int left, int right) {        if(left >= right)            return ;        int mid = (left + right) / 2;        mSort(arr, left, mid); //递归排序左边        mSort(arr, mid+1, right); //递归排序右边        merge(arr, left, mid, right); //合并    }    /**     * 合并两个有序数组     * @param arr 待合并数组     * @param left 左指针     * @param mid 中间指针     * @param right 右指针     */    public static void merge(int[] arr, int left, int mid, int right) {        //[left, mid] [mid+1, right]        int[] temp = new int[right - left + 1]; //中间数组        int i = left;        int j = mid + 1;        int k = 0;        while(i <= mid && j <= right) {            if(arr[i] <= arr[j]) {                temp[k++] = arr[i++];            }            else {                temp[k++] = arr[j++];            }        }        while(i <= mid) {            temp[k++] = arr[i++];        }        while(j <= right) {            temp[k++] = arr[j++];        }        for(int p=0; p<temp.length; p++) {            arr[left + p] = temp[p];        }    }}

总结

在前面的介绍和分析中我们提到了冒泡排序、选择排序、插入排序三种简单的排序及其变种快速排序、堆排序、希尔排序三种比较高效的排序。后面我们又分析了基于分治递归思想的归并排序还有计数排序、桶排序、基数排序三种线性排序。我们可以知道排序算法要么简单有效,要么是利用简单排序的特点加以改进,要么是以空间换取时间在特定情况下的高效排序。但是这些排序方法都不是固定不变的,需要结合具体的需求和场景来选择甚至组合使用。才能达到高效稳定的目的。没有最好的排序,只有最适合的排序。

下面就总结一下排序算法的各自的使用场景和适用场合。

这里写图片描述

1、从平均时间来看,快速排序是效率最高的,但快速排序在最坏情况下的时间性能不如堆排序和归并排序。而后者相比较的结果是,在n较大时归并排序使用时间较少,但使用辅助空间较多。

2、上面说的简单排序包括除希尔排序之外的所有冒泡排序、插入排序、简单选择排序。其中直接插入排序最简单,但序列基本有序或者n较小时,直接插入排序是好的方法,因此常将它和其他的排序方法,如快速排序、归并排序等结合在一起使用。

3、基数排序的时间复杂度也可以写成O(d*n)。因此它最使用于n值很大而关键字较小的的序列。若关键字也很大,而序列中大多数记录的最高关键字均不同,则亦可先按最高关键字不同,将序列分成若干小的子序列,而后进行直接插入排序。

4、从方法的稳定性来比较,基数排序是稳定的内排方法,所有时间复杂度为O(n^2)的简单排序也是稳定的。但是快速排序、堆排序、希尔排序等时间性能较好的排序方法都是不稳定的。稳定性需要根据具体需求选择。

5、上面的算法实现大多数是使用线性存储结构,像插入排序这种算法用链表实现更好,省去了移动元素的时间。具体的存储结构在具体的实现版本中也是不同的。

0 0
原创粉丝点击