数据结构(Java):七大排序算法【多方法、多优化、多细节】

news2024/9/22 15:36:08

目录

1、排序的概念

1.1 排序

1.2 排序的稳定性

1.3 内部排序&外部排序

1.4 各排序算法总结对比

2、 插入排序

2.1 🌸直接插入排序

2.2 🌸希尔排序

3、 选择排序

3.1 🌸直接选择排序

3.2 直接选择排序优化

3.3 🌸堆排序

4、 交换排序

4.1 🌸冒泡排序

4.2 🌸快速排序

4.2.1 Hoare法(次选)

4.2.2 🌜挖坑法(首选)

4.2.3 前后指针法(很少考察)

4.2.4 🌜递归实现快排 

4.2.5🌜🌜🌜递归实现快排的优化

4.2.5.1 🌟优化1:三数取中法

4.2.5.2 🌟优化2:末尾换用直接插入排序减少函数栈帧

4.2.6 非递归实现快排

5、🌸归并排序


1、排序的概念

1.1 排序

排序:就是使一串记录,按照其中的某个或某些关键字的大小递增或递减的排列起来的操作。

1.2 排序的稳定性

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

1.3 内部排序&外部排序

内部排序被排序的数据元素全部存放在计算机内存中的排序算法,也称内排序。

外部排序待排序的记录数太多,所有的记录不可能存放在内存中, 排序过程中必须在内、外存之间进行数据交换,这样的排序称为外部排序。

1.4 各排序算法总结对比

我们先放出总结对比,下文细谈。


2、 插入排序

2.1 🌸直接插入排序

步骤如下: 

  1. 从二个元素开始(第一个元素可认为已有序),用tmp记录当前元素,从后往前扫描已排序元素
  2. 若扫描到的元素比tmp大,则将该元素后移
  3. 若扫描到的元素小于等于tmp则将tmp插入到该元素后
  4. 若扫描到的所有元素都大于tmp,此时所有元素都已后移,则将tmp插入下标为0的位置
  5. 循环重复以上步骤,直至所有元素插入到正确位置使序列有序

通过分析直接插入排序的思想,我们可以做出以下总结

  • 直接插入排序具有稳定性
  • 最坏情况下,时间复杂度为:O(N^2),即逆序情况下,每次插入新元素都插入到0下标处,等差数列计算得O(N^2)。
  • 最好情况下,时间复杂度为:O(N),即有序情况下,每次插入元素就是插入到当前位置,无序移动,遍历一遍序列即可。
  • 越有序,时间效率越高;越无序,时间效率越低。
  • 空间复杂度:O(1),没有开辟额外空间。

 直接插入排序代码:

/**
     * 直接插入排序
     * 时间复杂度:最坏(逆序):O(N^2) 最好(顺序):O(N)
     * 空间复杂度:O(1)
     * @param arr: 待排序数组
     */
    public void insertSort(int[] arr) {
        //从1下标处往前插入元素
        for (int i = 1; i < arr.length; i++) {
            //下标i处元素会改变,使用临时变量tmp记录
            int tmp = arr[i];
            int j = i-1;
            for ( ; j >= 0; j--) {
                if (arr[j] > tmp) {//如果j处值比tmp大,往后移
                    arr[j+1] = arr[j];
                }else {
                    //如果j处值小于等于tmp,则说明找到了应插入的位置,插入到j后,跳出循环
                    arr[j+1] = tmp;
                    break;
                }
            }
            //待插入元素比当前有序序列中的所有元素都小(while循环结束后到此)
            arr[j+1] = tmp;
        }
    }

2.2 🌸希尔排序

希尔排序又称缩小增量排序,是插入排序的一种,是直接插入排序的优化。

希尔排序的步骤为:

  1. 选定第一增量gap,把待排序数据分成gap个组, 所有距离为gap的数据分在同一组内(跳跃式分组),并对每一组内的数据进行直接插入排序。
  2. 然后再取一个比第一增量小的整数作为第二增量,重复上述操作…(每次排序都会时序列更有序)
  3. 直至gap等于1时,即将整体看做一组进行直接插入排序(这时序列已接近有序,即使所有数据进行直接插入排序,时间效率也会很高)

动图演示: 

 

 希尔排序总结:

  • 希尔排序是插入排序的一种,是直接插入排序的优化。
  • 采用跳跃式分组,目的是将大的数据往后放,将小的数据往前拿。
  • 当gap > 1时都是预排序,目的是让数组更接近于有序。
  • 当gap == 1时,数组已经接近有序的了,这样再排序就会很快,整体会达到优化效果。
  • 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,通常按照:   O(N*logN)或者O(N^1.3)~ O(N^1.5)
  • 空间复杂度:O(1)
  • 希尔排序:不稳定

