排序算法总结

排序算法

性能评估:

  • 时间复杂度: 最好、最坏、平均,以及它们分别的对应情况; 时间复杂度反应的是当数据规模n很大时的一个增长趋势,常忽略系数、常数和低阶。在排序算法中,设计到的操作有比较和交换,因此计算次数时需将两者考虑在内。
  • 空间复杂度: 特别的,原地排序就是指空间复杂度为O(1)的排序算法
  • 稳定性: 是指序列中有相等的元素,在排序之后,相等元素在序列中的相对顺序是否改变。不变的就是稳定排序。为什么讲求稳定性: 在实际开发中,常常是给对象进行多次排序,每次排序依照不同的key,因此在新的一轮排序中,不希望影响之前的排序结果。例如在电商中我们需要对订单按金额进行排序,相同金额的订单按照时间的先后顺序进行排序,则可以按时间进行排序之后,再用稳定的排序算法对金额进行排序。

总结

  • 常见的排序算法:
    • 交换排序: 冒泡排序、快速排序
    • 插入排序: 直接插入、希尔排序
    • 选择排序: 直接选择、堆排序
    • 归并排序
    • 基数排序
  • 稳定性:
    • 稳定排序: 冒泡排序、直接插入、归并排序、基数排序
    • 非稳定排序:直接选择、 快速排序、希尔排序、堆排序

冒泡排序

原理

依次进行相邻元素进行比较,根据升序还是降序要求,确定是否将两个元素的位置进行交换,每一轮冒泡排序确定一个元素的位置。要对n个元素进行排序,需要进行n-1轮。
改进方法,在冒泡的过程中,发现所有的相邻两元素都不需要再交换位置,说明此时数组已经有序了,可以提前终止循环。

代码
public static void generalBubble(int[] nums){
        //对n个元素进行冒泡排序,只需便利n-1轮
        for(int i = 0; i < nums.length-1; i++){
            for(int j = 0; j < nums.length-1-i; j++){
                //不加等号,保证了稳定性
                //是原地排序,空间复杂度为o(1)
                if(nums[j] > nums[j+1]){
//                    int tmp = nums[j];
//                    nums[j] = nums[j+1];
//                    nums[j+1] = tmp;
                    nums[j] ^= nums[j+1];
                    nums[j+1] ^= nums[j];
                    nums[j] ^= nums[j+1];
                }
            }
        }
        return;
    }
    //在遍历的过程中发现都不需要执行交换操作,证明整个数组此时已经有序了
    public static void BubbleSort(int[] nums){
        boolean flag = true;
        for(int i = 0; i < nums.length-1; i++){
            for(int j = 0; j < nums.length-1-i; j++){
                if(nums[j] > nums[j+1]){
                    int tmp = nums[j];
                    nums[j] = nums[j+1];
                    nums[j+1] = tmp;
                    flag = false;
                }
            }
            if(flag) break;
        }
        return;
    }
性能分析
  1. 时间复杂度:最好都是顺序的O(n),最坏都是逆序的O(n2),平均O(n2)

  2. 空间复杂度:原地排序,O(1)

  3. 稳定性:稳定的,在进行交换的判断条件中没有加入等号

插入排序

原理

从第二个元素开始遍历,每次将当前元素插入到前面有序子序列中的正确位置,插入的过程中,需要将比该元素大的元素向后移动。经过n-1轮插入操作之后,实现整个数组的有序。

代码
public static void insertSort(int[] nums){
        //从第二个元素开始遍历
        for(int i = 1; i < nums.length; i++){
            //有序的,继续
            if(nums[i] > nums[i-1]) continue;
            //无序,把当前元素插入到时候它的位置
            else{
                int tmp = nums[i];
                int j = i -1;
                for(; j >= 0 && tmp < nums[j]; j--){
                    //将元素向后移动一位
                    nums[j+1] = nums[j];
                }
                //将该元素插入到正取的位置
                nums[j+1] = tmp;
            }
        }
        return;
    }
