Java 数据结构:七大排序算法,从 O(n²) 到 O(n log n)

本文系统讲解Java中七大基于比较的排序算法:插入排序(直接插入、希尔)、选择排序(直接选择、堆)、交换排序(冒泡、快速)、归并排序,涵盖基本思想、Java代码实现、时间/空间复杂度及稳定性分析;同时介绍计数、基数、桶等非比较排序及其适用场景;重点对比各算法性能特征,支持面试高频考点与工程选型决策。

排序是数据结构里最"实在"的一章:七大排序,面试必问,代码必须能手写。这篇笔记按"概念 → 插入 → 选择 → 交换 → 归并 → 复杂度对比 → 非比较排序 → 选择题"的顺序整理,每种排序都给了可以直接运行的 Java 实现,最后附上复杂度/稳定性总表和 6 道选择题的详细解析。

本节目标

  1. 掌握七大基于比较的排序算法基本原理及实现
  2. 掌握排序算法的性能分析
  3. 掌握 Java 中的常用排序方法

一、排序的基本概念

1.1 什么是排序

排序:使一串记录,按照其中的某个或某些关键字的大小,递增或递减地排列起来的操作。

比如把 {9, 5, 2, 7, 3, 6, 4, 5, 8, 0} 从小到大排列,得到 {0, 2, 3, 4, 5, 5, 6, 7, 8, 9}。

排序算法要解决的问题很朴素,但"怎么排得快、排得稳、排得省空间"衍生出了整整一章的内容。

1.2 稳定性:排序算法的"人品"

稳定性:假定在待排序的记录序列中,存在多个具有相同关键字的记录,若经过排序,这些记录的相对次序保持不变,则称这种排序算法是稳定的;否则是不稳定的。

看一个例子:原序列里有两个 5,分别记为 5a 和 5b,5a 在 5b 前面。


▲ 排序后 5a 仍在 5b 前面,说明算法稳定;顺序反过来,就是不稳定的

稳定性只在"相同关键字"上才有意义。对纯整数排序,稳不稳定似乎无所谓;但如果是按"成绩"排序学生记录,成绩相同的同学还希望保持原来的学号顺序,这时稳定性就很重要了。

记住一句话:稳定看"相等元素会不会被交换到彼此后面"。

1.3 内部排序与外部排序

  • 内部排序:数据元素全部放在内存中的排序。
  • 外部排序:数据元素太多,不能同时放在内存中,排序过程中需要不断在内存和外存之间移动数据。

七大排序讲的都是内部排序;后面归并排序会延伸出"1G 内存怎么排 100G 数据"的外部排序问题。

1.4 七大排序的分类

常见的基于比较的排序算法可以分成四类:


▲ 插入、选择、交换、归并四大类,共七种排序

大类具体算法核心动作
插入排序直接插入排序、希尔排序把元素插入到已有序的区间
选择排序直接选择排序、堆排序每轮选出最大/最小元素
交换排序冒泡排序、快速排序通过交换把元素放到正确位置
归并排序归并排序合并两个有序序列

二、插入排序

2.1 直接插入排序

2.1.1 基本思想

直接插入排序的思想,就是打扑克牌时整理手牌:


▲ 手里的牌已经有序,新摸到一张,就把它插到合适的位置

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有记录插入完为止,得到一个新的有序序列。

具体到数组:当插入第 i(i >= 1)个元素时,前面的 array[0] … array[i-1] 已经排好序。此时用 array[i] 的关键码与 array[i-1]、array[i-2] … 依次比较,找到插入位置后把 array[i] 插入,原来位置上的元素顺序后移。

2.1.2 代码实现
public static void insertSort(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        int key = arr[i];          // 待插入的元素
        int j = i - 1;
        // 从后往前找位置,比 key 大的元素统一后移
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}

以 {5, 3, 8, 1} 为例:

  1. i = 1,key = 3,5 > 3,5 后移,3 放到下标 0 → {3, 5, 8, 1};
  2. i = 2,key = 8,8 > 5,直接放在原位 → {3, 5, 8, 1};
  3. i = 3,key = 1,8、5、3 依次后移,1 放到下标 0 → {1, 3, 5, 8}。
2.1.3 特性总结
  1. 元素集合越接近有序,直接插入排序效率越高;
  2. 时间复杂度:最好 O(N)(已经有序),最坏和平均 O(N²);
  3. 空间复杂度:O(1);
  4. 稳定性:稳定(代码里用 arr[j] > key,相等元素不会继续后移)。

