搞懂基本排序算法

一个系列搞懂排序算法(一)(三大基本排序:选择、冒泡、插入)(C语言) 文章目录一个系列搞懂排序算法(一)(三大基本排序:选择、冒泡、插入)(C语言)前言:正文:排序种类的介绍: 排序种类介绍(导图) 基本三大排序:冒泡排序、选择排序、插入排序。看前须知三大排序对比表格:三个排序的相同之处:三个排序的不同之处:稳定排序的简单介绍1. 冒泡排序(交换类):2. 插入排序(插入类):3. 选择排序(选择类):结束语:心得: 一个系列搞懂排序算法(一)(三大基本排序:选择、冒泡、插入)(C语言) 前言:   学了那么久,都么有对排序进行系统的总结。刚好学校C语言的进度到了排序,就很 阅读详情

搞懂基本排序算法

上篇文章写了关于 Java 内部类的基本知识,感兴趣的朋友可以去看一下:搞懂 JAVA 内部类;本文写的内容是最近学习的算法相关知识中的基本排序算法,排序算法也算是面试中的常客了,实际上也是算法中最基本的知识。由于 Android 开发中用到的地方并不多,所以也很容易遗忘,但是为了进阶高级工程师巩固基本算法和数据结构也是必修课程之一。

基本排序算法按难易程度来说可以分为:冒泡排序,选择排序,插入排序,归并排序,选择排序。本文也将从这五种排序算法来讲解各自的中心思想,和 Java 实现方式。

冒泡排序

冒泡排序恐怕是我们计算机专业课程上以第一个接触到的排序算法,也算是一种入门级的排序算法。

冒泡排序虽然简单但是对于 n 数量级很大的时候,其实是很低效率的。所以实际生产中很少使用这种排序算法。下面我们看下这种算法的具体实现思路:

冒泡排序算法原理:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

一次比较过程如图所示(图片 Google 来的侵删)

冒泡排序 Java 代码实现:

/**
 * @param arr 待排序数组
 * @param n   数组长度
 */
 private static void BubbleSort(int[] arr, int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 1; j < n - i - 1; j++) {
            if (arr[j - 1] > arr[j]) {
                //交换两个元素
                int temp = arr[j];
                arr[j] = arr[j - 1];
                arr[j - 1] = temp;
            }
        }
    }
 }

冒泡排序时间空间复杂度及算法稳定性分析

对于长度为 n 的数组,冒泡排序需要经过 n(n-1)/2 次比较,最坏的情况下,即数组本身是倒序的情况下,需要经过 n(n-1)/2 次交换,所以其

冒泡排序的算法时间平均复杂度为O(n²)。空间复杂度为 O(1)。

可以想象一下:如果两个相邻的元素相等是不会进行交换操作的,也就是两个相等元素的先后顺序是不会改变的。如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个元素相邻起来,最终也不会交换它俩的位置,所以相同元素经过排序后顺序并没有改变。

所以冒泡排序是一种稳定排序算法。所以冒泡排序是稳定排序。这也正是算法稳定性的定义:

排序算法的稳定性:通俗地讲就是能保证排序前两个相等的数据其在序列中的先后位置顺序与排序后它们两个先后位置顺序相同。

冒泡排序总结:

  1. 冒泡排序的算法时间平均复杂度为O(n²)。
  2. 空间复杂度为 O(1)。
  3. 冒泡排序为稳定排序。

选择排序

选择排序是另一种简单的排序算法。选择排序之所以叫选择排序就是在一次遍历过程中找到最小元素的角标位置,然后把它放到数组的首端。我们排序过程都是在寻找剩余数组中的最小元素,所以就叫做选择排序。

选择排序的思想

选择排序的思想也很简单:

  1. 从待排序序列中,找到关键字最小的元素;起始假定第一个元素为最小
  2. 如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换;
  3. 从余下的 N - 1 个元素中,找出关键字最小的元素,重复1,2步,直到排序结束。

示意图:

选择排序 Java 代码实现:

public static void sort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            int minIndex = i;
            // for 循环 i 之后所有的数字 找到剩余数组中最小值得索引
            for (int j = i + 1; j < n; j++) {
                if (arr[j]< arr[minIndex]) {
                    minIndex = j;
                }
            }
            swap(arr, i, minIndex);
        }
    }

    /**
     * 角标的形式 交换元素
     */
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

选择排序时间空间复杂度及算法稳定性分析

上述 java 代码可以看出我们除了交换元素并未开辟额外的空间,所以额外的空间复杂度为O(1)。