注意:在每一组增量的希尔排序中,每次从gap下标处开始遍历要插入的新元素,当前位置插入完成后i++就可以,使得每组插入排序交叉进行。 

希尔排序代码:

/**
     * 希尔排序
     * 时间复杂度:(N*logN)或者O(N^1.3)~ O(N^1.5)
     * 空间复杂度:O(1)
     * 希尔排序:不稳定
     * @param arr: 待排序数组
     */
    public void shellSort(int[] arr) {
        int gap = arr.length;
        while (gap != 1) {
            gap /= 2;
            shell(arr,gap);
        }
    }

    public void shell(int[] arr,int gap) {
        //从gap下标处往前插入元素,当前位置插入完成后i++,每组插入排序交叉进行
        for (int i = gap; i < arr.length; i++) {
            //下标i处元素会改变,使用临时变量tmp记录
            int tmp = arr[i];
            int j = i-gap;
            for ( ; j >= 0; j -= gap) {
                if (arr[j] > tmp) {//如果j处值比tmp大,往后移
                    arr[j+gap] = arr[j];
                }else {
                    //如果j处值小于等于tmp,则说明找到了应插入的位置,插入到j的gap后,跳出循环
                    arr[j+gap] = tmp;
                    break;
                }
            }
            //待插入元素比当前有序序列中的所有元素都小(while循环结束后到此)
            arr[j+gap] = tmp;
        }
    }

3、 选择排序

3.1 🌸直接选择排序

基本思想:

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

时间复杂度:O(N^2) 和数据 是否有序无关,一定是O(N^2)

空间复杂度:O(1)

不稳定

直接选择排序代码:

/**
     * 选择排序
     * 时间复杂度:O(N^2) 和数据 是否有序无关,一定是O(N^2)
     * 空间复杂度O(1)
     * 不稳定
     * @param arr
     */
    public void selectSort(int[] arr) {
        int left = 0;
        int right = arr.length-1;
        for ( ; left < right; left++) {
            int minIndex = left;
            for (int i = left; i < arr.length; i++) {
                if (arr[i] < arr[minIndex]) {
                    minIndex = i;
                }
            }
            swap(arr,left,minIndex);
        }
    }

3.2 直接选择排序优化

直接选择排序一趟只选出了最小值; 

而优化的思想是:一趟遍历同时选出序列的最小值和最大值,将最小值和最大值与序列首尾数据交换。

 /**
     * 直接选择排序的优化
     * @param arr
     */

    public void selectSort2(int[] arr) {
        int left = 0;
        int right = arr.length-1;
        while (left < right) {
            int maxIndex = left;
            int minIndex = left;
            for (int i = left+1; i <= right ; i++) {
                if(arr[i] < arr[minIndex]) {
                    minIndex = i;
                }
                if (arr[i] > arr[maxIndex]) {
                    maxIndex = i;
                }
            }
            swap(arr,left,minIndex);
            //当最大值正好是  left下标时  此时 把最大值换到了minIndex的位置了
            if (maxIndex == left) {
                maxIndex = minIndex;
            }
            swap(arr,right,maxIndex);
            left++;
            right--;
        }
    }

3.3 🌸堆排序

若要升序排列,要建大根堆;若要降序排列,就要建小根堆。

以排升序为例:若要排升序,则为大堆,排序过程如下:

  1. 将堆顶元素和堆末元素交换,有效数据个数减一(因为堆顶元素为最大值元素,此时最大值元素已来到数组末尾)
  2. 将0下标处向下调整,重新调整为大堆
  3. 继续将堆顶元素和堆末元素交换,有效数据个数减一(堆顶元素为次大值元素,此时次大值元素已来到数组末尾倒数二个位置处)
  4. 将0下标处向下调整,重新调整为大堆
  5. 重复以上过程,直至只剩一个元素的时候,此时数组已有序且为升序排列

时间复杂度:O(N*logN)

空间复杂度:O(1)

稳定性:不稳定 

 堆排序排升序代码:

