排序算法

整理一下常见的排序算法。

1、插入排序

插入排序是基础的排序之一,插入排序的过程,脑补打扑克,分成两部分:一部分是手里的牌(已经排好序),一部分是要拿的牌(无序)。这种往一个有序的集合里面插入元素,插入后序列仍然有序这就是插入排序算法思路。

    public static void main(String[] args) {
        int[] a = {3, 43, 32, 2, 4, 6, 23, 9, 8, 2, 6, 7, 43};
        int n = a.length;
        // 无序的部分,从第二个开始,因为第一个是有序的
        for (int i = 1; i < n; i++) {
            int data = a[i];
            int j = i - 1;
            // 有序的部分,从后往前挨个遍历
            for (; j >= 0; j--) {
                // 如果当前元素大于data,表示data要往前移
                if (a[j] > data) { 
                    a[j + 1] = a[j];
                } else {
                    break;
                }
            }
            a[j + 1] = data;
        }
        System.out.println(Arrays.toString(a));
    }

输出结果:[2, 2, 3, 4, 6, 6, 7, 8, 9, 23, 32, 43, 43]

插入排序的时间复杂度是O(n^2),是稳定的,它是最基础的排序算法。如果对插入排序进行优化,我们从上面可以看到就是break的地方越多越好,就表示前面都是有序的,且当前比较元素已经找到了插入的位置。

2、希尔排序

希尔排序是插入排序的改良版。把数据下标按照一定的增量分组,对每组使用插入排序,当增量减至1时,排序终止。 其改良的地方尽可能分出来更多的有序段,因为从插入排序可知其对有序段的处理速度是很快的。

public static void main(String[] args) {
        int[] a = {3, 43, 32, 2, 4, 6, 23, 9, 8, 2, 6, 7, 43};
        int n = a.length;
        // 分组,每次下标按照gap递增
        for (int gap = n / 2; gap >= 1; gap /= 2) {
            // 下面的处理就和插入排序一样了
            for (int i = 1; i < n; i+=gap) {
                int data = a[i];
                int j = i - gap;
                for (; j >= 0; j-=gap) {
                    if (a[j] > data) {
                        a[j + gap] = a[j];
                    } else {
                        break;
                    }
                }
                a[j + gap] = data;
            }
        }
        System.out.println(Arrays.toString(a));
    }

希尔排序能一定程度的提高插入排序的性能,时间复杂度为O(n^2),但是这样分组,中间还会有交换,排序是不稳定的。

3、归并排序

归并排序是jdk源码中使用的排序,时间复杂度为O(nlogn),是非常高效的一种排序算法,原理有点类似于二分,分到最后一个,就是有序的,然后再进行合并,写这个代码要用到递归的思想。

public class MergeSort {
    static int[] a    = {3, 43, 32, 2, 4, 6, 23, 9, 8, 2, 6, 7, 43};
    // 借用数组临时保存数据
    static int[] temp = new int[a.length];
    public static void main(String[] args) {
        mergeSort(a, 0, a.length - 1);
        System.out.println(Arrays.toString(a));
    }
    // 归并排序的核心在于先分后合,数据拆到只剩一个就是有序的,再进行合并
    public static void mergeSort(int[] a, int left, int right) {
        // 终止条件,只有一个数,就不用再分了
        if (left < right) {
            int mid = (left + right) / 2;
            mergeSort(a, left, mid);
            mergeSort(a, mid + 1, right);
            merge(a, left, mid, right);
        }
    }
    // left-mid之间是有序的,mid-right之间是有序的,合并两个有序的数组
    public static void merge(int[] a, int left, int mid, int right) {
        // loc下标用来指向临时数组赋值的位置
        int loc = left;
        // 用来标记左边数组合并到哪个位置
        int p1 = left;
        // 用来标记用边数组合并到哪个位置
        int p2 = mid + 1;
        // 循环条件是左边和右边都没合并完成
        while (p1 <= mid && p2 <= right) {
            // 现在要做的就是左边和右边比较和交换
            if (a[p1] <= a[p2]) {
                temp[loc++] = a[p1++];
            } else {
                temp[loc++] = a[p2++];
            }
        }
        // 上面的循环终止了,但是不知道是左边的还是右边的合并完了,需要处理未合并的数据
        while (p1 <= mid) {
            temp[loc++] = a[p1++];
        }
        while (p2 <= right) {
            temp[loc++] = a[p2++];
        }
        // 最后,将临时数组中合并完的有序数据存入原数组
        for (int i = left; i <= right; i++) {
            a[i] = temp[i];
        }
    }
}

