涉及排序算法列表
排序算法:冒泡排序,插入排序,选择排序,归并排序,快速排序
算法分析评价涉及层面
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)); } }