/**
     * 堆排序:建堆-》堆排序
     * 时间复杂度:O(N*logN)
     * 空间复杂度:O(1)
     * 不稳定
     * @param arr:无序数组
     */
    public void sortHeap(int[] arr) {
        createHeap(arr);
        int end = arr.length-1;
        while (end > 0) {
            swap(arr, 0, end);
            siftDown(arr, 0, end);
            end--;
        }
    }
    /**
     * 向下调整建堆
     * @param arr :无序数组
     */
    public void createHeap(int[] arr) {
        int parent = (arr.length-1-1)/2;
        for ( ; parent >= 0 ; parent--) {
            siftDown(arr,parent, arr.length);
        }
    }
    /**
     * 向下调整算法
     * @param arr
     * @param parent
     */
    private void siftDown(int[] arr, int parent,int usedSize) {
        int child = parent*2+1;
        while (child < usedSize) {
            if (child+1 < usedSize) {
                if (arr[child+1] > arr[child]) {
                    child += 1;
                }
            }
            if (arr[parent] < arr[child]) {
                swap(arr,parent,child);
                parent = child;
                child = parent*2+1;
            }else {
                break;
            }
        }
    }

4、 交换排序

4.1 🌸冒泡排序

冒泡排序是一种极易理解的基本排序算法。

思路:
每一趟排序将待排序空间中每一个元素依次与后一个元素进行比较,使值较大的元素逐渐从前移向后部,进行冒泡,一趟排序下来以后,待排序空间中的最后一个元素最大。

时间复杂度:O(N^2)(讨论没有优化的情况下,也就是没有的boolean元素和-i操作)

空间复杂度:O(1)

稳定性:稳定

冒泡排序代码(含优化):

/**
     * 冒泡排序
     * 时间复杂度:【讨论没有优化的情况下,也就是 没有下方的boolean元素和-i操作】O(N^2)
     * 优化后的时间复杂度会达到O(N) (第一趟就排好序了)
     * 空间复杂度:O(1)
     * @param arr
     */
    public void bubble(int[] arr) {
        for (int i = 0; i < arr.length-1; i++) {
            boolean flg = false;
            for (int j = 0; j < arr.length-1-i; j++) {
                if (arr[j]>arr[j+1]) {
                    swap(arr,j,j+1);
                    flg = true;
                }
            }
            if (!flg) {
                break;
            }
        }
    }

4.2 🌸快速排序

快速排序(快排)的基本思想为:任取待排序元素序列中的某元素作为基准值(一般以最左侧元素为基准值),按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

所以实现快排的关键步骤是:如何将基准值放到指定位置。

将区间按照基准值划分为左右两半部分的常见方式有:Hoare法、挖坑法、前后指针法,我们逐个讲解。

4.2.1 Hoare法(次选)

Hoare法排基准值步骤:

  1. 选出基准值tmp,一般为序列第一个或最后一个。
  2. 定义left和right分别指向序列的头和尾,left向后扫描序列,right向前扫描序列。注意:若将序列第一个数据选为基准,则right先走;若将序列第最后一个数据选为基准,则left先走。
  3. right扫描到<tmp的数值时停下,left开始走,直到left遇到一个>tmp的数值时,将数组left和right位置处的内容交换,right再次开始走,如此进行下去,直到left和right最终相遇,此时将相遇点位置的内容与基准位置的内容交换。(注意:right一定要遇到小于tmp的数时才停,等于时不可以!!!left一定要遇到大于tmp的数时才停,等于时不可以!!!若等于时就停下来可能会造成死循环!!!如下图所示)
  4. 此时tmp的左边都是小于tmp的数,tmp的右边都是大于tmp的数。

单趟动图演示:  

Hoare法排基准值代码(只排一个基准值,快排的整体实现需递归或非递归实现):

/**
     * Hoare法(单趟)
     * @param arr
     * @param left 起始位置
     * @param right 结束位置
     * @return 排好序的基准值的下标
     */
    public int partitionHoare(int[] arr,int left,int right) {
        int tmpLeft = left;
        int tmp = arr[left];
        while (left < right) {
            //等于tmp时,right也要--!!!
            while (left < right && arr[right] >= tmp) {
                right--;
            }
            //等于tmp时,left也要++!!!
            while (left < right && arr[left] <= tmp) {
                left++;
            }
            swap(arr,left,right);
        }
        swap(arr,tmpLeft,left);
        return left;
    }

4.2.2 🌜挖坑法(首选)

挖坑法思想与Hoare法基本类似。

挖坑法排基准值步骤:

  1. 选出一个数据为基准值(一般是最左边或是最右边的)存放在tmp变量中,在该数据位置形成一个坑
  2. 还是定义一个left和一个right,left从左向右走,right从右向左走。(若在最左边挖坑,则需要right先走;若在最右边挖坑,则需要left先走)
  3. 若right先走,则若right找到小于tmp的值,则填入左边的坑中,right处形成以一个新坑;left再走,若left找到大于tmp的值,则填入右边的坑中,left处形成一个新坑。
  4. 直到left和right最终相遇,再将tmp中的数据放入left和right相遇的那个坑中。
  5. 此时tmp的左边都是小于tmp的数,tmp的右边都是大于tmp的数。

