我们平时常用的排序有四大类,分别为: 插入排序(直接插入排序,希尔排序),选择排序(选择排序,堆排序),交换排序(冒泡排序,快速排序),和归并排序.
下面我们来逐一分析实现它们
1.直接插入排序
直接插入排序在保证前面的元素已经有序时,一个一个的加入,向后平移的过程.
其次,它是稳定的,时间复杂度为最坏为O(n^2), 最好为O(n), 平均为O(n^2),空间复杂度为O(1) .
插入排序 : 数组越接近有序,速度越快 数组规模足够小时,插排最优快.(好)
public static void insertSort(int[] array) {
// bound 变量来把整个数组分成两个区间
// [0, bound) 已排序区间
// [bound, size) 待排序区间
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) {
array[cur + 1] = array[cur];
} else {
break;
}
}
array[cur + 1] = tmp;
}
}
2.希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时, 所有记录在统一组内排好序。
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
时间复杂度为最坏为O(n^2), 最好为O(n), 平均为O(n^1.3),空间复杂度为O(1),不稳定
public static void shellSort(int[] array) {
int gap = array.length;
while (gap > 1) {
insertSortGap(array, gap);
gap = gap / 2;
}
insertSortGap(array, 1);
}
private static void insertSortGap(int[] array, int gap) {
for (int bound = 1; bound < array.length; bound++) {
int tmp = array[bound];
int cur = bound - gap;
// 同组之内的相邻元素之间下标差了 gap
for (; cur >= 0; cur -= gap) {
if (array[cur] > tmp) {
array[cur + gap] = array[cur];
} else {
break;
}
}
array[cur + gap] = tmp;
}
}
3.选择排序
简单选择排序是选择排序中的一种,每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的 数据元素排完
。 它是不稳定的. 它的时间复杂度最好和最坏都为N^2,空间复杂度为O(1).
public static void selectSort(int[] array) {
// [0, bound) 已排序区间
// [bound, size) 待排序区间
for (int bound = 0; bound < array.length; bound++) {
for (int cur = bound + 1; cur < array.length; cur++) {
if (array[cur] < array[bound]) {
swap(array, cur, bound);
}
}
}
}
4.堆排序
堆排序是另外一种选择排序,堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是
通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。 它不稳定. 时间复杂度最好最坏都是O(n * log(n))
public static void heapSort(int[] array) {
// 1. 创建堆
createHeap(array);
// 2. 循环取出堆顶的最大值, 放到最后面
for (int i = 0; i < array.length; i++) {
// 待排序区间: [0, array.length - i)
// 已排序区间: [array.length - i, array.length)
swap(array, 0, array.length - i - 1);
// 第一个参数是数组
// 第二个参数是数组中的有效元素的个数
// 第三个参数是从哪个位置进行向下调整
shiftDown(array, array.length - i, 0);
}
}
private static void createHeap(int[] array) {
// 从最后一个非叶子节点, 开始出发, 从后往前向下调整
for (int i = (array.length - 1 - 1) / 2; i >= 0; i--) {
shiftDown(array, array.length, i);
}
}
// O(logN)
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 之后, child 指向左右子树的最大值
if (array[child] > array[parent]) {
// 建立大堆.
swap(array, child, parent);
} else {
break;
}
parent = child;
child = 2 * parent + 1;
}
}
5.冒泡排序
冒泡排序是在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序
它是稳定的
时间复杂度为最坏为O(n^2), 最好为O(n), 平均为O(n^2),空间复杂度为O(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);
}
}
}
}
6.快速排序
快速排序基本原理为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
时间复杂度最好为O(n * log(n)),最坏为 O(n^2) ,平均为O(n * log(n))
它是不稳定的
空间复杂度最好为O(log(n)) ,最坏为O(n),平均为O(log(n)) .
public static void quickSort(int[] array) {
// 参数的含义表示针对数组中的那段区间进行快速排序
// [0, array.length - 1]
quickSortHelper(array, 0, array.length - 1);
}
private static void quickSortHelper(int[] array, int left, int right) {
if (left >= right) {
// 如果只有一个元素或者没有元素, 都不需要排序
return;
}
// 这个方法就是刚才进行区间整理的方法
// 选取基准值, 并且把小于基准值的放到左侧, 大于基准值的放到右侧
// 返回值 [left, right] 最终整理完毕后, 基准值的下标
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 baseIndex = left;
int baseValue = array[baseIndex];
while (left < right) {
while (left < right && array[right] >= baseValue) {
right--;
}
// 循环结束之后, right 指向的位置, 就是从右往左第一个比基准值小的元素
while (left < right && array[left] <= baseValue) {
left++;
}
// 循环结束之后, left 指向的位置, 就是从左往右第一个比基准值大的元素.
// 交换 left 和 right 位置的元素
swap(array, left, right);
}
// 当前这个代码是把 left 和 right 重合位置的元素和基准值交换.
// 基准值选取的是最后一个元素, 如果要想交换
// 前提是 left 和 right 重合位置对应的元素, 必须得比基准值大.
// 循环结束有两种情况:
// 1. left++导致的循环结束
// 上次循环过程中, 进行了一个 swap 操作. 经过这个 swap 操作之后,
// right 一定指向一个大于基准值的元素. 此时如果 left 和 right 重合
// 也一定是指向一个大于基准值的元素
// 2. right-- 导致的循环结束
// 此时由于 left 刚刚找到一个比基准值大的元素, 此时 right 和 left
// 重合之后, 对应的元素也就是刚才的那个比基准值大的值
swap(array, left, baseIndex);
return left;
}
非递归版本的快速排序
public static void quickSortByLoop(int[] array) {
// 1. 先创建一个栈, 栈里面存的是待处理区间的下标
Stack<Integer> stack = new Stack<>();
// 2. 初始情况下待处理区间, 就是整个数组
stack.push(array.length - 1);
stack.push(0);
while (!stack.isEmpty()) {
// 3. 取栈顶元素, 栈顶元素就是我们要处理的区间
int left = stack.pop();
int right = stack.pop();
if (left >= right) {
continue;
}
// 4. 对当前待处理区间进行整理
int index = partition(array, left, right);
// 5. 接下来要处理的区间再入栈
// [left, index - 1]
// [index + 1, right]
stack.push(index - 1);
stack.push(left);
stack.push(right);
stack.push(index + 1);
}
}
7.归并排序
归并排序是一种分治思想,类似于二叉树的后序.归并排序借助临时空间. 归并排序实质是一个后序排列.
时间复杂度O(n * log(n))
空间复杂度O(n)
它是稳定的.
public static void mergeSort(int[] array) {
// 后两个参数表示要进行归并排序的区间.
// [0, array.length)
// new 足够大的数组, 把这个数组作为缓冲区传给
// 递归函数
mergeSortHelper(array, 0, array.length);
}
private static void mergeSortHelper(int[] array, int left, int right) {
// [left, right) 构成了要去进行归并排序的区间
// 如果区间为空区间, 或者只有一个元素, 都不用排序
if (left >= right || right - left == 1) {
// 空区间或者区间只有一个元素, 都不需要进行归并排序
return;
}
// 使用类似后序遍历的方式.
// 先把当前的待排序区间拆成两半,
// 递归的对这两个子区间进行归并排序, 保证两个区间有序之后
// 再进行合并
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 length = right - left;
int[] output = new int[length];
// 这个变量保存着当前 output 中的末尾元素的下标
int outputIndex = 0;
// i 和 j 是用来遍历两个区间的辅助变量
// [left, mid)
// [mid, right)
int i = left;
int j = mid;
while (i < mid && j < right) {
// 此处的 if 条件必须要 <= , 否则没法保证稳定性
if (array[i] <= array[j]) {
// i 对应的元素比 j 小
// 就把 i 对应的元素插入到 output 末尾
output[outputIndex++] = array[i++];
} else {
output[outputIndex++] = array[j++];
}
}
// 上面的循环结束之后, 两个区间至少有一个是遍历完了的.
// 就把剩下的区间的内容直接拷贝到 output 中即可.
while (i < mid) {
output[outputIndex++] = array[i++];
}
while (j < right) {
output[outputIndex++] = array[j++];
}
// 最后一步, 把 output 中的元素拷贝回原来的区间
for (int k = 0; k < length; k++) {
array[left + k] = output[k];
}
}
public static void mergeSortByLoop(int[] array) {
// 借助下标相关的规律来进行分组.
// 初始情况下, 每个元素单独作为一组
// [0] [1] [2] [3] [4] [5]
// [0, 1] 和 [2, 3] 合并. [4, 5] 和 [6, 7] 区间合并
// [0, 1, 2, 3] [4, 5, 6, 7]
for (int gap = 1; gap < array.length; gap *= 2) {
for (int i = 0; i < array.length; i += 2 * gap) {
// 这个循环负责在 gap 为指定值的情况下
// 把所有的区间进行归并
// 针对当前的 i, 也能划分出两个需要进行归并的区间
// [beg, mid)
// [mid, end)
int beg = i;
int mid = i + gap;
int end = i + 2 * gap;
if (mid > array.length) {
mid = array.length;
}
if (end > array.length) {
end = array.length;
}
merge(array, beg, mid, end);
}
}
}
private static void swap(int[] array, int x, int y) {
int tmp = array[x];
array[x] = array[y];
array[y] = tmp;
}
8.七种排序算法的性能比较
排序方法 | 最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
希尔排序 | O(n) | O(n^1.3) | O(n^2) | 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^2) | O(log(n)) ~ O(n) | 不稳定 |
归并排序 | O(n * log(n)) | O(n * log(n)) | O(n * log(n)) | O(n) | 稳定 |