对于时间复杂度而言,选择排序序冒泡排序一样都需要遍历 n(n-1)/2 次,但是相对于冒泡排序来说每次遍历只需要交换一次元素,这对于计算机执行来说有一定的优化。但是选择排序也是名副其实的慢性子,即使是有序数组,也需要进行 n(n-1)/2 次比较,所以其时间复杂度为O(n²)。

即便无论如何也要进行n(n-1)/2 次比较,选择排序仍是不稳定的排序算法,我们举一个例子如:序列5 8 5 2 9, 我们知道第一趟选择第1个元素5会与2进行交换,那么原序列中两个5的相对先后顺序也就被破坏了。

选择排序总结:

  1. 选择排序的算法时间平均复杂度为O(n²)。
  2. 选择排序空间复杂度为 O(1)。
  3. 选择排序为不稳定排序。

插入排序

对于插入排序,大部分资料都是使用扑克牌整理作为例子来引入的,我们打牌都是一张一张摸牌的,没摸到一张牌就会跟手里所有的牌比较来选择合适的位置插入这张牌,这也就是直接插入排序的中心思想,我们先来看下动图:

相信大家看完动图以后大概知道了插入排序的实现思路了。那么我们就来说下插入排序的思想。

插入排序的思想

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤 2~5

插入排序的 Java 实现:

下面先看下最基本的实现:

    public static void sort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            //内层循环比较 i 与前边所有元素值,如果 j 索引所指的值小于 j- 1 则交换两者的位置
            for(int j = i; j > 0 && arr[j-1] > arr[j]; j--){
                swap(arr,j-1,j);
            }
        }
    }

在上述算法实现中我们每次寻找 i 应该处在数组中哪个为位置的时候,都是以交换当前元素与上一个元素为代价的,我们知道交换操作是要比赋值操作要费时的,因为每次交换都需要经过三次赋值操作,我们想一下我们玩扑克的时候没有拿起一张牌一个个向前挪知道放到其该放的位置的吧,都是拿出这张牌,找到位置就插进去(突然邪恶),实际上我们是将这个位置以后的牌一次向后挪了一个位置,那么用Java 代码是否能实现呢?答案肯定是可以的:

   public static void sort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            //拎出来当前未排序的这样牌
            int e = arr[i];
            //寻找其该放的位置
            for(int j = i; j > 0 && arr[j-1] > arr[j]; j--){
                arr[j]= arr[j-1];
            }
            //循环结束后  arr[j] >= arr[j-1] 那么 j 角标就是e 应该在的位置。
             arr[j] = e;
        }
    }

插入排序的时间复杂度和空间复杂度分析

对于插入的时间复杂度和空间复杂度,通过代码就可以看出跟选择和冒泡来说没什么区别同属于 O(n²) 级别的时间复杂度算法 ,只是遍历方式有原来的 n n-1 n-2 … 1,变成了 1 2 3 … n 了。最终得到时间复杂度都是 n(n-1)/2。

对于稳定性来说,插入排序和冒泡一样,并不会改变原有的元素之间的顺序,如果遇见一个与插入元素相等的,那么把待插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序仍是排好序后的顺序,所以插入排序是稳定的。

对于插入排序这里说一个非常重要的一点就是:由于这个算法可以提前终止内层比较( arr[j-1] > arr[j])所以这个排序算法很有用!因此对于一些 NlogN 级别的算法,后边的归并和快速都属于这个级别的,算法来说对于 n 小于一定级别的时候(Array.sort 中使用的是47)都可以用插入算法来优化,另外对于近乎有序的数组来说这个提前终止的方式就显得更加又有优势了。

插入排序总结:

  1. 插入排序的算法时间平均复杂度为O(n²)。
  2. 插入排序空间复杂度为 O(1)。
  3. 插入排序为稳定排序。
  4. 插入排序对于近乎有序的数组来说效率更高,插入排序可用来优化高级排序算法

归并排序

接下来我们看一个 NlogN 级别的排序算法,归并算法。 归并算法正如其名字一样采用归并的方法进行排序:

我们总是可以将一个数组一分为二,然后二分为四直到,每一组只有两个元素,这可以理解为个递归的过程,然后将两个元素进行排序,之后再将两个元素为一组进行排序。直到所有的元素都排序完成。同样我们来看下边这个动图。

归并算法的思想

归并算法其实可以分为递归法和迭代法(自低向上归并),两种实现对于最小集合的归并操作思想是一样的区别在于如何划分数组,我们先介绍下算法最基本的操作:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾

假设我们现在在对一个数组的 arr[l...r] 部分进行归并,按照上述归并思想我们可将数组分为两部分 假设为 arr[l...mid] 和 arr[mid+1...r]两部分,注意这两部分可能长度并不相同,因为基数个数的数组划分的时候总是能得到一个 长度为1 和长度为2 的部分进行归并.

那么我们按照上述思路进行代码编写:

归并排序的 Java 实现:

   /**
     * arr[l,mid] 和  arr[mid+1,r] 两部分进行归并
     */
    private static void merge(int[] arr, int l, int mid, int r) {

        // 复制等待归并数组 用来进行比较操作,最将原来的 arr 每个角标赋值为正确的元素
        int[] aux = new int[r - l + 1];
        for (int i = l; i <= r; i++) {
            aux[i - l] = arr[i];
        }

        int i = l;
        int j = mid + 1;

        for (int k = l; k <= r; k++) {
            if (i > mid) {
                //说明左边部分已经全都放进数组了
                arr[k] = aux[j - l];
                j++;
            } else if (j > r) {
                //说明左边部分已经全都放进数组了
                arr[k] = aux[i - l];
                i++;
            } else if (aux[i - l] < aux[j - l]) {
                //当左半个数组的元素值小于右边数组元素值得时候 赋值为左边的元素值
                arr[k] = aux[i - l];
                i++;
            } else {
                //当左半个数组的元素值大于等于右边数组元素值得时候 赋值为左边的元素值 这样也保证了排序的稳定性
                arr[k] = aux[j - l];
                j++;
            }
        }
    }

相信大家配合刚才的动图和上述算法实现已经理解了归并算法了,如果感到迷糊的话可以试着拿个一个数组在纸上演算一下归并的过程,相信大家一定可以理解。上述只是实现了算法核心部分,那么我们应该怎么对整个数组来进行排序呢?上边也提到了有两种方法,一种是递归划分法,一种是迭代遍历法(自低向上)那么我们先来开来看递归实现:

    /**
     * 
     * @param arr 待排序数组
     * @param l  其实元素角标 0
     * @param r 最后一个元素角标 n -1 
     */
    private static void mergeSort(int[] arr, int l, int r) {
        if (l >= r) {
            return;
        }

        //开始归并排序 向下取整
        int mid = (l + r) / 2;

        //递归划分数组
        mergeSort(arr, l, mid);
        mergeSort(arr, mid + 1, r);

        //检查是否上一步归并完的数组是否有序,如果有序则直接进行下一次归并
        if (arr[mid] <= arr[mid + 1]) {
            return;
        }
        //将两边的元素归并排序
        merge(arr, l, mid, r);
    }

如果对递归过程不理解可以配合下边这个图来理解(图片来自网上,侵删):

当然我们merge先对左半部分进行的也就是先进行到Level3的左边最底层 8 | 6 ,然后归并完成后进行右边递归到底 最终是 8 6 2 3 | 1 5 7 4 进行归并。

对于迭代实现归并其实和递归实现有所不同,迭代的时候我们是将数组分为 一个一个的元素,然后每两个归并一次,第二次我们将数组每两个分一组,两个两个的归并,知道分组大小等于待归并数组长度为止,即先局部排序,逐步扩大到全局排序

  /**
     * 自低向上的归并排序
     *
     * @param n   为数组长度
     * @param arr 数组
     */
    private static void mergeSortBU(Integer[] arr, int n) {
        //外层遍历从归并区间长度为1 开始 每次递增一倍的空间 1 2 4 8 sz 需要遍历到数组长度那么大
        //sz = 1 : [0] [1]...
        //sz = 2 : [0,1] [2.3] ...
        //sz = 4 : [0..3] [4...7] ...
        for (int sz = 1; sz <= n; sz += sz) {

            //内层遍历要比较 arr[i,i+sz-1] arr[i+sz,i+sz+sz-1] 两个区间的大小 也就是每次对 sz - 1 大小的数组空间进行归并
            // 注意每次 i 递增  两个 sz 的长度 ,因为每次 merge 的时候已经归并了两个 sz 长度 部分的数组
            for (int i = 0; i + sz < n; i += sz + sz) {
                merge(arr, i, i + sz - 1, Math.min(i + sz + sz - 1, n - 1));
            }
        }
    }

