关于各种排序和涉及到的问题(持续更新)

排序问题

一:冒泡排序
1.冒泡排序是O(n^2)的时间复杂度,额外空间复杂度为O(1)
2.冒泡思想:每次找出一个最大值,放在最后一个位置,然后下次再找的时候,就忽略最后一个位置,然后将第二次的最大值放在倒数第二位,以此类推,当只剩下一个数字,就不需要再排了。
3.冒泡编程思路:第一层循环设置的是需要排序的终点,依次递减到1的位置,因为比较的过程是涉及到两个位置,所以不需要终点和起点重合,就可以比较完成,实际上起点不需要走到终点,只要走到终点-1的位置,就已经进行了比较了。,第二个循环从0一直到小于第一层设置的终点位置,不需要和终点重合,理由如上,第二层循环内,只要第i个数,比i+1大,就交换,保证将最终位置之前的最大值交换到最后位置。
4.代码:

 public static void bubbleSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            //如果数组为空或者只要一个,就不用排序
            return ;
        }
        for (int end = arr.length - 1; end > 0; end--) {//end表示从后往前开始的排序好的数字
            for (int i = 0; i < end; i++) {//由于这是基于前一个数字,比较后一个数字,所以end所在位置的数字
                //是最后也要用于比较的,所以不需要担心,end=arr.length-1的时候,最后一个位置无法比较的情况。
                if (arr[i] > arr[i + 1]) {//当第i个数比i+1大,就交换,目的就是将最大的数字,往后一直排
                    swap(arr,i,i+1);
                }
            }
        }
    }

    public static void swap(int[] arr, int i, int j) {//交换数组内的数
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;

    }

二:选择排序
1.选择排序的时间复杂度是O(n^2),额外空间复杂度O(1)
2.选择排序的思想:每次找出最小值,放在最第一个位置,然后从第二个位置开始找,第二个最小值放在第二个位置,依次类推,最后一个值,就放在他自己的位置
3.选择排序的编程思路:与冒泡相似,第一层循环,从0递增到倒数第二个位置(只剩下两个数的时候,假设第一个数是最小值,那么就已经会遍历到最后一个位置,就已经将倒数第二个数字和最后一个进行了比较,所以不需要走到尽头,只剩下一个数字的时候,它一定是,剩余没有排的数字里的最小值,因为只有它了,在之前的循环里,我们能够保证在它之前的所有值都比它小),第一层循环内,假设初始位置就是最小值,第二层循环是从第一层循环设置的初始位置开始,往下直到最后一个数字,只要比假设的最小值小,就交换(放到初始位置),然后直到结束。
4.代码:

  public static void selectionSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return ;
        }//数组为空或者只有一个数字,直接返回

        for (int i = 0; i < arr.length - 1; i++) {//排序位置从0开始,代表已排好的,直到最后一个
           //i作为起始位置,如果直接在最后一个位置arr[arr.length-1]中出现,那么j=i+1就会越界,因为我们
            //是从i的下一个位置开始找所有的数字和最小的进行比较
            int minIndex=i;//假设第一个位置的数字是最小的
            for (int j = i + 1; j < arr.length; j++) {//初始的时候,由于假设了第一个是最小的,所以就
                //不存在遗漏了第一个的情况,可以直接从i+1开始
                minIndex=arr[j]<arr[minIndex]?j:minIndex;
                //当前值arr[j]是否小于最小值,如果小,就返回当前值的下标,否则不变
            }
            swap(arr, i, minIndex);
        }
    }

    public static void swap(int[] arr, int i, int j) {//数组内交换数字
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }

三:插入排序
1.插入排序的时间复杂度涉及到最好时间复杂度,最差时间复杂度和平均时间复杂度,相比较前两个排序,固定的时间复杂度都是O(n^2),如果数组已经排好序,那么插入排序的算法就是O(n),如果完全逆序,就是O(n ^2)额外空间复杂度是O(1)
2.插入排序的思想:类似于整理牌,如果我只有一张牌,那么就已经有序了,(扩大有序的位置,直到全部)所以新拿一张牌,面临两种可能,比第一张牌小,或者大于等于,如果比第一张大或者相等,那就直接放手里(数组里)继续从乱牌堆里看下一张,如果比第一张小,那就再往前比较,但是因为已经是第一张牌了,所以,前面没有比它大的数字了,所以交换第一张牌和第二张牌的位置,继续看第三张牌,如此循环,直到结束
3.编程思路:要从第一张牌看到最后一张牌,需要一个循环,拿到一张牌,要和前面的数字进行比较,不只是比第一张牌,而是要比到前一张牌比它小为止,所以需要两个循环;第一个循环:从1到最后一张牌。(要清晰每一个变量的含义,第一个循环的变量,是需要拿来和已排序的牌进行比较的牌,所以第一个牌,只有一张,没有必要比较)第二个循环,由于第i个位置,是需要拿来比较的,那么i-1位置,一定是第一个和i进行比较的数,如果需要继续,那就i-1再–,所以第二个循环用j表示i-1,然后只要j在数组内,并且,j位置的数比i大,那就继续
4.代码:

 /*思想:如果我只有一张牌,那么它就是排好序的,所以第一个牌不用管,然后拿下一个
     * 数字和第一个牌比较,如果比第一个小,就交换,如果大就直接走到下一个位置,接下
     * 来就是普遍情况,也就是,这个位置的数字要和已经排序好的数字的最后的数字进行比较,
     * 如果大,就不管,如果小,就要一直交换到它在已经排序好的位置的前一个数比他小,为止*/
    public static void insertionSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return ;
        }//数组为空或者只有一个,就不需要排了
        for (int i = 1; i < arr.length; i++) {//0位置只有一张牌,所以
            //不需要排序,然后已经排序好的位置要走到最后一个位置才结束;
            /*i位置表示即将排序的位置,j表示与i进行比较的位置,这个j从i的前一个
            * 一直到0位置,我们的目的是i比j大,才可以,所以,一旦j比i大,就交换i和j位置的数字
            * 此时,j位置的数字,就是我们要排序的位置的数字,也就是原来的i,所以,再
            * j--,让j位置继续表示要和要排序的位置进行比较的位置。当j=0时,如果i仍然
            * 比j要小,那么交换,然后j无法递减,从而结束*/
            for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
                //其中之所以用j+1表示i,就是因为要排序的位置,可能会一直变,所以
                //需要用j+1表示,方便j递减的时候,让j+1一直表示将要排序的位置
                swap(arr,j,j+1);
            }

        }
    }

    public static void swap(int[] arr, int i, int j) {
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }

四:归并排序
1.归并排序:时间复杂度是O(n*logn),额外空间复杂度是O(n)(用于外排)
2.归并排序的思想:如果要排序一个数组,就把这个数组划分为两个,如果左边和右边都排好序了,那么只需要将这两个数组,进行外排,外排:就是将两个有序数组,排成一个有序数组。归并排序是很好的利用了已经排序的部分,做到了,每个数据之间的比较可以高效的利用
3.归并排序的编程思想:我们用到了“如果”这个字,也就是说,假如有人替我们排好左边和右边,我们只需要将两个有序数组合并,但是,问题是没人帮我们将两个子数组变成有序数组,可是我们自己的目的,不就是将这个大数组变成有序数组么?所以,想到可以使用递归(只要能用递归,就能用循环解决,无非就是自己压栈而已,这个问题,以后其他文章会谈到),如果我们把数组不停的二分下去,最终会把数组的每一个数字,都变成一个单独的数组,如果数组只有一个数字,就不需要排序了,所以设置的返回条件就是假如需要排序的起点==终点,那就直接返回,那么划分的过程就解决了,重点就在于,如何将两个有序数组合并成一个了,首先我们用到了help数组,这个数字要恰好容纳两个数组的所有数,如果做到呢?,首先我们要知道,n-m意味着两者之差,这个差值,往往是从m到n的[m,n)或者(m,n]之间的个数,但是我们要的是[m,n]也就是两个位置我们都需要所以,这个help数组大小是:R-L+1,接下来,设定两个指针,分别指向两个数组,从零到最后一个位置,不停的比较两个指向的值,谁小,我们就把谁放在help数组中,同时也意味着指向的数字没有意义了,所以++,指向下一个数字,但是另一个指针指的数字还没有放进help,所以还有意义,就继续比较,最终一定有一个指针会越界,意味着两个“数组”,有一个已经全部放完了,那我们只需要将另一个数组的全部放进去help中,因为很明显,剩余的所有值都比之前的大。最后,由于两个数组是我们假定的,实际上两个数组只是大数组中间的一个片段,所以我们需要用help去覆盖整个片段,那么如何覆盖呢?help数组是从零到help.length-1,原数组我们是截取了L到R的片段,那么我们只需要原数组从L+0一直到L+arr.length-1位置,正好用help覆盖,即可,这就是找寻两个数组之间的联系
4.代码:

  //归并的开头,用于检验是否需要排序
    public static void mergeSort(int[] arr) {
        if (arr.length < 2 || arr == null) {
            return ;
        }
        sortProcess(arr, 0, arr.length - 1);
    }

    public static void sortProcess(int[] arr, int L, int R) {
        //当起点等于终点的时候,意味着不需要继续分,已经排序号了
        if (L == R) {
            return ;
        }
        /*
        * 在数组很大的时候,那么L+R很有可能越界,会超过这个类型承载的限度,所以需要修改*/
//        int mid = (L + R)/2;//找到这次排序的中点位置
        int mid=L+(R-L)/2;
        sortProcess(arr, L, mid);//排序左边
        sortProcess(arr, mid + 1, R);//排序右边
        merge(arr,L,mid,R);
    }

    public static void merge(int[] arr, int L,int mid, int R) {
        int[] help=new int[R-L+1];//由于传的值是左闭,右闭的[L,R],
        //但是R-L则是有一个闭合,有一个开,例如2~4,4-2=2,但是[2,4]
        //有三个数字,所以,当两个值都是闭合的,那么就要+1;
        //所以此时help数组,正好存储[L,R],那么只需要将help填满,然后再
        //覆盖到原数组即可
        int i=0;//表示help数组的指针
        int p1=L;//表示左边起点
        int p2=mid+1;//表示右边起点
        while(p1<=mid && p2<=R){//当两个都没有越界的时候,
            help[i++]=arr[p1]<arr[p2]? arr[p1++] :arr[p2++];
            /*help数组i位置的值,等于左,右较小的值,选中了哪边,那边的指针
            * 就要++,方便下次比较*/
        }
        //两个指针必有一个最终会越界,哪个越界了,我们将剩下那部分的余下全部
        //copy到help中
        while (p1 <= mid) {
            help[i++] = arr[p1++];
        }
        while (p2 <= R) {
            help[i++]=arr[p2++];
        }
        //至此,help数组填写完成,接下来,要将这部分,覆盖到原
        //数组的L~R部分
        for (int j = 0; j < help.length; j++) {
            arr[L+j]=help[j];
            /*精髓在于,help数组与arr差异在于,要开始的位置,
            * 恰好和arr数组相差L,也就是说,L+j,且j=0,正好对应arr[L]
            * 的位置,所以找到这两个数组的联系,就很好解决这段代码*/
        }
    }

