• 常见排序算法实现


      💕"每一天都是值得被热爱的"💕

    作者:Mylvzi 

     文章主要内容:常见排序算法实现 

    1.排序的概念

      所谓排序,就是按照特定顺序重新排列序列的操作

    排序的稳定性:

    当一个序列中存在相同的元素时  排序过后  他们出现的顺序和原来序列的顺序一致就说明此排序稳定;否则,就不稳定

    内部排序:

    • 内部排序是指对数据集合完全加载到内存中进行排序的过程。
    • 这种排序方法适用于数据量较小,可以在计算机内存中容纳整个数据集的情况。
    • 常见的内部排序算法包括冒泡排序、选择排序、插入排序、快速排序、归并排序等。
    • 内部排序的优点是速度较快,因为所有数据都在内存中,不需要频繁的读取和写入操作。

    外部排序:

    • 外部排序是指对大规模数据集合进行排序的方法,数据量太大,无法一次性加载到内存中。
    • 这种排序方法涉及将数据分割成适当大小的块,然后在内存中对这些块进行排序,最后将排序后的块写回磁盘或其他存储介质,并合并这些块以得到最终的排序结果。
    • 外部排序常用于需要处理大量数据的场景,比如数据库系统中对大型表进行排序或外部存储设备上的数据排序。
    • 常见的外部排序算法包括归并排序、多路归并排序等,这些算法允许对大规模数据进行高效排序,因为它们能够有效地利用磁盘I/O操作。

    常见的排序算法

    1.插入排序

      插入排序是一种非常简单的排序  比如在玩扑克牌时  在发牌阶段  我们每抽取一张牌  都要从后往前去比较大小  把抽取的牌插入到合适的位置

      所以,插入排序就是将待排序的元素(抽取的牌)插入到一个已经有序的序列之中

    代码实现

    1. /**
    2. * 插入排序 在一个已经存在的序列中 插入到合适位置
    3. * 时间复杂度:
    4. * 最好情况:O(N)
    5. * 最坏情况:O(N^2)
    6. * 空间复杂度:
    7. * O(1)
    8. * 稳定性:是一个稳定的排序
    9. * 所以对于一个有序的序列来说 插入排序就是最快的
    10. */
    11. public static void insertSort(int[] arr) {
    12. for (int i = 1; i < arr.length; i++) {
    13. int tmp = arr[i];
    14. int j = i-1;
    15. for (; j >= 0; j--) {
    16. // >tmp j向后挪动
    17. if(arr[j] > tmp) {
    18. arr[j+1] = arr[j];
    19. }else {
    20. // 要插入的元素已经是最大的了 不需要再去比较了
    21. //arr[j+1] = tmp;
    22. break;
    23. }
    24. }
    25. // 跳出循环有两种情况 1.tmp是最小的需要插入到第一个元素 此时j=-1 结束条件是j不>=0了 2.else语句中的break;
    26. arr[j+1] = tmp;
    27. }
    28. }

     2.希尔排序(缩小增量排序)

    是根据插入排序的优点来进行优化的插入排序

    我们知道,插入排序对于有序化高的序列来说速度是更快的,也就是说一个序列有序化越高,使用插入排序的时间复杂度就越低,速度就越快  

    所以,对于一大堆的数据来说,我们可以先进行“预排”,使其有序化程度越来越高,从而实现效率更高

    设置gap  利用插入排序的思想  分组进行优化  组数不断降低  直到最后为1  最后一个进行排序时  序列的有序化程度已经很高  速度很快  

    希尔排序看似繁琐  实则提高了效率  虽然要进行多次插入排序  但时间优化了很多  主要原因在于以下几个方面:

    1.分组会使得待排序的数据量减小  每次排序的数据量少  时间快

    2.当gap = 1时,也就是要对整个序列进行排序  虽然数据量很大  但是有序化程度高  时间快

    希尔排序的分析过

      代码实现

    1. /**
    2. * 希尔排序 优化的插入排序
    3. * 先进行预排序 跳跃式进行分组 分的组数逐渐减少 直到组数为1
    4. * 分组优化
    5. * 时间复杂度:O(N^1.3)
    6. * 空间复杂度:O(1)
    7. * 稳定性:不稳定
    8. */
    9. public static void shellSort(int[] arr) {
    10. int gap = arr.length;
    11. while (gap > 1) {
    12. gap /= 2;
    13. shell(arr,gap);
    14. }
    15. }
    16. private static void shell(int[] arr,int gap) {
    17. for (int i = gap; i < arr.length; i++) {
    18. int tmp = arr[i];
    19. int j = i-gap;
    20. for (; j >= 0; j-= gap) {
    21. // >tmp j向后挪动
    22. if(arr[j] > tmp) {
    23. arr[j+gap] = arr[j];
    24. }else {
    25. // 要插入的元素已经是最大的了 不需要再去比较了
    26. //arr[j+1] = tmp;
    27. break;
    28. }
    29. }
    30. arr[j+gap] = tmp;
    31. }
    32. }

    3.选择排序

      选择排序也是一个比较简单的排序  其核心思想在于每次都要选择一个最小的/最大的元素位于最左边

    选择排序无论你的顺序如何  都要遍历整个数组去寻找最小值/最大值  所以对于初始顺序不敏感

    代码实现

    1. public static void selectSort(int[] arr) {
    2. for (int i = 0; i < arr.length; i++) {
    3. int minIndex = i;
    4. for (int j = i+1; j < arr.length; j++) {
    5. if(arr[j] < arr[minIndex]) {
    6. minIndex = j;
    7. }
    8. }
    9. swap(arr,i,minIndex);
    10. }
    11. }
    12. private static void swap(int[] arr,int i,int j) {
    13. int tmp = arr[i];
    14. arr[i] = arr[j];
    15. arr[j] = tmp;
    16. }

    4.堆排

    堆排 就是利用堆的特性进行排序的一种方式  

    思路:

    1.看条件创建堆  升序--》大根堆   降序--》小根堆

    2.交换首元素和末元素  向下调整

    代码实现:

    1. /**
    2. * 堆排 从小到大
    3. * 1.创建大根堆
    4. * 2.交换 向下调整
    5. */
    6. public static void heapSort(int[] arr) {
    7. creatBigHeap(arr);
    8. int end = arr.length-1;
    9. while(end > 0) {
    10. swap(arr,0,end);
    11. shiftDown(arr,0,end);
    12. end--;
    13. }
    14. }
    15. private static void creatBigHeap(int[] arr) {
    16. for (int parent = (arr.length-1-1)/2; parent >= 0; parent--) {
    17. shiftDown(arr,parent,arr.length);
    18. }
    19. }
    20. private static void shiftDown(int[] arr, int parent, int end) {
    21. int child = 2*parent+1;
    22. while (child < end) {
    23. // 判断左右孩子谁是最大的
    24. if(child+1 < end && arr[child] < arr[child+1]) {
    25. child++;
    26. }
    27. if(arr[child] > arr[parent]) {
    28. swap(arr,child,parent);
    29. parent = child;
    30. child = 2*parent+1;
    31. }else {
    32. break;
    33. }
    34. }
    35. }

    5.快速排序

      核心思路:分而治之

    概念:任取待排序元素序列中的某元 素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有 元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

    快速排序  本质上是一个递归的过程  有序时  会出现单叉树  此时时间复杂度最高  达到0(N^2)

      

    代码实现

    1. /**
    2. * 快速排序
    3. *时间复杂度:O(N*logN); 每次分而治之需要遍历的数字都是N 最好情况是一颗完全二叉树 高度为logN 所以时间复杂度是N*logn
    4. *
    5. * 容易溢出 占用内存大
    6. * 三种写法:hore法 挖坑法(推荐) 前后指针法
    7. * 递归方法 最好的情况是一种完全二叉树
    8. */
    9. public static void quickSort(int[] arr) {
    10. quick(arr,0,arr.length-1);
    11. }
    12. private static void quick(int[] arr,int start,int end) {
    13. if(start >= end) return;
    14. if(end - start + 1 > 7) {
    15. insertSortRange(arr,start,end);
    16. return;
    17. }
    18. midOfThree(arr,start,end);
    19. // 获得按照规则交换后的基准值的下标
    20. int pivot = parttion(arr,start,end);
    21. // 遍历左子树 分而治之
    22. quick(arr,start,pivot-1);
    23. // 遍历右子树 分而治之
    24. quick(arr,pivot+1,end);
    25. }
    26. public static void insertSortRange(int[] arr,int begin,int end) {
    27. for (int i = 1; i <= end; i++) {
    28. int tmp = arr[i];
    29. int j = i-1;
    30. for (; j >= begin; j--) {
    31. // >tmp j向后挪动
    32. if(arr[j] > tmp) {
    33. arr[j+1] = arr[j];
    34. }else {
    35. // 要插入的元素已经是最大的了 不需要再去比较了
    36. //arr[j+1] = tmp;
    37. break;
    38. }
    39. }
    40. // 跳出循环有两种情况 1.tmp是最小的需要插入到第一个元素 此时j=-1 结束条件是j不>=0了 2.else语句中的break;
    41. arr[j+1] = tmp;
    42. }
    43. }
    44. private static void midOfThree(int[] arr, int left, int right) {
    45. int mid = (left + right) / 2;
    46. if(arr[left] > arr[right]) swap(arr,left,right);// 保证左边元素是较小值
    47. if(arr[mid] > arr[right]) swap(arr,mid,right);// 保证中间元素是较小值
    48. if(arr[mid] > arr[left]) swap(arr,mid,left);// 此时保证left下标的值是中间大小
    49. }
    50. private static int parttionHoare(int[] arr,int left,int right) {
    51. int i = left;
    52. // 每次都选取第一个元素为基准值
    53. int key = arr[left];
    54. // 遍历交换
    55. // left 从左往右 找比Key大的
    56. // right 从右往左 找比key小的
    57. while (left < right) {
    58. /**
    59. * 为什么先走右边
    60. * 先走right保证他们相遇时 一定是比key值小的数据
    61. * 如果先走left 相遇时碰到的一定是比key大的 此时再交换 则key的左边存在比key大的数据了
    62. */
    63. // 先从右边找
    64. while (left < right && arr[right] >= key) { // 等号必须要取 万一两个都是6 会陷入死循环
    65. right--;
    66. }
    67. // 此时right下标的元素比key小
    68. while (left < right && arr[left] <= key) {
    69. left++;
    70. }
    71. swap(arr,left,right);
    72. }
    73. // 使基准值位于中间(即左边都比key小 右边都比key大)
    74. swap(arr,i,left);
    75. return left;
    76. }
    77. /**
    78. * 快排的优化
    79. * 1.三数取中法:解决特殊情况 --》第一个数字是最小的 或者是最大的 减少了树的高度 开辟的空间更小
    80. * 2.小区间内采用插入排序 减少递归的次数(但时间有可能会增加) 降低了内存的要求
    81. */

    parttion的第二种写法:挖坑法

    1. // 挖坑法
    2. private static int parttion2(int[] arr,int left,int right) {
    3. // 每次都选取第一个元素为基准值
    4. int key = arr[left];
    5. // 遍历交换
    6. // left 从左往右 找比Key大的
    7. // right 从右往左 找比key小的
    8. while (left < right) {
    9. // 先从右边找
    10. while (left < right && arr[right] >= key) { // 等号必须要取 万一两个都是6 会陷入死循环
    11. right--;
    12. }
    13. // 此时right下标的元素比key小
    14. arr[left] = arr[right];
    15. while (left < right && arr[left] <= key) {
    16. left++;
    17. }
    18. arr[right] = arr[left];
    19. }
    20. // 使基准值位于中间(即左边都比key小 右边都比key大)
    21. arr[left] = key;
    22. return left;
    23. }

    parttion的第三种写法:前后指针法

    1. private static int parttion(int[] arr,int left,int right) {
    2. // 前后指针法
    3. int prev = left;
    4. int cur = left+1;
    5. while (cur <= right) {
    6. // 后面那个条件:cur和prev之间必须至少间隔一个元素
    7. // 如果没有间隔元素 交换后又把比left大的移到了左边
    8. if(arr[cur] < arr[left] && arr[++prev] != arr[cur]) {
    9. swap(arr,cur,prev);
    10. }
    11. cur++;
    12. }
    13. // 遍历完整个数组了
    14. swap(arr,left,prev);
    15. return prev;
    16. }

    注意:

    1.parttion一共有三种写法,推荐以挖坑法为先  前后指针或者Hoare法次之

    2.为什么快速排序要先走right?因为这样能够保证left和right最后相遇的位置的元素是比key小的元素交换过后仍满足条件

    快速排序的非递归写法

    1. // 快排的非递归写法
    2. public static void quickSortNor(int[] arr) {
    3. Stack stack = new Stack<>();
    4. int left = 0;
    5. int right = arr.length-1;
    6. int pivot = parttion(arr,left,right);
    7. // 如果等于 证明pivot左边只有一个数据 此时不需要再去排列了 下面的right同理
    8. if(pivot-1 > left) {
    9. stack.push(left);
    10. stack.push(pivot-1);
    11. }
    12. if (pivot + 1 < right) {
    13. stack.push(pivot+1);
    14. stack.push(right);
    15. }
    16. while(!stack.isEmpty()) {
    17. right = stack.pop();
    18. left = stack.pop();
    19. pivot = parttion(arr,left,right);
    20. if(pivot-1 > left) {
    21. stack.push(left);
    22. stack.push(pivot-1);
    23. }
    24. if (pivot + 1 < right) {
    25. stack.push(pivot+1);
    26. stack.push(right);
    27. }
    28. }
    29. }

     6.冒泡排序法(不做讲解)

    1. /**
    2. * 冒泡排序
    3. * 时间复杂度:O(N^2) 最好(加了优化)O(N)
    4. * 空间复杂度:O(1)
    5. * 稳定性好
    6. */
    7. public static void bubbleSort(int[] arr) {
    8. for (int i = 0; i < arr.length; i++) {
    9. boolean flg = false;
    10. for (int j = 0; j < arr.length-1-i; j++) {
    11. if(arr[j] > arr[j+1]) {
    12. swap(arr,j,j+1);
    13. flg = true;
    14. }
    15. }
    16. if(!flg) {
    17. return;
    18. }
    19. }
    20. }

    7.归并排序

       归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使 子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

    分解左边   分解右边   合并

    代码实现

    1. /**
    2. * 归并排序:
    3. * 时间复杂度:O(n*logN)
    4. * 空间复杂度:O(N);
    5. * 稳定
    6. *
    7. * 分解左边 分解右边 归并(合并两个有序数组)
    8. *
    9. * 稳定的排序:冒泡 插入 归并
    10. */
    11. public static void mergeSort(int[] arr) {
    12. mergeSortFunc(arr,0,arr.length-1);
    13. }
    14. private static void mergeSortFunc(int[] arr, int left, int right) {
    15. if(left >= right) return;
    16. int mid = (left+right) / 2;
    17. mergeSortFunc(arr,left,mid);
    18. mergeSortFunc(arr,mid+1,right);
    19. merge(arr,left,mid,right);
    20. }
    21. private static void merge(int[] arr, int left, int mid, int right) {
    22. // 这里边的思路相当于是 合并两个有序序列
    23. int[] tmpArr = new int[right-left+1];
    24. int k = 0;
    25. // 分别定义两个需要合并的数组的首元素的下标
    26. int s1 = left;
    27. int s2 = mid+1;
    28. // 遍历两个数组
    29. while(s1 <= mid && s2 <= right) {
    30. if(arr[s1] < arr[s2]) {
    31. tmpArr[k++] = arr[s1++];
    32. }else {
    33. tmpArr[k++] = arr[s2++];
    34. }
    35. }
    36. // 出循环证明有一个数组不为空
    37. while(s1 <= mid) {
    38. tmpArr[k++] = arr[s1++];
    39. }
    40. while (s2 <= right) {
    41. tmpArr[k++] = arr[s2++];
    42. }
    43. // 将排序好的元素返回给原数组
    44. for (int i = 0; i < tmpArr.length; i++) {
    45. arr[i+left] = tmpArr[i];
    46. }
    47. }

    非递归写法

    1. // 归并排序的非递归写法
    2. public static void mergeSortNor(int[] arr) {
    3. int gap = 1;
    4. while(gap < arr.length) {
    5. for (int i = 0; i < arr.length; i+= 2*gap) {
    6. int left = i;
    7. int mid = i+gap-1;
    8. int right = mid+gap;
    9. if(mid >= arr.length) {
    10. mid = arr.length-1;
    11. }
    12. if(right >= arr.length) {
    13. right = arr.length-1;
    14. }
    15. merge(arr,left,mid,right);
    16. }
    17. gap *= 2;
    18. }
    19. }

    8.计数排序

      设置一个计数数组  用于记录待排序数组中相应元素出现的次数  并按照下标排列  最后返回给原数组

    1. /**
    2. * 计数排序
    3. * 时间复杂度:O(N+范围)
    4. * 空间复杂度:O(范围)
    5. * 适用于数据范围集中 数据量较小的情况
    6. * 稳定性好
    7. */
    8. public static void countSort(int[] arr) {
    9. // 1.得到数组的最大值和最小值(数组的下标只能从0开始)
    10. int minVal = arr[0];
    11. int maxVal = arr[0];
    12. for (int i = 0; i < arr.length; i++) {
    13. if (arr[i] < minVal) {
    14. minVal = arr[i];
    15. }
    16. if (arr[i] > maxVal) {
    17. maxVal = arr[i];
    18. }
    19. }
    20. //2.遍历arr 并在计数数组中存放对应的次数
    21. int[] count = new int[maxVal - minVal + 1];
    22. for (int i = 0; i < arr.length; i++) {
    23. count[arr[i] - minVal]++;
    24. }
    25. //3.重新写入到原数组
    26. int index = 0;
    27. for (int i = 0; i < count.length; i++) {
    28. while(count[i] > 0) {
    29. arr[index] = i + minVal;
    30. index++;
    31. count[i]--;
    32. }
    33. }
    34. }

    总结:

    1. 稳定的排序:插入排序  冒泡排序  归并排序

    2.希尔排序的时间复杂度很复杂,到现在也没有一个具体的结论

    3.初始序列顺序对排序的影响 

    选择排序无论你的顺序如何  都要遍历整个数组去寻找最小值/最大值  所以对于初始顺序不敏感

    4.表格汇总 

  • 相关阅读:
    Redis 主从复制
    三谈exception——错误处理
    iOS——FMDB的介绍与使用
    C语言与内存息息相关的重要概念有哪些?
    JVM学习(尚硅谷)之垃圾回收相关概念
    mybatis-generator新版本代码生成器
    深度解析React 18应用性能提升
    一文详解贝叶斯优化(Bayesian Optimization)原理
    什么是响应式设计?响应式设计的基本原理是什么?如何兼容低版本的 IE?
    【Java UI】HarmonyOS添加日历事件
  • 原文地址:https://blog.csdn.net/Mylvzi/article/details/134403474