直接插入排序是后面希尔排序的基础,也是很多库函数在"数据量很小"时采用的排序方式。

2.2 希尔排序(缩小增量排序)

2.2.1 基本思想

直接插入排序在"数据基本有序"时非常快,但在"完全逆序"时要移动很多次。希尔排序的思路是:先让数组变得基本有序,再做一次直接插入排序。

具体做法:先选定一个整数 gap,把待排序的所有记录分成 gap 组,所有距离为 gap 的记录分在同一组,并对每一组内的记录进行插入排序;然后缩小 gap,重复上述分组和排序;当 gap == 1 时,所有记录在同一组内做最后一次直接插入排序。


▲ 初始序列按 gap = 5、2、1 三轮分组插入,最后整体有序

从图里可以看到:

  • gap = 5 时,相隔 5 个位置的元素两两一组,组内排序,让小的元素大步往前跳;
  • gap = 2 时,组更小,数组已经比之前有序;
  • gap = 1 时,就是普通的直接插入排序,但因为前面已经"预排序"过,移动次数大大减少。
2.2.2 代码实现
public static void shellSort(int[] arr) {
    int gap = arr.length;
    while (gap > 1) {
        gap = gap / 3 + 1;       // Knuth 增量序列的写法
        shellInsert(arr, gap);   // 对每个 gap 分组做插入排序
    }
}

private static void shellInsert(int[] arr, int gap) {
    for (int i = gap; i < arr.length; i++) {
        int key = arr[i];
        int j = i - gap;
        while (j >= 0 && arr[j] > key) {
            arr[j + gap] = arr[j];
            j -= gap;
        }
        arr[j + gap] = key;
    }
}

shellInsert 和直接插入排序几乎一样,只是把 1 换成了 gap:每次不是和前一个元素比较,而是和"同组的前一个元素"比较。

2.2.3 gap 的取法

gap 的取法有很多种,课件里提到:

  • 最初 Shell 提出取 gap = n / 2,然后 gap = gap / 2,直到 gap = 1;
  • Knuth 提出取 gap = gap / 3 + 1;
  • 也有人提出都取奇数好,或者各 gap 互质好。


▲ n = 9、gap = 3 时分成 3 组,每组 n/gap = 3 个数据

无论哪一种取法,都必须保证最后一个 gap 等于 1,否则最后一趟不是完整的插入排序,数组不可能整体有序。

2.2.4 时间复杂度

希尔排序的时间复杂度不好精确计算,因为它是 gap 序列的函数。课件里用"分组移动次数"做了估算:

  • gap = n/3 时,移动总数约为 n;
  • gap = n/9 时,移动总数约为 4n;
  • 最后一趟 gap = 1,移动次数约为 n。


▲ 一趟比较次数随 gap 先升后降,整体很难给出精确的渐进界

所以很多教材给出的希尔排序复杂度并不统一:

  • 严蔚敏《数据结构(C 语言版)》给出大约 O(n^1.3);
  • Knuth 通过大量实验统计,认为平均比较和移动次数大约在 n^1.25 ~ 1.6 n^1.25;
  • 课件表格里记的是"最好 O(n)、最坏 O(n²)"。

考试时按教材/课件记;面试时回答"依赖增量序列,常见说法是 O(n^1.3) 左右,最坏 O(n²)"即可。

2.2.5 特性总结
  1. 希尔排序是对直接插入排序的优化;
  2. gap > 1 时都是预排序,目的是让数组更接近有序;gap == 1 时再做一次插入排序,此时数组已经接近有序,很快;
  3. 时间复杂度:依赖 gap 序列,约 O(n^1.3),最坏 O(n²);
  4. 空间复杂度:O(1);
  5. 稳定性:不稳定(相同元素可能被分到不同组,相对顺序会改变)。

三、选择排序

3.1 直接选择排序

3.1.1 基本思想

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

具体步骤:

  1. 在 array[i] ~ array[n-1] 中选择关键码最小(大)的数据元素;
  2. 若它不是这组元素中的第一个(最后一个)元素,则将它和这组元素的第一个(最后一个)元素交换;
  3. 在剩余的 array[i+1] ~ array[n-1] 集合中,重复上述步骤,直到集合剩余 1 个元素。