单趟动图演示:  

 挖坑法排基准值代码(只排一个基准值,快排的整体实现需递归或非递归实现):

/**
     * 挖坑法(单趟)
     * @param arr 无序序列
     * @param left 起始位置
     * @param right 结束位置
     * @return 排好序的基准值的下标
     */
    public int partitionHole(int[] arr,int left,int right) {
        int tmp = arr[left];
        while (left < right) {
            //等于tmp时,right也要--!!!
            while (left < right && arr[right] >= tmp) {
                right--;
            }
            //挖坑法只需覆盖数据 填坑即可,不需要交换
            arr[left] = arr[right];
            //等于tmp时,left也要++!!!
            while (left < right && arr[left] <= tmp) {
                left++;
            }
            //挖坑法只需覆盖数据 填坑即可,不需要交换
            arr[right] = arr[left];
        }
        //将相遇位置的坑 填上tmp(基准值)
        arr[left] = tmp;
        return left;
    }

4.2.3 前后指针法(很少考察)

 思路:

  1. 定义key来存储数组中最左边的数据,perv指向数组开始的位置,cur指向prev的下一个位置。
  2. 当cur下标中的元素小于key时,则让prev开始往后走。如果此时prev下标中的元素不等于cur下标中的元素,则两者进行交换。
  3. 否则cur一直往下走,当cur走完时,将prev下表中的元素和key中的数据进行交换
  4. 不断重复上述操作。

单趟动图演示: 

 前后指针法排基准值代码(只排一个基准值,快排的整体实现需递归或非递归实现):

 /**
     * 前后指针法(单趟)
     * @param arr 无序数组
     * @param left 起始位置
     * @param right 结束位置
     * @return 排好序的基准值的下标
     */
    public int partitionFrontRearPointer(int[] arr, int left, int right) {
        int prev = left ;
        int cur = left+1;
        while (cur <= right) {
            if(array[cur] < array[left] && array[++prev] != array[cur]) {
                swap(array,cur,prev);
            }
            cur++;
        }
        swap(array,prev,left);
        return prev;
    }

4.2.4 🌜递归实现快排 

快速排序的实现,我们可以根据二叉树的前序遍历的思想递归去实现快排

  1. 将整个序列的第一个基准值排好序,将基准值当做根节点
  2. 以排好序的基准值(根节点)为界划分为左右两部分(左子树和右子树)
  3. 递归排左子树
  4. 递归排右子树
  5. 当递归的序列中只有一个节点时,说明有序,递归回退;
  6. 当没有左右子树时,递归回退。

 

排左子树时,end=pivot-1;排右子树,start=pivot+1

因为通过递归实现快排,所以我们需要注意递归回退的条件:当 start >= end 时,递归要进行回退。

递归实现快排(最好使用挖坑法):

 /**
     * 快速排序
     * 时间复杂度:最好情况下:O(N*logN)   最坏情况下:O(N^2)
     * 空间复杂度:最坏情况:O(N)          最好情况:O(logN)
     * @param arr :待排序数组
     */
    public void quickSort(int[] arr) {
        quick(arr,0, arr.length-1);
    }

    public void quick(int[] arr,int start,int end) {
        if (start >= end) {
            return;
        }
        int pivot = partitionHole(arr, start, end);
        quick(arr,start,pivot-1);
        quick(arr,pivot+1,end);
    }

    /**
     * Hoare法
     * @param arr 无序序列
     * @param left 起始位置
     * @param right 结束位置
     * @return 排好序的基准值的下标
     */
    public int partitionHoare(int[] arr,int left,int right) {
        int tmpLeft = left;
        int tmp = arr[left];
        while (left < right) {
            //等于tmp时,right也要--!!!
            while (left < right && arr[right] >= tmp) {
                right--;
            }
            //等于tmp时,left也要++!!!
            while (left < right && arr[left] <= tmp) {
                left++;
            }
            swap(arr,left,right);
        }
        swap(arr,tmpLeft,left);
        return left;
    }

    /**
     * 挖坑法
     * @param arr 无序序列
     * @param left 起始位置
     * @param right 结束位置
     * @return 排好序的基准值的下标
     */
    public int partitionHole(int[] arr,int left,int right) {
        int tmp = arr[left];
        while (left < right) {
            //等于tmp时,right也要--!!!
            while (left < right && arr[right] >= tmp) {
                right--;
            }
            //挖坑法只需覆盖数据 填坑即可,不需要交换
            arr[left] = arr[right];
            //等于tmp时,left也要++!!!
            while (left < right && arr[left] <= tmp) {
                left++;
            }
            //挖坑法只需覆盖数据 填坑即可,不需要交换
            arr[right] = arr[left];
        }
        //将相遇位置的坑 填上tmp(基准值)
        arr[left] = tmp;
        return left;
    }


    /**
     * 前后指针法
     * @param arr 无序数组
     * @param left 起始位置
     * @param right 结束位置
     * @return 排好序的基准值的下标
     */
    public int partitionFrontRearPointer(int[] arr, int left, int right) {
        int prev = left ;
        int cur = left+1;
        while (cur <= right) {
            if(arr[cur] < arr[left] && arr[++prev] != arr[cur]) {
                swap(arr,cur,prev);
            }
            cur++;
        }
        swap(arr,prev,left);
        return prev;
    }


    private void swap(int[] arr, int left, int right) {
        int tmp = arr[left];
        arr[left] = arr[right];
        arr[right] = tmp;
    }

