排序算法

376 阅读7分钟

冒泡排序、选择排序、插入排序、快速排序、归并排序计数排序的java实现,以及桶排序和基数排序的实现原理。

冒泡排序

冒泡排序,每次都是相邻2个数字比较,
每次都是从第一个数字开始比较,
循环一次之后,队尾的数字总是当前比较队列中最大的,
最好时间复杂度O(1),最坏时间复杂度O(n^2),平均时间复杂度O(n^2)
数据交换次数为原始数据的逆序度
是原地排序
是稳定排序

代码实现很简单,两个for循环,外层for循环控制循环次数,内层for循环控制比较的数据

	private void bubbleSort(int[] array) {

        int length = array.length;

        for (int i = 0; i < length - 1; i++) {
            for (int j = 0; j < length - i - 1; j++) {
                if (array[j] > array[j + 1]) {
                    int temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                }
            }
        }
    }

选择排序

每次从未排序的区域比较出最小的值,放置在未排序区域的第一位
时间复杂度O(n^2)
是原地排序
非稳定排序

	private void selectSort(int[] array) {

        int length = array.length;

        for (int i = 0; i < length - 1; i++) {

            int min = i;

            for (int j = i + 1; j < length; j++) {
                if(array[j] < array[i]){
                    min = j;
                }
            }

            if(min != i){
                int temp = array[i];
                array[i] = array[min];
                array[min] = temp;
            }
        }
    }

插入排序

