排序算法学习笔记一

0 概述

如下图所示,给出了常见排序算法,本文主要总结基于比较排序算法。
在这里插入图片描述

1 基于比较排序算法

基于比较排原地序算法常见的主要有冒泡排序、选择排序、插入排序。

1.1 冒泡排序

  • 冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。冒泡排序包含两种基本操作:比较和交换。最好情况时间复杂度O(n),平均和最坏的时间复杂度为O(n2),是稳定排序。

    /**
     * 一次冒泡会让至少一个元素移动到它应该在的位置,所以最坏情况需要扫描n次
     * 只会操作相邻的两个数据,内循环是比较相邻的两个元素
     */
    public static void bubbleSort(int arr[]) {
    
    
        if (arr == null || arr.length <= 1) {
    
    
            return;
        }
        int len = arr.length;
        //是否存在交换的标志位
        boolean existSwap = false;
        //需要n次冒泡
        for (int i = 0; i < len; i++) {
    
    
            for (int j = 0; j < len - i - 1; j++) {
    
    
                //交换
                if (arr[j] > arr[j + 1]) {
    
    
                    int tmp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = tmp;
                    
                    existSwap = true;
                }
            }
            // 没有数据交换,提前退出
            if (!existSwap) {
    
    
                break;
            }

        }
    }

1.2 插入排序

插入排序算法基本思想将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。插入排序基本操作:比较和移动。最好情况时间复杂度O(n),平均和最坏的时间复杂度为O(n2),是稳定排序。

    /**
     * 插入排序
     *
     */
    public static void insertSort(int arr[]) {
    
    
        if (arr == null || arr.length <= 1) {
    
    
            return;
        }
        int len = arr.length;
        //
        for (int i = 1; i < len; i++) {
    
    
            int value = arr[i];
            int j = i - 1;
            // 和前面进行比较,数据后移动
            for (; j >= 0; j--) {
    
    
                //数据移动,如果已经有序直接break
                if (arr[j] > value) {
    
    
                    arr[j + 1] = arr[j];
                } else {
    
    
                    break;
                }
            }
            //插入合适的数据
            arr[j + 1] = value;
        }
    }

1.3 选择排序

选择排序基本思想:第一次从待排序的数据元素选择最小(或最大)的一个元素,放到第一位置。第二次从待排序的数据元素选择最小(或最大)的一个元素,放到第二个位置。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。最好、平均和最坏的时间复杂度为O(n2)。

   /**
     * 选择排序
     *
     */
    public static void selectSort(int arr[]) {
    
    
        if (arr == null || arr.length <= 1) {
    
    
            return;
        }
        int len = arr.length;
        // 每次选择一个元素
        for (int i = 0; i < len; i++) {
    
    
            int minIndex = i;
            for (int j = i + 1; j < len; j++) {
    
    
                if (arr[j] < arr[minIndex]) {
    
    
                    minIndex = j;
                }
            }
            //交换选择出来的最小值,放到相应的位置
            int tmp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = tmp;
        }
    }

排利用的也是分治思想。乍看起来,它有点像归并排序,但是思路其实完全不一样。我们待会会讲两者的区别。现在,我们先来看下快排的核心思想。快排的思想是这样的:如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。

1.4 归并排序

归并排序使用的分治思想;就是将一个大问题逐步分解成小的子问题来解决,可以使用递归来实现归并排序。
下面给出递归公式&递归结束条件

mergeSort(a[],p,r)=merge(mergeSort(a[],p,q)+mergeSort(a[],q+1,r));
递归终止条件
p>=r
    public static void mergeSort(int arr[]) {
    
    
        if (arr == null || arr.length <= 1) {
    
    
            return;
        }
        mergeSortUseRecursion(arr, 0, arr.length-1);
    }

    private static void mergeSortUseRecursion(int arr[], int p, int r) {
    
    
        //递归结束条件
        if (p >= r) {
    
    
            return;
        }
        //取中间位置
        int q = p + (r - p) / 2;
        mergeSortUseRecursion(arr, p, q);
        mergeSortUseRecursion(arr, q + 1, r);
        //数据做merge(p,r)
        int[] left = new int[q - p + 2];
        int[] right = new int[r - q + 1];

        for (int i = 0; i <= q - p; i++) {
    
    
            left[i] = arr[p + i];
        }
        for (int i = 0; i < r - q; i++) {
    
    
            right[i] = arr[q + 1 + i];
        }
        // 第一个数组添加哨兵(最大值)
        left[q - p + 1] = Integer.MAX_VALUE;
        // 第二个数组添加哨兵(最大值)
        right[r - q] = Integer.MAX_VALUE;
        int i = 0;
        int j = 0;
        int k = p;
        while (k <= r) {
    
    
            // 当左边数组到达哨兵值时,i不再增加,直到右边数组读取完剩余值,同理右边数组也一样
            if (left[i] <= right[j]) {
    
    
                arr[k++] = left[i++];
            } else {
    
    
                arr[k++] = right[j++];
            }
        }

    }

1.5 快速排序

快速排序用的也是分治的思想,乍一看可能和归并有点相似,其实思路有很大差异的。其基本思想:如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。

quickSort(a[],p,r)=quickSort(a[],p,q)+quickSort(a[],q+1,r);
递归终止条件
p>=r
    public static void quickSort(int arr[]) {
    
    
        if (arr == null || arr.length <= 1) {
    
    
            return;
        }
        quickSortUseRecursion(arr, 0, arr.length-1);
    }

    private static void quickSortUseRecursion(int arr[], int p, int r) {
    
    
        if (p >= r) {
    
    
            return;
        }
        int q = partition(arr, p, r);
        quickSortUseRecursion(arr, p, q);
        quickSortUseRecursion(arr, q + 1, r);

    }

    private static int partition(int arr[], int p, int r) {
    
    
        int pivot = arr[p];

        int leftIndex = p;
        for (int index = p + 1; index <= r; index++) {
    
    
            //比哨兵大不用处理
            if (arr[index] < pivot) {
    
    
                leftIndex++;
                //如果不是一个位置,需要把小的放到左边大的放到右边
                if (leftIndex != index) {
    
    
                    int tmp = arr[leftIndex];
                    arr[leftIndex] = arr[index];
                    arr[index] = tmp;
                }
            }
        }
        int tmp = arr[leftIndex];
        arr[leftIndex] = arr[p];
        arr[p] = tmp;
        return leftIndex;
    }

根据快速排序思想,求k大元素。

   public static int kSmall(int arr[], int k) {
    
    
        if (arr == null || arr.length < k) {
    
    
            return -1;
        }
        int p = partition(arr, 0, arr.length - 1);
        while (p + 1 != k) {
    
    
            if (p < k) {
    
    
                p = partition(arr, p + 1, arr.length - 1);
            } else {
    
    
                p = partition(arr, 0, p);
            }
        }
        return arr[p];
    }

2 总结

虽然冒泡排序和插入排序的时间复杂度都是 O(n2),都是稳定排序算法,但是从代码上可以看出,插入排序是优于冒泡排序的,冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个。

排序算法 是否是稳定排序 最好、平均、最坏 空间复杂度
冒泡 O(n)、O(n2)、O(n2) O(1)
插入 O(n)、O(n2)、O(n2) O(1)
选择 O(n2)、O(n2、O(n2) O(1)
归并 O(nlogn)、O(nlogn)、O(nlogn) O(n)
快速 O(nlogn)、O(nlogn)、O(n2) O(logn)

おすすめ

転載: blog.csdn.net/huangshanchun/article/details/114762896