一 、冒泡排序
1、基本思想:
两个数比较大小,较大的数下沉,较小的数冒起来。
2、过程:
- 比较两个相邻的元素,看是否符合升序要求,如果不符合就交换。
- 一趟遍历下去,就能找到最大值,并且把最大值放到最后(从前往后遍历)。也能找到最小值,并且把最小值放到最前(从后往前遍历)。
- 继续重复上述过程,依次将第2.3…n-1个最小数排好位置。
3、各项指标:
- 时间复杂度:O(NlogN)
- 空间复杂度:O(1)
- 稳定性:不稳定
4、源程序:
public static void swap(int[] arr, int x, int y) {
int tmp = arr[x];
arr[x] = arr[y];
arr[y] = tmp;
}
public static void bubbleSort(int[] arr) {
// [0, bound) 已排序区间
// [bound, length) 待排序区间
int bound = 0;
for (; bound < arr.length; bound++) {
for (int cur = arr.length - 1; cur > bound; cur--) {
if (arr[cur] < arr[cur - 1]) {
// 不符合升序 , 交换
swap(arr, cur, cur - 1);
}
}
}
}
二、选择排序
1、基本思想:
基于打擂台的方式,每次从待排序区间中,找出最小值,放到擂台上(擂台就是待排序区间的最开始的位置)。
2、过程:
- 在长度为N的无序数组中,第一次遍历n-1个数,找到最小的数值与第一个元素交换;
- 第二次遍历n-2个数,找到最小的数值与第二个元素交换;
- ······
- 第n-1次遍历,找到最小的数值与第n-1个元素交换,排序完成。
3、各项指标:
- 时间复杂度:O(N2)
- 空间复杂度:O(1)
- 稳定性:不稳定
4、源程序:
public static void swap(int[] arr, int x, int y) {
int tmp = arr[x];
arr[x] = arr[y];
arr[y] = tmp;
}
public static void selectSort(int[] arr) {
// [0, bound) 已排序区间
// [bound, length) 待排序区间
int bound = 0;
for (; bound < arr.length; bound++) {
// 里层循环就是进行具体的打擂台的过程
for (int cur = bound + 1; cur < arr.length; cur++) {
// 擂台就是 bound 位置的元素.
// 取 cur 位置的元素和擂台进行比较.
if (arr[cur] < arr[bound]) {
// 新元素胜出了, 就需要交换两个元素的位置.
// 让新的元素称为擂主
swap(arr, cur, bound);
}
}
}
}
三、插入排序
1、基本思想:
按照顺序表插入的方式,分为:已排序区间和待排序区间。取到待排序区间的第一个元素, 尝试往已排序区间中插入。保证插入完成后,已排序区间仍然是有序的。
2、过程:
在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。
3、各项指标:
- 时间复杂度:O(N2)
- 空间复杂度:O(1)
- 稳定性:稳定
4、源程序:
public static void insertSort(int[] arr) {
// 这个循环就是在控制代码进行 N 次插入过程
for (int bound = 1; bound < arr.length; bound++) {
// 循环内部要实现插入一次的过程
// 需要找到待排序区间的第一个元素, 放在哪里合适, 并且进行搬运赋值
// 已排序区间: [0, bound)
// 待排序区间: [bound, length)
// 此处的 v 就是待排序区间的第一个元素, 也就是要被插入的元素
int v = arr[bound];
int cur = bound - 1;
for (; cur >= 0; cur--) {
if (arr[cur] > v) {
// 如果 cur 位置的元素比待插入元素大
// 说明 v 要插入到 arr[cur] 的前面.
// 就需要把 arr[cur] 给往后搬运
arr[cur + 1] = arr[cur];
} else {
// 此时就相当于找到了要放置 v 的位置
break;
}
}
arr[cur + 1] = v;
}
}
四、希尔排序
1、基本思想:
在要排序的一组数中,根据某一增量分为若干子序列,并对子序列分别进行插入排序。
然后逐渐将增量减小,并重复上述过程。直至增量为1,此时数据序列基本有序,最后进行插入排序。
2、过程:
3、各项指标:
- 时间复杂度:O(N2)/O(N1.3)
- 空间复杂度:O(1)
- 稳定性:不稳定
4、源程序:
public static void shellSort(int[] arr) {
// 先指定 gap 序列. 此处使用希尔序列
int gap = arr.length / 2;
while (gap >= 1) {
// 通过这个辅助方法, 进行分组插排
_shellSort(arr, gap);
gap = gap / 2;
}
}
// 这个代码和上节课的插入排序代码机会一模一样
public static void _shellSort(int[] arr, int gap) {
// 分组插排的时候, 同组中的相邻元素的下标差就是 gap
// 注意这里面取元素的顺序:
// 先取 0 组中的第 1 个元素, 尝试往前插入排序;
// 再取 1 组中的第 1 个元素, 尝试往前插入排序;
// 再取 2 组中的第 1 个元素, 尝试往前插入排序;
// ....
// 再去 0 组中的第 2 个元素, 尝试往前插入排序;
for (int bound = gap; bound < arr.length; bound++) {
// 循环内部就要完成比较搬运的过程.
// 比较搬运都是局限在当前组内的(不同组之间不能影响)
int v = arr[bound];
int cur = bound - gap;
for (; cur >= 0; cur -= gap) {
if (arr[cur] > v) {
// 就需要进行搬运,
// v 要插入到 arr[cur] 的前面
// 就得把 arr[cur] 往后搬运一个格子, 给 v 腾个地方
arr[cur + gap] = arr[cur];
} else {
break;
}
}
// 如果发现 v 比 arr[cur] 大, 就把 v 放到 arr[cur]
// 的后面. 后面的位置不是 cur + 1, 而是 cur + gap
arr[cur + gap] = v;
}
}
五、快速排序
1、基本思想:
- 先从数列中取出一个数作为key值;
- 将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
- 对左右两个小数列重复第二步,直至各区间只有1个数。
2、各项指标:
- 时间复杂度:
- 最坏情况:O(N2)
- 平均情况:O(NlogN)
- 空间复杂度:
- 最坏情况:O(N)
- 平均情况:O(NlogN)
- 稳定性:不稳定
3、源程序:
(1)使用递归方法:
public static void quickSort(int[] arr) {
// 使用一个辅助方法进行递归.
// 辅助方法多了两个参数, 用来表示针对数组上的哪个区间
// 进行整理
_quickSort(arr, 0, arr.length - 1);
}
// [left, right]
public static void _quickSort(int[] arr, int left, int right) {
if (left >= right) {
// 如果区间为空或者区间只有一个元素, 不必排序
return;
}
// 使用 partition 方法来进行刚才描述的整理过程
// index 就是 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 i = left;
int j = right;
while (i < j) {
// 先从左往右找到一个比基准值大的元素
while (i < j && arr[i] <= v) {
i++;
}
// 再从右往左找到一个比基准值小的元素
while (i < j && arr[j] >= v) {
j--;
}
swap(arr, i, j);
}
// 如果发现 i 和 j 重叠了, 此时就需要把当前基准值元素
// 和 i j 重叠位置进行交换
swap(arr, i, right);
return i;
}
(2)使用非递归方法
public static void quickSortByLoop(int[] arr) {
// 1. 创建一个栈, 栈里面存放要去处理的区间
Stack<Integer> stack = new Stack<>();
// 2. 把第一组要去处理的区间入栈
stack.push(0);
stack.push(arr.length - 1);
// 3. 循环取栈顶元素的区间, 进行 partition 操作
while (!stack.isEmpty()) {
int end = stack.pop();
int beg = stack.pop();
if (beg >= end) {
// 只有一个元素/空区间, 不需要处理
continue;
}
// 4. 调用 partition 方法, 进行整理
int index = partition(arr, beg, end);
// 5. 把得到的子区间再入栈
// [beg, index - 1] [index + 1, end]
stack.push(index + 1);
stack.push(end);
stack.push(beg);
stack.push(index - 1);
}
}
4、优化手段:
(1)三数取中;
(2)当待处理区间已经比较小的时候,就不继续递归了,直接针对该区间进行插入排序;
(3)当递归深度达到一定深度,并且当前待处理区间还是比较大,还可以使用堆排序。
六、归并排序
1、基本思想:
首先考虑下如何将2个有序数列合并。这个非常简单,只要从比较2个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
2、过程:
3、各项指标:
- 时间复杂度:O(NlogN)
- 空间复杂度:O(N)
- 稳定性:稳定
4、源程序:
(1)使用递归方法:
public static void mergeSort(int[] arr) {
// 创建一个新的方法辅助递归. 新方法中多了两个参数
// 表示是针对当前数组中的哪个部分进行排序
// 前闭后开区间
_mergeSort(arr, 0, arr.length);
}
// [left, right) 前闭后开区间
// right - left 区间中的元素个数
public static void _mergeSort(int[] arr, int left, int right) {
if (right - left <= 1) {
// 如果当前待排序的区间里只有 1 个元素或者没有元素
// 就直接返回, 不需要任何排序动作
return;
}
// 先把当前 [left, right) 区间一分为二
int mid = (left + right) / 2;
// 分成了两个区间
// [left, mid) [mid, right)
// 当左侧区间的 _mergeSort 执行完毕后,
// 就认为 [left, mid) 就已经是有序区间了
_mergeSort(arr, left, mid);
// 当右侧区间的 _mergeSort 执行完毕后,
// 就认为 [mid, right) 就已经是有序区间了
_mergeSort(arr, mid, right);
// 接下来把左右两个有序的数组, 合并到一起!!
merge(arr, left, mid, right);
}
// merge 方法本身功能是把两个有序数组合并成一个有序数组.
// 待合并的两个数组就分别是:
// [left, mid)
// [mid, right)
public static void merge(int[] arr, int left, int mid, int right) {
if (left >= right) {
return;
}
// 创建一个临时的数组, 用来存放合并结果.
// 我们是希望这个数组能存下合并后的结果 right - left
int[] tmp = new int[right - left];
// 当前要把新的元素放到 tmp 数组的哪个下标上
int tmpSize = 0;
int l = left;
int r = mid;
while (l < mid && r < right) {
// 归并排序是稳定排序!!
// 此处的条件不要写作 arr[l] < arr[r]
if (arr[l] <= arr[r]) {
// arr[l] 比较小, 就把这个元素先插入到 tmp 数组末尾
tmp[tmpSize] = arr[l];
tmpSize++;
l++;
} else {
// arr[r] 比较小, 就把这个元素插入到 tmp 数组的末尾
tmp[tmpSize] = arr[r];
tmpSize++;
r++;
}
}
// 当其中一个数组遍历完了之后, 就把另外一个数组的剩余部分都拷贝过来
while (l < mid) {
// 剩下的是左半边数组
tmp[tmpSize] = arr[l];
tmpSize++;
l++;
}
while (r < right) {
// 剩下的是右半边数组
tmp[tmpSize] = arr[r];
tmpSize++;
r++;
}
// 最后一步, 再把临时空间的内容都拷贝回参数数组中.
// 需要把 tmp 中的内容拷贝回 arr 的 [left, right) 这一段空间里
// [left, right) 这个空间很可能不是从 0 开始的额.
for (int i = 0; i < tmp.length; i++) {
arr[left + i] = tmp[i];
}
}
(2)使用非递归方法:
public static void mergeSortByLoop(int[] arr) {
// gap 就表示当前待合并的有序数组的长度
for (int gap = 1; gap < arr.length; gap *= 2) {
// 外层循环
// 第一次是把所有长度为 1 的有序数组两两合并
// 第二次是把所有长度为 2 的有序数组两两合并
// 第三次是把所有长度为 4 的有序数组两两合并
for (int i = 0; i < arr.length; i += 2*gap) {
// 里层循环执行一次就是让两个 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);
}
}
}
七、堆排序
1、基本思想:
堆排序,可以理解成是针对选择排序的优化。
如果是升序排序的话,直观上,建立一个小堆,每次取堆顶元素,循环取N次,就得到了一个有序序列。
2.过程:
3.各项指标:
- 时间复杂度:O(NlogN)
- 空间复杂度:O(N)
- 稳定性:稳定
4、源程序:
public static void heapSort(int[] arr) {
// 1. 先进行建堆
createHeap(arr);
// 2. 循环进行交换堆顶元素和最后一个元素的过程, 并且删除该元素, 进行向下调整
int heapSize = arr.length;
for (int i = 0; i < arr.length; i++) {
swap(arr, 0, heapSize - 1);
// 删除最后一个元素
heapSize--;
// 从 0 这个位置进行向下调整
shiftDown(arr, heapSize, 0);
}
}
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;
}
// 经过上面的 if 之后, child 就指向了左右子树中的较大值.
// 比较 child 和 parent 的大小
if (arr[parent] < arr[child]) {
// 不符合大堆要求
swap(arr, parent, child);
} else {
break;
}
parent = child;
child = 2 * parent + 1;
}
}
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 swap(int[] arr, int x, int y) {
int tmp = arr[x];
arr[x] = arr[y];
arr[y] = tmp;
}
八、排序总结
排序方法 | 最好时间复杂度 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n) | O(n2) | O(n2) | O(1) | 稳定 |
插入排序 | O(n) | O(n2) | O(n2) | O(1) | 稳定 |
选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
希尔排序 | O(n) | O(n1.3) | O(n2) | O(1) | 不稳定 |
堆排序 | O(NlogN) | O(NlogN) | O(NlogN) | O(1) | 不稳定 |
快速排序 | O(NlogN) | O(NlogN) | O(n2) | O(log(n) - O(n) | 不稳定 |
归并排序 | O(NlogN) | O(NlogN) | O(NlogN) | O(n) | 稳定 |