十大经典排序算法总结(附Java代码实现)

323 阅读19分钟

选择排序

从数组中选择最小元素,将它与数组的第一个元素交换位置。再从数组剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。

选择排序需要 ~N²/2 次比较和 ~N 次交换,它的运行时间与输入无关,这个特点使得它对一个已经排序的数组也需要这么多的比较和交换操作。

public class Selection {
    public static void sort(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            int min = i;
            for (int j = i + 1; j < array.length; j++) {
                if (array[j] < array[min]) {
                    min = j;
                }
            }
            int temp = array[i];
            array[i] = array[min];
            array[min] = temp;
        }
    }
}

插入排序

每次都将当前元素插入到左侧已经排序的数组中,使得插入之后左侧数组依然有序。

插入排序的时间复杂度取决于数组的初始顺序,如果数组已经部分有序了,那么逆序较少,需要的交换次数也就较少,时间复杂度较低。

  • 平均情况下插入排序需要 ~N²/4 比较以及 ~N²/4 次交换;
  • 最坏的情况下需要 ~N²/2 比较以及 ~N²/2 次交换,最坏的情况是数组是倒序的;
  • 最好的情况下需要 N-1 次比较和 0 次交换,最好的情况就是数组已经有序了。
public class Insertion {
    public static void insertionSort(int[] array) {
        for (int i = 1; i < array.length; i++) {
            for (int j = i; j > 0 && array[j] > array[j - 1]; j--) {
                int temp = array[j];
                array[j] = array[j - 1];
                array[j - 1] = temp;
            }
        }
    }
}

希尔排序

对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,每次只能将逆序数量减少 1。希尔排序的出现就是为了解决插入排序的这种局限性,它通过交换不相邻的元素,每次可以将逆序数量减少大于 1。

public class Shell {
    public static void sort(int[] array) {
        int n = array.length;
        int h = 1;
        while (h < n / 3) {
            h = h * 3 + 1;
        }

        while (h >= 1) {
            for (int i = h;i < n;i++) {
                for (int j = i;j >= h && array[j] < array[j - h];j -= h) {
                    int temp = array[j];
                    array[j] = array[j - h];
                    array[j - h] = temp;
                }
            }
            h = h / 3;
        }
    }
}

冒泡排序

从左到右不断交换相邻逆序的元素,在一轮的循环之后,可以让未排序的最大元素上浮到右侧。

在一轮循环中,如果没有发生交换,那么说明数组已经是有序的,此时可以直接退出。

public class Bubble {
    public static void sort(int[] array) {
        boolean isSorted = false;
        for (int i = array.length - 1; i > 0 && !isSorted; i--) {
            isSorted = true;
            for (int j = 0; j < i; j++) {
                if (array[j] > array[j + 1]) {
                    isSorted = false;
                    int temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                }
            }
        }
    }
}

快速排序

算法描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

代码实现

public class QuickSort {
    public static void quickSort(int[] nums, int left, int right) {
        if (left < right) {
            int pivotIndex = partition(nums, left, right);
            quickSort(nums, left, pivotIndex - 1);
            quickSort(nums, pivotIndex + 1, right);
        }
    }

    private static int partition(int[] nums, int left, int right) {
        int pivot = nums[left];
        int i = left;
        int j = right;
        while (i < j) {
            // 注意这里一定是右边的指针先移动,使得返回的index对应的nums[index]是<=pivot的
            while (i < j && nums[j] > pivot) j--;
            while (i < j && nums[i] <= pivot) i++;
            if (i < j) {
                int temp = nums[i];
                nums[i] = nums[j];
                nums[j] = temp;
            }
        }
        nums[left] = nums[i];
        nums[i] = pivot;
        return i;
    }

    public static void main(String[] args) {
        int[] nums = {37624819519 , 9};
        quickSort(nums,0,nums.length - 1);
        System.out.println(Arrays.toString(nums));
    }
}

复杂度分析

最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(nlogn)

归并排序

算法描述

  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。

代码实现

public class MergeSort {

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

    public static void sort(int[] nums, int left, int right) {
        if (left < right) {
            int mid = (left + right) / 2;
            sort(nums, left, mid);
            sort(nums, mid + 1, right);
            mergeNums(nums, left, mid, right);
        }
    }

    public static void mergeNums(int[] nums, int left, int mid, int right) {
        int[] array = new int[right - left + 1];
        int p = 0;
        int p1 = left;
        int p2 = mid + 1;
        while (p1 <= mid && p2 <= right) {
            if (nums[p1] <= nums[p2]) {
                array[p++] = nums[p1++];
            } else {
                array[p++] = nums[p2++];
            }
        }
        while (p1 <= mid) {
            array[p++] = nums[p1++];
        }
        while (p2 <= right) {
            array[p++] = nums[p2++];
        }

        for (int i = 0;i < array.length;i++) {
            nums[i + left] = array[i];
        }
    }
}

复杂度分析

最佳情况:T(n) = O(n) 最差情况:T(n) = O(nlogn) 平均情况:T(n) = O(nlogn)

堆排序

public class Heap {
    public static void heapify(int tree[], int n, int i) {
        if (i >= n) {
            return;
        }
        int c1 = 2 * i + 1;
        int c2 = 2 * i + 2;
        int max = i;
        if (c1 < n && tree[c1] > tree[max]) {
            max = c1;
        }
        if (c2 < n && tree[c2] > tree[max]) {
            max = c2;
        }
        if (max != i) {
            swap(tree, i, max);
            heapify(tree, n, max);
        }
    }

