排序算法的奇妙冒险

news2024/11/17 2:57:42

排序算法的奇妙冒险

  • 一.排序的概念
    • 1.1 排序的定义
    • 1.2 排序的稳定性
    • 1.3 排序的内排序和外排序
  • 二.插入排序
    • 2.1 直接插入排序
    • 2.2 希尔排序
  • 三.选择排序
    • 3.1直接选择排序
    • 3.2 堆排序
  • 四.交换排序
    • 4.1 冒泡排序
    • 4.2 快速排序
      • **选取基准值的方法**
      • 快速排序的优化
      • 非递归实现快速排序
  • 五.归并排序
    • 递归实现递归排序
    • 非递归实现归并排序
  • 六.总结

一.排序的概念

1.1 排序的定义

排序是我们生活中经常会面对的问题。同学们做操时会按照从矮到高排列;老师查看上课出勤情况时,会按学生学号顺序点名;高考录取时,会按成绩总分降序依次进行录取.
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作.

1.2 排序的稳定性

什么是排序的稳定性呢?
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
看下面的图示:
在这里插入图片描述

1.3 排序的内排序和外排序

内部排序:数据全部放在内部的排序
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
因此,根据排序过程中借助的主要操作,我们把内排序分为:插入排序、交换排序、选
择排序和归并排序。

二.插入排序

2.1 直接插入排序

基本思想
在介绍插入排序之前,我们来说一个生活场景,大家都玩过扑克牌吧?最基本的扑克玩法都是一边摸牌,一边理牌。假如我们拿到了这样一手牌,如下面的图片所示,似乎是同花顺呀,别急,我们得理一理顺序才知道是否是真的同花顺。请问,如果是你,应该如何理牌呢?
在这里插入图片描述

我们使用下面的方法,这种方法也不太复杂,应该说,哪怕你是第一次玩扑克牌,只要认识这些数字,理牌的方法都是不用教的。将3和4移动到5的左侧,再将2移动到最左侧,顺序就算是理好了。这里,我们的理牌方法,就是直接插入排序法。

具体做法
直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
先看代码思路

 public static void insertSort(int[] array) {
        for (int i = 1; i < array.length; i++) {
            int tmp = array[i];
            int j = i-1;
            for (; j >= 0 ; j--) {
                if(array[j] > tmp) {
                    array[j+1] = array[j];
                }else {
                    //array[j+1] = tmp;
                    break;
                }
            }
            array[j+1] = tmp;
        }
    }

具体的一个分析过程如下:
在这里插入图片描述
代码思路如下:

  1. 将数组的第一个元素认为是有序序列,其余元素均为无序序列。
  2. 取出无序序列中的第一个元素,在有序序列中找到适当的位置并插入。
  3. 重复步骤 2,直到无序序列中没有元素为止。
  4. 该算法实现是通过不断将无序序列中的元素插入到有序序列中的正确位置,最终实现整个序列有序

时间复杂度分析:
最好:O(n)
最坏:O(n^2)
平均:O(n^2)
在插入排序的代码实现中,有两个for循环:
外层for循环控制待插入元素的个数,循环n次。
内层for循环控制查找插入位置,循环次数的上界为n-1,因此内层for循环的复杂度最坏可达O(n)。

2.2 希尔排序

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成多个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。

实际上我们先来看一下希尔排序分组究竟是怎么一回事
在这里插入图片描述

具体思路:

  1. 选择一个增量序列gap,gap初始值为数组长度的一半, gap /= 2不断减小增量。
  2. 每一轮按gap的值对数组进行分组,然后每组进行插入排序。
  3. gap不断减小,最后gap = 1时,进行最终的插入排序,完成排序。

具体代码


