7种经典排序算法Java代码实现(冒泡+选择+插入+归并+快排+堆排序+希尔)

冒泡排序

冒泡排序的原理是:比较两个相邻的元素,将值较大的元素交换到至右端(比较+移动)

  1. public static void sortMpao(int[] num) {
  2.     for(int i = 1; i < num.length; i++) { //控制循环多少趟(n-1趟)
  3.         for(int j = 1; j <= num.length - i; j++) { //控制每一趟的循环次数(n-i次)
  4.             if(num[j - 1] > num[j]) {
  5.                 int temp = num[j - 1];
  6.                 num[j - 1] = num[j];
  7.                 num[j] = temp;
  8.             }
  9.       }
  10.    }
  11. }

时间复杂度:O(n^2)

空间复杂度:O(n^2)

选择排序

选择排序原理:每一趟从待排序的记录中选出最小的元素,顺序放在已排好序的序列最后,直到全部记录排序完毕。也即是在 n - i + 1(i = 1, 2, ..., n - 1) 个记录中选取关键字最小的记录作为有序数组的第 i 个记录。

  1. public static void sortXze(int[] nums) {
  2.     for(int i = 0; i < nums.length - 1; i++) { //n - 1 趟循环,第 i 趟排序
  3.         int k = i; //用于记录最小数值的位置
  4.         for(int j = k + 1; j < nums.length; j++) { //选择最小值记录位置,然后交换(min操作,依次和min值进行比较并更新min值)
  5.             if(nums[j] < nums[k]) {
  6.                 k = j; //记录下目前找到的最小值所在的位置
  7.             }
  8.         }
  9.         //在内层循环结束,也即是找到了本轮循环的最小的数之后,再进行交换
  10.         if(i != k) {
  11.             int temp = nums[i];
  12.             nums[i] = nums[k];
  13.             nums[k] = temp;
  14.         }
  15.     }
  16. }

时间复杂度:O(n^2)

插入排序

插入排序原理:把n个待排序的元素看成是一个有序表和一个无序表,开始时有序表中只有一个元素,无序表中有 n - 1 个元素。排序过程中,即每次从无序表中取出第一个元素,将它插入到有序表中,使之成为新的有序表,重复 n - 1 次完成整个排序过程

  1. public static void sortCru(int[] nums) {
  2.     for(int i = 1; i < nums.length; i++) { //循环 n - 1 趟,标记带插入元素
  3.  
  4.         int temp = nums[i]; //temp为本次循环待插入到有序表中的数
  5.         int j = i - 1; //因为for循环外还要使用变量j,用于标记插入位置下标,j = i - 1,表示有序表最后一位数
  6.         for(; j >= 0 && nums[j] > temp; j--) {
  7.             nums[j + 1] = nums[j]; //依次将大于temp的元素往后移动一位,为插入temp作准备
  8.         }
  9.         //找到了合适的位置,要么是temp > nums[j],要么是j = -1,则temp插入下标为 j + 1
  10.         nums[j + 1] = temp;
  11.     }
  12. }

时间复杂度:O(n^2)

空间复杂度:O(1)

稳定性:相同元素的相对位置不会改变

归并排序