    public static void swap(int tree[], int i, int j) {
        int temp = tree[i];
        tree[i] = tree[j];
        tree[j] = temp;
    }

    public static void buildHeap(int[] tree, int n) {
        int lastNode = n - 1;
        int parent = (lastNode - 1) / 2;
        for (int i = parent; i >= 0; i--) {
            heapify(tree, n, i);
        }
    }

    public static void heapSort(int[] tree, int n) {
        buildHeap(tree, n);
        for (int i = n - 1; i >= 0; i--) {
            swap(tree, 0, i);
            heapify(tree, i, 0);
        }
    }

    public static void main(String[] args) {
        int[] tree = {35195195};
        heapSort(tree, tree.length);
        System.out.println(Arrays.toString(tree));
    }
}

计数排序

计数排序 的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

计数排序(Counting sort) 是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数,然后根据数组C来将A中的元素排到正确的位置,它只能对整数进行排序。

public class Counting {
    public static void sort(int[] array) {
        int min = array[0], max = array[0], bias = 0;
        for (int i = 0; i < array.length; i++) {
            if (array[i] < min) {
                min = array[i];
            }
            if (array[i] > max) {
                max = array[i];
            }
        }
        bias = 0 - min;
        int[] bucket = new int[max - min + 1];
        for (int i = 0; i < array.length; i++) {
            bucket[array[i] + bias]++;
        }
        int i = 0,index = 0;
        while (i < bucket.length) {
            if (bucket[i] != 0) {
                array[index++] = i - bias;
                bucket[i]--;
            }else {
                i++;
            }
        }
    }
}

桶排序

public class Bucket {
    /**
     * 桶排序 将待排序数列 放入n个桶中 对于每个数列有
     * 最大值 max     最小值 min     桶数 n(最后一个桶只存储最大值)
     * 所以(1-(n-1))中每个桶的区间大小应为(max-min)/(n-1)
     * 此时有==> 元素i入桶的序列号为:int t=(arr[i]-min)*(n-1)/(max-min)
     *
     * @param arr 待排序数列
     * @param n     桶数
     */

    public static void buckSort(double[] arr, int n) {

        /* 创建n个桶 使用ArrayList作桶 */
        List<Double>[] bucks = new ArrayList[n];
        for (int i = 0; i < bucks.length; i++) {
            bucks[i] = new ArrayList<>();
        }

        /* 获取数列的最大值最小值 */
        double max = arr[0], min = arr[0];
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] < min) {
                min = arr[i];
            } else if (arr[i] > max) {
                max = arr[i];
            }
        }

        /* 将元素以此放入桶中 */
        for (int i = 0; i < arr.length; i++) {
            /* 计算元素该入哪个桶 */
            int t = (int) ((arr[i] - min) * (n - 1) / (max - min));
            bucks[t].add(arr[i]);
        }
        /* 对每个桶单独排序 */
        for (int i = 0; i < bucks.length; i++) {
            Collections.sort(bucks[i]);
        }

        /* 将桶中元素 放回原数组 */
        for (int i = 0, k = 0; i < bucks.length; i++) {
            for (int j = 0; j < bucks[i].size(); j++) {
                arr[k++] = bucks[i].get(j);
            }
        }
    }
}

桶排序中可能存在的两个小问题:

  • 计算桶区间时 为什么是(max-min)/(桶数-1)

    我们直到如果将[1-5] (最大值5 最小值1)的元素序列分为5个桶,那么我们如果按5计算区间范围,有区间大小=(5-1)/5==>0.8,那么我们按照此区间计算每个桶的范围:

    [1,1.8), [1.8,2.6), [2.6,3.4), [3.4.4.2), [4.2,5) 此时我们发现5消失了!!! 此时再计算5的桶下标会得到 桶下标=(5-1)/0.8==>5 而我们生成了5个桶,最大下标只能到4,也就下标越界了,为了解决这个问题,我们使最后一个桶只存储一个数值(最大值),即只有四个桶是存在区间的,此时我们有桶:

    [1-2),[2-3),[3-4),[4-5),[5] 再用5测试一下 桶下标=(5-1)/1==>4,完美!

  • 计算桶下标的方法int t=(int) ((arr[i]-min)*(n-1)/(max-min));是怎么来的?

    当我们从上个问题知道了(n-1)代表有区间的桶时,就已经比较简单了,我们反推一下

    (arr[i]-min)*(n-1)/(max-min)

    = (arr[i]-min)/((max-min)/(n-1))

    = (arr[i]-min)/区间大小

基数排序

public class Radix {
    public static void radixSort(int[] arr) {
        // 创建一个10*arr.length的二维数组
        int[][] bucket = new int[10][arr.length];
        // 先获取最大值
        int max = arr[0];
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        for (int i = 1; max > 0; i *= 10) {
            // 记录每个桶的下标
            int[] count = new int[10];
            for (int j = 0; j < arr.length; j++) {
                int t = (arr[j] / i) % 10;
                bucket[t][count[t]++] = arr[j];
            }
            // 将桶中的数放回原数组,等待下一位数的排序
            for (int j = 0, c = 0; j < 10; j++) {
                for (int k = 0; k < count[j]; k++) {
                    arr[c++] = bucket[j][k];
                }
            }
            max /= i;
        }
    }
}