排序问题汇总

本文详细介绍了四种经典的排序算法:冒泡排序、归并排序、快速排序和堆排序。冒泡排序通过相邻元素比较交换实现排序,时间复杂度为O(N^2)。归并排序采用分治策略,时间复杂度为O(NlogN),但需要额外空间。快速排序在平均情况下时间复杂度也为O(NlogN),但最坏情况达到O(N^2)。堆排序利用堆的性质,时间复杂度同样为O(NlogN),但稳定性较差。四种排序算法各有优缺点,适用于不同场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

冒泡排序

思路

  1. 从头到尾比较相邻两个元素的大小,如果前面的元素比后面的元素大就交换他们的位置(从小到大排序)。这样第一次就会把最大的数排在最后,需要n-1次交换。
  2. 然后重复上面的做法,因为第一次已经得到最大的数,所以第二次只需要n-2次交换操作,以此类推。

代码

//按照刚才那个动图进行对应
//冒泡排序两两比较的元素是没有被排序过的元素--->
public void bubbleSort(int[] array){
    for(int i=0;i<array.length-1;i++){//控制比较轮次,一共 n-1 趟
        for(int j=0;j<array.length-1-i;j++){//控制两个挨着的元素进行比较
            if(array[j] > array[j+1]){
                int temp = array[j];
                array[j] = array[j+1];
                array[j+1] = temp;
            }
        }
    }
}

算法复杂度

O = N^2
计算过程:
在这里插入图片描述

该排序的不足

有重复操作,如有一个数组 3,2,1,在该排序的过程中我们会依次比较3>2,3>1,2>1,但实际上当3>2,2>1时,我们就可以知道3>1了.

归并排序

思路

在这里插入图片描述

  1. 设有一个待排序的数组,他的长度是n,我们需要对其从小到大排序。首先我们把它分成2份a1、a2,每份元素个数为n/2,
  2. 假设a1,a2他们都已经排好序了,这时我们只需要先比较他们的第一个元素哪个小,然后把小的元素排在第一个。如a1的第一个元素a1-1比较小,我们接着就拿a2的第一个元素继续与a1的第二元素比较,谁小谁放在a1-1后面,以此类推。
  3. 接下来再思考如何得到上述的a1,a2.同样的方法我们把a1和a2分成2份,然后像这样递归。一直递归到只剩一个元素。然后按照2中的方法进行比较。

代码

public class MergeSort{
   	//归并排序(升序,调用时left传数组第一个元数的标号,right传最后一个元素的标号)
	public static void mergeSort(int[] sorted,int left,int right){
		int min = (left + right) / 2;
		if(left < right){
			//将序列分为左右两个子序列
			//再分别对左右两个子序列再分为左右两个子序列
			mergerSort(sorted,left,min);
			mergerSort(sorted,min + 1,right);

			//排序并合并
			int[] temp = new int[right - left + 1]
			int i = left;
			int j = min + 1;
			int k = 0;
			while(i <= min && j <= right){
				if(sorted[i] < sorted[j]){
					temp[k++] = sorted[i++];
				}else{
					temp[k++] = sorted[j++];
				}
			}
			//将左边剩余的数据放入新数组
			while(i <= min){
				temp[k++] = sorted[i++];
			}
			//将右边剩余的数据放入新数组
			while(j <= right){
				temp[k++] = sorted[j++];
			}
			//排好序的新数组覆盖原数组
			for(int n = 0;n < temp.length;n++){
				sorted[n + left] = temp[n];
			}
		}
	}
}

算法复杂度

O = NlogN

计算过程

  1. 因为二等分,所以一共需要递归logN次
  2. 每次的排序时需要计算N次
  3. 因此算法复杂度 O = NlogN

该排序的不足

  1. 需要利用一倍的空间存储计算中间结果temp[]
  2. 采用二法,没有利用数组的本身的特性,大多数数组其实在局部都存在一些已经排好序的情况,其实可以利用一下(如蒂姆排序算法),如4123,如果分成4,123计算效率可以更高

快速排序