/*
gap 控制着 i 的步长
i 的值决定了 j 的初始值
j 会根据 tmp 和有序序列的比较,向左移动
*/
 public static void shellSort(int[] array) {
        int gap = array.length;
        while (gap > 1) {
            shell(array,gap);
            gap /= 2;
        }
        //整体进行插入排序
        shell(array,1);
    }

    //插入排序 -》GAP
    public static void shell(int[] array,int gap) {
        for (int i = gap; i < array.length; i++) {
            int tmp = array[i];
            int j = i-gap;
            for (; j >= 0 ; j-=gap) {
                if(array[j] > tmp) {
                    array[j+gap] = array[j];
                }else {
                    break;
                }
            }
            array[j+gap] = tmp;
        }
    }

时间复杂度的分析
在这里插入图片描述

三.选择排序

3.1直接选择排序

先来看一遍动画,来理解选择排序的具体过程是什么样子的.
在这里插入图片描述

基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
在这里插入图片描述
代码思路:

  1. 首先找到数组中的最小值,其索引为minIndex。
  2. 然后将最小值与数组的第一个元素交换位置。
  3. 重复步骤1和2,对剩余的元素进行选择排序,直到数组有序。
    具体代码
  public static void selectSort(int[] array) {
        for (int i = 0; i < array.length; i++) {
            int minIndex = i;
            int j = i+1;
            for (; j < array.length; j++) {
                if(array[j] < array[minIndex]) {
                    minIndex = j;
                }
            }
            swap(array,i,minIndex);
        }
    }

    private static void swap(int[] array,int i,int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

时间复杂度分析:
最好:O(n^2)
最坏:O(n^2)
平均:O(n^2)
外层for循环控制排序轮数,循环n次。
内层for循环找出剩余元素中的最小值,循环n-1次。
将内外层for循环的复杂度相乘,得出选择排序总的时间复杂度为O(n^2)。

3.2 堆排序

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
先来看一下,堆排序的动画
在这里插入图片描述

具体思路:

  1. 将待排序序列构建成一个大根堆或小根堆。
  2. 此时,整个序列的最大值或最小值就是堆顶元素。
  3. 将堆顶元素与末尾元素交换,此时末尾元素为最大值或最小值。
  4. 然后将剩余n-1个元素重新构建成一个大根堆或小根堆。
  5. 重复步骤3和4,直到堆变为空,排序完成。

具体代码

public static void heapSort(int[] array) {
        createBigHeap(array);
        int end = array.length-1;
        while (end > 0) {
            swap(array,0,end);
            shiftDown(array,0,end);
            end--;
        }
    }

    private static void createBigHeap(int[] array) {
        for (int parent = (array.length-1-1)/2; parent >= 0 ; parent--) {
            shiftDown(array,parent,array.length);
        }
    }

    private static void shiftDown(int[] array,int parent,int len) {
        int child = 2*parent+1;
        while (child < len) {
            if(child+1 < len && array[child] < array[child+1]) {
                child++;
            }
            if(array[child] > array[parent]) {
                swap(array,child,parent);
                parent = child;
                child = 2*parent+1;
            }else {
                break;
            }
        }
    }

时间复杂度分析:
最好:O(nlogn)
最坏:O(nlogn)
平均:O(nlogn)
在堆排序的代码实现中,主要有两个部分:

  1. 构建初始堆:需要进行n/2次下滤操作,复杂度为O(n)。
  2. 堆排序:需要进行n-1次删除最大元素及下滤操作,每个下滤操作的时间复杂度为O(logn),所以总的时间复杂度为O((n-1)logn)=O(nlogn)。
    将构建初始堆和堆排序两个部分的复杂度相加,得出堆排序的总时间复杂度为O(nlogn)。

四.交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特
点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

4.1 冒泡排序

在这里插入图片描述
具体思路

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
    具体代码:
 public static void bubbleSort(int[] array) {
        for (int i = 0; i < array.length-1; i++) {
            boolean flg = false;
            for (int j = 0; j < array.length-1-i; j++) {
                if(array[j] > array[j+1]) {
                    swap(array,j,j+1);
                    flg = true;
                }
            }
            if(flg == false) {
                return;
            }
        }
    }

时间复杂度分析
最好:O(n)
最坏:O(n^2)
平均:O(n^2)
在冒泡排序的代码实现中,有两个for循环:
外层循环控制排序的轮数,内层循环控制每轮比较的次数。
当数组已经有序时,外层循环只需要运行1次,内层循环不运行,时间复杂度为O(n)。
当数组完全反序时,外层循环需要运行n次,内层循环每次运行n-1次,时间复杂度为O(n^2)。
在一般情况下,冒泡排序的时间复杂度为O(n^2)。

4.2 快速排序

基本概念
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有无素都排列在相应位置上为止。
算法步骤

  1. 选取基准值:从数列中选出一个元素作为基准值。常用的方法是选取第一个元素或者最后一个元素作为基准值。
  2. 分区:将比基准值小的元素放在基准值的左边,比基准值大的元素放在基准值的右边。分区结束后,基准值所在位置就是其最终位置。
  3. 递归调用:对基准值左边和右边的数列分别进行递归调用,实现排序。
  4. 终止条件:当数列的大小为1时,递归结束。

我这里先列出快速排序的基本代码框架

   public static void quickSort1(int[] array) {
        quick(array,0,array.length-1);
    }
       private static void quick(int[] array,int start,int end) {
        //为什么取大于号  : 1 2 3 4 5 6
        if(start >= end) {
            return;
        }
        //找基准
        int pivot = partition(array,start,end);//划分
        quick(array,start,pivot-1);
        quick(array,pivot+1,end);
    }

看了上面的代码之后,我们现在来分析分基准是怎么样的,下面提供了三种分基准的方法.

选取基准值的方法

方法一:挖坑法
具体步骤:

  1. 选取第一个元素作为基准值pivot。
  2. 从下标为1的元素开始遍历,将比pivot小的元素移到左边,比pivot大的元素移到右边,相等的元素暂且不动。
  3. 遍历完成后,pivot的最终位置就在左边的空隙,此空隙即为“坑”。
  4. 将pivot放入“坑”中,分区完成。

具体图示
在这里插入图片描述

 private static int partition(int[] array,int left,int right) {
        int tmp = array[left];
        while (left < right) {
            while (left< right && array[right] >= tmp) {
                right--;
            }
            array[left] = array[right];
            while (left< right && array[left] <= tmp) {
                left++;
            }
            array[right] = array[left];
        }
        array[left] = tmp;
        return left;
    }

方法二: Hoare

具体步骤

  1. 选取数组的第一个元素和最后一个元素作为左右指针,初始时指针i指向第一个元素,指针j指向最后一个元素。
  2. i和j指针向中间遍历,将比基准值小的元素换到左边,将比基准值大的元素换到右边。
  3. 当i和j指针重合时,基准值的正确位置找到,进行交换。
  4. 递归调用基准值左边和右边的两部分

具体图示
在这里插入图片描述

 private static int partition2(int[] array,int left,int right) {
        int tmp = array[left];
        int i = left;
        while (left < right) {
            while (left< right && array[right] >= tmp) {
                right--;
            }
            while (left< right && array[left] <= tmp) {
                left++;
            }
            swap(array,left,right);
        }
        swap(array,left,i);
        return left;
    }

方法三:前后指针
具体步骤

  1. 选取第一个元素作为基准值pivot。
  2. 定义两个指针prev和cur,prev初始指向第一个元素,cur指向第二个元素。
  3. cur从左向右遍历,如果遇到比pivot小的元素,prev同时右移,并将该元素交换到prev的位置。
  4. 重复步骤3,当cur遍历完成后,prev的位置就是pivot的最终位置。
  5. 交换pivot和prev指向的元素,完成一次分割。
  6. 递归调用pivot左边和右边的两部分。

具体图示:
在这里插入图片描述

 private static int partition3(int[] array,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;
    }

快速排序的优化

其实针对上面的快速排序,我们还可以做一些优化,我提供了,俩种优化方案.具体为什么这样做,我会给出原因和解释方法,大家不用担心.

1. 三数取中法选key
三数取中法是快速排序中选取基准值的一种常用方法。它的做法是:
选取数组左端、右端和中间三个元素,取其中间大小的元素作为基准值。
相比直接选取第一个或最后一个元素,三数取中法可以避免出现基准值过大或过小的情况,实现更为平衡的分割。这可以在一定程度上提高快速排序的性能,尤其是在最差情况下。
所以,三数取中法是一个较好的选key方法,能够一定程度优化快速排序。

至于为什么使用三数取中,只是为了能够在找基准的时候,能够实现平衡的分割.
大家看一下我对三数取中情况的判定
在这里插入图片描述

具体代码:

 private static int midThree(int[] array,int left,int right) {
        int mid = (left+right) / 2;
        //6  8
        if(array[left] < array[right]) {
            if(array[mid] < array[left]) {
                return left;
            }else if(array[mid] > array[right]) {
                return right;
            }else {
                return mid;
            }
        }else {
            //array[left] > array[right]
            if(array[mid] < array[right]) {
                return right;
            }else if(array[mid] > array[left]) {
                return left;
            }else {
                return mid;
            }
        }

    }

添加优化后的代码

   public static void quickSort1(int[] array) {
        quick(array,0,array.length-1);
    }
       private static void quick(int[] array,int start,int end) {
        //为什么取大于号  : 1 2 3 4 5 6
        if(start >= end) {
            return;
        }
              //三数取中法
        int index = midThree(array, start,end);
        swap(array,index,start);

        //找基准
        int pivot = partition(array,start,end);//划分
        quick(array,start,pivot-1);
        quick(array,pivot+1,end);
    }
    private static int midThree(int[] array,int left,int right) {
        int mid = (left+right) / 2;
        //6  8
        if(array[left] < array[right]) {
            if(array[mid] < array[left]) {
                return left;
            }else if(array[mid] > array[right]) {
                return right;
            }else {
                return mid;
            }
        }else {
            //array[left] > array[right]
            if(array[mid] < array[right]) {
                return right;
            }else if(array[mid] > array[left]) {
                return left;
            }else {
                return mid;
            }
        }

    }

2. 递归到小的子区间时,可以考虑使用插入排序

当快速排序递归层数较深,子区间长度较小时,继续递归的性能开销会变大。此时,可以考虑改用其他更为高效的排序方法,以提高效率。
插入排序对于较小的子区间更加高效,它的时间复杂度为O(n^2)。而快速排序的时间复杂度为O(nlogn),但它的性能并不依赖于区间大小。
所以,在快速排序递归到一定层数,子区间足够小时,可以改用插入排序来排序此子区间。这可以减少继续递归带来的性能损失,优化快速排序的性能。
至于我们为什么要使用这种去优化快速排序,让我们再来看一下快速排序的过程.
在这里插入图片描述
大家可以看到上述动画过程中,实际上我们在递归到最后俩层的时候,区间就趋于有序了,这个时候如果还继续使用快速排序找基准去排序的话,就有些慢了.所以我们在倒数俩层的时候使用插入排序.
在这里插入图片描述

代码展示:

 public static void quickSort1(int[] array) {
        quick(array,0,array.length-1);
    }
    private static void quick(int[] array,int start,int end) {
        //为什么取大于号  : 1 2 3 4 5 6
        if(start >= end) {
            return;
        }

        //使用这个优化 主要解决 减少递归的次数,使用插入排序优化
       if(end - start + 1 <= 14) {
            //插入排序
            insertSort2(array,start,end);
           return;
     }
        System.out.println("start:"+start+"  end: "+end);
        //三数取中法
        int index = midThree(array, start,end);
        swap(array,index,start);

        int pivot = partition(array,start,end);//划分
        quick(array,start,pivot-1);
        quick(array,pivot+1,end);
    }
private static void insertSort2(int[] array,int left,int right) {
        for (int i = left+1; i <= right; i++) {
            int tmp = array[i];
            int j = i-1;
            for (; j >= left ; j--) {
                if(array[j] > tmp) {
                    array[j+1] = array[j];
                }else {
                    break;
                }
            }
            array[j+1] = tmp;
        }
    }

非递归实现快速排序

具体思路:

  1. 选取第一个元素和最后一个元素作为左右边界,进行一次分割操作。
  2. 根据分割结果,将左右两边的子区间入栈。
  3. 从栈中弹出子区间,重复步骤1和2,直到栈为空。
    该方法使用栈来代替递归来实现快速排序。它通过不断从栈中弹出子区间,并进行分割和入栈的操作,最终实现整个数组的排序。
    在这里插入图片描述

具体代码:

public static void quickSort(int[] array) {
        Deque<Integer> stack = new LinkedList<>();
        int left = 0;
        int right = array.length-1;
        int pivot = partition(array,left,right);
        if(pivot > left+1) {
            stack.push(left);
            stack.push(pivot-1);
        }
        if(pivot < right-1) {
            stack.push(pivot+1);
            stack.push(right);
        }

        while (!stack.isEmpty()) {
            right= stack.pop();
            left = stack.pop();
            pivot = partition(array,left,right);
            if(pivot > left+1) {
                stack.push(left);
                stack.push(pivot-1);
            }
            if(pivot < right-1) {
                stack.push(pivot+1);
                stack.push(right);
            }
        }

    }

五.归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and
Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并排序核心步骤:
在这里插入图片描述

递归实现递归排序

具体思路:

  1. 递归将数组分成左右两半,直到每个子数组只有一个元素。
  2. 合并左右两半,使之有序。
  3. 重复步骤1和2,直至整个数组有序。
    具体代码:
 public static void mergeSort(int[] array) {
        mergeSortFunc(array,0,array.length-1);
    }
    private static void mergeSortFunc(int[] array,int left,int right) {
        if(left >= right) {
            return;
        }
		//分解过程
        int mid = (left+right) / 2;
        mergeSortFunc(array,left,mid);
        mergeSortFunc(array,mid+1,right);
        //合并过程
        merge(array,left,right,mid);
    }
		/*
	1. 定义s1和s2指针,初始指向两个子数组起点。
	2. 比较s1和s2指向元素,小的存入tmp并移动对应指针。
	3. 重复步骤2,直到s1或s2遍历完对应子数组。
	4. 将未遍历完的子数组元素存入tmp。 
	5. 将tmp中的元素存回原数组。
		*/
    private static void merge(int[] array,int start,int end,int mid) {
        int s1 = start;
        //int e1 = mid;
        int s2 = mid+1;
        //int e2 = end;
        int[] tmp = new int[end-start+1];
        int k = 0;//tmp数组的下标
        while (s1 <= mid && s2 <= end) {
            if(array[s1] <= array[s2]) {
                tmp[k++] = array[s1++];
            }else {
                tmp[k++] = array[s2++];
            }
        }
        while (s1 <= mid) {
            tmp[k++] = array[s1++];
        }
        while (s2 <= end) {
            tmp[k++] = array[s2++];
        }

        for (int i = 0; i < tmp.length; i++) {
            array[i+start] = tmp[i];
        }

    }

时间复杂度分析:
归并排序是一种典型的分治算法。它将原问题划分为两个子问题,并递归求解。然后将两个子问题的解进行合并,得到原问题的解。
假设有n个元素的数组,每次都能等分为两个n/2元素的子数组。那么:
第1层递归有2个子问题,每个子问题的规模为n/2;
第2层递归有4个子问题,每个子问题的规模为n/4;
第k层递归有2k个子问题,每个子问题的规模为n/2k;
直到数组中每个子数组都只有1个元素。
此时,一共递归了log2n层。每层的时间复杂度都是O(n),因为需要对n/2个元素进行合并。
所以,总的时间复杂度为O(nlogn)。

具体的计算过程如下:
第1层:2 * (n/2) = n //2个子问题,每个n/2个元素,合并需n时间
第2层:4 * (n/4) = n //4个子问题,每个n/4个元素,合并需n时间
第3层:8 * (n/8) = n //8个子问题,每个n/8个元素,合并需n时间

第k层:2^k * (n/2^k) = n //2k个子问题,每个n/2k个元素,合并需n时间
一共log2n层,所以时间复杂度为O(nlogn)。

非递归实现归并排序

具体思路:

  1. 定义gap初始为1,表示当前有1组有序数据。
  2. 遍历数组,每gap个元素进行一次合并,将gap组数据合并成gap/2组有序数据。
  3. gap乘2,表示现在有gap/2组有序数据。
  4. 重复步骤2和3,直到gap大于数组长度,说明整个数组有序。
    具体代码:
/*
几个坐标之间的关系
left = i;
mid = left+gap-1;
right = mid+gap;
*/
public static void mergeSort(int[] array) {
        int gap = 1;
        while (gap < array.length) {
            // i += gap * 2 当前gap组的时候,去排序下一组
            for (int i = 0; i < array.length; i += gap * 2) {
                int left = i;
                int mid = left+gap-1;//有可能会越界
                if(mid >= array.length) {
                    mid = array.length-1;
                }
                int right = mid+gap;//有可能会越界
                if(right>= array.length) {
                    right = array.length-1;
                }
                merge(array,left,right,mid);
            }
            //当前为2组有序  下次变成4组有序
            gap *= 2;
        }
    }
    private static void merge(int[] array,int start,int end,int mid) {
        int s1 = start;
        //int e1 = mid;
        int s2 = mid+1;
        //int e2 = end;
        int[] tmp = new int[end-start+1];
        int k = 0;//tmp数组的下标
        while (s1 <= mid && s2 <= end) {
            if(array[s1] <= array[s2]) {
                tmp[k++] = array[s1++];
            }else {
                tmp[k++] = array[s2++];
            }
        }
        while (s1 <= mid) {
            tmp[k++] = array[s1++];
        }
        while (s2 <= end) {
            tmp[k++] = array[s2++];
        }

        for (int i = 0; i < tmp.length; i++) {
            array[i+start] = tmp[i];
        }

    }

时间复杂度分析
该非递归归并排序的时间复杂度仍然为O(nlogn)。
理由如下:

  1. 每次合并操作的时间复杂度为O(n),因为需要对n/2个元素进行合并。
  2. 第1次合并操作后,有2组有序数据;第2次有4组;第3次有8组;以此类推。
  3. 所以,总的合并次数为log2n次。
  4. 每次合并时间复杂度为O(n),所以总时间复杂度为O(nlogn)。
    具体分析如下:
    第1次合并:2 *(n/2) = n //2组数据,每个n/2元素,合并需n时间
    第2次合并:4 *(n/4) = n //4组数据,每个n/4元素,合并需n时间
    第3次合并:8 *(n/8) = n //8组数据,每个n/8元素,合并需n时间

    第k次合并:2^k *(n/2^k) = n //2k组数据,每个n/2k元素,合并需n时间
    一共log2n次合并,所以时间复杂度为O(nlogn)。

六.总结

最后来一个简单的总结,列出一个表格,讲七种排序算法,进行一个对比.
在这里插入图片描述

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

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

相关文章

Web3简述MetaMask并演示谷歌安装MetaMask扩展程序方式

Web3到现在理论这段是说的有点太多了 那么 我们先来看个东西 叫 MetaMask 这个在我们项目开发过程中需要使用 其实 你如果去找MetaMask 可能会被一些关键字下到 可能会看到 虚假 诈骗等关键字 因为 这个东西本事就是一个开源的以太坊的一个钱包 那么 钱包肯定就是用来管理资产…

LoadDef python工具包学习一:计算love数

首先在github下载这个工具箱&#xff1a;https://github.com/hrmartens/LoadDef 下载完毕&#xff0c;解压缩进入如下的界面&#xff0c;点击进入working工作文件夹。在doc文件夹里面有帮助文档和教程&#xff0c;有需要的可以仔细阅读。 loaddef主要的功能模块包括&#xff1…

力扣日记1494

1. 题目 [LeetCode 1494. 并行课程 II]https://leetcode.cn/problems/parallel-courses-ii/) 1.1 题意 严格按照选课先修顺序选课&#xff0c;每个学期选课数有上限&#xff0c;求选完所有课程的最短学期数 1.2 分析 这道题的数据量很小&#xff0c;而且作为困难题&#xff…

一起学 WebGL:纹理对象学习

大家好&#xff0c;我是前端西瓜哥&#xff0c;今天我们来了解 WebGL 的纹理对象&#xff08;Texture&#xff09; 纹理对象&#xff0c;是将像素&#xff08;texels&#xff09;以数组方式传给 GPU 的对象&#xff0c;常见场景是贴图&#xff0c;就是将图片的数据应用到 3D 物…

vue 根据word摸板导出word文档,并压缩为zip

yarn add jszip3.10.1 yarn add jszip-utils0.1.0 yarn add pizzip3.1.4 yarn add docxtemplater3.29.5 yarn add docxtemplater-image-module-free1.1.1 yarn add file-saver2.0.5 注意&#xff1a;这里的fileUrl必须是绝对路径&#xff0c;否则可能会报 is not zip的错误&…

当pytest遇上poium会擦出什么火花

当pytest遇上poium会擦出什么火花 首先&#xff0c;创建一个test_sample/test_demo.py 文件&#xff0c;写入下面三行代码。 def test_bing(page):page.get("https://www.bing.com")assert page.get_title "必应"不要问题 page 从哪里来&#xff0c;打开…

(LLM) 的所有知识;10分钟了解向量数据库;微软 Bing 可以识别图片了;

&#x1f989; AI新闻 &#x1f680; 微软 Bing 可以识图」了&#xff0c;吊打 GPT-4&#xff1f; 摘要&#xff1a;微软 Bing 最新识图功能让用户可以上传图片并进行编程、做题、看病等操作&#xff0c;还能分析梗图笑点。然而在某些情况下表现不佳&#xff0c;例如无法数清…

技术分享 | i.MX8M Plus开发板 固定IP地址以及单网口多IP设置

以启扬IMX8MP开发板为例&#xff0c;给大家分享固定IP地址以及单网口多IP设置的步骤流程。 固定IP地址设置 20-wired.network 重启Network生效 网口多ip设置 对于一些网络管理的命令 connman设置&#xff08;参考&#xff09; imx8 yocto系统的init system使用systemd&#xff…

精选Java SSM 框架基础面试题

一、Spring面试题 1、Spring 在ssm中起什么作用&#xff1f; Spring&#xff1a;轻量级框架作用&#xff1a;Bean工厂&#xff0c;用来管理Bean的生命周期和框架集成。两大核心&#xff1a;1、IOC/DI(控制反转/依赖注入) &#xff1a;把dao依赖注入到service层&#xff0c;se…

STM32 GPIO 详解

0. 实验平台 基于STM32F407ZG 1. GPIO 简介 1.1 简介 GPIO全称&#xff1a;General Purpose Input Output&#xff0c;即通用输入输出端口&#xff0c;一般用来采集外部器件的信息或者控制外部器件工作&#xff0c;即输入输出 1.2 STM32 的 GPIO 特点 不同型号&#xff0…

SpringBatch从入门到实战(五):执行上下文和单步骤重启

一&#xff1a;执行上下文 1.1 Job Context 作业上下文 JobContext 绑定 JobExecution 执行对象&#xff0c;为Job作业执行提供执行环境(上下文)。 1.2 Step Context 步骤上下文 StepContext 绑定 StepExecution 执行对象&#xff0c;为Step步骤执行提供执行环境(上下文)。 …

【剑指offer专项突破版】栈篇——“C“

文章目录 前言一、后缀表达式题目分析思路分析代码 二、小行星碰撞题目分析思路分析代码 三、每日温度题目分析思路分析代码 四、直方图最大矩形面积题目分析思路分析代码 五、矩阵中最大的矩形题目分析思路分析代码 总结 前言 剑指offer专项突破版&#xff08;力扣官网&#x…

IBM不藏私:深刻解析量子计算机的突破和机遇

​ 巴伐利亚科学部长Markus Blume在莱布尼茨超级计算中心与Dieter Kranzlmlle&#xff08;左&#xff09;一起观看量子计算机的部分构件。&#xff08;图片来源&#xff1a;网络&#xff09; 关于量子计算机的研究已进行了数十年&#xff0c;目前还尚未生产一台能够掀起计算革命…

Vue全家桶(一):Vue基础+Vue-Cli+Vue组件化+过渡动画

目录 1.Vue概述1.1 认识Vue1.2 Vue的两核心1.3 Vue的初体验1.4 Vue的生命周期 2. Vue-CLI (Command Line Interface)3. Vue基本使用3.1 传统开发模式对比3.2 Vue.js引入3.3 Vue.js 案例分析3.3.1 实例参数el、data、methods的写法 4. Vue模板语法4.1 插值语法 {{xxx}}4.2 指令语…

vue3+ts:shims-vue.d.ts

一、本文引子 uniapp&#xff08;3.8.4.20230531&#xff09; vue3 ts vite 项目 在搭建这个base项目的时候出现红素波浪线如图&#xff0c;代码运行正常&#xff0c;但是看起来很难受&#xff0c;于是各种查找&#xff0c;能找到的资料很少&#xff0c;可能和我提问不够准…

【备战秋招】每日一题:4月23日美团春招第一题:题面+题目思路 + C++/python/js/Go/java带注释

为了更好的阅读体检&#xff0c;为了更好的阅读体检&#xff0c;&#xff0c;可以查看我的算法学习博客第一题-申请奖学金 在线评测链接:P1245 题目内容 塔子哥是一个热爱学习的大学生&#xff0c;他的梦想是成为一名优秀的算法竞赛高手。为了实现自己的梦想&#xff0c;他需…

代理ip匿名原理及那些行业需要代理ip

互联网的高速发展&#xff0c;连带了代理ip也受到了更多人的使用&#xff0c;不同的行业都存在使用代理ip的情况&#xff0c;同时代理ip也以为匿名程度分成了高匿、普匿、透明代理&#xff0c;那么代理ip匿名的原理是什么呢&#xff1f;又有哪些行业需要代理ip呢&#xff1f;下…

flume环境配置-传输Hadoop日志(namenode或datanode日志)

解压文件 修改文件名 配置环境变量 执行flume-ng version 将flume-env.sh.template改名为flume-env.sh&#xff0c; 并修改其配置 启动Flume传输Hadoop日志 启动flume 解压文件 tar -zxvf apache-flume-1.9.0-bin.tar.gz -C /opt 修改文件名 mv apache-flume-1.9.0-b…

全面安全防护,加速企业创新发展——亚马逊云科技re:Inforce全球大会

亚马逊云科技re:Inforce 2023全球大会于当地时间2023年6月13日在美国加州安纳海姆拉开帷幕。在大会上&#xff0c;亚马逊云科技宣布推出十多项安全新服务及功能&#xff0c;下面就来一览本次大会的风采。 “Security is our top priority.” “安全是我们的首要优先级”&#…

Java 面向对象 | 详细知识图谱式讲解

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; ☢Java入门 基础知识&#xff1a;了解 Java 基本语法、面向对象编程&#xff08;OOP&#xff09;概念、流程控制语句、数据类型、方法等基础知识。可以通过 Java 编程…