五(1):荷兰国旗问题(快速排序的引子)
1.问题抽象:将一堆数字划分成三部分,以m为边界,比m小的在左边,和m一样大的在中间,比m大的在右侧.
2.问题解决思想:将数组划分为三部分,小于区域,等于区域,大于区域,然后逐渐扩大这三部分,直到整个数组
3.编程思想:首先,划分好各个区域,整个数组所有的位置都是待划分区域,所以数组的前一个位置,是小于区域的开始位置,数组最后位置的右侧,是大于区域开始的位置,那么等于区域呢?把等于区域,放在小于区域的右侧(目前为空)然后小于区域着等于区域前进。有了以上的思想,开始写代码,界定区域,需要指针标识所以标识小于区域p1和大于区域p2,至于等于区域,既然可以通过小于区域推着,那就可以少用一个,然后用一个指针i,遍历待划分区域,如果大于标准值,大于区域–1,然后将p2位置的值与之交换,然后继续看i的位置这个值与标准值的关系;如果值小于划分值,小于区域++,然后将p1位置的值与之交换此时,小于区域的值,仍旧保证小于标准值,并且i此时指向的位置的值一定是已经遍历过的,想象一个过程,i指的位置一开始就在小于区域紧贴着的右侧,所以i指向的值,如果小,那么交换过程就相当于小于区域向前吞并了一个位置,如果i不++,那么待划分区域就会和小于区域有重合了,所以需要++,如果等于标准值,i++,当再次遇到小于标准值的时候,此时注意:i和小于区域中间只有一个相等区域的值,也就是说,交换的时候,会把相等值交换到i位置,所以i位置还是已经划分过的,这就是,小于区域推动等于区域的过程;所以i位置必须++,因为交换完成后,i指向的值,其实是在小于区域或者等于区域的,已经划分过了;那么,i走到哪呢?如果走到数组的尽头那么就会将会乱,因为遇到大于区域后,值比标准值大,所以大于区域扩大,但是由于大于区域的左边已经紧贴着等于区域或者小于区域了,一旦大于区域扩大,就会使得交换过程变乱,所以i走到大于区域前侧就好
4.代码:

  public static int[] flag(int[] arr, int L, int R, int num) {
         int less=L-1;//小于区域在需要判断的区域的左侧
         int more=R+1;//大于区域在需要判断的区域的右侧
         int index=L;//待定区域的指针
        while (index <more) {//指针走遍待定区域,重点是待定区域,如果走遍右侧,那么会把划分过的大于区域搞乱
            //index<=R是错误的
            if (arr[index] < num) {//当前值小于num
                swap(arr, ++less, index++);//将这个值与小于区域的后一个数字交换
            } else if (arr[index] > num) {//当前值大于num
                swap(arr,--more,index);//将这个值和大于区域的前一个数交换,
                //但是此时不清楚被交换的是什么数字,所以index不动
            }else {//当前值==num,则忽略,
                index++;
                //本质上,这是小于区域,推动着等于区域前进。
            }

        }
        return new int[]{less + 1, more - 1};  //返回等于区域的第一个值和最后一个值
        //当没有等于区域的时候,第一个值会大于第二个值,所以无效
    }

    public static void swap(int[] arr, int i, int j) {
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }

五(2):快速排序
1.快速排序的时间复杂度:长期期望是O(n*logN)额外空间复杂度(随机快排)就是O(logN)因为每一次断点就是一个二分,那么二分多少次,就多少个断点
2.快速排序的思想:有了荷兰国旗问题作为前因,我们就了解了partition过程(划分后,它做了啥),那么未完结,待续
六(1)堆排序的前提知识
1.完全二叉树和满二叉树
2.数组表示完全二叉树
3.堆的概念
4.堆的强大之处(举例)
六(2):堆排序
七:桶排序(不基于比较的排序,主要举一个例子)

总结:涉及到的知识点
1.递归的认知
2.对数器
3.小和问题
4.逆序数问题
5工程中的综合排序
6.比较器
7.闲碎知识漫谈

发布了12 篇原创文章 · 获赞 0 · 访问量 138

猜你喜欢

转载自blog.csdn.net/weixin_44065691/article/details/105546528