数据结构之内排序

内排序:适合数据规模相对较少,要加载在内存中排序。

排序动图演示

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

这里的时间复杂度是平均时间复杂度,也就是正常情况下的时间复杂度。

分析排序算法

排序算法的执行效率
  1. 最好情况、最坏情况、平均情况时间复杂度
  2. 时间复杂度的系数、常数 、低阶
    实际的软件开发中,我们排序的可能是 10 个、100 个、1000 个这样规模很小的数据,所以,在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。
  3. 比较次数和交换(或移动)次数
排序算法的内存消耗

就是空间复杂度
原地排序(Sorted in place):原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。

排序算法的稳定性

稳定性是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。


冒泡排序(Bubble Sort)

优化:当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作
在这里插入图片描述

//冒泡排序
    public static void bubbleSort(int[] a){
        if (a == null || a.length <= 1) return;
        int n = a.length;
        for (int i=0;i<n;++i){
            // 提前退出冒泡循环的标志位
            boolean flag = false;
            for (int j=1;j<n-i;++j){
                if (a[j-1] > a[j]){
                    int temp = a[j];
                    a[j] = a[j-1];
                    a[j-1] = temp;
                    flag = true;// 表示有数据交换
                }
            }
            if (!flag) return;// 没有数据交换,提前退出
        }
    }

总结

  • 冒泡排序是原地排序
  • 冒泡排序是稳定的排序算法
  • 冒泡排序的时间复杂度
    在这里插入图片描述
    平均情况下,需要 n*(n-1)/4 次交换操作(有序度),比较操作肯定要比交换操作多,而复杂度的上限是 O( n 2 n^2 n2),所以平均时间复杂度是 O( n 2 n^2 n2)

插入排序(Insertion Sort)

在这里插入图片描述

//插入排序
    public static void insertSort(int[] a){
        if (a == null || a.length <= 1) return;
        int n = a.length;
        for (int i=1;i<n;++i){
            int value = a[i];
            //注意:这种写法是因为比较时比第0个还要小时的特殊性 记住这种写法
            int j=i;
            for (;j>0;--j){
                //将大于的值往后移
                if (a[j-1]>value){
                    a[j] = a[j-1];
                    continue;
                }
                break;
            }
            a[j] = value;
        }
    }

总结

  1. 插入排序是原地排序算法
  2. 插入排序是稳定的排序算法
  3. 插入排序的时间复杂度:
    最好是时间复杂度为 O(n)
    最坏情况时间复杂度为 O( n 2 n^2 n2)
    平均时间复杂度为 O( n 2 n^2 n2)

选择排序(Selection Sort)

在这里插入图片描述

//选择排序
    public static void selectSort(int[] a){
        if (a == null || a.length <= 1) return;
        int n = a.length;
        for (int i=0; i<n; ++i){
            //定位最小值下标(默认是开始下标)
            int value = i;
            for (int j=i+1; j<n; ++j){
                if (a[j] < a[value]){
                    value = j;
                }
            }
            //如果找到最小值是非开始下标 则最小值与默认下标值进行交换
            if (value != i){
                int temp = a[value];
                a[value] = a[i];
                a[i] = temp;
            }
        }
    }

总结

  1. 选择排序是原地排序算法
  2. 选择排序是不稳定的排序算法(如:5,8,5,2,9)
  3. 选择排序的最好情况时间复杂度、最坏情况和平均情况时间复杂度都为 O( n 2 n^2 n2)

思考题:为什么插入排序比冒泡排序更受欢迎?

虽然复杂度完全相同,但是插入排序效率更高:
在这里插入图片描述

总结

在这里插入图片描述

三种时间复杂度为 O( n 2 n^2 n2) 的排序算法中,冒泡排序、选择排序,可能就纯粹停留在理论的层面了,学习的目的也只是为了开拓思维,实际开发中应用并不多,但是插入排序还是挺有用的

但这三种算法都不适合大规模数据排序,这个时间复杂度还是稍微有点高。


归并排序(Merge Sort)

在这里插入图片描述

归并排序使用的就是分治思想:
在这里插入图片描述

//归并排序
    public static void mergeSort(int[] a,int from,int to){
        //递归终止条件 不能再分了 已经剩一个元素了
        if (from >= to) return;
        //折半分治思想
        int p = from + (to - from)/2;
        //递归 继续分
        mergeSort(a,from,p);
        mergeSort(a,p+1,to);
        //将分好的两个数组有序合并
        merge(a, from, p, p+1, to);
    }

在这里插入图片描述

//合并函数
    private static void merge(int[] a,int from,int p,int r,int to) {
        int i = from;
        int j = r;
        int[] tmp = new int[to-from+1];
        int k =0;

        //合并排序到tmp数组 (难点)
        while (i<=p && j<=to){
            if (a[i] > a[j]){
                tmp[k++] = a[j];
                ++j;
            } else {
                tmp[k++] = a[i];
                ++i;
            }
        }

        //判断哪个数组还没有都copy到tmp数组中 对没有copy的数组进行copy
        if (j <= to){
            for (int t=j;j<=to;++j){
                tmp[k++] = a[j];
            }
        }else {
            for (int t=i;i<=p;++i){
                tmp[k++] = a[i];
            }
        }

        //再将tmp排好序的数组copy到原数组
        for (int t=0;t<tmp.length;++t){
            a[from+t] = tmp[t];
        }
    }

总结

  1. 归并排序是稳定的排序算法
  2. 最好情况、最坏情况,还是平均情况都是 O(nlogn)(不管怎样的数据,它所做的步骤都是一样的,都是逐步划分为单个数据,再逐步合并成最终排序结果。)
  3. 空间复杂度是 O(n)(注意,空间复杂度不会像时间复杂度一样累加,是指算法某一时刻的空间复杂度)