时间复杂度o(nlogn),是稳定的

4、选择排序

选择排序和插入排序很类似,也分有序和无序两部分,不同的思想是插入排序对数据的操作是移动,选择排序是交换,每次都能找到最小的数。

public static void main(String[] args) {    
        int[] a = {3, 43, 32, 2, 4, 6, 23, 9, 8, 2, 6, 7, 43};
        int n = a.length;
        // 从第一个开始遍历到倒数第二个
        for (int i = 0; i < n - 1; i++) {
            // 一次找到一个最小的值,记录下标
            for (int j = i + 1; j < n; j++) {
                int index = i;
                if (a[index] > a[j]) {
                    index = j;
                }
                // 将a[i]和最小值交换
                a[i] = (a[i] + a[index]) - (a[index] = a[i]);
            }
        }
        System.out.println(Arrays.toString(a));
    }

时间复杂度O(n^2),不稳定

5、冒泡排序

每次冒泡操作都会对相邻的两个元素进行比较,不满足大小关系就交换,一次冒泡能让一个元素移动到它应该在的位置,n次冒泡排序完成。

    public static void main(String[] args) {
        int[] a = {3, 43, 32, 2, 4, 6, 23, 9, 8, 2, 6, 7, 43};
        int n = a.length;
        for (int i = 0; i < n - 1; i++) {
            for (int j = i + 1; j < n; j++) {
                if (a[i] > a[j]) {
                    a[i] = (a[i] + a[j]) - (a[j] = a[i]);
                }
            }
        }
        System.out.println(Arrays.toString(a));
    }

冒泡的排序时间复杂度是O(n^2),交换和比较的次数是比较多的,是稳定的

6、快速排序

快排的思路是找一个基准数,从后往前找比之小的交换,从前往后找比之大的交换,这样循环处理之后比它小的都在左边,比它大的都在右边,对左右分别递归,完成排序。

    public static void qSort(int[] a, int left, int right) {
        // 定义基准数为第一个数
        int base = a[left];
        // 左指针,从左往右找比之大的数
        int p1 = left;
        // 右指针,从右往左找比之小的数
        int p2 = right;
        // 终止条件,已经找到了同一个位置
        while (p1 < p2) {
            // 左右指针还未指向同一个位置且基准数比右边的小
            while (p1 < p2 && base <= a[p2]) {
                p2--;
            }
            // 左右指针仍未指向同一个位置,说明上面循环退出的条件是:从右到左找到了比base小的数,进行交换
            if (p1 < p2) {
                a[p1] = (a[p1] + a[p2]) - (a[p2] = a[p1]);
                p1++;
            }
            // 和上面类似,这边是从左向右找比基准数大的数进行交换
            while (p1 < p2 && base >= a[p1]) {
                p1++;
            }
            if (p1 < p2) {
                a[p1] = (a[p1] + a[p2]) - (a[p2] = a[p1]);
                p2--;
            }
        }
        // 左半部分递归
        if (left < p1) {
            qSort(a, left, p1 - 1);
        }
        // 右半部分递归
        if (p2 < right) {
            qSort(a, p2 + 1, right);
        }
    }

快排的时间复杂度是O(nlogn),最坏的情况是O(n^2)。优化快排性能主要从优化基准数方向考虑,比如取三个数计算出合适的基准数。

快排和归并有些相似处,都会对数据进行拆分,不同的是归并排序从上到下处理,先处理子问题,然后再合并。而快排就是从上到下分区再处理子问题,不用合并,类似于尾递归。

 

这么多排序算法,如何选择,首先要看场景,需不需要稳定排序,然后看数据量,小的话直接选插入也没什么问题(虽然它是O(n^2)),然后还要分析空间,归并排序就需要额外开辟部分空间。所有没有说一定适用的排序算法,视情况而定,如果不好分析,选归并或者快排,基本能解决问题。

 

posted @ 2020-12-13 20:43  以战止殇  阅读(67)  评论(0编辑  收藏  举报