【排序算法】01:冒泡排序、插入排序、选择排序

本文为王争《数据结构与算法之美》笔记。
代码实现都有很多细节,仔细体会。

冒泡排序(Bubble Sort)

代码

public int[] bubbleSort(int[] nums) {
    int n = nums.length;
    if(n<=1) return nums;
    //冒泡,最多需要冒泡n次
    for(int i=0; i<n; i++){
        boolean flag = false;
        //冒泡一次,结尾多一个位置正确的数,不用再比较,所以是n-i-1
        for(int j=0; j<n-i-1; j++) {
            if (nums[j] > nums[j + 1]) {
                int temp = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = temp;
                flag = true;
            }
        }
        //如果本次冒泡没有数据交换,则说明已经有序
        if(!flag) break;
    }
    return nums;
}

内存消耗

只有交换数据时的常量级内存消耗,空间复杂度O(1),即为原地排序算法。

稳定性

相邻数据相等时不交换,所以值相等的元素排序后顺序不变,所以冒泡排序为稳定的排序算法。

时间复杂度

最好时间复杂度:O(n)。数组已经有序(升序),只需一次冒泡,发现没有数据交换,直接返回。
最坏时间复杂度:O(n^2)。数组倒序(降序),需要n次冒泡,才能将所有元素“上浮”到正确位置。

一般情况怎么分析呢?
需要引入有序度概念:数组中具有序关系(升序)的元素对个数。
比如数组[2, 4, 3, 1, 5, 6]的有序度为11,因其有11个有序对:<2, 4>、<2, 3>、<2, 5>、<2, 6>、<4, 5>、<4, 6>、< 3, 5>、< 3, 6>、<1, 5>、<1, 6>、<5, 6>。
易知,有序数组的有序度为n*(n-1)/2,即为满有序度。倒序数组的有序度为0。
逆序度的定义与有序度相反,易知逆序度 = 满有序度 - 有序度
一个数组达到有序所需要的交换次数就等于逆序度,不管算法怎么变,这个交换次数是固定的。交换次数是0~n*(n-1)/2之间的整数。

对于平均时间复杂度,交换次数可取n*(n-1)/4。而冒泡排序有两个操作原子:比较和交换。比较次数总是大于等于交换次数,小于等于n*(n-1)/2。所以平均时间复杂度仍是O(n^2)。

插入排序(Insertion Sort)

代码

public int[] sortArray(int[] nums) {
    int n = nums.length;
    if(n<=1) return nums;
    //注意i从1开始,避免nums[j]越界
    for(int i=1; i<n; i++){
        int value = nums[i];
        int j = i-1;
        //0~i-1为已排序区间
        //核心代码,寻找插入点并移动数据
        //注意是从尾到头遍历
        for(; j>=0; j--){
            if(nums[j] > value){
                nums[j+1] = nums[j];
            }else{
                break;
            }
        }
        //注意最后是j+1
        nums[j+1] = value;
    }
    return nums;
}

内存消耗

不需要额外内存空间,空间复杂度O(1),是原地排序算法。

稳定性

根据代码可知,一个元素会插入在具有相同值的前面的元素之后,所以插入排序是稳定的排序算法。

时间复杂度

最好时间复杂度:O(n),数组已经有序。这就体现出从尾到头遍历的好处了:发现前面相邻元素小于等于当前元素,直接确定不必移动,跳出循环。于是,每个元素只需进行一个比较操作,故时间复杂度为O(n)。
最坏时间复杂度:O(n^2),数组倒序。每个元素都要插入到最前面。
一般情况呢?插入排序,都需要进行n次插入操作。而数组中插入一个元素的平均时间复杂度为O(n),所以插入排序的平均时间复杂度是O(n^2)。(注:感觉数组插入元素平均复杂度O(n)不是很严格)

为什么插入排序比冒泡排序好?

两者时间复杂度都是O(n^2),都是原地排序算法,为何插入排序更好?
之前说过,不管怎么优化,冒泡排序的交换次数都是原始数据的逆序度;插入排序也是,移动次数等于原始数据逆序度。但是从代码的角度来看,冒泡排序交换一次需要4个赋值操作,而插入排序只需要1个赋值操作:

冒泡排序的交换操作:
if (nums[j] > nums[j + 1]) {
    int temp = nums[j];
    nums[j] = nums[j + 1];
    nums[j + 1] = temp;
    flag = true;
}
插入排序的数据移动操作:
if(nums[j] > value){
    nums[j+1] = nums[j];
}else{
    break;
}

设一个数组的逆序度为K,执行一条赋值语句的时间为单位时间。纯按理论分析,冒泡排序交换操作消耗4*K个单位时间,而插入排序移动操作只消耗K个单位时间。这也是有实践证明的,插入排序确实比冒泡排序快。

选择排序(Selection Sort)

代码

public int[] sortArray(int[] nums) {
    int n = nums.length;
    if(n<=1) return nums;
    for(int i=0; i<n; i++){
        int min = nums[i];
        int index = i;
        for(int j=i; j<n; j++){
            if(nums[j] < min) index = j;
        }
        int temp = nums[index];
        nums[index] = nums[i];
        nums[i] = temp;
    }
    return nums;
}

内存消耗

选择排序是原地排序算法。

稳定性

选择排序是不稳定的。比如数组[5, 6, 5, 2, 9],使用选择排序,则第一次交换是2交换开头的5。这样,两个5的先后顺序就改变了。

时间复杂度

选择排序的最好情况时间复杂度、最坏情况和平均情况时间复杂度都为 O(n^2),因为都是一样的操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值