算法原理:归并排序是将多个有序数据表合并成一个有序数据表。如果参与合并的只有两个有序表,则称为二路合并,而对于一个原始的待排序数列,往往可以通过分割(二分)的方法来归结为多路合并排序。归并排序算法采用了经典的分治(divide-and0conquer)策略,将问题先分成一些小问题,然后递推下去,在"治"的阶段将分的阶段得到的各答案"修补"在一起。

  1. //二分法进行递归排序
  2. public static int[] sort(int[] nums, int low, int high) {
  3.     int mid = low + (high - low)/2; //将输入数组分成两个部分
  4.     if(low < high) {
  5.         //左边
  6.         sort(nums, low, mid);
  7.         //右边
  8.         sort(nums, mid + 1, high);
  9.         merge(nums, low, mid, high); //递归终止时,数组长度为2,因此此时的合并即包含排序过程
  10.     }
  11. return nums;
  12. }
  13. //合并两个有序数组方法,这里由于是分割原数组,因此可以直接用辅助数组填补原数组即可,不需要返回结果了
  14. public static void merge(int[] nums, int low, int mid, int high) {
  15.     int[] temp = new int[high - low + 1]; //辅助数组,和原数组大小一样
  16.     int i = low; //左指针
  17.     int j = mid + 1; //右指针
  18.     int k = 0;
  19.     //把较小的数移到新数组中去
  20.     while(i <= mid && j <= high) {
  21.         temp[k++] = nums[i] < nums[j] ? nums[i++] : nums[j++];
  22.     }
  23.     //如果左边有剩余,则把左边剩余的数移入数组
  24.     while(i <= mid) {
  25.         temp[k++] = nums[i++];
  26.     }
  27.     //如果右边剩余,则把右边剩余的数移入数组
  28.     while(j <= high) {
  29.         temp[k++] = nums[j++];
  30.     }
  31.     //新数组中的数字覆盖nums数组
  32.     for(int k2 = 0; k2 < temp.length; k2++) {
  33.         nums[k2 + low] = temp[k2]; //注意新数组是在原数组的low位置开始的,high位置结束的(受mid影响)
  34.     }
  35. }

时间复杂度:O(nlogn)

空间复杂度:O(n)

快速排序

算法原理:选择一个关键值作为基准值(一般选择序列的第一个元素作为基准值),比基准值小的都在左边序列(一般是无序的),比基准值大的都在右边(一般是无序的)。一次循环:从后往前比较,用基准值和最后一个值比较,如果比基准值小的交换位置,如果没有继续比较下一个,直到找到第一个比基准值小的值才交换。找到这个值之后,又从前往后开始比较,如果有比基准值大的,交换位置,如果没有继续比较下一个,直到找到第一个比基准值大的值才交换。直到从前往后的比较索引等于从后往前比较的索引,结束第一次循环,此时,对于基准值来说,左右两边就是有序的了(左边均小于基准值,右边均大于基准值,但是左右两边仍然可能是无序的)。接着分别比较左右两边的序列,重复上述的循环。

  1. public static void sort(int[] nums, int low, int high) {
  2.     int start = low;
  3.     int end = high;
  4.     int key = nums[low]; //基准值,一般选取左边序列第一个元素
  5.     while(end > start) { 
  6.         //从后往前比较
  7.         //如果没有比关键值小的,比较下一个,直到有比基准值小的值,记录下位置end
  8.        while(end > start && nums[end] > key) {
  9.             end--;
  10.         }
  11.        //比基准值小的位置上的元素和左边基准值进行交换(此时基准值换到了右边)
  12.        if(nums[end] <= key) {
  13.            int temp = nums[end];
  14.             nums[end] = nums[start]; 
  15.             nums[start] = temp; //注意交换的是start和end索引对应的值
  16.     }
  17.     //然后从前往后比较
  18.     //如果没有比关键值大的,比较下一个,直到有比关键值大的元素,记录下位置start
  19.     while(end > start && nums[start] < key) {
  20.         start++;
  21.     }
  22.     //比基准值大的位置上的元素和右边基准值进行交换(此时基准值换到了左边)
  23.     if(nums[start] >= key) {
  24.         int temp = nums[start];
  25.         nums[start] = nums[end]; //此时nums[start]和key值相等
  26.         nums[end] = temp; //注意交换的是start和end索引对应的值
  27.     }
  28. }
  29. //第一次循环比较结束,关键值位置也已经确定(此时start=end,位于key值处)。左边的值都比关键值小,右边的值都比关键值大,但是两边的顺序还有可能是不一样的,进行下面的递归调用,直到 low == start == end == high时,也即是一个元素时,则所有系必有序(相对于分治中先小的有序然后逐层合并有序数组,快速排序则是先大致有序,逐层缩小排序范围,直到最后一个元素,共同点是都使用了递归算法,且递归终止条件类似)
  30.     if(start > low) sort(nums, low, start-1); //左边序列,第一个索引位置到关键值索引-1
  31.     if(end < high) sort(nums, end+1, high); //右边序列,从关键值索引+1到最后一个
  32. }

