排序算法

113 阅读5分钟

  因为还在学习的阶段,其实我对于八个排序算法中的堆排序和归并排序还是挺懵的.....

1、冒泡排序

  冒泡排序是一个简单的排序算法,核心思想是两两比较,每次循环将最大(最小)的数移动到数组末端,然后进行第二次循环....直到数组有序。算法的时间复杂度为〇(n²),效率较低。 代码:

 public static void bubbleSort(int[] arr) {
        int n;
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - i - 1; j++) {
                if (arr[ j ] > arr[ j + 1 ]) {
                    n = arr[ j ];
                    arr[ j ] = arr[ j + 1 ];
                    arr[ j + 1 ] = n;
                }
            }
        }
    }

  冒泡排序的效率很低,所以我们对他进行改进,试想一下,如果一个数组是这样的 [1,0,2,3,4,5],(这个数组确实太极端了,但是我们只是想有一个可以对算法改进的例子而已) 对它进行排序,其实在第一次交换后就已经有序了,但还是需要循环n²次,所以我们对它改进:

public static void bubbleSort(int[] arr) {
        int n;
        boolean flag;
        for (int i = 0; i < arr.length - 1; i++) {
            flag = true;
            for (int j = 0; j < arr.length - i - 1; j++) {
                if (arr[ j ] > arr[ j + 1 ]) {
                    n = arr[ j ];
                    arr[ j ] = arr[ j + 1 ];
                    arr[ j + 1 ] = n;
                    flag = false;
                }
            }
            if (flag) {
                break;
            }
        }
    }

  定义一个标志,如果需要交换就将标志赋值为false,每次外层循环开始时把标志置为true,内层循环遍历完数组后判断是否发生交换,若没有,falg的值应为true,说明数组已经有序,退出循环。

2、选择排序

  选择排序的宗旨是每次循环选择最大或最小的数,放在数组的第一位,然后进入下一次循环...直至数组有序,选择排序的每次排序仅仅只交换需要交换的数,而不是两两交换,时间复杂度为〇(n²)。 代码:

public static void selectSort(int[] arr) {
        int min;
        int index;
        for (int i = 0; i < arr.length - 1; i++) {
            index = i;
            min = arr[ i ];
            for (int j = i + 1; j < arr.length; j++) {
                if (min > arr[ j ]) {
                    min = arr[ j ];
                    index = j;
                }
            }
            arr[ index ] = arr[ i ];
            arr[ i ] = min;
        }
    }

还有个聊胜于无的优化,当需要交换的索引为自身的时候,不需要交换:

public static void selectSort(int[] arr) {
        int min;
        int index;
        for (int i = 0; i < arr.length - 1; i++) {
            index = i;
            min = arr[ i ];
            for (int j = i + 1; j < arr.length; j++) {
                if (min > arr[ j ]) {
                    min = arr[ j ];
                    index = j;
                }
            }
            if (index != i) {
                arr[ index ] = arr[ i ];
                arr[ i ] = min;
            }
        }
    }

3、插入排序

  插入排序的主要思想是把一个数组分为两个部分,前一个部分是有序的数组,后一个部分是无序的数组,每次把无序数组的第一个数插入到有序数组中,第一次数组第一个元素为一个有序的序列,时间复杂度为〇(n²)。 代码:

public static void insertionSort(int[] arr) {
        int indexVal;
        int index;
        for (int i = 1; i < arr.length; i++) {
            indexVal = arr[ i ];
            index = i;
            //在索引还能往前走的情况下,比较要插入有序序列的值 应该在哪个位置,
            // 每次循环前一个元素把当前元素覆盖,相当于把比目标元素小的元素后移,直到找到可以插入的位置 退出
            while (index - 1 >= 0 && indexVal < arr[ index - 1 ]) {
                arr[ index ] = arr[ index - 1 ];
                index--;
            }
            arr[ index ] = indexVal;
        }
    }

4、希尔排序

  希尔排序是选择排序的一个进阶版本,对选择排序进行了改进,使得效率更高,平均时间复杂度为〇(n ㏒n),最坏时间复杂度接近于〇(n²)。

  希尔排序又称为缩小增量排序,增量可以理解为把一个数组分为若干个小组,一般初始增量为数组长度的一半,然后每个小组进行选择排序,每次循环将增量折半,直到增量缩减为1,完成排序。其实比起增量我更喜欢叫步长,因为同一组元素的距离就是步长。

public static void shellSort(int[] arr) {
        int len = arr.length;
        int step = len;
        int index;
        int indexVal;
        while (step != 0) {
            step = step >> 1;
            //step指的是步长,即隔step个数为一组,step也指的是每次第一组的第二个数(因为第一个数默认有序)
            //例如:10/2=5,第一组第一个数是arr[0],第二个是arr[5]。。。5/2=2,第一组第一个数是arr[0],第二个数是arr[2]
            for (int i = step; i < len; i++) {
                index = i;
                indexVal = arr[ i ];
                if (arr[ i ] < arr[ i - step ]) {
                    while (index - step >= 0 && indexVal < arr[ index - step ]) {
                        arr[ index ] = arr[ index - step ];
                        index -= step;
                    }
                    arr[ index ] = indexVal;
                }
            }
        }
    }

