排序算法的学习和理解

        周六参加孤尽老师的柚子班,有一个独特的环节:在纸上手写冒泡排序、插入排序、快速排序,要求15分钟写完,且把代码写到IDE里面能编辑通过且运行正确,正确一个算一分,结果现场21人能得分的只有4人,本人也是一分未得,感觉很羞愧🤦‍♂️。后面孤尽老师秀了一手:在txt文件编辑器中写快排,然后copy到IDE运行一把通过,赢得了在场阵阵掌声。孤尽老师本人跟大家说,他自己其实没有多少时间去记快排的代码(又要給我们准备讲课的内容,还有工作的事情),但是已经理解通透原理,所以手写出来不成问题。

        柚子班第一期就讲过学习四部曲:记忆、理解、表达然后融汇贯通。排序算法之前其实都记过,但是并没有理解通透,才会忘记,借着这次机会,我决定好好写一写这几种排序算法。

1、冒泡排序

冒泡排序每趟会从左到右依次比较当前位置和下一个位置的两个数A、B,如果A比B大,则交换位置,这样每趟比较过后,最大的数字会被移到最后面,数组长度为n,总共需要比较 n-1趟,用i(从1开始)来表示第几趟比较,每趟需要比较 n-i 次。

public static void sort(int[] nums) {
        if (nums==null || nums.length<2) {
            return ;
        }
        //比较n-1趟
        for (int i=1; i<=nums.length-1; i++) {
            boolean flag = true;
            //每趟比较n-1次,从前往后开始比较
            for (int j=0; j<=nums.length-1-i; j++) {
                if (nums[j]>nums[j+1]) {
                    int temp = nums[j];
                    nums[j]=nums[j+1];
                    nums[j+1]=temp;
                    flag = false;
                }
            }
            //如果一趟比较过后都没有交换过,说明数组已经有序,可以直接返回
            if (flag) return;
        }
    }

平均时间复杂度:(n-1) + (n-2) + (n-3) + ... + 1 = n*(n-1)/2 = O(n^2)

空间复杂度:O(1)

2、插入排序

插入排序和玩斗地主的时候,給扑克牌排序很相似,从左到右,給每张牌找到合适的位置。先把当前要排序的牌A抽出来,和前面的牌一一比较,如果前面的牌比较大则前面的牌依次往后移动,直到遇到前面的牌比A小,则把A放在它后面。給从第二张牌开始到最后一张牌找到合适的位置,需要走n-1趟,每趟需要比较i次。

public static void sort(int[] nums) {
        if (nums==null || nums.length<2) {
            return;
        }
        //从第2张牌到最后一张牌进行每趟比较
        for (int i=1;i<=nums.length-1;i++) {
            //先把当前要比较的牌a抽出来
            int a = nums[i];
            //和前面i-1张牌依次比较
            int j = i-1;
            for (;j>=0;j--) {
                //如果前面的牌比当前的牌a大,则后移
                if (a < nums[j]) {
                    nums[j+1]=nums[j];
                } else {
                    //否则退出
                    break;
                }
            }
            //退出的牌的后一位,就是合适牌a的位置
            nums[j+1]=a;
        }
    }

平均时间复杂度:1 + 2 + 3 + ... + (n-1) = n*(n-1)/2 = O(n^{2})

 空间复杂度:O(1) 

3、快速排序

快速排序是一种基于分治思想的算法,选数组第一个值作为枢轴,重复这个循环:先让最后面的值和枢轴比较大小,如果值比枢轴大,则位置前移(indexRight--),否则和枢轴交换位置;接着让前面的值和枢轴比较,如果值比枢轴小,则位置后移(indexLeft++),否则和枢轴交换位置;直到前后位置相等(indexLeft==indexRight)。每次交换位置都是和枢轴交换,目的是让所有比枢轴小的值移动到枢轴左边,否则移动到右边。最后枢轴的位置就在中间,以枢轴所在位置分割为两个数组,递归。

写递归算法有一个小技巧,递归模版写出来了基本就对了一半:

1、在特定条件下直接return;

2、每次递归的参数在变化

    public static void sort(int[] nums, int left, int right) {
        //递归模版 : 1、特定条件下return    
        if (nums == null || left>=right) {
            return;
        }
        int pivot = nums[left];
        int indexLeft = left;
        int indexRight = right;
        while(indexLeft<indexRight) {
            while(indexLeft < indexRight && nums[indexRight]>= pivot) {
                indexRight--;
            }
            swap(nums, indexLeft, indexRight);

            while(indexLeft < indexRight && nums[indexLeft]<= pivot) {
                indexLeft ++;
            }
            swap(nums, indexLeft, indexRight);
        }
        //递归模版 : 2、参数在变化
        sort(nums, left, indexLeft-1);
        sort(nums, indexLeft+1, right);
    }

    private  static void swap(int[] nums, int a, int b) {
        int temp = nums[a];
        nums[a] = nums[b];
        nums[b] = temp;
    }

平均时间复杂度:O(n*lgn)

 空间复杂度:O(1)