4.2.5🌜🌜🌜递归实现快排的优化

4.2.5.1 🌟优化1:三数取中法

当序列为有序序列时,每趟right都会一直向左扫描至left处,快速排序的时间复杂度会退化为O(N^2),递归会形成"单分支的树",空间复杂度也会升至O(N)即高度次。

我们可以通过改善来使快排的时间复杂度稳定在O(N*logN),即三数取中法。

三数取中法大致思想如下:

  • 定义一个mid变量来记录序列中间位置的下标,即 mid =(left+right)/ 2;
  • 选出left、mid、right三个位置处中间的值,将这个中间值和left位置的值相交换,即将中等大小的数据尽可能的成为基准值。
  • 这样会避免树单分支的情况出现,提升了效率。
4.2.5.2 🌟优化2:末尾换用直接插入排序减少函数栈帧

 在快排递归的最后几层,函数的递归会大量增加,造成大量函数栈帧从而降低时间效率

而在快排的最后几层,虽然组数多,但每组数据量少,且已接近有序,所以我们可以在快排的末尾使用直接插入排序来进行排序,不用再继续递归排序。

将两种方法结合优化快排:

 /**
     * 快速排序
     * 时间复杂度:最好情况下:O(N*logN)   最坏情况下:O(N^2)
     * 优化后可以稳定为:O(N*logN) 
     * 空间复杂度:最坏情况:O(N)          最好情况:O(logN)
     * @param arr :待排序数组
     */
    public void quickSort(int[] arr) {
        quick(arr,0, arr.length-1);
    }


    public void quick(int[] arr,int start,int end) {
        if (start >= end) {
            return;
        }
        //避免末尾的大量递归
        if(end - start + 1 <= 7) {
            insertSortRange(arr,start,end);
            return;
        }
        //三数取中
        int midIndex = findMiddle(arr,start,end);
        swap(arr,midIndex,start);

        int pivot = partitionHole(arr, start, end);
        quick(arr,start,pivot-1);
        quick(arr,pivot+1,end);
    }

    /**
     * 直接插入排序-》区间内的插入排序
     * @param arr
     * @param start 区间的起始
     * @param end 区间的结束,[start,end]
     */
    private void insertSortRange(int[] arr, int start, int end) {
        for (int i = start+1; i <= end; i++) {
            int tmp = arr[i];
            int j = i-1;
            for ( ; j >= start ; j--) {
                if (arr[j] > tmp) {
                    arr[j+1] = arr[j];
                }else {
                    arr[j+1] = tmp;
                    break;
                }
            }
            arr[j+1] = tmp;
        }
    }


    /**
     * 快排优化-》三数取中法
     * @param arr 待排序数组
     * @param left  序列的起始位置
     * @param right 序列的结束位置
     * @return 返回中间值的下标
     */
    private int findMiddle(int[] arr, int left, int right) {
        int mid = (left+right)/2;

        //选出中间值
        if (arr[left] > arr[right]) {
            if (arr[mid] > arr[left]) {
                return left;
            }else if (arr[mid] < arr[right]) {
                return right;
            }else {
                return mid;
            }
        }else {
            if (arr[left] > arr[mid]) {
                return left;
            } else if (arr[mid] > arr[right]) {
                return right;
            }else {
                return mid;
            }
        }
    }