插入排序
第二位数据开始,每次遍历,从数据的当前位置往前查找需要插入的位置,
遍历过程中碰到的数据,如果比当前数据大,则往后移动一位,
退出的条件是碰到的数据小于等于当前数据,则插在这个数据的后一位,退出循环
最好时间复杂度O(1),最坏时间复杂度O(n^2),平均时间复杂度O(n
数据交换次数为原始数据的逆序度
是原地排序
是稳定排序

	public static int[] insertionSort(int[] nums) {
        if (null == nums || nums.length == 0 || nums.length == 1) {
            return nums;
        }

        int length = nums.length;
        for (int i = 1; i < length; ++i) {
            int x = nums[i];
            int j = i - 1;
            for (; j >= 0; --j) {
                if (nums[j] > x) {
                    nums[j + 1] = nums[j];
                } else {
                    break;
                }
            }

            //不放在break中的原因是,里面的for循环遍历完了,会漏执行
            nums[j + 1] = x;
        }

        return nums;
    }

快速排序

快速排序是对冒泡排序的一种改进,
基本思想是选定一个锚点数据,循环遍历调换数据,遍历结束后,锚点位置改变,
同时锚点左边的所有数据都小于等于锚点数据,锚点有点的数据都大于等于锚点数据。
左右两边依次递归调用,直到遍历完成。
时间复杂度O(nlogn)
是原地排序
非稳定排序

private void quickSort(int[] array) {                                    
                                                                         
    if (array.length == 0) {                                             
        return;                                                          
    }                                                                    
                                                                         
    quickSort(array, 0, array.length - 1);                               
}                                                                        
                                                                         
/**                                                                      
 * 快速排序算法                                                                
 */                                                                      
private void quickSort(int[] array, int low, int high) {                 
                                                                         
    if (low > high) {                                                    
        return;                                                          
    }                                                                    
                                                                         
    int i = low;                                                         
    int j = high;                                                        
    int anchor = array[low];                                             
                                                                         
    //循环遍历,所有比anchor小的数据都放在anchor左边,比anchor大的数据放在右边                      
    while (i < j) {                                                      
                                                                         
        //这里开始从队尾寻找比anchor小的数据的位置                                        
        while (anchor <= array[j] && j > i) {                            
            j--;                                                         
        }                                                                
                                                                         
        //这里开始从队首寻找比anchor大的数据的位置                                        
        while (anchor >= array[i] && i < j) {                            
            i++;                                                         
        }                                                                
                                                                         
        //不符合要求的数据位置进行替换                                                 
        if (i < j) {                                                     
            int temp = array[i];                                         
            array[i] = array[j];                                         
            array[j] = temp;                                             
        }                                                                
    }                                                                    
                                                                         
    //到这里可以确定,j之后所有的数据都比anchor大                                          
    array[low] = array[j];                                               
    array[j] = anchor;                                                   
                                                                         
    quickSort(array, low, j - 1);                                        
    quickSort(array, j + 1, high);                                       
}                                                                                                                                  

归并排序

归并排序
时间复杂度 O(nlogn) 空间复杂度 O(n)
非原地排序
是稳定的排序

public static int[] mergeSort(int[] nums) {
        if (nums.length == 1) {
            return nums;
        }

        //分成2份,分别归并
        int middle = nums.length / 2;
        int[] p = new int[middle];
        int[] q = new int[nums.length - middle];
        for (int index = 0; index < nums.length; ++index) {
            if (index < middle) {
                p[index] = nums[index];
            } else {
                q[index - middle] = nums[index];
            }
        }

        p = mergeSort(p);
        q = mergeSort(q);

        int i = 0;
        int j = 0;

        int index = 0;
        int[] result = new int[nums.length];

        //开始合并
        while (i < p.length && j < q.length) {
            if (p[i] <= q[j]) {
                result[index] = p[i];
                ++i;
            } else {
                result[index] = q[j];
                ++j;
            }
            ++index;
        }

        //还有剩余数据,都放置到队尾
        if (index != nums.length) {
            if (j == q.length) {
                for (; i < p.length; ++i) {
                    result[index] = p[i];
                    ++index;
                }
            } else {
                for (; j < q.length; ++j) {
                    result[index] = q[j];
                    ++index;
                }
            }
        }

        return result;
    }

计数排序

计数排序,可以看成桶排序的一种特殊情况
先决条件,需要明确数据的范围,同时数据范围不大的情况下适用.
同时只能给非负整数排序
计数排序的巧妙之处在于,用另外一个数组保存,每个大小区间内数据量的多少
时间复杂度为O(n + k) k是数据的范围
非原地排序
从头开始遍历就是非稳定算法,从尾开始遍历就是稳定算法。

public static int[] countingSort(int[] array) {

        //假设数据都在0-10之间,那计数数组的长度就是0-10 = 11

        int max = array[0];
        int min = array[0];
        for (int value : array) {
            if (value > max) {
                max = value;
            }

            if (value < min) {
                min = value;
            }
        }
        int[] countArray = new int[max - min + 1];

        //遍历每个数字的个数
        for (int value : array) {
            countArray[value - min] = countArray[value - min] + 1;
        }

        //累加,最小值开始到当前数字区间内,数据量大小
        for (int index = 1; index < countArray.length; index++) {
            countArray[index] = countArray[index] + countArray[index - 1];
        }

        //遍历要排序的数据,直接通过数据量,确定位置
        int[] result = new int[array.length];
//        //非稳定
//        for (int value : array) {
//            int count = countArray[value - min];
//            --count;
//            result[count] = value;
//            countArray[value - min] = count;
//            //这里的意思是,比如10以下的数据还有5个,当我遍历到value = 10的时候,也就是count =5
//            //这个时候我只需要把这个10放置在count -1 也就是 4的位置,同时10以下的数字就少了一个了,count -1 保存到10的位置
//            //下次再来10的时候,就同理操作
//        }

        //稳定
        for (int index = array.length - 1; index >= 0; --index) {
            int value = array[index];
            int count = countArray[value - min];
            --count;
            result[count] = value;
            countArray[value - min] = count;
            //这里的意思是,比如10以下的数据还有5个,当我遍历到value = 10的时候,也就是count =5
            //这个时候我只需要把这个10放置在count -1 也就是 4的位置,同时10以下的数字就少了一个了,count -1 保存到10的位置
            //下次再来10的时候,就同理操作
        }

        return result;
    }

桶排序

桶排序,将要排序的数据分到几个有序的桶里面,桶内数据用快速排序完毕之后,再合并桶,省去桶之间排序的时间
桶排序要求数据能比较容易的划分成m个桶,同时各个桶之间的数据要求是比较均匀的,否则会退化成快排
桶排序比较适合用在外部排序,所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载进内存,这时就可以用桶排序,分批导入某个范围的数据进行排序。

时间复杂度为O(n),
是否是稳定原地排序,由桶内排序算法决定
是稳定排序

基数排序

基数排序对要排序的数据是有要求的,需要可以分割出独立的“位”来比较,而且位之间有递进的关系,如果 a 数据的高位比 b 数据大,那剩下的低位就不用比较了。除此之外,每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到 O(n) 了。
非原地排序
是稳定排序

桶排序、计数排序、基数排序比较

桶排序、计数排序、基数排序,它们对要排序的数据都有比较苛刻的要求,应用不是非常广泛。           
但是如果数据特征比较符合这些排序算法的要求,应用这些算法,会非常高效,线性时间复杂度可以达到 O(n)。 
                                                     
桶排序和计数排序的排序思想是非常相似的,都是针对范围不大的数据,将数据划分成不同的桶来实现排序      
基数排序要求数据可以划分成高低位,位之间有递进关系,比较两个数,我们只需要比较高位,高位相同的再比较低位 
而且每一位的数据范围不能太大,因为基数排序算法需要借助桶排序或者计数排序来完成每一个位的排序工作。    

总结

时间复杂度 是否稳定排序 是否原地排序
冒泡排序 O(n2)
插入排序 O(n2)
选择排序 O(n2) ×
快速排序 O(nlogn) ×
归并排序 O(nlogn) ×
计数排序 O(n+k) k是数据范围 ×
桶排序 O(n) ×
基数排序 O(dn) d是纬度 ×