比如我们看第一次是 sz = 1 个长度的归并即 i = 0 i = 1 的元素归并 下次归并应该为 i= 2 i = 3 一次类推 所以内层循环 i 每次应该递增 两个 sz 那么大 为了避免角标越界且保证归并的右半部分存在 所以 i + sz < n ,又考虑到数组长度为奇数的情况,所以右半边的右边为 Math.min(i + sz + sz - 1, n - 1);可以参考下边的图片:

归并排序的时间复杂度和空间复杂度分析

其实对于归并排序的时间复杂对有一个递归公式来推断出时间复杂度,但简单来讲假设数组长度为 N ,那么我们就有 logN 次划分区间,而最终会划分为常数 级别的归并,将所有层的归并时间加起来得到了一个 NlogN,想要了解归并排序时间复杂度讲解的同学可以左转 归并排序及其时间复杂度分析,这里不再过多讲解。

对于空间复杂度,我们通过算法实现可以看出我们归并过程申请了 长度为 N 的临时数组,来进行归并所以空间复杂度为 O(n);

又由于我们在排序过程中对于 aux[i - l] = aux[j - l] 并没有进行位置交换直接取得靠前的元素先赋值,所以算法是稳定的。

* 归并排序总结:*

  1. 归并排序的算法时间平均复杂度为O(nlog(n))。
  2. 归并排序空间复杂度为 O(n)。
  3. 归并排序为稳定排序。
  4. 对于

快速排序

快速排序为应用最多的排序算法,因为快速二字而闻名。快速排序和归并排序一样,采用的都是分治思想。分治法的基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。我们只需关注最小问题该如何求解,和如何去递归既可以得到正确的算法实现。快速排序可以分为:单路快速排序,双路快速排序,三路快速排序,他们区别在于选取几个指针来对数组进行遍历下面我们依次来讲解。

单路快速算法的思想:

首先我们选取数组中的一个数,将其放在合适的位置,这个位置左边的数全部小于该数值,这个位置右边的数全部大于该数值 。

  1. 假设数组为 arr[l...r] 假设指定数值为数组第一个元素 int v = arr[l],假设 j 标记为比 v 小的最后一个元素, 即 arr[j+1] > v。当前考察的元素为 i 则有arr[l + 1 ... j] < v , arr[j+1,i) >= v 如上图所示。

  2. 假设正在考察的元素值为 e ,e >= v 的时候我们只需交将不动,直接 i++ 去考察下一个元素,

  3. 当e < v 由上述假设我们需要将 e 放在

单路快速排序的 Java 实现:

 private static void quickSort(int[] arr, int l, int r) {

        if (l >= r) {
            return;
        }
        // p 为 第一次 排序完成后 v 应该在的位置,即分治的划分点
        int p = partition(arr, l, r);

        quickSort(arr, l, p - 1);
        quickSort(arr, p + 1, r);
    }

    private static int partition(Integer[] arr, int l, int r) {

        // 为了提高效率,减少造成快速排序的递归树不均匀的概率,
        // 对于一个数组,每次随机选择的数为当前 partition 操作中最小最大元素的可能性为 1/n 
        int randomNum = (int) (Math.random() * (r - l + 1) + l);
        swap(arr, l, randomNum);

        int v = arr[l];
        int j = l;

        for (int i = l + 1; i <= r; i++) {
            if (arr[i] < v) {
                swap(arr, j + 1, i);
                j++;
            }
        }
        swap(arr, l, j);
        return j;
    }

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

对于上述算法中为什么选取了当前排序数组中随机一个元素进行比较,假设我们在考察的数组已经为已经排序好的数组,那么我们递归树就会向右侧延伸 N 的深度,这种情况使我们不想要看到的,如果我们每次 partition 都随机从数组中取一个数,那么这个数是当前排序数组中最小元素可能性为 1/n 那么每次都取到最小的数的可能性就很低了。

双路快速排序算法思想:

  1. 跟单路一样,双路快速排序,同样选择数组的第一个元素当做标志位(经过随机选择后的)

  2. 双路快速排序要求有两个指针,指针 i j 分别指向 l+1 和 r 的位置然后两者同时向数组中间遍历 在遍历过程中要保证arr[l+1 ... i) <= v, arr(j....r] >= v 因此我们可以初始化 i = l+1 以保证左侧区间初始为空,j = r 保证右侧空间为空

  3. 遍历过程中要 i <= r 且 arr[i] <= v 的时候 i ++ 就可以了 当 arr[i] > v 时表示遇到了 i 的值大于 v 数值 此刻能等待 j 角标的值,从右向左遍历数组 当 arr[i] < v 表示遇到了 j 的值小于 v 的元素,它不该在这个位置呆着,
  4. 得到了 i j 的角标后 先要判断是否到了循环结束的时候了,即 i 是否已经 大于 j 了。
  5. 否则 应该讲 i 位置的元素和 j 位置的元素交换位置,然后 i++ j– 继续循环
  6. 遍历结束的条件是 i>j 此时 arr[j]为最后一个小于 v 的元素 arr[i] 为第一个大于 v 的元素 因此 j 这个位置 就应该是 v 所应该在数组中的位置 因此遍历结束后需要交换 arr[l] 与 arr[j]