/**
     * 挖坑法
     * @param arr 无序序列
     * @param left 起始位置
     * @param right 结束位置
     * @return 排好序的基准值的下标
     */
    public int partitionHole(int[] arr,int left,int right) {
        int tmp = arr[left];
        while (left < right) {
            //等于tmp时,right也要--!!!
            while (left < right && arr[right] >= tmp) {
                right--;
            }
            //挖坑法只需覆盖数据 填坑即可,不需要交换
            arr[left] = arr[right];
            //等于tmp时,left也要++!!!
            while (left < right && arr[left] <= tmp) {
                left++;
            }
            //挖坑法只需覆盖数据 填坑即可,不需要交换
            arr[right] = arr[left];
        }
        //将相遇位置的坑 填上tmp(基准值)
        arr[left] = tmp;
        return left;
    }

快速排序时间复杂度:

优化前:

最好情况(乱序):O(N*logN)(logN层,每层递归区间之和都是N)

最坏情况(有序):O(N^2)(单分支的情况)

优化后:

稳定在:O(N*logN)

快速排序空间复杂度:

最好:O( logN)(完全二叉树时,高度层)

最坏:O(N)(单分支的情况)


4.2.6 非递归实现快排

非递归的实现需要借助

  • 还是先求出pivot,若其左右序列不为空且数量不为1,则将左右序列的范围即left和right入栈
  • 若左右序列数量为1或无左右序列,说明左右序列有序,不入栈
  • 接着出栈两个元素,即新序列的范围,求出新pivot后,再判断其左右序列是否可入栈;循环往复,直至栈为空,说明序列已有序。
  • 若 start+1 < pivot 说明pivot左序列数据个数>1,可入栈
  • 若 end-1 > pivot 说明pivot右序列数据个数>1,可入栈

非递归实现快排代码:

 public void quickSort(int[] arr) {
        quickNor(arr,0, arr.length-1);
    }

 /**
     * 非递归实现快排
     * @param arr
     * @param start
     * @param end
     */
    public void quickNor(int[] arr,int start,int end) {
        Deque<Integer> stack = new ArrayDeque<>();
        //排基准值
        int pivot = partitionHole(arr,start,end);
        //若有左序列且数量不为1,则入栈
        if (start+1 < pivot) {
            stack.push(start);
            stack.push(pivot-1);
        }
        //若有右序列且数量不为1,则出栈
        if (end-1 > pivot) {
            stack.push(pivot+1);
            stack.push(end);
        }
        //循环,直至栈为空
        while (!stack.isEmpty()) {
            end = stack.pop();
            start = stack.pop();
            pivot = partitionHole(arr,start,end);
            if (start+1 < pivot) {
                stack.push(start);
                stack.push(pivot-1);
            }
            if (end-1 > pivot) {
                stack.push(pivot+1);
                stack.push(end);
            }
        }
    }


 /**
     * 挖坑法
     * @param arr 无序序列
     * @param left 起始位置
     * @param right 结束位置
     * @return 排好序的基准值的下标
     */
    public int partitionHole(int[] arr,int left,int right) {
        int tmp = arr[left];
        while (left < right) {
            //等于tmp时,right也要--!!!
            while (left < right && arr[right] >= tmp) {
                right--;
            }
            //挖坑法只需覆盖数据 填坑即可,不需要交换
            arr[left] = arr[right];
            //等于tmp时,left也要++!!!
            while (left < right && arr[left] <= tmp) {
                left++;
            }
            //挖坑法只需覆盖数据 填坑即可,不需要交换
            arr[right] = arr[left];
        }
        //将相遇位置的坑 填上tmp(基准值)
        arr[left] = tmp;
        return left;
    }

5、🌸归并排序

归并排序的基本思想是:

将序列的数据分解,当分解到只有一个数据时,可认为序列有序,再将两个有序序列合并为一个有序序列,直至整个序列有序。

我们可使用递归实现归并排序:

  1. 使用left记录序列起始位置,right记录末尾位置,mid记录中间位置
  2. 向左递归时,left=left,right=mid
  3. 向右递归时,left=mid+1,right=right
  4. 当left==right时,说明序列只有一个元素,可认为序列有序,递归回退(代码上可写为left>=right时递归回退)
  5. 将回退的左右两个有序序列合并为一个有序序列
  6. 最终将整体合并为有序序列
  7. 注意:合并有序序列时,我们需要借助新数组存排好序的数据,再将数据改进原数组中,故有空间损耗,空间复杂度为O(N)
  8. 计算时间复杂度时,也可转化为二叉树,层数为logN,每层合并时的总数为N次,故时间复杂度为:O(N*logN)

递归实现归并排序代码:

public void mergeSort(int[] arr) {
        mergeSortTmp(arr,0,arr.length - 1);
    }
    public void mergeSortTmp(int[] arr,int left,int right) {
        int mid = (left + right) / 2;
        if (left >= right) {
            return;
        }

        //分解
        mergeSortTmp(arr, left, mid);
        mergeSortTmp(arr, mid + 1, right);
        //走到这里,元素已经分解,开始合并
        merge(arr,left,right,mid);
    }

    //合并
    private void merge(int[] arr, int left, int right, int mid) {
        int start1 = left;
        int end1 = mid;
        int start2 = mid + 1;
        int end2 = right;
        int len = right - left + 1;
        int[] tmp = new int[len];

        int k = 0;
        while (start1 <= end1 && start2 <= end2) {
            if (arr[start1] <= arr[start2]) {
                tmp[k++] = arr[start1++];
            }else {
                tmp[k++] = arr[start2++];
            }
        }
        while (start1 <= end1) {
            tmp[k++] = arr[start1++];
        }
        while (start2 <= end2) {
            tmp[k++] = arr[start2++];
        }
        //i+left 保证不覆盖其他区域的元素
        for (int i = 0; i < tmp.length; i++) {
            arr[i + left] = tmp[i];
        }
    }


END

到这里本篇博客就结束了,共计1.3w字,详细列举了七大排序算法的各种实现细节、优化以及多种方法的实现,希望能够对你有所帮助!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1939898.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【PyTorch】图像多分类项目

【PyTorch】图像二分类项目 【PyTorch】图像二分类项目-部署 【PyTorch】图像多分类项目 【PyTorch】图像多分类项目部署 多类图像分类的目标是为一组固定类别中的图像分配标签。 目录 加载和处理数据 搭建模型 定义损失函数 定义优化器 训练和迁移学习 用随机权重进行训…

HC-SR04超声波测距模块使用方法和例程(STM32快速移植)

基于STM32和HC-SR04模块实现超声波测距功能 HC-SR04硬件概述HC-SR04超声波距离传感器的核心是两个超声波传感器。一个用作发射器&#xff0c;将电信号转换为40 KHz超声波脉冲。接收器监听发射的脉冲。如果接收到它们&#xff0c;它将产生一个输出脉冲&#xff0c;其宽度可用于…

磁盘作业1

新添加一块硬盘&#xff0c;大小为5g&#xff0c;给这块硬盘分一个mbr格式的主分区&#xff08;大小为3g&#xff09;&#xff0c;给此主分区创建ext2的文件系统&#xff0c;挂载到/guazai1目录&#xff0c;并写入文件内容为 "this is fist disk" 文件名为1.txt的文件…

五分钟学会 Docker Registry 搭建私有镜像仓库

在上一篇文章《前端不懂 Docker &#xff1f;先用它换掉常规的 Vue 项目部署方式》中&#xff0c;我们学习了如何使用 aliyun 私有镜像仓库&#xff0c;也了解到可以使用 Docker Registry 搭建私有镜像仓库。这篇文章就分享下实操过程。 registry 是官方提供的 registry 镜像&…

【数据结构--查找】

目录 一、查找&#xff08;Searching&#xff09;的概念1.1、基本概念1.2、算法的评价指标 二、顺序查找2.1、算法思想2.2、算法实现2.2.1、常规顺序查找2.2.2、带哨兵的顺序查找 2.3、效率分析2.4、优化2.4.1、针对有序表2.4.2、被查效率不相等 三、折半查找3.1、算法思想3.2、…

<数据集>学生课堂行为识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;13899张 标注数量(xml文件个数)&#xff1a;13899 标注数量(txt文件个数)&#xff1a;13899 标注类别数&#xff1a;8 标注类别名称&#xff1a;[js, tt, dk, zt, dx, zl, jz, xt] # 举手 js # 抬头听课 …

新版GPT-4omini上线!快!真TM快!

大半夜&#xff0c;OpenAI突然推出了GPT-4o mini版本。 当我看到这条消息时&#xff0c;正准备去睡觉。mini版本质上是GPT-4o模型的精简版本&#xff0c;没有什么革命性的创新&#xff0c;因此我并没有太在意。 结果今天早上一觉醒来发现伴随GPT-4o mini上线&#xff0c;官网和…

Vue3+ element plus 前后分离admin项目安装教程

前后分离admin项目安装 前后分离admin项目安装基于 vue3.x CompositionAPI typescript vite element plus vue-router-next pinia&#xff0c;适配手机、平板、pc 的后台开源免费模板&#xff0c;希望减少工作量&#xff0c;帮助大家实现快速开发。 下载源码 前往gite…

Flink SQL 实时读取 kafka 数据写入 Clickhouse —— 日志处理(三)