快速排序是一种不稳定的排序算法,即数组有两个相等的数值a,b,排序前a在b前面,排序之后a可能在b后面。当数组是有序的情况下(不管是升序还是降序或者值都相等),每次枢轴的位置都在边界,这样每次都只能拆分出一个成员,时间复杂度退化为O(n^2)

4、归并排序

归并排序,同样是利用分治的思想,将数组均分为两份,两份数组递归进行归并操作,最后将归并好的两份数组合并为一个有序的数组。

归并排序递归的模版:

1、return条件是分成的数组长度<=1

2、参数的left和right边界,按照中间拆分数组的原理变化。

public static void sort(int[] nums) {
        //避免频繁创建空间
        int[] temp = new int[nums.length];
        sort(nums, 0, nums.length-1, temp);
    }

    private static void sort(int[] nums, int left, int right, int[] temp) {
        //递归模版:1、return条件是数组长度<=1的时候
        if (nums == null || left >= right) {
            return;
        }
        //均分为两个数组
        int mid = (left + right)/2;
        //递归模版:2、参数在变化,从中间分为了两个数组
        sort(nums, left, mid, temp);
        sort(nums, mid+1, right, temp);
        //合并
        merge(nums,left,mid,right, temp);
    }

    public static void merge(int[] nums, int left, int mid, int right, int[] temp) {
        int i=left;
        int j = mid+1;
        int k = left;
        while(i<=mid && j<=right) {
            if (nums[i]<nums[j]) {
                temp[k++] = nums[i++];
            } else {
                temp[k++]=nums[j++];
            }
        }
        while(i<=mid) {
            temp[k++]=nums[i++];
        }
        while(j<=right) {
            temp[k++]=nums[j++];
        }
        for (int ii=left;ii<=right;ii++) {
            nums[ii] = temp[ii];
        }
    }

时间复杂度:O(n*lgn)

空间复杂度:O(n)

归并排序是稳定的排序算法,且时间复杂度最好、最坏的情况下都是一样的O(n*lgn)。

5、选择排序

选择排序的思路是从未排序的数组中选出最小的,放到未排序数组的最前面。

需要进行n-1趟,每趟做n-i次比较。

public static void sort(int[] nums) {
        if (nums==null ||nums.length<2) {
            return;
        }
        for(int i=0;i<=nums.length-2;i++) {
            int min =i;
            for (int j=i+1;j<=nums.length-1;j++) {
                if (nums[j]<nums[min]) {
                    min =j;
                }
            }
            int temp = nums[min];
            nums[min]=nums[i];
            nums[i]=temp;
        }
    }

 时间复杂度(n-1) + (n-2)+...+1=n*(n-1)/2=O(n^2)

空间复杂度O(1)

选择排序很简单且稳定,时间复杂度永远是O(n2)

6、堆排序

所谓的堆是利用完全二叉树的结构来维护的一维数组。

比如下面这个数据用堆来表示:

父的位置i,对应子的位置分别是2*i+1、2*i+2

堆又分为

大顶堆: 每棵子树根的值都是树里面所有节点最大的。

小顶堆: 每棵子树根的值都是树里面所有节点最小的。

堆排序就是利用堆结构来排序的算法,步骤如下:
(1)建堆。

(2)堆的根节点和堆的最后一个节点交换位置,堆的长度减1(即把最大(小)的值移动到数组后面)

(3)重新调整堆,重复第二步,直到堆长度为1。

    public static void sort(int[] nums) {
        if (nums==null || nums.length<2) {
            return;
        }
        // 1、建堆
        buildMaxHeap(nums);
        //每次堆的长度减1,直到堆只有一个元素
        for(int i=nums.length-1;i>0;i--) {
            //2、堆顶和堆的最后一个元素交换
            swap(nums,0,i);
            //3、调整堆
            heapify(nums,0,i);
        }
    }

    /**
        建堆思路:
        从最后一个父节点:nums.length-1/2 开始进行调整堆的操作,
        一直到调整到第一个节点。
    **/
    private static void buildMaxHeap(int[] nums) {
        for(int i=(nums.length-1)/2;i>=0;i--) {
            heapify(nums,i,nums.length);
        }
    }

    /**
        调整堆的思路很简单:
        当前节点和左右子节点比较,找出最大的,
        如果最大的不是父节点,则交换父节点和最大的子节点的值,
        然后重新调整被交换的子堆
    **/
    private static void heapify(int[] nums, int i, int length) {
        int largest = i;
        int left = 2*i+1;
        int right = 2*i+2;
        if (left < length && nums[left]>nums[largest]) {
            largest = left;
        }
        if (right < length && nums[right]>nums[largest]) {
            largest = right;
        }
        if (largest == i) {
            return;
        }
        swap(nums,i,largest);
        heapify(nums, largest, length);
    }

    private static void swap(int[] nums, int i, int i1) {
        int temp = nums[i];
        nums[i] = nums[i1];
        nums[i1] = temp;
    }

堆排序是一种不稳定的排序。

平均时间复杂度:O(n*lgn)

空间复杂度:O(1)

动图来自:十大经典排序算法(动图演示) - 一像素 - 博客园

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值