5、快速排序

  快排利用递归,每次递归将数组(不是完整的数组,因为方法需要两个参数,一个左边界,一个右边界)看做两半,取中心轴,然后把数组中比中轴小的数放在数组的左边,大的放在数组的右边,最后把中轴插在中心,再次对中轴的左子数组和右子数组进行相同的操作,最后数组有序。

  jdk源码中Arrays工具类的sort方法使用的是双轴快速排序,平均时间复杂度为〇(n ㏒n),最坏时间复杂度为〇(n²)。

public static void quickSort(int[] arr, int left, int right) {
        if (left > right) {
            return;
        }
        int l = left;
        int r = right;
        //这个中轴取数组中的哪个数都是一样的,只是逻辑要修改一点
        int pivot = arr[ l ];
        while (l < r) {
            while (l < r && arr[ r ] >= pivot) {
                r--;
            }
            if (l < r) {
                arr[ l ] = arr[ r ];
            }
            while (l < r && arr[ l ] <= pivot) {
                l++;
            }
            if (l < r) {
                arr[ r ] = arr[ l ];
            }
        }
		arr[ l ] = pivot;
        quickSort(arr, left, l - 1);
        quickSort(arr, l + 1, right);
    }

    /**
     * 重载方法
     * @param arr 要排序的数组
     */
    public static void quickSort(int[] arr) {
        quickSort(arr, 0, arr.length - 1);
    }

5.1、快排应用

剑指 Offer 40. 最小的k个数

  输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例:

输入:arr = [3,2,1], k = 2 输出:[1,2] 或者 [2,1]

  这个题目的解法有很多中,最容易想到的就是排序,然后输出数组的前k个即可,重要的是选择哪种排序算法最好。

  看到例题上的输出:只需要输出前k个较小数即可,可以无序。想到快排,快排就是选出轴,将比轴小的数放在轴的左边,比轴大的数放在轴的右边,也就是说:当轴的下标等于k的时候就完成了算法。

  递归停止条件是左边界大于等于右边界,以及轴的下标等于k。

  最重要的是递归参数。

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        quickSort(arr,0,arr.length-1,k);
        int[] res=new int[k];
        System.arraycopy(arr,0,res,0,k);
        return res;
    }
    public void quickSort(int[] arr,int l,int r,int k){
        if(l>=r) return;
        int i=l,j=r,p=arr[i];
        while(i<j){
            while(i<j && arr[j]>=p){
                j--;
            }
            if(i<j){
                arr[i]=arr[j];
            }
            while(i<j && arr[i]<=p){
                i++;
            }
            if(i<j){
                arr[j]=arr[i];
            }
        }
            arr[i]=p;
        // 当i==k 时, 即 有i (0~i-1) 个数比 arr[i]小 ,不需要排序直接退出即可。
            if(i==k){
                return ;
            }
        // 这里递归参数是:当i>k , 即 k 在中轴左边,那么只需要再次对当前中轴左边的序列进行排序,可以抛弃右边的序列,反之一样。
            if(i>k){
                quickSort(arr,l,i-1,k);
            }else{
                quickSort(arr,i+1,r,k);
            }     
    }
}

6、基数排序

思想:将所有待比较数值统一为同样的数位长度,数位较短的数在前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就成为了一个有序的序列。 基数排序动态图

    public static void radixSort(int[] nums) {
        int max = nums[ 0 ];
        // 求出数组中最大的数 , 用来控制最外层循环次数
        for (int num : nums) {
            max = Math.max(max, num);
        }
        int maxLen = String.valueOf(max).length();
        int[][] bucket = new int[ 10 ][ nums.length ];
        // 位于 bucket[i] 中的桶有 bucketElement[i] 个元素
        // 即 当前位(个位 | 十位 。。)是 i 的 数有 bucketElement[i] 个
        int[] bucketElement = new int[ 10 ];
        for (int m = 0; m < maxLen; m++) {
            // 将 元素 放入桶中
            for (int num : nums) {
                // 第一次循环求出个位,第二次求出十位 .....
                int n = num / (int) Math.pow(10, m) % 10;
                bucket[ n ][ bucketElement[ n ] ] = num;
                bucketElement[ n ]++;
            }
            int index = 0;
            // 从桶中取出元素 ,复制到原数组中
            for (int i = 0; i < bucket.length; i++) {
                if (bucketElement[ i ] != 0) {
                    for (int j = 0; j < bucketElement[ i ]; j++) {
                        nums[ index++ ] = bucket[ i ][ j ];
                    }
                }
                // 将 这个桶中元素个数置为0
                bucketElement[ i ] = 0;
            }
        }
    }