本博文是排序算法的第一篇,后续指路:【算法】Java实现常用排序二
前言
学习算法最绕不开的就是排序,虽然这是个信息爆炸的时代,但搜索到的毕竟是别人的,特此总结了一下常用的几种排序,并根据自己的理解用Java实现出来。若存在理解不到位或者有更好的优化,欢迎指出。
先列出测试类和用到的一些方法,主要是简化执行排序时交换等步骤。
public class MySort {
public static void main(String[] args) {
int[] t1 = { 2, 1, 5, 3, 9 };
int[] t2 = { 6, 2, 0, 3, 5, 1 };
writeSort(t1);
writeSort(t2);
}
// 复制数组
public static int[] copy(int[] li) {
int[] copy = new int[li.length];
for (int i = 0; i < li.length; i++) {
copy[i] = li[i];
}
return copy;
}
// 交换
public static void swap(int[] li, int a, int b) {
int t = li[a];
li[a] = li[b];
li[b] = t;
}
// 打印
public static void writeList(int[] li) {
System.out.print("[ ");
for (int i : li) {
System.out.print(i + " ");
}
System.out.println("]");
}
// 批量打印
public static void writeSort(int[] li) {
System.out.print(" 原序列:");
writeList(li);
System.out.print("冒泡排序:");
writeList(buddle(li));
System.out.print("选择排序:");
writeList(choice(li));
System.out.print("插入排序:");
writeList(insert(li));
System.out.print(" 堆排序:");
writeList(heap(li));
System.out.print("快速排序:");
writeList(quick(li));
}
}
插入排序
原理
将序列中首个元素视为长度为1的有序序列,而后将下一个元素插入到已排序好的有序序列中。
流程分析
原序列:[ 2 1 5 3 9 ]
| 执行轮次 | 有序序列 | 待排序元素 | 未排序序列 |
|---|---|---|---|
| 0 | - | 2 | 1 5 3 9 |
| 1 | 2 | 1 | 5 3 9 |
| 2 | 1 2 | 5 | 3 9 |
| 3 | 1 2 5 | 3 | 9 |
| 4 | 1 2 3 5 | 9 | - |
排序后序列:[ 1 2 3 5 9 ]
代码实现
/*
* 直接插入排序 平均时间复杂度:O(n^2)
* 最坏时间复杂度:O(n^2)
* 空间复杂度:O(1)
* 稳定性:稳定
*/
public static int[] insert(int[] li) {
int[] c = copy(li);
// 从前往后将元素插入到已有序的元素列表中
// 最开始的有序列表为{li[0]}
for (int i = 1; i < c.length; i++) {
for (int j = i; j > 0 && c[j] < c[j - 1]; j--) {
swap(c, j, j - 1);
}
}
return c;
}
冒泡排序
原理
从序列首元素开始与下一个元素比较,更大的元素会一直往后走,每一轮就会找到遍历序列中最大的元素并将其放在最后,最坏执行length轮可以实现升序。
流程分析
| 执行轮次 | 比较位置 | 比较序列 | 固定序列 |
|---|---|---|---|
| 0 | 0 | 2 1 5 3 9 | - |
| 0 | 1 | 1 2 5 3 9 | - |
| 0 | 2 | 1 2 5 3 9 | - |
| 0 | 3 | 1 2 3 5 9 | - |
| 0 | 4 | 1 2 3 5 9 | - |
此时已经找到了比较序列中最大的元素为9,后续的比较序列就变为1 2 3 5。在这里我加入了这样的代码:
// 优化:在循环全部结束前若已达成有序序列则立即结束
if (!flag)
break;
也就是说在刚刚执行的第0轮中发生了交换,则不能确定当前序列已有序,所以继续执行。
| 执行轮次 | 比较位置 | 比较序列 | 固定序列 |
|---|---|---|---|
| 1 | 0 | 1 2 3 5 | 9 |
| 1 | 1 | 1 2 3 5 | 9 |
| 1 | 2 | 1 2 3 5 | 9 |
| 1 | 3 | 1 2 3 5 | 9 |
| 1 | 4 | 1 2 3 5 | 9 |
在第1轮比较结束后,swap仍为false,即这一轮中都没有发生过交换,则每一个元素都小于等于它的下一个元素,那么就可以确定当前序列是有序序列,就不用再进行剩下的没有意义的比较了。
这时候可以直接返回排序好的序列:1 2 3 5 9
代码实现
/*
* 冒泡排序
* 平均时间复杂度:O(n^2)
* 最坏时间复杂度:O(n^2)
* 空间复杂度:O(1)
* 稳定性:稳定
*/
public static int[] buddle(int[] li) {
int[] c = copy(li);
// 用于减少循环的变量
boolean swap = false;
for (int i = 0; i < c.length; i++) {
swap = false;
for (int j = 0; j < c.length - i - 1; j++) {
// 升序
if (c[j] > c[j + 1]) {
swap(c, j, j + 1);
swap = true;
}
}
// 优化:在循环全部结束前若已达成有序序列则立即结束
if (!swap)
break;
}
return c;
}
选择排序
原理
冒泡排序是在多次的交换中得到当前序列的最大元素,选择排序则是直接在当前序列中找到最大元素,然后将其与末尾元素进行交换。
流程分析
| 执行轮次 | 最大元素 | 比较序列 | 固定序列 |
|---|---|---|---|
| 0 | 9 | 2 1 5 3 9 | - |
| 1 | 5 | 2 1 5 3 | 9 |
| 2 | 3 | 2 1 3 | 5 9 |
| 3 | 2 | 2 1 | 3 5 9 |
| 4 | 1 | 1 | 2 3 5 9 |
选择排序不同于冒泡一样有一个可以在过程中检验当前序列已有序的指标,所以每次选择排序都需要执行完length*length次比较与length次交换才可以确定序列有序。
代码实现
/*
* 选择排序
* 平均时间复杂度:O(n^2)
* 最坏时间复杂度:O(n^2)
* 空间复杂度:O(1)
* 稳定性:不稳定
*/
public static int[] choice(int[] li) {
int[] c = copy(li);
// 每次循环选择出第i小的数与当前i位置的数交换
for (int i = 0; i < c.length; i++) {
int max = 0;
for (int j = 0; j < c.length - i; j++) {
if (c[j] > c[max]) {
max = j;
}
}
if (max != c.length - i - 1) {
swap(c, c.length - i - 1, max);
}
}
return c;
}
堆排序
原理
堆排序的主要原理就是构建最大堆(降序则是最小堆),而构建最大堆就是将当前序列转化为二叉树,使树的每一个非叶子节点都大于它的两个子节点。每次构建最大堆都会使堆顶元素为序列中最大元素。
流程分析
对于一个序列a来说,以a构建的二叉树的最后一个非叶子节lastParent=a.length/2-1,从lastParent到0依次构建最大堆,循环结束后序列a就是一个最大堆的序列。
关于实现最大堆的代码如下:
// 构建大顶堆
public static void makeMaxHeap(int[] li, int length) {
// 最后一个非叶子节点
int lastParent = length / 2 - 1;
// 记录每个i的最大子节点
int son;
for (int i = lastParent; i >= 0; i--) {
// 先指向i的左子节点
son = i * 2 + 1;
// 不能保证i的右子节点一定存在
if (son + 1 < length && li[son] < li[son + 1]) {
son++;
}
if (li[i] < li[son]) {
swap(li, i, son);
}
}
}
话不多说上表格
| 当前非叶子节点位置 | 当前非叶子节点 | 最大子节点 | 当前序列 | 交换后当前序列 | 固定序列 |
|---|---|---|---|---|---|
| 1 | 1 | 9 | 2 1 5 3 9 | 2 9 5 3 1 | - |
| 0 | 2 | 9 | 2 9 5 3 1 | 9 2 5 3 1 | - |
这就是一次构建最大堆的过程,序列中的首元素就是最大元素,这时把首元素与最后元素交换位置,继续将剩下序列构建为最大堆,进行length次构建->交换就可以得到一个有序序列。
代码实现
/*
* 堆排序
* 平均时间复杂度: O(nlogn)
* 最坏时间复杂度: O(nlogn)
* 空间复杂度:O(1)
* 稳定性:不稳定
*/
public static int[] heap(int[] li) {
int[] c = copy(li);
for (int i = 0; i < c.length; i++) {
makeMaxHeap(c, c.length - i);
swap(c, 0, c.length - i - 1);
}
return c;
}
// 构建大顶堆
public static void makeMaxHeap(int[] li, int length) {
// 最后一个非叶子节点
int lastParent = length / 2 - 1;
// 记录每个i的最大子节点
int son;
for (int i = lastParent; i >= 0; i--) {
// 先指向i的左子节点
son = i * 2 + 1;
// 不能保证i的右子节点一定存在
if (son + 1 < length && li[son] < li[son + 1]) {
son++;
}
if (li[i] < li[son]) {
swap(li, i, son);
}
}
}
快速排序
原理
快速排序其实是冒泡排序pro plus尊享豪华版,在前面我加入的flag标记位带来的效率优化在快排面前就是九牛里的一根毛。
简单来说快速排序就是找定一个基准数,再设定待排序序列的头尾两个指针,分别从左到右寻找大于基准数的元素和从右到左寻找小于基准数的元素,然后将其交换位置。当头尾指针相遇时,将相遇点的元素与基准数交换位置,此时在基准数左侧的元素都小于基准数,右侧的都大于基准数。
如此反复,每一轮快速排序核心代码的执行都可以将基准数放到确定的位置上,且每次都将序列一分为二,所以时间复杂度为O(nlogn)。而最坏情况下每次递归不能把序列分成两部分,基准数都被确定在序列的最左或最右,此时时间复杂度和冒泡其实时一样的,都是O(n^2)
流程分析
先从右往左查找小于基准数的元素,确定后再从左向右查找大于基准数的元素。这里要注意一个问题,当以最左边元素为基准数,必须要先从右往左查找。
用一个简单的例子:
| 基准数 | 左指针 | 右指针 | 左指针数 | 右指针数 | 当前序列 |
|---|---|---|---|---|---|
| 1 | 0 | 2 | 1 | 3 | 1 2 3 |
| 1 | 1 | 2 | 2 | 3 | 1 2 3 |
| 1 | 2 | 2 | 3 | 3 | 1 2 3 |
{1, 2, 3}可以发现开始快排时基准数为1,如果从左往右开始查找,左指针一直找不到小于基准数的元素,最后在序列最右侧与右指针相遇。
这时需要将基准数与相遇点的元素交换位置了,因为左指针不可控地跑到了最右边,此时交换得到的基准数位置并不是基准数应该在的位置。
严谨一点说,基准数应该在当前序列的最左侧或最右侧,开始查找时先移动的指针应是反方向的指针,这样才可以保证基准数已在正确位置时不会被移动到错误位置上。
然后来看我们的经典例子:
| 基准数 | 左指针 | 右指针 | 左指针数 | 右指针数 | 当前序列 |
|---|---|---|---|---|---|
| 2 | 0 | 4 | 2 | 9 | 2 1 5 3 9 |
| 2 | 0 | 3 | 2 | 3 | 2 1 5 3 9 |
| 2 | 0 | 2 | 2 | 5 | 2 1 5 3 9 |
| 2 | 0 | 1 | 2 | 1 | 2 1 5 3 9 |
| 2 | 1 | 1 | 1 | 1 | 2 1 5 3 9 |
这时左右指针相遇,将基准数与相遇点元素交换位置,分布递归执行基准数左侧序列和右侧序列,即{1}和{5, 3, 9}。
左侧只有一个元素,直接返回。
右侧(实际运行时递归序列仍为原序列,图表为了方便理解仅将指针改成待排序序列中元素的位置):
| 基准数 | 左指针 | 右指针 | 左指针数 | 右指针数 | 当前序列 |
|---|---|---|---|---|---|
| 5 | 0 | 2 | 5 | 9 | 5 3 9 |
| 5 | 0 | 1 | 5 | 3 | 5 3 9 |
| 5 | 1 | 1 | 3 | 3 | 5 3 9 |
交换5与3的位置,这个序列又分为左右两部分,{3}和{9},因为都只有一个元素,直接返回,便得到了排序完成的序列{1, 2, 3, 5, 9}。
代码实现
/*
* 快速排序
* 平均时间复杂度:O(nlogn)
* 最坏时间复杂度:O(n^2)
* 空间复杂度:(logn)
* 稳定性:不稳定
*/
public static int[] quick(int[] li) {
int[] c = copy(li);
quickSort(c, 0, c.length - 1);
return c;
}
public static void quickSort(int[] li, int left, int right) {
if (left >= right) {
return;
}
// 基准数
int index = left;
int i = left;
int j = right;
int tep = li[i];
while (i < j) {
// 从右向左找到第一个小于li[index]的元素
while (li[j] > li[index] && j > i) {
j--;
}
li[i] = li[j];
// 从左向右找到第一个大于li[index]的元素
while (li[i] < li[index] && j > i) {
i++;
}
li[j] = li[i]
}
// 循环结束后i和j相遇
li[i] = tep;
// 排序基准数左侧
quickSort(li, left, index - 1);
// 排序基准数右侧
quickSort(li, index + 1, right);
}
本文详细介绍了五种常见的排序算法:插入排序、冒泡排序、选择排序、堆排序和快速排序。每种算法均通过Java代码实现,并附有详细的流程分析及时间复杂度说明。
649

被折叠的 条评论
为什么被折叠?



