涉及排序算法列表
排序算法:冒泡排序,插入排序,选择排序,归并排序,快速排序
算法分析评价涉及层面
- 1.最好情况、最坏情况、平均情况时间复杂度分析
- 2.原地排序(Sorted in place)。原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。
- 3.稳定性。这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
时间复杂度O(n2)算法:冒泡排序,插入排序,选择排序
冒泡排序,插入排序,选择排序代码
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Random;
/**
* 排序算法的执行效率分析
* <p>
* 1.最好情况、最坏情况、平均情况时间复杂度
* 2.原地排序(Sorted in place)。原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。
* 3.稳定性。这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
*
* @author gaoge
* @since 2022/11/29 17:07
*/
public class Sort {
/**
* 冒泡排序
* <p>
* 冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。
* 如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。
* 冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1),是一个原地排序算法。
* 最好情况下,要排序的数据已经是有序的了,我们只需要进行一次冒泡操作,就可以结束了,所以最好情况时间复杂度是 O(n)。
* 而最坏的情况是,要排序的数据刚好是倒序排列的,我们需要进行 n 次冒泡操作,所以最坏情况时间复杂度为 O(n2)。
* 平均情况下的时间复杂度就是 O(n2)
*
* @param a 数组
* @param n 数组大小
*/
public static void bubbleSort(int[] a, int n) {
for (int i = 0; i < n; i++) {
boolean flag = false;
for (int j = 0; j < n - i - 1; j++) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
flag = true;
}
}
if (!flag) {
break;
}
}
}
/**
* 插入排序
* <p>
* 首先,我们将数组中的数据分为两个区间,已排序区间和未排序区间。
* 初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,
* 在已排序区间中找到合适的插入位置将其插入,
* 并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。
* 插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是 O(1),也就是说,这是一个原地排序算法。
* 在插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素的后面,
* 这样就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法。
* 最好是时间复杂度为 O(n),最坏情况时间复杂度为 O(n2),平均时间复杂度为 O(n2)。
*
* @param a 数组
* @param n 数组大小
*/
public static void insertionSort(int[] a, int n) {
if (n <= 1) {
return;
}
for (int i = 1; i < n; ++i) {
int value = a[i];
int j = i - 1;
// 查找插入的位置
for (; j >= 0; --j) {
if (a[j] > value) {
// 数据移动
a[j + 1] = a[j];
} else {
break;
}
}
// 插入数据
a[j + 1] = value;
}
}
/**
* 选择排序
* <p>
* 选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。
* 但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
* 选择排序是一种不稳定的排序算法。比如 5,8,5,2,9 这样一组数据,使用选择排序算法来排序的话,
* 第一次找到最小元素 2,与第一个 5 交换位置,那第一个 5 和中间的 5 顺序就变了,所以就不稳定了。
* 正是因此,相对于冒泡排序和插入排序,选择排序就稍微逊色了。
* 首先,选择排序空间复杂度为 O(1),是一种原地排序算法。
* 选择排序的最好情况时间复杂度、最坏情况和平均情况时间复杂度都为 O(n2)。
*
* @param a 数组
* @param n 数组大小
*/
public static void selectionSort(int[] a, int n) {
for (int i = 0; i < n; i++) {
int value = a[i];
int min = value;
int num = i;
for (int j = i; j < n; j++) {
if (a[j] < min) {
min = a[j];
num = j;
}
}
a[i] = min;
a[num] = value;
}
}
/**
* 生成20个随机数组
*
* @return 随机数组
* @author gaoge
* @since 2022/11/29 17:07
*/
public static int[] randomArray() throws NoSuchAlgorithmException {
int[] arr = new int[10];
Random rand = SecureRandom.getInstanceStrong();
for (int i = 0; i < arr.length; i++) {
//生成随机数100以内;
arr[i] = rand.nextInt(100);
}
return arr;
}
/**
* 测试排序算法
*
* @param args
* @throws NoSuchAlgorithmException
*/
public static void main(String[] args) throws NoSuchAlgorithmException {
int[] ints;
String start = "原始数组:";
String end = "排序数组:";
//冒泡排序
System.out.println("冒泡排序");
ints = randomArray();
System.out.println(start + Arrays.toString(ints));
bubbleSort(ints, ints.length);
System.out.println(end + Arrays.toString(ints));
//插入排序
System.out.println("插入排序");
ints = randomArray();
System.out.println(start + Arrays.toString(ints));
insertionSort(ints, ints.length);
System.out.println(end + Arrays.toString(ints));
//选择排序
System.out.println("选择排序");
ints = randomArray();
System.out.println(start + Arrays.toString(ints));
selectionSort(ints, ints.length);
System.out.println(end + Arrays.toString(ints));
}
}
时间复杂度O(nlogn)算法:归并排序、快速排序
归并排序代码
import java.util.Arrays;
/**
* 归并排序
* <p>
* 归并排序的核心思想:如果要排序一个数组,我们先把数组从中间分成前后两部分,
* 然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了
* 我们申请一个临时数组 tmp,大小与 A[p...r]相同。我们用两个游标 i 和 j,
* 分别指向 A[p...q]和 A[q+1...r]的第一个元素。
* 比较这两个元素 A[i]和 A[j],如果 A[i]<=A[j],我们就把 A[i]放入到临时数组 tmp,
* 并且 i 后移一位,否则将 A[j]放入到数组 tmp,j 后移一位。
* 继续上述比较过程,直到其中一个子数组中的所有数据都放入临时数组中,
* 再把另一个数组中的数据依次加入到临时数组的末尾,这个时候,
* 临时数组中存储的就是两个子数组合并之后的结果了。
* 最后再把临时数组 tmp 中的数据拷贝到原数组 A[p...r]中。
* <p>
* 在合并的过程中,如果 A[p...q]和 A[q+1...r]之间有值相同的元素,
* 先把 A[p...q]中的元素放入 tmp 数组。这样就保证了值相同的元素,
* 在合并前后的先后顺序不变。所以,归并排序是一个稳定的排序算法。
* <p>
* 不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)
* <p>
* 临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)。
* <p>
* 归并排序不是原地排序算法。
* 这是因为归并排序的合并函数,在合并两个有序数组为一个有序数组时,需要借助额外的存储空间。
*
* @author gaoge
* @since 2022/11/29 17:07
*/
public class MergeSort {
/**
* 归并排序算法
*
* @param a 是数组
* @param n 表示数组大小
*/
public static void mergeSort(int[] a, int n) {
mergeSortInternally(a, 0, n - 1);
}
/**
* 递归调用函数
*
* @param a 数组
* @param p 开始索引
* @param r 结束索引
*/
private static void mergeSortInternally(int[] a, int p, int r) {
// 递归终止条件
if (p >= r) {
return;
}
// 取p到r之间的中间位置q,防止(p+r)的和超过int类型最大值
int q = p + (r - p) / 2;
// 分治递归,光看第一层
mergeSortInternally(a, p, q);
mergeSortInternally(a, q + 1, r);
// 将A[p...q]和A[q+1...r]合并为A[p...r]
merge(a, p, q, r);
}
private static void merge(int[] a, int p, int q, int r) {
int i = p;
int j = q + 1;
int k = 0;
// 申请一个大小跟a[p...r]一样的临时数组
int[] tmp = new int[r - p + 1];
while (i <= q && j <= r) {
if (a[i] <= a[j]) {
tmp[k++] = a[i++];
} else {
tmp[k++] = a[j++];
}
}
// 判断哪个子数组中有剩余的数据
int start = i;
int end = q;
if (j <= r) {
start = j;
end = r;
}
// 将剩余的数据拷贝到临时数组tmp
while (start <= end) {
tmp[k++] = a[start++];
}
// 将tmp中的数组拷贝回a[p...r]
for (i = 0; i <= r - p; ++i) {
a[p + i] = tmp[i];
}
}
/**
* 合并(哨兵)
*
* @param arr 数组
* @param p 开始索引
* @param q 中间索引
* @param r 结束所有
*/
private static void mergeBySentry(int[] arr, int p, int q, int r) {
int[] leftArr = new int[q - p + 2];
int[] rightArr = new int[r - q + 1];
for (int i = 0; i <= q - p; i++) {
leftArr[i] = arr[p + i];
}
// 第一个数组添加哨兵(最大值)
leftArr[q - p + 1] = Integer.MAX_VALUE;
for (int i = 0; i < r - q; i++) {
rightArr[i] = arr[q + 1 + i];
}
// 第二个数组添加哨兵(最大值)
rightArr[r - q] = Integer.MAX_VALUE;
int i = 0;
int j = 0;
int k = p;
while (k <= r) {
// 当左边数组到达哨兵值时,i不再增加,直到右边数组读取完剩余值,同理右边数组也一样
if (leftArr[i] <= rightArr[j]) {
arr[k++] = leftArr[i++];
} else {
arr[k++] = rightArr[j++];
}
}
}
public static void main(String[] args) {
int[] ints = new int[]{9, 8, 4, 6, 7, 7, 5, 4, 0, 3, 34, 12};
System.out.println(Arrays.toString(ints));
mergeSort(ints, ints.length);
System.out.println(Arrays.toString(ints));
}
}
快速排序代码
import java.util.Arrays;
/**
* 快速排序
* <p>
* 快排的思想是这样的:如果要排序数组中下标从 p 到 r 之间的一组数据,
* 我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。
* 我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,
* 将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,
* 数组 p 到 r 之间的数据就被分成了三个部分,
* 前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,
* 后面的 q+1 到 r 之间是大于 pivot 的
* 根据分治、递归的处理思想,
* 我们可以用递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到 r 之间的数据,
* 直到区间缩小为 1,就说明所有的数据都有序了。
* <p>
* 如果每次分区操作,都能正好把数组分成大小接近相等的两个小区间,
* 那快排的时间复杂度递推求解公式跟归并是相同的。所以,快排的时间复杂度也是 O(nlogn)。
* 但是,公式成立的前提是每次分区操作,我们选择的 pivot 都很合适,
* 正好能将大区间对等地一分为二。但实际上这种情况是很难实现的。
* 我举一个比较极端的例子。如果数组中的数据原来已经是有序的了,比如 1,3,5,6,8。如果我们每次选择最后一个元素作为 pivot,
* 那每次分区得到的两个区间都是不均等的。我们需要进行大约 n 次分区操作,
* 才能完成快排的整个过程。每次分区我们平均要扫描大约 n/2 个元素,
* 这种情况下,快排的时间复杂度就从 O(nlogn) 退化成了 O(n2)。
* T(n) 在大部分情况下的时间复杂度都可以做到 O(nlogn),只有在极端情况下,才会退化到 O(n2)。
* 而且,我们也有很多方法将这个概率降到很低,
* <p>
* 快速排序通过设计巧妙的原地分区函数,可以实现原地排序,
* 解决了归并排序占用太多内存的问题。
* <p>
* 因为分区的过程涉及交换操作,如果数组中有两个相同的元素,比如序列 6,8,7,6,3,5,9,4,
* 在经过第一次分区操作之后,两个 6 的相对先后顺序就会改变。所以,快速排序并不是一个稳定的排序算法。
*
* @author gaoge
* @since 2022/11/29 17:07
*/
public class QuickSort {
/**
* 快速排序算法
*
* @param a 是数组
* @param n 表示数组大小
*/
public static void quickSort(int[] a, int n) {
quickSortInternally(a, 0, n - 1);
}
/**
* 递归调用函数
*
* @param a 数组
* @param p 开始索引
* @param r 结束索引
*/
private static void quickSortInternally(int[] a, int p, int r) {
if (p >= r) {
return;
}
// 获取分区点
int q = partition(a, p, r);
quickSortInternally(a, p, q - 1);
quickSortInternally(a, q + 1, r);
}
private static int partition(int[] a, int p, int r) {
int pivot = a[r];
int i = p;
for (int j = p; j < r; ++j) {
if (a[j] < pivot) {
if (i == j) {
++i;
} else {
int tmp = a[i];
a[i++] = a[j];
a[j] = tmp;
}
}
}
int tmp = a[i];
a[i] = a[r];
a[r] = tmp;
return i;
}
public static void main(String[] args) {
int[] ints = new int[]{9, 8, 4, 6, 7, 7, 5, 4, 0, 3, 34, 12};
System.out.println(Arrays.toString(ints));
quickSort(ints, ints.length);
System.out.println(Arrays.toString(ints));
}
}