思路

  1. 随机选取数组其中一个数,比如取第一个n1
  2. 把小于他的数放左边得到数组a1,大于他的数放右边得到a2
    这一部的具体做法为:先从最右向左与n1比较,记录第一个小于n1的位置j;然后从最左向最右比找到第一个大于n1的位置i。然后交换i和j位置上的元素。一直到i和j都指向同一个位置,即把数组遍历完成的时候。最后交换n1和i位置上的元素。参考:https://blog.youkuaiyun.com/qq_40941722/article/details/94396010
  3. 对2中数组的a1,a2重复1中的操作,如此一直递归操作
  4. 当数组都只有1个数时,结束递归,按照“左边-中枢-右边”的次序合并,就得到了从小到大排序好的数组。

总体流程:
在这里插入图片描述

代码

/**
     * 快速排序(从小到大)
     * @param arr 待排序数组
     * @param left 最外层调用时填0
     * @param right 最外层调用时选arr.length-1
     */
    public static void quickSort(int arr[], int left, int right){
        if(left > right){
            return;
        }
        //基准值(一般为首个)
        int tmp = arr[left];
        int i = left;
        int j = right;
        //当i与j 相遇时,循环结束
        while(i != j){
            //先从右边向左走,如果遇到小于基准值的数,则j不动
            while(arr[j] >= tmp && j > i) {
                j--;
            }
            //i向前走,遇到大于基准值的数,i不动
            while(arr[i] <= tmp && j > i) {
                i++;
            }
            if(j > i){ //当i与j都找到相应的数后,i与j指向的数值互换
                int t = arr[i];
                arr[i] = arr[j];
                arr[j] = t;
            }
        }
        /*当i=j时,说明i与j相遇
        * 此时将a[i]与一开始定义的基准值交换
        * 从而保证了 基准值的右边都比基准值大,左边都比基准值小
        * */
        arr[left] = arr[i];
        arr[i] = tmp;
        //对左边进行排序
        quickSort(arr, left, i-1);
        //对右边进行排序
        quickSort(arr, i+1, right);
    }

算法复杂度

O = [NlogN,N^2]
计算过程

  1. 假设每次子序列分的比例为r:1-r
  2. 那么递归的第一层我们需要比较n次,第二层为nr+n(1-r)次,即也为n次,因此每层都需要计算n次
  3. 而递归的深度L,为在log1/rn和n-1中取一个小的。如r=1/2时他的深度为logN,当r=1//N时,即次都是分成一个元素和其他元素,他的深度就为n
  4. 因O=n×L,所以O的范围为nlogn到n^2之间

