常见的七大排序
一.插入排序:
基于线性表插入元素的方式而演化出来的
1.实现方法:
(1) 将整个区间分为[0, bound]已排序区间和[bound, size]待排序区间
(2) 把bound位置的元素往前面已排序区间的合适位置进行插入,保证插入完毕后整体数组仍然有序
public static void insertSort(int[] array) {
for (int bound = 1; bound < array.length; bound++) {
//处理bound位置的元素如何插入
int tmp = array[bound];
int cur = bound - 1;
for (; cur >= 0; cur--) {
if (array[cur] > tmp) {
//tmp元素还需要往前查找,同时就需要把cur位置的元素往后搬运
array[cur + 1] = array[cur];
} else {
break;
}
}
array[cur + 1] = tmp;
}
}
2.性能分析:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定排序
3.重要特性:
(1) 如果一个数组本身已经比较小了,此时使用插入排序效率就特别高
(2) 如果一个数组本身已经接近有序,此时使用插入排序效率也很高
二.希尔排序
1.实现方法:
先把整个数组分成若干组,分成的组数称为gap。针对每组数据分别进行插排(此时数组没有彻底完成最终排序,但有序性整体来说提高了不少),再针对gap = gap - 1重复进行分组插排,最后再针对gap = 1进行插排。
public static void shellSort(int[] array) {
int gap = array.length / 2;
while (gap > 1) {
insertSortGap(array, gap);
gap = gap / 2;
}
insertSortGap(array, 1);
}
private static void insertSortGap(int[] array, int gap) {
for (int bound = gap; bound < array.length; bound++) {
int tmp = array[bound];
int cur = bound - gap;
for (; cur >= 0; cur -= gap) {
if (array[cur] > tmp) {
array[cur + gap] = array[cur];
} else {
break;
}
}
array[cur + gap] = tmp;
}
}
2.性能分析:
时间复杂度:O(N^2) 理论上最快能达到O(N^1.3)
空间复杂度:O(1)
稳定性:不稳定
三.选择排序
1.实现方法:
(1) 创建一个bound变量,作为边界,划分为两个区间 [0, bound]已排序区间和[bound, size]待排序区间
(2) 每次从当前待排序区间取出一个最小值(打擂台的方式取出),放到bound位置上,同时bound++
public static void selectSort(int[] array) {
for (int bound = 0; bound < array.length; bound++) {
for (int cur = bound; cur < array.length; cur++) {
if (array[cur] < array[bound]) {
// 以 bound 位置的元素作为擂台.
// 拿当前元素和擂台上的元素进行 pk.
// pk 赢了就进行交换. 当前是升序排序, 谁小, 谁就赢了
swap(array, cur, bound);
}
}
}
}
2.性能分析:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
四.堆排序
1.实现方法(以升序为例) :
(1) 建立一个大堆(根节点一定是最大值):从最后一个非叶子节点出发从后往前遍历,针对当前位置进行向下调整
(2) 拿堆顶元素和最后一个元素进行交换,此时再从根节点开始进行向下调整
public static void heapSort(int[] array) {
//1.先建立堆
creatHeap(array);
int heapSize = array.length;
for (int i = 0; i < array.length - 1; i++) {
// 2. 交换堆顶元素和堆中的最后一个元素
swap(array, 0, heapSize - 1);
// 3. 把最后一个元素从堆中删除掉
heapSize--;
// 4. 针对当前的堆从 根节点 开始进行向下调整
shiftDown(array, heapSize, 0);
}
}
private static void shiftDown(int[] array, int size, int index) {
int parent = index;
int child = 2 * parent + 1;
while (child < size) {
if (child + 1 < size && array[child + 1] > array[child]) {
child = child + 1;
}
if (array[child] > array[parent]) {
swap(array, child, parent);
} else {
break;
}
parent = child;
child = 2 * parent + 1;
}
}
private static void creatHeap(int[] array) {
for (int i = (array.length - 1 - 1) / 2; i >= 0; i--) {
shiftDown(array, array.length, i);
}
}
2.性能分析:
时间复杂度:O(NlogN)
空间复杂度:O(1)
稳定性:不稳定
五.冒泡排序
1.实现方法(以升序为例) :
循环比较相邻的值,最终就能找出在当前待排序区间中的最大值或最小值。
从前往后遍历,每次找最大,放到最后。
从后往前遍历,每次找最小,放到最前面。
public static void bubbleSort(int[] array) {
// 从后往前遍历, 每次找最小元素放前面
// [0, bound), 已排序区间
// [bound, size), 待排序区间
for (int bound = 0; bound < array.length; bound++) {
// 接下来就需要在待排序区间中找到当前的最小值.
// 具体的找法就是, 比较相邻元素, 看是否符合升序要求, 不符合就交换元素
for (int cur = array.length - 1; cur > bound; cur--) {
if (array[cur - 1] > array[cur]) {
swap(array, cur - 1, cur);
}
}
}
}
2.性能分析:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定排序
六.快速排序
1.实现方法:
核心操作:partition,针对整个数组进行整排,选定一个基准值(可以选最后/第一个元素),整理之后的数字,基准值左侧的元素都小于基准值,右侧的元素都大于基准值。
partition具体操作:选定一个基准值(此时选最后一个元素为基准值为例),让left从左往右找到一个比基准值大的,让right从右往左找到一个比基准值小的元素,交换left和right位置的元素,重复刚才的过程,直到left和right重合。当left和right重合的值,此时当前元素一定是大于基准值,就把重合位置和基准值进行交换,再针对两个区间,递归进行刚才的过程。
注:
如果选择第一个元素为基准值,那么必须要先从右往左,再从左往右。
如果选择最后一个为基准值,必须先要从左往右,再从右往左。
public static void quickSort(int[] array) {
quickSortHelper(array, 0, array.length - 1);
}
// [left, right] 前闭后闭区间. 针对当前范围进行快速排序
private static void quickSortHelper(int[] array, int left, int right) {
if (left >= right) {
return;
}
int index = partition(array, left, right);
quickSortHelper(array, left, index - 1);
quickSortHelper(array, index + 1, right);
}
private static int partition(int[] array, int left, int right) {
int baseValue = array[right];
int i = left;
int j = right;
while (i < j) {
// 1. 先从左往右找到一个大于基准值的元素
while (i < j && array[i] <= baseValue) {
i++;
}
// 此时 i 指向的位置要么和 j 重合, 要么就是一个比基准值大的元素
// 2. 再从右往左找到一个小于基准值的元素
while (i < j && array[j] >= baseValue) {
j--;
}
// 此时 j 指向的元素要么和 i 重合, 要么就是比基准值小的元素
// 3. 交换 i 和 j 的值
if (i < j) {
swap(array, i, j);
}
}
// 当整个循环结束, i 和 j 就重合了. 接下来就把 基准值 位置的元素交换到 i j 重合位置上.
// 此时 i 和 j 重合位置的元素一定是大于基准值的元素.
swap(array, i, right);
return i;
}
2.性能分析;
时间复杂度:最坏情况O(N^2) 平均情况O(NlogN)
空间复杂度:最坏O(N) 平均O(logN)
稳定性:不稳定
七.归并排序
把两个有序数组归并成一个数组
1.实现方法:
借助一个临时的空间,先把数组对等的分为两组,如果当前这两个数组都是有序的话直接进行归并,就能得到一个最终有序的数组。不是有序的,继续往下分组,当组成只有一个元素时,这个组一定是有序的,就可以把两个相邻的组进行归并了。
public static void mergeSort(int[] array) {
mergeSortHelper(array, 0, array.length);
}
private static void mergeSortHelper(int[] array, int left, int right) {
if (right - left <= 1) {
return;
}
// 针对 [left, right) 区间, 分成对等的两个区间
int mid = (left + right) / 2;
// [left, mid)
// [mid, right)
mergeSortHelper(array, left, mid);
mergeSortHelper(array, mid, right);
// 通过上面的递归, 认为这两个区间都被排好序了. 接下来就可以进行合并了
merge(array, left, mid, right);
}
private static void merge(int[] array, int left, int mid, int right) {
int cur1 = left;
int cur2 = mid;
// 临时空间需要能容纳下 两个数组合并后的结果
int[] output = new int[right - left];
int outputIndex = 0;
while (cur1 < mid && cur2 < right) {
if (array[cur1] < array[cur2]) {
// 把 cur1 位置的元素插入到 output 中
output[outputIndex] = array[cur1];
cur1++;
outputIndex++;
} else {
output[outputIndex] = array[cur2];
cur2++;
outputIndex++;
}
}
while (cur1 < mid) {
output[outputIndex] = array[cur1];
cur1++;
outputIndex++;
}
while (cur2 < right) {
output[outputIndex] = array[cur2];
cur2++;
outputIndex++;
}
// 最后一步要把数据从临时空间中拷贝回原来的数组中.
for (int i = 0; i < right - left; i++) {
array[left + i] = output[i];
}
}
2.性能分析:
时间复杂度:O(NlogN)
空间复杂度:O(n)
稳定性:稳定排序
3.重要特性:
(1) 归并排序能高效的针对链表进行排序,数组能够随机访问,链表只能取next,堆排序,希尔排序是无法高效的应用在链表上的。
(2) 归并排序是一种“外部排序”(数据量非常大,无法全部加载到内存中)的主要实现方式。