3.1.2 代码实现
public static void selectSort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        int min = i;
        // 在 [i, n-1] 中找最小元素的下标
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[min]) {
                min = j;
            }
        }
        swap(arr, i, min);
    }
}

小优化:一趟循环里同时找最小值和最大值,分别放到两端,循环次数可以减少一半。但比较次数仍然是 O(N²),复杂度不变。

3.1.3 特性总结
  1. 思路非常直观,但效率不高,实际中很少单独使用;
  2. 时间复杂度:最好、最坏、平均都是 O(N²)(无论数据是否有序,都要完整比较);
  3. 空间复杂度:O(1);
  4. 稳定性:不稳定。

为什么不稳定?例如 {5a, 5b, 2},第一轮把最小的 2 和 5a 交换,得到 {2, 5b, 5a},5a 和 5b 的相对顺序就变了。

3.2 堆排序

堆排序是选择排序的一种:利用堆这种数据结构来"选数"。排升序建大堆,排降序建小堆。


▲ 建大堆后,每次把堆顶换到末尾,再向下调整,末尾逐渐有序

核心步骤:

  1. 建堆:从最后一个非叶子节点开始,从后往前向下调整,O(N);
  2. 排序:每次把堆顶(最大值)与末尾元素交换,有效长度减一,再对新堆顶向下调整;
  3. 重复 N-1 次,数组升序。
public static void heapSort(int[] arr) {
    // 1. 建大堆
    for (int root = (arr.length - 2) / 2; root >= 0; root--) {
        shiftDown(arr, arr.length, root);
    }
    // 2. 每轮把堆顶(最大值)换到末尾,再调整剩余部分
    for (int end = arr.length - 1; end > 0; end--) {
        swap(arr, 0, end);
        shiftDown(arr, end, 0);
    }
}

private static void shiftDown(int[] arr, int size, int parent) {
    int child = 2 * parent + 1;
    while (child < size) {
        if (child + 1 < size && arr[child + 1] > arr[child]) {
            child++;
        }
        if (arr[parent] >= arr[child]) {
            break;
        }
        swap(arr, parent, child);
        parent = child;
        child = 2 * parent + 1;
    }
}

特性总结:

  1. 堆排序使用堆来选数,效率比直接选择排序高很多;
  2. 时间复杂度:O(N*logN)(建堆 O(N),N 次调整每次 O(logN));
  3. 空间复杂度:O(1);
  4. 稳定性:不稳定。

堆排序的细节(向下调整、建堆、插入删除)在上一讲《优先级队列与堆》里已经拆过,这里只保留排序相关的主干。


四、交换排序

基本思想:根据序列中两个记录键值的比较结果,对换这两个记录在序列中的位置。交换排序的特点是:将键值较大的记录向序列尾部移动,键值较小的记录向序列前部移动。

4.1 冒泡排序

4.1.1 基本思想

从前往后,两两比较相邻元素,如果前一个比后一个大就交换。每一趟都会把当前未排序部分的最大值"冒"到末尾,像气泡上浮一样。

public static void bubbleSort(int[] arr) {
    for (int end = arr.length - 1; end > 0; end--) {
        boolean sorted = true;
        for (int i = 0; i < end; i++) {
            if (arr[i] > arr[i + 1]) {
                swap(arr, i, i + 1);
                sorted = false;
            }
        }
        // 本趟没有发生交换,说明已经有序,可以提前结束
        if (sorted) {
            break;
        }
    }
}
4.1.2 特性总结
  1. 非常容易理解,适合入门;
  2. 时间复杂度:最好 O(N)(已经有序 + 提前结束),最坏和平均 O(N²);
  3. 空间复杂度:O(1);
  4. 稳定性:稳定(只有 > 才交换,相等元素不会互换)。

4.2 快速排序

快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法。

基本思想:任取待排序元素序列中的某元素作为基准值(pivot),按照该基准值将待排序集合分割成两个子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后对左右子序列重复该过程,直到所有元素都排列在相应位置上。

4.2.1 递归框架
public static void quickSort(int[] arr) {
    quickSort(arr, 0, arr.length - 1);
}

private static void quickSort(int[] arr, int left, int right) {
    if (left >= right) {
        return;                       // 区间只剩 0 或 1 个元素
    }
    int div = partition(arr, left, right);  // 划分,返回基准值的下标
    quickSort(arr, left, div - 1);          // 递归排左半部分
    quickSort(arr, div + 1, right);         // 递归排右半部分
}