快速排序(Quick Sort)

在这里插入图片描述

步骤
  1. 找pivot(分区点): 随机选择一个元素作为 pivot(一般情况下,可以选择 p 到 r 区间的最后一个元素)
  2. 通过游标 i 把 A[p…r-1]分成A[p…i-1](元素都是小于等于 pivot 的)、A[i…r-1](元素都是大于 pivot 的)
  3. 通过游标 j每次都逐个从A[i…r-1]中取一个元素 A[j],与 pivot 对比,如果小于等于pivot,则将其放到A[p…i-1]后一位,也就是 A[i]与A[j]进行交换,并且i与j都向后移一位。

在这里插入图片描述

//快速排序
    public static void quickSort(int[] a,int p,int r){
        if (p >= r) return;
        //分区
        int i = partition(a,p,r);
        //将数组按原pivot值的新位置(i-1) 进行划分 再度递归
        //注意:这里一定是用i-2和i-1划分
        quickSort(a,p,i-2);//因为:i-1划分到这一个里面的话 可能遇到死循环(i-1已经是这里面最大值了)
        quickSort(a,i-1,r);
    }

//分区函数
    private static int partition(int[] a, int p, int r) {
        int i = p;//[0~i-1]是小于pivot的区域
        int j = p;//j指针元素与pivot进行比较
        while (j<=r){//当j与最后一个元素比较完后跳出循环(注意,此时的[i~j-1]都是大于pivot的值)
            if (a[j] > a[r]){//a[r]:选取最后一个作为pivot
                ++j;
                continue;
            }
            //执行到这说明遇到了小于等于pivot的元素
            //则需要交换到[0~i-1]区域中
            if (j>i){//如果该元素已经是a[i]元素了 则无需交换 只需要把i和j向前移动一位
                int tmp = a[j];
                a[j] = a[i];
                a[i] = tmp;
            }
            ++i;
            ++j;
        }
        return i;
    }

总结

  1. 快速排序是一种原地排序算法
  2. 快速排序是不稳定的排序算法
  3. 快速排序最好、平均时间复杂度O(nlogn),最坏O( n 2 n^2 n2)。

堆排序(Heap Sort)

算法步骤(从上往下堆化)

堆化函数是:关注单个节点的递归函数

一、创建大/小顶堆
  • 从最后一个非叶子节点开始堆化
  • 直到堆化完根节点
  • 此时,就是一个大顶堆,根节点为最大值
二、排序
  • 此时根节点已经为最大值了
  • 将最后节点与根节点呼唤
  • 此时 最后节点为最大值,不再参与排序
  • 然后将换上来的新根节点作为关注节点进行堆化
  • 堆化结束后根节点为此时的最大值(循环到第一步)

在这里插入图片描述

在这里插入图片描述

// 1+2 = nlogn + nlogn = nlogn
func heapSort(n []int) {
	// 1. nlogn
	buildMaxHeap(n)
	// 2. nlogn
	// n
	for i := len(n)-1; i > 0; i-- {
		n[0],n[i] = n[i],n[0]
		// logn
		heapify(n[:i],0)
	}
}

// nlogn
func buildMaxHeap(n []int) {
	// n
	for i := len(n)/2; i >= 0; i-- { //因为len(n)/2是第一个非叶子节点(从第一个非叶子节点堆化)
		// logn
		heapify(n,i)
	}
}

// logn
func heapify(n []int,i int){
	left,right := i*2+1,i*2+2
	largest := i
	if left < len(n) && n[left] > n[largest] {
		largest = left
	}
	if right < len(n) && n[right] > n[largest] {
		largest = right
	}
	if largest != i {
		n[largest],n[i] = n[i],n[largest]
		heapify(n,largest)
	}
}
面试题:如何在 O(n) 的时间复杂度内查找一个无序数组中的第 K 大元素?

答:快速排序思想:

  1. 选择数组最后一个元素作为 pivot,对数组 A[0…n-1]原地分区,这样数组就分成了三部分,A[0…p-1](大于A[p])、A[p]、A[p+1…n-1](小于A[p])。
  2. 如果 p+1=K,那 A[p]就是要求解的元素;如果 K>p+1, 说明第 K 大元素出现在 A[p+1…n-1]区间,我们再按照上面的思路递归地在 A[p+1…n-1]这个区间内查找。
  3. 每步的元素比较次数相加:n+n/2+n/4+n/8+…+1= 2n-1。所以,时间复杂度就为 O(n)。

注意:等差数列求和公式是 n 2 n^2 n2级,而等比是n级
等比数列求和公式:

快速排序比归并排序应用广泛的原因

  1. 归并排序算法是一种在任何情况下时间复杂度都比较稳定的排序算法,这也使它存在致命的缺点,即归并排序不是原地排序算法,空间复杂度比较高,是 O(n)
  2. 快速排序算法虽然最坏情况下的时间复杂度是 O(n2),但是平均情况下时间复杂度都是 O(nlogn)。不仅如此,快速排序算法时间复杂度退化到 O(n2) 的概率非常小,我们可以通过合理地选择 pivot 来避免这种情况。

总结思想

  1. 归并排序是递归均分,分成单个元素后利用merge()函数有序合并,然后递归合并成最终结果(注意:合并过程需要额外空间,空间复杂度为O(n)
  2. 快速排序是先用partition()函数排序分区,然后按区划分,然后递归分区、划分。最后划分到单个元素后就已经排序完成!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值