双路快速排序的 Java 实现:

 private static void quickSort(int[] arr, int l, int r) {

        if (l >= r) {
            return;
        }
        // 这里 p 为 小于 v 的最后一个元素,=v 的第一个元素 
        int p = partition(arr, l, r);

        quickSort(arr, l, p - 1);
        quickSort(arr, p + 1, r);
    }


    private static int partition(int[] arr, int l, int r) {
        // 为了提高效率,减少造成快速排序的递归树不均匀的概率,
        // 对于一个数组,每次随机选择的数为当前 partition 操作中最小最大元素的可能性降低

        int randomNum = (int) (Math.random() * (r - l + 1) + l);
        swap(arr, l, randomNum);

        int v = arr[l];

        int i = l + 1;
        int j = r;

        while (true) {

            while (i <= r && arr[i] <= v) i++;
            while (j >= l + 1 && arr[j] >= v) j--;

            if (i > j) break;

            swap(arr, i, j);
            i++;
            j--;
        }
        //j 最后角标停留在 i > j 即为 比 v 小的最后一个一元素位置
        swap(arr, l, j);

        return j;
    }

双路快速排序为最经常使用的快速排序实现,java 中对基本数据类型的排序 Arrays.sort() Collections.sort() 内部原理就是通过这种快速排序实现.

三路快速排序

上述两种算法我们发现对于与标志位相同的值得处理总是,做了多余的交换处理,如果我们能够将数组分为> = <三部分的话效率可能会有所提高。
如下图所示:

  1. 我们将数组划分为 arr[l+1...lt] <v arr[lt+1..i) =v arr[gt...r] > v三部分 其中 lt 指向 < v 的最后一个元素前一个元素,gt 指向>v的第一个元素的前一个元素,i 为当前考察元素

  2. 定义初始值得时候依旧可以保证这初始的时候这三部分都为空
    int lt = l;
    int gt = r;
    int i = l + 1;

  3. 当 e > v 的时候我们需要将 arr[i] 与 arr[gt-1] 交换位置,并将 > v 的部分扩大一个元素 即 gt-- 但是此时 i 指针并不需要操作,因为换过过来的数还没有被考察。
  4. 当 e = v 的时候 i ++ 继续考察下一个
  5. 当 e < v 的时候我们需要将 arr[i] 与 arr[lt+1] 交换位置
  6. 当循环结束的时候 lt 位于小于 v 的最后一个元素位置所以最后我们需要将arr[l] 与 arr[lt] 交换一下位置。如下图2所示

三路快速排序 Java 代码实现:


 private static void quickSort3(int[] num, int length) {
        quickSort(num, 0, length - 1);
    }

    private static void quickSort(int[] arr, int l, int r) {

        if (l >= r) {
            return;
        }

        // 为了提高效率,减少造成快速排序的递归树不均匀的概率,
        // 对于一个数组,每次随机选择的数为当前 partition 操作中最小最大元素的可能性 降低 1/n!

        int randomNum = (int) (Math.random() * (r - l + 1) + l);
        swap(arr, l, randomNum);

        int v = arr[l];
        // 三路快速排序即把数组划分为大于 小于 等于 三部分
        //arr[l+1...lt] <v  arr[lt+1..i) =v  arr[gt...r] > v 三部分
        // 定义初始值得时候依旧可以保证这初始的时候这三部分都为空
        int lt = l;
        int gt = r;
        int i = l + 1;

        while (i < gt) {
            if (arr[i] < v) {
                swap(arr, i, lt + 1);
                i++;
                lt++;
            } else if (arr[i] == v) {
                i++;
            } else {
                swap(arr, i, gt - 1);
                gt--;
                //i++ 注意这里 i 不需要加1 因为这次交换后 i 的值仍不等于 v 可能小于 v 也可能等于 v 所以交换完成后 i 的角标不变
            }
        }
        //循环结束的后 lt 所处的位置为 <v 的最后一个元素 i 肯定与 gt 重合
        //但是 最终v 要放的位置并不是 i 所指的位置 因为此时 i 为大于 v 的第一个元素 v
        //而 v 应该处的位置为 lt 位置 并不是 i-1 所处的位置(arr[i-1] = arr[l])
        swap(arr, l, lt);
    }