这个框架和二叉树的前序遍历非常像:先处理根(划分),再递归处理左子树、右子树。写递归框架时,照着前序遍历的模板套即可,剩下的问题就是"怎么划分"。

4.2.2 划分方式一:Hoare 版

Hoare 版用两个指针 i、j 从两端向中间走:

  • j 从右往左找小于基准值的元素;
  • i 从左往右找大于基准值的元素;
  • 找到后交换,直到 i 和 j 相遇;
  • 最后把基准值和相遇位置交换。


▲ L 和 R 两个哨兵从两端出发,相向而行

private static int partitionHoare(int[] arr, int left, int right) {
    int i = left;
    int j = right;
    int pivot = arr[left];
    while (i < j) {
        // 先从右边找小于 pivot 的元素
        while (i < j && arr[j] >= pivot) {
            j--;
        }
        // 再从左边找大于 pivot 的元素
        while (i < j && arr[i] <= pivot) {
            i++;
        }
        swap(arr, i, j);
    }
    swap(arr, i, left);
    return i;
}

注意:一定要先从右边开始找。如果先从左边找,相遇位置可能比基准值大,最后交换会破坏"左小右大"的性质。

4.2.3 划分方式二:挖坑法

挖坑法把基准值先"挖"出来存到临时变量里,数组里留下一个坑位;然后右边找小的填左坑,左边找大的填右坑,最后把基准值填进最后的坑。


▲ 先把第一个数存进 key,形成一个坑位,再左右交替填坑

private static int partitionHole(int[] arr, int left, int right) {
    int i = left;
    int j = right;
    int pivot = arr[left];      // 挖出基准值,left 位置成为坑
    while (i < j) {
        // 右边找小于 pivot 的,填到左边的坑
        while (i < j && arr[j] >= pivot) {
            j--;
        }
        arr[i] = arr[j];
        // 左边找大于 pivot 的,填到右边的坑
        while (i < j && arr[i] <= pivot) {
            i++;
        }
        arr[j] = arr[i];
    }
    arr[i] = pivot;             // 基准值填回最后一个坑
    return i;
}

挖坑法比 Hoare 版少了几次交换,写起来也更直观,是很多教材和面经里推荐的写法。

4.2.4 划分方式三:前后指针

前后指针用 prev 和 cur 两个指针:

  • prev 指向"小于基准值的最后一个元素";
  • cur 从 left + 1 开始向后遍历;
  • 遇到小于基准值的元素,就把它换到 ++prev 的位置;
  • 遍历结束后,把基准值换到 prev 位置。


▲ prev 指向序列开头,cur 指向 prev 的后一个位置

private static int partitionTwoPointers(int[] arr, int left, int right) {
    int prev = left;
    int cur = left + 1;
    while (cur <= right) {
        // cur 找到小于基准值的元素,就把它换到前面
        if (arr[cur] < arr[left] && arr[++prev] != arr[cur]) {
            swap(arr, cur, prev);
        }
        cur++;
    }
    swap(arr, prev, left);
    return prev;
}

arr[++prev] != arr[cur] 是为了避免自己和自己交换,属于一个小优化。

4.2.5 快速排序的优化
  1. 三数取中法选 key:取 left、mid、right 三个位置的中间值作为基准值,避免"数据已经有序时,每次取到端点导致退化成 O(N²)"。
  2. 递归到小的子区间时改用插入排序:小区间用快排递归的 overhead 比较大,通常在区间长度小于 15 左右时改用直接插入排序。
  3. 三路划分(进阶):把区间分成 < pivot、== pivot、> pivot 三部分,处理大量重复元素时效果显著。

三数取中的代码:

int mid = left + (right - left) / 2;
if (arr[left] > arr[mid]) swap(arr, left, mid);
if (arr[left] > arr[right]) swap(arr, left, right);
if (arr[mid] > arr[right]) swap(arr, mid, right);
// 此时 arr[mid] 是中间值,把它换到 left 作为基准值
swap(arr, left, mid);
4.2.6 快速排序非递归

递归快排的递归深度最坏是 O(N),数据量很大时可能栈溢出。可以用栈模拟递归过程:

public static void quickSortNonR(int[] arr) {
    Deque<Integer> stack = new ArrayDeque<>();
    stack.push(0);
    stack.push(arr.length - 1);
    while (!stack.isEmpty()) {
        int right = stack.pop();
        int left = stack.pop();
        if (left >= right) {
            continue;
        }
        int div = partitionHoare(arr, left, right);
        // 先压右区间,再压左区间(顺序无所谓,能遍历到即可)
        stack.push(left);
        stack.push(div - 1);
        stack.push(div + 1);
        stack.push(right);
    }
}
4.2.7 快速排序总结
  1. 快速排序整体的综合性能和使用场景都很好,所以叫"快速排序";
  2. 时间复杂度:最好和平均 O(N*logN),最坏 O(N²)(数据有序且固定取端点);
  3. 空间复杂度:O(logN) 到 O(N)(递归栈的深度);
  4. 稳定性:不稳定。

面试常问:“快排最坏什么时候发生?怎么避免?” 答:数据已经有序、每次取到端点时退化为冒泡;用三数取中、随机取 key、小区间插入排序可以缓解。


五、归并排序

5.1 基本思想

归并排序(MERGE-SORT)建立在归并操作上,是分治法的典型应用:将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。


▲ 先不断二分"分解",再两两"合并",最终得到整体有序

核心步骤只有两步:

  1. 分解:把数组从中间分成两半,递归地对左右两半排序;
  2. 合并:把两个已经有序的子数组合并成一个有序数组。

5.2 递归实现

public static void mergeSort(int[] arr) {
    int[] tmp = new int[arr.length];   // 归并需要 O(N) 的辅助空间
    mergeSort(arr, 0, arr.length - 1, tmp);
}

private static void mergeSort(int[] arr, int left, int right, int[] tmp) {
    if (left >= right) {
        return;
    }
    int mid = left + (right - left) / 2;
    mergeSort(arr, left, mid, tmp);        // 排左半部分
    mergeSort(arr, mid + 1, right, tmp);   // 排右半部分
    merge(arr, left, mid, right, tmp);     // 合并两个有序区间
}

private static void merge(int[] arr, int left, int mid, int right, int[] tmp) {
    int i = left;       // 左区间的指针
    int j = mid + 1;    // 右区间的指针
    int k = left;       // tmp 的指针
    while (i <= mid && j <= right) {
        // 用 <= 保证稳定性:相等时先取左边的
        if (arr[i] <= arr[j]) {
            tmp[k++] = arr[i++];
        } else {
            tmp[k++] = arr[j++];
        }
    }
    while (i <= mid) {
        tmp[k++] = arr[i++];
    }
    while (j <= right) {
        tmp[k++] = arr[j++];
    }
    // 把合并结果拷回原数组
    for (int t = left; t <= right; t++) {
        arr[t] = tmp[t];
    }
}

5.3 非递归实现

递归是"从上往下"二分,非递归可以"从下往上":先让相邻的 2 个元素有序,再让相邻的 4 个有序,再 8 个……直到整个数组。

public static void mergeSortNonR(int[] arr) {
    int n = arr.length;
    int[] tmp = new int[n];
    for (int gap = 1; gap < n; gap *= 2) {
        for (int left = 0; left < n; left += 2 * gap) {
            int mid = Math.min(left + gap - 1, n - 1);
            int right = Math.min(left + 2 * gap - 1, n - 1);
            if (mid >= right) {
                continue;   // 右区间为空,不需要合并
            }
            merge(arr, left, mid, right, tmp);
        }
    }
}

5.4 特性总结

  1. 归并的缺点在于需要 O(N) 的空间复杂度;
  2. 时间复杂度:最好、最坏、平均都是 O(N*logN)(每次合并都是 O(N),共 logN 层);
  3. 空间复杂度:O(N);
  4. 稳定性:稳定(合并时相等元素先取左边)。

归并排序是少有的"无论数据长什么样都稳定 O(NlogN)"的排序,代价是要额外空间。

5.5 海量数据的排序:外部排序

外部排序:排序过程需要在磁盘等外部存储进行的排序。

举个经典问题:内存只有 1G,需要排序的数据有 100G,怎么办?

因为内存放不下全部数据,所以必须用外部排序,而归并排序是最常用的外部排序:

  1. 先把文件切分成 200 份,每个 512M;
  2. 分别对每个 512M 排序(内存放得下,任意排序方式都可以),得到 200 个有序小文件;
  3. 进行二路归并(或多路归并),同时对 200 份有序文件做归并,最终结果就有序了。