该排序的不足

  1. 该排序计算效率不稳定,最坏的情况需要计算n^2次。不过根据经验这种情况很少见,通常因为此种算法不需要开辟而外的存储空间,一般能比归并排序还快3倍左右。 (更多快速排序为什么快的原因参考:http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/
  2. 该排序对如:班级里有这两名学生a1(小王,16,北京),a2(小王,16,上海) 进行先根据姓名排序,再根据年龄排序时无法保证a1都能排到a2后面*原因描述需完善*

堆排序

参考:https://blog.youkuaiyun.com/TaylorSwiftiiln/article/details/119865970

  1. 堆分大顶堆和小顶堆,每个节点的值都大于或者等于其左右子节点的值,称为大顶堆;或者每个节点的值都小于或者等于其左右子节点的值,称为小顶堆。
  2. 大顶堆用升序排序,小顶堆用于降序
  3. 堆的逻辑结果为平衡二叉树,实际存储结构为数组
    在这里插入图片描述
    在这里插入图片描述
    该数组从逻辑上讲就是一个堆结构,并且有以下特点:
    大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
    小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

思路

  1. 将无序序列建成一个堆,具体做法为:
    从最后一个非叶子节点a开始检查,从他的叶子节点中挑选大的节点b与a比较,交换a和b;按照这个方式,从右向左,然后再从下向上完成交换(交换后需要考虑其页子节点是否满足堆结构,不满足也按上述步骤进行交换)。等执行到根节点,就表示对所有的节点完成了遍历。即把无序数组排成了堆。
  2. 将堆顶元素与末尾元素交换,将最大元素c沉到数组末端。
  3. 调整数组,使除了c外,其他的元素构成堆,具体做法:
    因为此时末尾的元素已经排好序,所以只需要我们对剩下n-1个元素进行1中的操作,使其变为堆。
    因为之前除了堆顶元素因为与最大元素c的交换,不满足堆结构,其他节点已满足堆结构,所以我们从堆节点开始重复1的操作,使新的数组满足堆结构。
  4. 重复以上操作,直到数组的所有元素都像c一样排好序。

代码

/**
     * 堆排序
     * @param array 待排序数组
     */
    public static void heapSort(int[] array) {
        //从倒数第一个非叶子节点开始
        for (int i = array.length / 2 - 1; i >= 0; i--) {
            //从第一天非叶子节点从左至右,从下至上调整结构
            adjustHeap(array, i, array.length);
        }
        //将堆顶元素与末尾元素交换 将最大元素沉到数组末尾 + 重新调整堆结构
        for (int i = array.length - 1; i > 0; i--) {
            //交换堆顶元素和末尾元素
            swap(array, i);
            //交换后的末尾元素忽略(j--) 不再参与堆结构的调整
            //重新调整堆结构
            adjustHeap(array, 0, i);
        }
    }

    /**
     * 交换堆顶元素和末尾元素
     * @param array 
     * @param end 末尾元素序号
     */
    private static void swap(int[] array, int end) {
        int temp = array[0];
        array[0] = array[end];
        array[end] = temp;
    }

    /**
     * 使当前节点和其子节点满足堆结构
     * @param array
     * @param index
     * @param length
     */
    private static void adjustHeap(int[] array, int index, int length) {
        //取出当前元素
        int temp = array[index];
        //i节点是index节点的左子节点
        for (int i = 2 * index + 1; i < length; i = 2 * i + 1) {
            //表明左子节点小于右子节点
            if (i + 1 < length && array[i] < array[i + 1]) {
                //将指针移至较大节点
                i++;
            }
            //如果子节点大于父节点
            if (array[i] > temp) {
                //将较大值赋给当前节点
                array[index] = array[i];
                //指针移向子节点
                index = i;
            } else {
                break;
            }
        }
        //循环结束,已经将最大值放在了堆顶
        //将temp值放到最终的位置
        array[index] = temp;
    }

算法复杂度

O=NlogN
参考:https://blog.youkuaiyun.com/yizhiniu_xuyw/article/details/109596961

  1. 由于堆排序是由两部分(初始化建堆 + 排序重建堆)完成的,所以时间复杂度也应该是两部分之和。
  2. 初始化建堆当每一次比较都是父节点大于子节点时,只在比较上用了时间,也就是初始化建堆的最好算法复杂度为O=n. 当每一次比较父节点都不比子节点大时,就需要在比较的基础上做调整的工作,一直调整到倒数第二层,他的公式为:
    在这里插入图片描述

3.对于排序重建堆,一共需要重建n-1次,每一次需要重建的次数-1,且需要比较的次数为log(剩余元素个数),因此他的复杂度为;
:log2+log3+…+log(n-1)+log(n)≈log(n!)。可以证明log(n!)和nlog(n)是同阶函数:
  ∵(n/2)n/2≤n!≤nn,∵(n/2)n/2≤n!≤nn,
  ∴n/4log(n)=n/2log(n1/2)≤n/2log(n/2)≤log(n!)≤nlog(n)∴n/4log⁡(n)=n/2log⁡(n1/2)≤n/2log⁡(n/2)≤log⁡(n!)≤nlog⁡(n)
  所以时间复杂度为O(nlogn)
4. 所以堆排序的时间复杂度为nlogn

该排序不足

1.局部性差,堆排序的建堆过程是整个数组各个位置都访问到的,后面则是所有未排序数据各个位置都可能访问到的,所以不利于缓存发挥作用。
2. 主要就是两大类:1.基于划分(partition)的,先划分再递归;2.基于归并(merge)的,先递归再归并。基于划分的算法根据划分标准的不同,包括:快速排序、抽样排序、桶排序、自顶向下的基数排序,等等。其中递归部分可以简单地并行。为了不跑题就不讲划分过程和归并过程怎么并行了,这两个部分的并行效率虽然不高,但是也比堆排序好多了。

常见高效排序算法对比

算法平均时间复杂度最坏时间复杂度额外空间复杂度稳定性
归并排序NlogNNlohNN
快速排序NlogNNlohN1
堆排序NlogNN^2logN
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值