我们可以看到三路快速排序没有了递归的过程通过一次循环既可以完成排序。

快速排序时间复杂度空间复杂度

由于我们最常使用的是双路快排因此我们以此来分析:我们为了方便分析我们假定元素不是随机选取的而是取得数组第一个元素,在选取的标准元素和 partition 得到位置交换的时候,很有可能把前面的元素的稳定性打乱,

比如序列为 5 3 3 4 3 8 9 10 11

现在基准元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱。所以快速排序是一个不稳定的排序算法,不稳定发生在基准元素和a[partition]交换的时刻。

对于快速排序的时间度取决于其递归的深度,如果递归深度又决定于每次关键值得取值所以在最好的情况下每次都取到数组中间值,那么此时算法时间复杂度最优为 O(nlogn)。当然最坏情况就是之前我们分析的有序数组,那么每次都需要进行 n 次比较则 时间复杂度为 O(n²),但是在平均情况 时间复杂度为 O(nlogn),同样若想看详细的推到这里推荐一个链接 快速排序最好,最坏,平均复杂度分析

快速排序的空间复杂度主要取决于表示为选择的时候的临时空间,所以跟时间复杂度挂钩,所以平均的空间复杂度也是 O(nlogn)。

总结

本文总结了常见的排序算法的实现,通过研究这些算法的思想,也有助于算法题的解题思路。对于这几种算法都是需要我们熟练掌握的,但是 Android 工作平时不会接触太多的数据处理,因此我们需要刻意的去经常复习,本文的图片大部分来自于网上,如果有问题的话可以私信我删掉。如果文章所说的内容有技术问题也欢迎联系我。

简书地址
优快云
Github 地址

参考链接:
几种常见排序算法
常用排序算法稳定性、时间复杂度分析(转,有改动)
慕课网波波老师的数据结构课程

一次性搞懂排序算法,常用的排序算法总结 前言 今天对校招面试常问的排序算法,做个总结,如果觉得文章不错的话,希望点赞再看,谢谢!文末有福利哦,记得查看~ 前置知识 排序算法的稳定性 排序算法的稳定性是指:当输入元素中有两个元素的值相同时,排序后完成之后,这两个元素的前后位置是否发生了前后变化。 应用场景 1.订单排序 假设需要为用户按照时间顺序来展示订单, 相同时间点的订单按照订单金额顺序来展示。 实现上,可以先订单金额对订单排一次序, 再按时间对订单排一次序。 那么,这里排序算法的稳定性就体现用武之地了 假设按时间排序时不是稳定的,那么相同 阅读详情

相关推荐

冒泡排序(超详细)

1、什么是冒泡排序? 冒泡排序的英文Bubble Sort,是一种最基础的交换排序。之所以叫做冒泡排序,因为每一个元素都可以像小气泡一样,根据自身大小一点一点向数组的一侧移动。 冒泡排序的原理: 每一趟只能确定将一个数归位。即第一趟只能确定将末位上的数归位,第二趟只能将倒数第 2 位上的数归位,依次类推下去。如果有 n 个数进行排序,只需将 n-1 个数归位,也就是要进行 n-1 趟操作。 而 “每一趟 ” 都需要从第一位开始进行相邻的两个数的比较,将较大的数放后面,比较完毕之后向后挪一位继续比较下面

hcz666的博客 39万+

漫画:什么是冒泡排序?

————— 当天上午 —————什么是冒泡排序?冒泡排序的英文Bubble Sort,是一种最基础的交换排序。大家一定都喝过汽水,汽水中常常有许多小小的气泡,哗啦哗啦...

优快云资讯 6563

【校招】SHL 的 General Ability (GA, 通用能力测试)

SHL 的 General Ability (GA, 通用能力测试) 文章目录SHL 的 General Ability (GA, 通用能力测试)1、归纳推理题1.1 圈圈线条推理1.2 图形颜色位置推理1.3 箭头指向推理2. 数字推理题攻略3. 语言推理题 1、归纳推理题   归纳推理题我把它总结为三种类型:圈圈线条推理;图形颜色位置推理;箭头指向推理。 1.1 圈圈线条推理   你别看这个题目看起来很复杂,其实很简单,而且这个题型的题目背后原理都一样!我把我的 tips 偷偷分享给你们嘿嘿~先从 A