性能分析
  1. 时间复杂度: 顺序的时候最好O(n),逆序的时候最坏O(n2),平均O(n2):循环遍历的复杂度是O(n),在数组中插入元素的复杂度是O(n)
    数组本身就基本有序的时候,插入排序几乎不用移动数据,此时的时间复杂度最好
  2. 空间复杂度: 原地排序,O(1)
  3. 稳定性: 稳定的,在是否进行交换的判断条件中没有加入等号

选择排序

原理

每一轮遍历过程,从未排序的序列中选出最小(或者最大)的元素,放到有序序列的末尾,直到待排序的序列为空。

代码
public static void selectSort(int[] nums){
        for(int i = 0; i < nums.length; i++){
            int minIndex = i;
            for(int j = i+1; j < nums.length; j++){
                if(nums[minIndex] > nums[j]){
                    minIndex = j;
                }
            }
            if(minIndex != i){
                int tmp = nums[i];
                nums[i] = nums[minIndex];
                nums[minIndex] = tmp;
            }
        }
        return;
    }
性能分析
  1. 时间复杂度: 最好、最坏、平均都是O(n^2) ,因为每一轮查找最小(或者最大)元素,都需要将无序序列中的所有元素遍历完
  2. 空间复杂度: O(1) 原地排序
  3. 稳定性: 不稳定,因为在排序的过程中,可能会将当前位置的元素与需要放在这个位置的元素进行调换,交换的过程中可能改变相等元素在数组中的相对位置。

希尔排序

原理

是插入排序的改进版,缩小增量排序,先按照增量进行分组,对一个组内的元素进行插入排序,减小增量继续分组和排序,直到增量为1。
先让数组中的元素达到部分有序,然后实现整体有序。

代码
public static void shellSort(int[] nums){
        for(int d = nums.length>>2; d > 0; d>>=2){
            for(int i = d; i < nums.length;i++){
                for(int j = i-d; j >= 0; j -= d){
                    if(nums[j] > nums[j+d]){
                        int tmp = nums[j+d];
                        nums[j+d] = nums[j];
                        nums[j] = tmp;
                    }
                }
            }
        }
        return;
    }
性能分析

时间复杂度:与选取的增量d有关,最好O(n)平均O(n^1.3),最坏增量为d=1 O(n^2)
空间复杂度:O(1)
稳定性: 不稳定
注意: 顺序表可以实现,但是链表无法实现,因为需要随机访问增量d

快速排序

原理

每次从数组中选取一个数作为基准,将小于它的数放右边,大于它的数放左边;完成之后,在按照同样的方法对左右两边子数组做同样的操作,直到左右两边的子数组都为空,则结束。一种分治的思想

代码
public static void quickSort(int[] nums,int start,int end){
        //退出递归的判断
        if(start >= end) return;
        int tmp = nums[start];
        int left = start;
        int right = end;
        while(left < right){
            while(left < right && tmp < nums[right]){
                right--;
            }
            nums[left] = nums[right];
            while(left < right && tmp >= nums[left]){
                left++;
            }
            nums[right] = nums[left];
        }
        nums[left] = tmp;
        quickSort(nums,start,left-1);
        quickSort(nums,left+1,end);
        return;
    }
性能分析
  1. 时间复杂度:==O(n*递归的层数)==最好和平均是O(nlogn)每次划分是按照折半划分,最坏是O(n^2)数组本身就为有序,递归深度为n;选取的基准元素将序列划分越均匀,时间复杂度越好
  2. 空间复杂度:O(递归层数) 最好O(logn),最坏O(n)
  3. 稳定性:不稳定
    递归层数可以用一棵二叉树的高度来表示,n个节点的二叉树,最小高度为logn向下取整+1,最大高度为n,与“划分”是否均匀有关

归并排序

原理

二路排序: 在基于两个有序子序列的基础上,分别从头到尾遍历两个子序列,比较相对大小,将更小那个保存到返回数组中,从而将两个有序的子序列合并成为了一个有序的序列。采用分治的思想,子序列的长度分别为1,2,4…