进一步优化:二路归并需要多趟读写磁盘,实际工程中常用多路归并 + 小根堆(每次从 k 个文件头中取最小),减少磁盘 I/O 次数。


六、七大排序复杂度与稳定性总表

这张表必须背下来,考试和面试都直接考:

排序方法最好平均最坏空间复杂度稳定性
冒泡排序O(n)O(n²)O(n²)O(1)稳定
直接插入排序O(n)O(n²)O(n²)O(1)稳定
直接选择排序O(n²)O(n²)O(n²)O(1)不稳定
希尔排序O(n)约 O(n^1.3)O(n²)O(1)不稳定
堆排序O(n log n)O(n log n)O(n log n)O(1)不稳定
快速排序O(n log n)O(n log n)O(n²)O(log n) ~ O(n)不稳定
归并排序O(n log n)O(n log n)O(n log n)O(n)稳定

几个记忆点:

  • 稳定的只有三个:冒泡、插入、归并(口诀:“冒插归,稳如狗”);
  • 空间 O(1) 的:冒泡、插入、选择、希尔、堆;
  • 空间 O(n) 的:归并;
  • 快排最坏 O(n²),但平均性能最好;
  • 希尔排序的复杂度与 gap 序列有关,表中"最好 O(n)"按课件/教材记,实际常见说法是 O(n^1.3) 左右。

面试追问:

  1. “快排和归并都是 O(nlogn),怎么选?”——快排原地、常数小、实际更快,但最坏 O(n²) 且不稳定;归并稳定、最坏也有保证,但要 O(n) 空间,适合链表排序和外排序。
  2. “堆排序既然是 O(nlogn) 且 O(1),为什么实际不如快排?”——堆排序的常数大,而且跳跃访问对缓存不友好。
  3. “为什么说快排是二叉树结构?”——每次划分把区间分成左右两半,递归过程就是一棵二叉树,最好情况树高 logN,最坏退化成链表高度 N。

七、其他非基于比较排序(了解)

前面七种都是基于比较的排序,比较排序的下界是 O(NlogN)。如果数据有特殊性质(比如都是小范围整数),可以突破这个下界。

7.1 计数排序

思想:计数排序又叫鸽巢原理,是对哈希直接定址法的变形应用。

步骤:

  1. 统计相同元素出现的次数;
  2. 根据统计结果,把序列回收到原来的序列中。
public static void countSort(int[] arr) {
    if (arr == null || arr.length <= 1) {
        return;
    }
    // 1. 找到最小值和最大值,确定计数范围
    int min = arr[0], max = arr[0];
    for (int v : arr) {
        if (v < min) min = v;
        if (v > max) max = v;
    }
    // 2. 统计每个元素出现的次数(用 min 做偏移,支持负数)
    int[] count = new int[max - min + 1];
    for (int v : arr) {
        count[v - min]++;
    }
    // 3. 按顺序回填
    int idx = 0;
    for (int v = 0; v < count.length; v++) {
        while (count[v]-- > 0) {
            arr[idx++] = v + min;
        }
    }
}

特性总结:

  • 数据范围集中时效率很高,但适用范围和场景有限(只适合整数、范围不大);
  • 时间复杂度:O(N + 范围);
  • 空间复杂度:O(范围);
  • 稳定性:可以做到稳定(用前缀和 + 倒序回填的版本,相同元素保持原顺序)。

7.2 基数排序

按位排序:从最低位(个位)开始,依次按每一位做一次稳定排序,直到最高位。因为每一位的排序是稳定的,所以最终整体有序。

  • 时间复杂度:O(d * (N + r)),d 是位数,r 是进制;
  • 空间复杂度:O(N + r);
  • 稳定性:稳定;
  • 适合固定位数的整数、定长字符串等。

7.3 桶排序

把数据分到若干个有序的桶里,每个桶内部再排序,最后按桶的顺序合并。

  • 平均时间复杂度:O(N + k),k 是桶的个数;
  • 最坏情况:所有数据落到一个桶里,退化为 O(N²);
  • 空间复杂度:O(N + k);
  • 稳定性:取决于桶内排序算法是否稳定。

这三种非比较排序了解思想即可,面试偶尔会问"什么场景下能突破 O(NlogN)"。


八、选择题

1. 快速排序算法是基于( )的一个排序算法。

A:分治法  B:贪心法  C:递归法  D:动态规划法

答案:A

快排把大区间划分成两个子区间,分别递归求解,是典型的分治思想。递归只是实现手段,不是算法思想本身。