Sunnyside_ 2万+

刷题精选:顺时针输出递增数组(螺旋递增升天数组)

由上图描述,输入一个数字n,代表n行n列,之后经过函数之后顺时针的将从1到n打印出来。通过找规律,我们可以发现,最终的结果是一个正方形,那么我们可以联想到用一个二维数组按顺序来存储这些数字,最后输出二维数组即可,同时,这些数字是一圈一圈按照顺时针递增的,因此我们可以将这个二维数组拆开一圈一圈来看,我们画图来表示一下 以n=5为例 最后的二维数组需要储存成这个样子,那么我们可以将其分为三圈 第一圈: 第二圈: 第三圈: 我们如果按照圈来储存的话,那么他们按照顺时针存储的规律就是一样的了,即先顺序的储存

bit_zhy的博客 384

Claude Code 完整使用教程(2026最新版)

《Claude Code 完整使用教程(2026最新版)》摘要: Claude Code 是 Anthropic 公司推出的智能编程工具,通过自然语言对话实现代码编写、调试和重构。本教程基于 v2.1.101 版本,涵盖安装配置、核心功能及最新特性。主要内容包括: 多平台安装方式(macOS/Linux/Windows) 桌面应用专属功能如并行会话、定时任务和语音模式 模型选择指南(Sonnet/Opus/Haiku) 快速入门示例:5分钟创建Node.js项目 核心功能:代码读写、命令执行、Git集成等

程序猿_三产 1万+

[亚信线上教学] AX58100: EtherCAT从站设备仿真功能设计教学

什么情况需要开启EtherCAT从站设备仿真功能呢? 一般来说, 当EtherCAT从站产品,不需要使用额外的微控制器介入,或使用不具备EtherCAT从站通讯协议栈的微控制器一同协作的情况下,就必须开启EtherCAT从站设备仿真功能,这样EtherCAT主站才能够正常设置从站设备的工作状态. 亚信AX58100 EtherCAT从站控制芯片的优点 AX58100 EtherCAT从站控制器, 集成2个百兆以太网Phis, 并支持一些额外的控制接口, 例如脉冲宽度调变, 增量, 霍尔编码器, .

allanchou1的专栏 7422

【数据结构】排序

各种排序(冒泡、插入、希尔、选择、堆、快速、归并、计数)的分析+代码+动图

2301_80277275的博客 1654

安信可TB蓝牙模组系列 实战Ble Mesh智能照明系统搭建

本文详细介绍了如何使用安信可TB蓝牙模组系列搭建Ble Mesh智能照明系统。从硬件准备、固件烧录到手机APP组网全流程,提供了实战步骤和优化建议,特别适合智能家居开发者入门。Ble Mesh的自组网能力解决了蓝牙传输距离限制,实现全屋灯光智能控制。

weixin_30856725的博客 352

排序算法——一篇文章搞懂常用的排序算法

1.排序的概念及应用 1.1排序的概念 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。 内部排序...

海盗船长y的博客 3448

STM32固定翼增稳飞控设计:姿态解算、双环PID与一键救机实现

姿态稳定控制是无人机与航模飞控系统的核心基础,其本质是通过传感器融合(如IMU)实时感知飞行器姿态,并基于控制理论(如PID)生成修正指令以抑制扰动。该技术广泛应用于教育航模、轻型固定翼平台及嵌入式飞控开发场景,具有低延迟、高确定性、资源敏感等典型工程特征。本文围绕STM32F401与MPU6050构建的硬件闭环系统,深入解析互补滤波姿态解算原理与俯仰/横滚双环PID控制结构,同时聚焦‘一键救机’这一面向新手的关键安全机制——通过分阶段角速率抑制与平滑角度拉平策略,在80ms内启动响应,显著提升飞行容错能力

weixin_36328210的博客 400

基于Web的智能家居(源码+开题)

实践上,智能家居系统的普及和应用将极大改善人们的居住体验,提高生活质量,同时也有助于促进智能家居产业链的发展和完善,为社会经济发展注入新的活力。此外,本研究还将对基于Web的智能家居系统的用户体验进行深入研究,通过用户调研和数据分析,不断优化系统功能和界面设计,提升用户的使用体验和满意度。针对以上问题,本研究将综合运用物联网、云计算、大数据等新一代信息技术,通过系统架构设计、功能模块开发、安全性保障、用户体验优化等多个方面的研究工作,力求为基于Web的智能家居系统的研发和应用提供全面而有效的解决方案。

sryue06的博客 1902

那些年,让我面试头大的几个排序算法,今天终于搞懂了!