代码
// 采用二路排序的方法
public static void mergeSort(int[] nums, int start, int end){
        int mid = start + (end - start)/2;
        //采用递归的方法,一定要有跳出递归的判断条件
        if(start < end) {
            mergeSort(nums, start, mid);
            mergeSort(nums, mid + 1, end);
            merge(nums, start, end, mid);
        }
    }
    public static void merge(int[] nums,int start,int end,int mid){
        //开辟一个和原数组空间一样大的数组来保存结果
        int[] numsTmp = new int[nums.length];
        //将两个有序序列的数值都复制到另一个数组中
        for(int i = start; i <= end; i++){
            numsTmp[i] = nums[i];
        }
        int i = start;
        int j = mid + 1;
        int index = start;
        while(index <= end){
            if(i > mid && j <= end){
                while(j <= end){
                    nums[index++] = numsTmp[j++];
                }

            }
            else if(j > end && i <= mid){
                while(i <= mid){
                    nums[index++] = numsTmp[i++];
                }

            }
            else nums[index++] = numsTmp[i] <= numsTmp[j] ? numsTmp[i++] : numsTmp[j++];
        }
    }
性能分析
  1. 时间复杂度:归并的次数*每次归并的操作数 ; 最好、最坏和平均 O(nlogn)

  2. 空间复杂度:额外开辟了一个和数组一样大的空间O(n),递归工作站的保存O(logn) 所以最终归并排序的空间复杂度为O(n)

  3. 稳定性分析:稳定的

基数排序

原理

例如将一组数进行排序,可以先按照各位的大小进行“分配”和“收集”,将个位相等的数放在同一个队列中,将所有的数都进行分配后,再按照个位数从小到大进行收集;接着再按照十位进行相同的操作,直到最高位也完成。达到的效果就是: 数先按最高位进行排序,当最高位相同时在按照它的下一位进行排序。

代码
性能分析
  1. 时间复杂度: O(d*(n+r)) 进行了d趟的分配和收集,r是指r个队列,n为元素数
  2. 空间复杂度: O(r*d+n)需要r个辅助队列
  3. 稳定性: 稳定
    适用于:数据元素的关键字可以查分为d组, n很大,d小,关键字的取值范围r较小的情况

堆排序

原理

属于选择排序中的一种,将原始数组转换成为堆的形式保存,更易找到最大值和最小值

  • 堆: 分为大根堆和小根堆,可以看做是一棵完全二叉树,在大根堆中:完全二叉树中,根节点的值大于左右孩子的值; 小根堆: 完全二叉树中,根节点的值小于左右孩子的值。
  • 基于大根堆实现递增序列,基于小根堆实现递减序列
  • 实现堆排序的过程:建堆,建立一个大根堆,对小于n/2的节点进行逐层下坠,时间复杂度为O(n);排序,进行n-1趟排序,每次将堆顶元素与堆底元素进行交换,并重新维护大根堆,时间复杂度为O(nlogn)
代码
public static void heapSort(int[] nums,int len){
        //创建大根堆
        for (int i = len >> 2; i >= 0 ; i--) {
            heapAdjust(nums,i,len);
        }
        for(int i = len; i > 0; i--){
            swap(nums,0,i);
            heapAdjust(nums,0,i-1);
        }
    }
    public static void swap(int[] nums, int i, int j){
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
        return;
    }
    public static void heapAdjust(int[] nums,int index, int len){
        int tmp = nums[index];
        for(int j = index*2; j <= len; j *= 2){
            if(j < len && nums[j] < nums[j+1]){
                j++;
            }
            if(tmp < nums[j]){
                nums[index]  = nums[j];
                index = j;
            }
            if(tmp >= nums[j]) break;
        }
        nums[index] = tmp;
    }
性能分析
  1. 时间复杂度:最好、最坏和平均 O(nlogn)
  2. 空间复杂度:O(1)并不需要额外地开辟空间
  3. 稳定性:不稳定

参考:https://yichun.blog.csdn.net/article/details/103021742?spm=1001.2014.3001.5502

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值