文章目录 前言Clickhouse 表设计adlp_log_local 本地表adlp_log 分布式表 Flink SQL 说明创建 Source Table (Kafka) 连接器表创建 Sink Table (Clickhouse) 连接器解析 Message 写入 Sink 日志查询演示总结 前言 在之前的文章中&#xff0c;我们总结了如何在 Django 项目中进…

甄选范文“论系统安全架构设计及其应用”,软考高级论文,系统架构设计师论文

论文真题 随着社会信息化进程的加快,计算机及网络已经被各行各业广泛应用,信息安全问题也变得愈来愈重要。它具有机密性、完整性、可用性、可控性和不可抵赖性等特征。信息系统的安全保障是以风险和策略为基础,在信息系统的整个生命周期中提供包括技术、管理、人员和工程过…

Noah-MP陆面生态水文模拟与多源遥感数据同化技术

了解陆表过程的主要研究内容以及陆面模型在生态水文研究中的地位和作用&#xff1b;熟悉模型的发展历程&#xff0c;常见模型及各自特点&#xff1b;理解Noah-MP模型的原理&#xff0c;掌握Noah-MP模型在单站和区域的模拟、模拟结果的输出和后续分析及可视化等方法&#xff1b;…

【Spring Boot】网页五子棋项目实现,手把手带你全盘解析(长达两万3千字的干货,坐好了,要发车了......)

目录 网页五子棋项目一、项目核心流程二、 登录模块2.1 前端输入用户信息2.2 后端进行数据库查询用户信息 三、 游戏大厅模块3.1 前端通过Ajax请求用户数据&#xff0c;后端从Session中拿取并从数据库中查询后返回3.2 前后端建立WebSocket连接&#xff0c;并进行判断&#xff0…

xxl-job登录没反应问题解决方法

最近在写一个关于xxl-job的项目&#xff0c;然后遇到了如下的问题&#xff0c;可以正常访问到xxl-job的登录界面但是点击登录按钮发现没有反应&#xff0c;并且没有发送任何请求。 排查步骤&#xff08;使用docker&#xff09; 1.重启mysql 2.重启docker 3.重写安装mysql 4.查看…

Mysql-索引结构

一.什么是索引&#xff1f; 索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引 二.无索引的情况 找到…

【Linux】Linux的基本使用

一.Linux的背景知识. 1.1什么是Linux Linux是一种开源的类Unix操作系统内核. 和Windows是" 并列 "的关系. 1.2Linux的发行版本. Linux 严格意义来说只是一个 “操作系统内核”.一个完整的操作系统 操作系统内核 配套的应用程序. 由于 Linux 是一个完全开源免费…

基于JSP的高校二手交易平台

开头语&#xff1a;你好&#xff0c;我是专注于计算机技术的学姐码农小野&#xff0c;如果有任何技术需求&#xff0c;欢迎随时联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;JSP技术 JAVA MySQL 工具&#xff1a;常见Web浏览器&#xff0…

【开发踩坑】 MySQL不支持特殊字符(表情)插入问题

背景 线上功能报错&#xff1a; Cause:java.sql.SQLException:Incorrect string value:xFO\x9F\x9FxBO for column commentat row 1 uncategorized SQLException; SQL state [HY000]:error code [1366]排查 初步觉得是编码问题&#xff08;utf8 — utf8mb4&#xff09; 参考上…

Linux环境下dockes使用MongoDB,上传zip文件如何解压并备份恢复到MongoDB数据库中

1、准备 Docker 和 MongoDB 容器 建议主机端口改一下 docker run --name mongodb -d -p 27018:27017 mongo 2. 创建一个工作目录并将 zip 文件上传到dockers容器中 docker cp data.zip mongodb:/data.zip 3. 在 MongoDB 容器中解压 zip 文件&#xff08;也可以解压完再复制…

大语言模型LLM-三种模型架构

架构&#xff1a;由Transformer论文衍生出来的大语言模型&#xff0c;主要有三种模型架构预训练目标&#xff1a;FLM&#xff0c;PLM&#xff0c;MLM调整&#xff1a;微调&#xff1a; Transformer transfomer可以并行地计算&#xff1f; transformer中encoder模块是完全并行…

深入理解Linux网络(四):TCP接收阻塞

TCP socket 接收函数 recv 发出 recvfrom 系统调用。 进⼊系统调⽤后&#xff0c;⽤户进程就进⼊到了内核态&#xff0c;通过执⾏⼀系列的内核协议层函数&#xff0c;然后到 socket 对象的接收队列中查看是否有数据&#xff0c;没有的话就把⾃⼰添加到 socket 对应的等待队列⾥…