时间复杂度:平均 O(nlog(n)),最坏情况下 O(n^2)

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

算法原理:现将待排序的数组元素分成多个子序列,使得每个子序列的元素个数相对较少,然后对各个子序列分别进行直接插入排序,待整个待排序列“基本有序”后,最后在对所有元素进行一次直接插入排序。因此,我们要采用跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。希尔排序是对直接插入排序算法的优化和升级。

  1. public static void shellSortSmallToBig(int[] nums) {
  2.     int j = 0;
  3.     int temp = 0;
  4.     //最开始的步长为原数组长度的一半,步长依次为原来的 1/2
  5.     for(int incre = nums.length / 2; incre > 0; incre /= 2) { //控制步长,最小为1,之后有序
  6.         for(int i = incre; i < nums.length; i++) { //每一个i表示同一步长不同子序列的某一个待插入元素,i = incre 表示第一个子序列的第一个待插入元素
  7.         temp = nums[i]; //待插入元素,在下标为i处
  8.         j = i - incre; //j等于 i减去步长incre
  9.         //完成一个子序列的一个元素的插入,步长为incre,故 j = j - incre
  10.         while(j >= 0 && temp < nums[j]) {
  11.             nums[j + incre] = nums[j]; //按照一定步长进行插入排序,元素后移为插入元素做准备
  12.             j = j - incre;
  13.         }
  14.             nums[j + incre] = temp;
  15.         }
  16.     }
  17. }

希尔排序优点:希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些

堆排序

reference:https://blog.csdn.net/qq_33535433/article/details/74968686

算法原理:将数据看成是完全二叉树,根据完全二叉树的特性来进行排序的一种算法,堆排序的过程就是在..建堆—>交换...之间重复。一次建堆完成之后,我们的最大值(或最小值)就在堆的根节点之上,随后将堆顶最大值(或最小值)和数组最后的元素进行替换,于是就完成一趟排序了

  1. //原地堆排序(从索引0开始)
  2. public static void heapSort(int[] nums, int n) {
  3.     //将一个完全无序的数组构造成最大堆
  4.     for(int i = (n - 1) / 2; i >= 0; i--) {
  5.         shiftDown(nums, n, i);
  6.     }
  7.     //进行堆排序:依次将根节点(最大值)与最后一个值(最右叶子节点)进行交换
  8.     for(int i = n - 1; i > 0; i--) {
  9.         swap(nums, 0, i);
  10.         shiftDown(nums, i, 0); //将最大值放在原数组最后一个位置(n - 1),然后待排序数组长度正好为 n - 1
  11.     }
  12. }
  13. public static void _shiftDown(int[] nums, int n, int k) {
  14.     //数组下标从0开始,则满二叉树父子节点间关系是:left = 2 * parent + 1, right = 2 * parent + 2, 终止条件为 n - 1
  15.     //数组下标从1开始,则满二叉树父子节点间关系是:left = 2 * parent, right = 2 * parent + 1, 终止条件为 n (正好吻合节点总数)
  16.     while((2 * k + 1) < n){ //有左子节点(n - 1才为最后一个节点下标)
  17.         int j = 2 * k + 1; //记录最大子节点下标(初始为左子节点)
  18.         if((j + 1) < n && (nums[j + 1] > nums[j])){ //有右子节点且右边的更大
  19.             j += 1;
  20.         }
  21.         if(nums[k] >= nums[j]) //如果父节点大于等于子节点,则停止循环
  22.             break;
  23.         swap(nums, k, j);
  24.         k = j; //k被赋为当前位置,为下次循环做初始化
  25.     }
  26. }
  27. public static void swap(int[] arr,int a,int b){
  28.     int c=arr[a];
  29.     arr[a]=arr[b];
  30.     arr[b]=c;
  31. }

时间复杂度:O(nlog(n)),最坏情况下也和平均一样,均如此

猜你喜欢

转载自blog.csdn.net/zhouqj1913/article/details/84836991