作者 |逆流的鱼yuiop转载自何俊林(ID:smartyuge)算法上,最基础的就是排序算法,几乎在面试中,或多或少会要求你手写一些基础算法。今天鱼哥带大家这些基础算...

AI科技大本营 5387

新手必看:LiteRadio 2c SE遥控器从开箱到飞行的完整指南(附常见问题解答)

本文为新手提供LiteRadio 2c SE遥控器的完整使用指南,涵盖从开箱到首飞的20个关键步骤。详细介绍了遥控器的功能设置、与无人机的对频技巧以及首飞注意事项,帮助用户快速掌握飞行控制,避免常见错误。特别适合无人机初学者阅读。

weixin_29209801的博客 378

【排序算法】一篇文章带你搞懂所有排序!

排序算法

2403_87459748的博客 2142

别再花屏了!手把手教你用STM32CubeIDE搞定LVGL 8.3移植(附完整源码)

本文详细介绍了如何在STM32CubeIDE环境下成功移植LVGL 8.3,解决常见的花屏问题。从环境配置、显示驱动优化到编译警告处理,提供完整的实战方案和源码示例,帮助开发者快速实现流畅的嵌入式GUI界面。特别针对STM32平台的DMA2D加速和内存管理进行了深度优化。

weixin_33739646的博客 241

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

彻底搞懂排序算法!从稳定排序到不稳定排序的全面解析

排序算法是编程世界的基础,也是面试中的常客。但你真的搞清楚了哪些算法是的,哪些是的吗?别担心,这篇文章将带你轻松搞懂常见排序算法,并彻底掌握“稳定”和“不稳定”排序的核心区别。无论你是新手还是想加深理解,本文都将是你不可错过的宝藏。

qq_53199129的博客 3000

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

彻底搞懂排序算法,看这篇就够了

文章目录1.排序算法的介绍和分类2.算法的时间复杂度和空间复杂度2.1 时间频度忽略常数、低次项、系数的说明2.2 时间复杂度的计算2.3 常见的时间复杂度2.3.1 常数阶O( 1 )2.3.2 对数阶O( log2n )2.3.3 线性阶O( n )2.3.4 线性对数阶O( nlogN )2.3.5 平方阶O( n² )2.3.6 立方阶O( n³ )、K次方阶O( n^k )2.4 算法的平均时间复杂度和最坏时间复杂度2.5 空间复杂度3.冒泡排序3.1 代码实现4.选择排序4.1 代码实现5.插入

JavaAlenboy 1159

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

常见的初级排序算法,这次全搞懂

本文已被Github仓库收录 https://github.com/silently9527/JavaCore 程序员常用的IDEA插件:https://github.com/silently9527/ToolsetIdeaPlugin 完全开源的淘客项目:https://github.com/silently9527/mall-coupons-server 微信公众号:贝塔学Java 前言 相信所有的程序员刚开始接触到的算法都会是排序算法,因为排序在对数据处理和计算有这重要的地位,排序算法往往是其他算.

Silently9527’s Blog 175

一篇搞懂 Java 常用排序算法:从原理到实现

本文详细介绍了8种常用排序算法及其Java实现,包括插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序和计数排序。每种算法从核心思想、实现代码到性能分析(时间复杂度、空间复杂度、稳定性)进行全方位解析,并针对特定算法提供优化版本(如快速排序的递归和非递归实现)。文章还特别介绍了排序算法的基础概念(稳定性、时空复杂度等),帮助读者全面理解排序算法的特性和适用场景。所有代码均封装完整,可直接运行测试,适合需要系统学习排序算法的开发者和算法爱好者。

2402_86887479的博客 1834

一次搞懂排序算法!!!

一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。说明:平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(m*n)

开源驱动,创造价值;坦诚清晰,快速失败;让复杂可计算。 481

一文搞懂Java中的排序算法

Java中的排序算法简介:初学者指南

u013643074的博客 243

20张图带你搞懂十大经典排序算法

交换类:通过元素之间的两两交换来实现排序 插入类:将数分为2部分,依次将无序的数插入到有序的数列中 选择类:从待排序数列中找到最小值或者最大值元素,放到已拍好序的序列后面 「计数排序和基数排序可以认为是桶排序的一种特殊实现,都不是通过元素之间的比较来实现排序的」

2401_83384536的博客 1174
上一篇: 搞懂 JAVA 内部类
下一篇: 搞懂单链表常见面试题
Yoda_wang
博客等级 码龄11年 60粉丝 37原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值