2. 对记录(54,38,96,23,15,72,60,45,83)进行从小到大的直接插入排序时,当把第 8 个记录 45 插入到有序表时,为找到插入位置需比较( )次?(采用从后往前比较)

A:3  B:4  C:5  D:6

答案:C

前 7 个记录插入排序后:15, 23, 38, 54, 60, 72, 96。

插入 45,从后往前比较:96(1 次)→ 72(2 次)→ 60(3 次)→ 54(4 次)→ 38(5 次)。45 > 38,停止,插到 38 后面。共 5 次。

3. 以下排序方式中占用 O(n) 辅助存储空间的是( )

A:简单排序  B:快速排序  C:堆排序  D:归并排序

答案:D

归并排序需要和原数组等大的临时数组,空间 O(n);快排是 O(logN)~O(N),堆排序是 O(1)。

4. 下列排序算法中稳定且时间复杂度为 O(n²) 的是( )

A:快速排序  B:冒泡排序  C:直接选择排序  D:归并排序

答案:B

冒泡稳定且 O(n²);快排不稳定,选择排序不稳定,归并是 O(nlogn)。

5. 关于排序,下面说法不正确的是( )

A:快排时间复杂度为 O(N*logN),空间复杂度为 O(logN)
B:归并排序是一种稳定的排序,堆排序和快排均不稳定
C:序列基本有序时,快排退化成"冒泡排序",直接插入排序最快
D:归并排序空间复杂度为 O(N),堆排序空间复杂度的为 O(logN)

答案:D

堆排序是原地排序,空间复杂度为 O(1),不是 O(logN)。

6. 设一组初始记录关键字序列为(65,56,72,99,86,25,34,66),则以第一个关键字 65 为基准而得到的一趟快速排序结果是( )

A:34,56,25,65,86,99,72,66
B:25,34,56,65,99,86,72,66
C:34,56,25,65,66,99,86,72
D:34,56,25,65,99,86,72,66

答案:A

按课件的挖坑法走一遍(基准值 65,坑在 0 号位):

  1. j 从右往左:66 ≥ 65,跳过;34 < 65,填到 0 号坑 → 34,56,72,99,86,25,_,66;
  2. i 从左往右:34、56 ≤ 65,到 72 停;72 填到 6 号坑 → 34,56,_,99,86,25,72,66;
  3. j 继续:72 ≥ 65,25 < 65,填到 2 号坑 → 34,56,25,99,86,_,72,66;
  4. i 继续:25 ≤ 65,99 > 65,填到 5 号坑 → 34,56,25,_,86,99,72,66;
  5. j 继续:99、86 都 ≥ 65,i 和 j 在 3 号位相遇,65 填入 → 34,56,25,65,86,99,72,66。

得到 A。

不同划分写法(Hoare、前后指针)左右区间内部的顺序可能不同,但基准值 65 一定在最终分界位置;考试时按题目对应的划分方法走。


九、小结

这一讲的内容可以浓缩成一张"选择排序算法"的决策图:

  1. 数据量小、基本有序:直接插入排序,简单又快;
  2. 数据量小、允许不稳定:选择排序,交换次数少;
  3. 数据量大、要求原地:快速排序(三数取中 + 小区间插入),实际工程首选;
  4. 数据量大、要求稳定:归并排序,代价是 O(N) 空间;
  5. 数据量大、要求最坏也有保证:堆排序或归并排序;
  6. 数据是范围内的整数:计数排序可以做到 O(N + 范围);
  7. 数据在磁盘上、内存放不下:归并 + 外部排序。

代码方面,插入、希尔、选择、堆、冒泡、快排(三种划分)、归并(递归 + 非递归)、计数排序都已经手写并跑通,建议自己再默写一遍,尤其是快排的划分和归并的合并这两个核心片段。

下一篇进入 Map 和 Set,会用到这一讲的"哈希"和"比较"思想,可以先把复杂度表背熟。


如果这篇笔记帮你把七大排序理顺了,麻烦点个赞 👍、收藏 ⭐ 支持一下,也欢迎关注我,后续会继续更新《Java 数据结构》系列笔记。

你的每一个赞和收藏,都是我继续写下去的动力,感谢阅读!

本文整理自课程课件《11. 排序》,代码经过重新排版、修正并做了随机对拍验证,仅作个人学习记录使用。

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值