排序算法
排序算法是属于《数据结构和算法》中最基本的算法只之一,今天我们使用 Java 语言来实现常用的排序算法,下面的代码全部以升序为例。
目录
插入排序
思路
插入排序的思路是:先将待排序序列分成两个部分,前一部分为已排序序列,剩余部分为未排序序列,然后从未排序的部分中选择第一个元素,在已排序序列中选择合适的位置插入。
代码实现
public static void insertSort(int[] arr) {
int bound = 1;
// [0, bound) 是已排序区间.
// [bound, length) 待排序区间.
for (; bound < arr.length; bound++) {
int v = arr[bound];
int cur = bound - 1;
for (; cur >= 0; cur--) {
if (arr[cur] > v) {
arr[cur + 1] = arr[cur];
} else {
break;
}
}
arr[cur + 1] = v;
}
}
复杂度与稳定性
最好时间复杂度为 O(N),发生在数据已经是升序的情况
最坏时间复杂度为 O(N^2),发生在数据是逆序的情况
平均时间复杂度为 O(N^2)
空间复杂度为 O(1)
是稳定的。
特点
数据越有序排序的效率越高,数据越短排序的效率越高。
希尔排序
思路
希尔排序是插入排序的优化,由于插入排序有数据越有序排序的效率越高,数据越短排序的效率越高的特点,所以将数据分组,对每一组进行插入排序。
代码
// 谢尔/希尔
public static void shellSort(int[] arr) {
// 指定 gap 序列, len/2, len/4, len/8,...,1
int gap = arr.length / 2;
while (gap >= 1) {
_shellSort(arr, gap);
gap = gap / 2;
}
}
public static void _shellSort(int[] arr, int gap) {
// 进行分组插排. 分组依据就是 gap.
// gap 同时也表示分的组数.
// 同组的相邻元素, 下标差值就是 gap
// 下面的代码其实和插入排序是一样的. 尤其是把 gap 设为 1
int bound = gap;
for (; bound < arr.length; bound++) {
int v = arr[bound];
int cur = bound - gap;
for (; cur >= 0; cur -= gap) {
if (arr[cur] > v) {
// 进行搬运
arr[cur + gap] = arr[cur];
} else {
break;
}
}
arr[cur + gap] = v;
}
}
注意分组是根据 gap 来分的。
复杂度和稳定性
最好时间复杂度 O(N)
最坏时间复杂度 O(N^2)
平均时间复杂度 O(N^1.3)
空间复杂度 O(1)
不稳定的。
选择排序
思路
选择排序是一种打擂台的思想,每一次从无序区间选出最大(或最小)的一个元素,存放在无序区间的最后(或最前),直到全部待排序的数据元素排完。
代码
public static void selectSort(int[] arr) {
// 创建一个变量 bound 表示已排序区间和待排序区间的边界.
// [0, bound) 已排序区间
// [bound, length) 待排序区间
int bound = 0;
for (; bound < arr.length; bound++) {
// 里层循环要进行打擂台的过程.
// 擂台的位置就是 bound 下标的位置
for (int cur = bound + 1; cur < arr.length; cur++) {
if (arr[cur] < arr[bound]) {
// 如果发现挑战者比擂主小, 就交换两个元素
swap(arr, cur, bound);
}
}
}
}
private static void swap(int[] arr, int index1, int index2) {
int tmp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = tmp;
}
复杂度和稳定性
时间复杂度 O(N^2)
空间复杂度 O(1)
不稳定的
堆排序
思路
堆排序借助堆,先将元素建立成一个大堆,取出堆顶元素,将堆顶元素和数组末尾交换,此时末尾就是最大值,将堆的元素个数减一,从 0 开始向下调整,调整完毕后取出堆顶元素,放在数组的倒数第二个位置,重复此步骤,直到整个数组有序。
代码
public static void heapSort(int[] arr) {
// 1. 先建立堆!!
createHeap(arr);
// 2. 需要循环的取出堆顶元素, 和最后一个元素交换并删除之
// 再从 0 位置进行调整
int heapSize = arr.length;
for (int i = 0; i < arr.length; i++) {
// 交换 0 号元素和堆的最后一个元素
swap(arr, 0, heapSize - 1);
// 把最后一个元素从堆上删除
heapSize--;
// 从 0 号位置开始往下进行调整
shiftDown(arr, heapSize, 0);
}
}
public static void createHeap(int[] arr) {
for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--) {
shiftDown(arr, arr.length, i);
}
}
public static void shiftDown(int[] arr, int size, int index) {
int parent = index;
int child = 2 * parent + 1;
while (child < size) {
// 先找出左右子树比较大的~
if (child + 1 < size && arr[child + 1] > arr[child]) {
child = child + 1;
}
// 再去比较 child 和 parent
if (arr[parent] < arr[child]) {
swap(arr, parent, child);
} else {
break;
}
parent = child;
child = 2 * parent + 1;
}
}
复杂度和稳定性
时间复杂度 O(N*logN)
空间复杂度 O(1)
不稳定
冒泡排序
思路
在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序。
代码
public static void bubbleSort(int[] arr) {
int bound = 0;
for (; bound < arr.length; bound++) {
for (int index = arr.length - 1; index - 1 >= 0; index--) {
if (arr[index - 1] > arr[index]) {
swap(arr, index - 1, index);
}
}
}
}
复杂度和稳定性
最好时间复杂度 O(N),数据有序时
最坏时间复杂度 O(N^2),数据逆序时
平均时间复杂度 O(N^2)
空间复杂度 O(1)
稳定的
快速排序
思路
1. 从待排序区间选择一个数,作为基准值(pivot);
2. Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
3. 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1,代表已经有序,或者小区间的长度 == 0,代表没有数据。
代码
下面是递归写法
public static void quickSort(int[] arr) {
// 创建一个辅助递归的方法.
// 在这个方法的参数中, 明确指定针对哪个区间进行递归.
// [0, length - 1]
_quickSort(arr, 0, arr.length - 1);
}
public static void _quickSort(int[] arr, int left, int right) {
if (left >= right) {
// 如果当前的区间为空, 或者只有一个元素
// 都不需要进行任何处理
return;
}
// 现针对当前 [left, right] 区间进行 partition 操作
// 方法的返回值, 表示整理完当前区间后, 基准值所在的位置.
// 遍历过程中的 left 和 right 的重合位置
int index = partition(arr, left, right);
// 递归的对左侧区间进行快速排序
_quickSort(arr, left, index - 1);
// 递归的对右侧区间进行快速排序
_quickSort(arr, index + 1, right);
}
public static int partition(int[] arr, int left, int right) {
// 选取最右侧元素作为基准值.
int v = arr[right];
int l = left;
int r = right;
// 如果 l 和 r 重合, 说明遍历完成
while (l < r) {
// 先从左往右, 找一个比基准值大的数字.
while (l < r && arr[l] <= v) {
l++;
}
// 当循环结束的时候, l 就指向了比基准值大的元素
// 再从右往左, 找一个比基准值小的数字
while (l < r && arr[r] >= v) {
r--;
}
swap(arr, l, r);
}
// 当 l 和 r 重合的时候, 就把重合位置的元素和基准值位置进行交换
swap(arr, l, right);
// 最终方法返回基准值所在的位置(l 和 r 重合的位置)
return l;
}
非递归写法
public static void quickSortByLoop(int[] arr) {
// 1. 先创建一个栈, 这个栈用来保存当前的每一个待处理区间
Stack<Integer> stack = new Stack<>();
// 2. 把根节点入栈, 整个数组对应的区间
stack.push(0);
stack.push(arr.length - 1);
// 3. 循环取栈顶元素
while (!stack.isEmpty()) {
// 取的元素就是当前的待处理区间
// 取的顺序正好和插入的顺序相反
int right = stack.pop();
int left = stack.pop();
if (left >= right) {
// 如果是空区间或者只有一个元素, 不需要排序
continue;
}
// 调用 partition 方法整理当前区间
int index = partition(arr, left, right);
// 右侧区间: [index + 1, right]
stack.push(index + 1);
stack.push(right);
// 左侧区间: [left, index - 1]
stack.push(left);
stack.push(index - 1);
}
}
public static int partition(int[] arr, int left, int right) {
// 选取最右侧元素作为基准值.
int v = arr[right];
int l = left;
int r = right;
// 如果 l 和 r 重合, 说明遍历完成
while (l < r) {
// 先从左往右, 找一个比基准值大的数字.
while (l < r && arr[l] <= v) {
l++;
}
// 当循环结束的时候, l 就指向了比基准值大的元素
// 再从右往左, 找一个比基准值小的数字
while (l < r && arr[r] >= v) {
r--;
}
swap(arr, l, r);
}
// 当 l 和 r 重合的时候, 就把重合位置的元素和基准值位置进行交换
swap(arr, l, right);
// 最终方法返回基准值所在的位置(l 和 r 重合的位置)
return l;
}
复杂度和稳定性
最好时间复杂度 O(NlogN),当每次选择基准值都为区间中位数时,达到了折半的目的
最坏时间复杂度 O(N^2),当每次选择区间都为数据的最大值或者最小值时
平均时间复杂度 O(NlogN)
空间复杂度主要由递归提供
最好空间复杂度 O(logN),当每次选择基准值都为区间中位数时,达到了折半的目的
最坏空间复杂度 O(N),当每次选择区间都为数据的最大值或者最小值时
平均空间复杂度 O(logN)
不稳定的。
归并排序
思路
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
代码
递归写法
public static void mergeSort(int[] arr) {
_mergeSort(arr, 0, arr.length);
}
// 辅助递归的方法
public static void _mergeSort(int[] arr, int left, int right) {
if (right - left <= 1) {
// 判定当前区间是不是只有一个元素或者没有元素
// 此时不需要进行排序
return;
}
int mid = (left + right) / 2;
// 先让 [left, mid) 区间变成有序
_mergeSort(arr, left, mid);
// 再让 [mid, right) 区间变成有序
_mergeSort(arr, mid, right);
// 合并两个有序区间
merge(arr, left, mid, right);
}
// 归并排序中的关键操作, 就是归并两个有序数组.
// 使用该 merge 方法完成数组归并的过程
// 此处两个数组就通过参数的 left, mid, right 描述
// [left, mid) 左侧数组
// [mid, right) 右侧数组
public static void merge(int[] arr, int left, int mid, int right) {
// 进行具体的归并操作
// 需要创建一个临时的空间用来保存归并的结果
// 临时空间得能保存下带归并的两个数组.
// right - left 这么长
if (left >= right) {
// 空区间就直接忽略~~
return;
}
int[] tmp = new int[right - left];
int tmpIndex = 0; // 这个下标表示当前元素该放到临时空间的哪个位置上.
int cur1 = left;
int cur2 = mid;
while (cur1 < mid && cur2 < right) {
// 此处 最好写成 <= , 目的就是稳定性.
// 由于 cur1 是在左侧区间, cur2 是在右侧区间.
// 此时如果发现 cur1 和 cur2 的值相等,
// 就希望左侧区间的 cur1 在最终结果中仍然是在左侧.
// 于是就先把 cur1 对应的元素给先放到结果中.
if (arr[cur1] <= arr[cur2]) {
// 把 cur1 对应的元素插入到临时空间中
tmp[tmpIndex] = arr[cur1];
tmpIndex++;
cur1++;
} else {
// 把 cur2 对应的元素插入到临时空间中
tmp[tmpIndex] = arr[cur2];
tmpIndex++;
cur2++;
}
}
// 循环结束之后, 需要把剩余的元素也都给拷贝到最终结果里.
while (cur1 < mid) {
tmp[tmpIndex] = arr[cur1];
tmpIndex++;
cur1++;
}
while (cur2 < right) {
tmp[tmpIndex] = arr[cur2];
tmpIndex++;
cur2++;
}
// 还需要把 tmp 的结果再放回 arr 数组. (原地排序)
// 把原始数组的 [left, right) 区间替换回排序后的结果
for (int i = 0; i < tmp.length; i++) {
arr[left + i] = tmp[i];
}
}
非递归写法
public static void mergeSortByLoop(int[] arr) {
// gap 用于限定分组.
// gap 值的含义就是每个待归并数组的长度
int gap = 1;
for (; gap < arr.length; gap *= 2) {
// 当前两个待归并的数组
for (int i = 0; i < arr.length; i += 2*gap) {
// 在这个循环中控制两个相邻的数组进行归并
// [left, mid) 和 [mid, right) 就要进行归并
int left = i;
int mid = i + gap;
if (mid >= arr.length) {
mid = arr.length;
}
int right = i + 2 * gap;
if (right >= arr.length) {
right = arr.length;
}
merge(arr, left, mid, right);
}
}
}
复杂度和稳定性
时间复杂度 O(NlogN)
空间复杂度 O(N)
稳定的