时间复杂度为O(nlogn)的算法

mergeSort

口诀:

左拆分,左合并,右拆分,右合并,最后合并左右。

归并排序的逻辑

归并排序的战略(宏观)逻辑

先将原数组拆分为arr.length个小数组,每个数组长度为1。
小数组之间合并,进行第一次合并
第一次合并后,此时大数组分为了arr.length/2个小数组,每个小数组有两个元素,再次合并
每次合并后,将新的最小数组合并。

拆分的逻辑是递归,需要先推导出递归的公式和退出低轨的条件:

忽略常数项 参数l,r代表数组左右索引,数组允许左右索引想等
mergeSort(l,r) = merge(mergeSort(l,p),merge_sort(p+1,r))
退出条件为:
l>=r

归并排序合并的逻辑

本质:最小数组(左右数组)元素之间的比较,构造一个有序数组,完成合并

while循环,左右数组均从个字其实索引位开始比较,并将本次比较的小值放到临时数组中。直到一侧数组中所有元素都放入了临时数组(即一侧数组元素清空)
判断余下了那测数组,将该测数组遍历并放入临时数组中(此时是一测数组内部循坏,没测数组都是有序的,所以直接遍历赋值即可)
将新数组遍历赋值给原数组
image.png

代码实现

//递归拆分,直到拆分到最小数组长度为1
public static void mergeSort(int[] arr,int left,int right,int[] temp){
    int mid = (left+right)/2;
    if (left<right){
        mergeSort(arr,left,mid,temp);
        mergeSort(arr,mid+1,right,temp);
        merge(arr,left,mid,right,temp);
    }
}
//合并
private static void merge(int[] arr, int left,int mid, int right, int[] temp) {
    //左侧数组的索引,赋予初始值为左侧数组的起始索引
    int leftIndex = left;
    //右侧数组的索引,赋予初始值为右侧数组的起始索引
    int rightIndex = mid+1;
    int tIndex = 0;
    //todo 最小数组之间的元素比较并给临时数组赋值   eg:数组中四个元素,左右各两个元素,左面数组和右侧数组均从起始索引遍历,放入临时数组
    while(leftIndex<=mid && rightIndex<=right){
        //起始判定:左侧数组最小值,和右侧数组最小值比较, 左右数组都从各自的起始元素开始比较,放入临时数组,跳出循环的条件为,当一侧数组先比较完,即出现一侧数组清空的情况,跳出循环。
        //注意此处一定要是<=  为了应对值相同的场景,保证其的稳定性
        if (arr[leftIndex]<=arr[rightIndex]){
            temp[tIndex] = arr[leftIndex];
            leftIndex++;
            tIndex++;
        }else {
            temp[tIndex] = arr[rightIndex];
            rightIndex++;
            tIndex++;
        }
    }
    //todo 将剩下的值都放到临时数组中
    //判断哪测数组清空了,将未清空的一侧数组按照其索引位开始遍历,放到临时数组中(没测数组均是有序的)
    while (leftIndex <= mid){
        temp[tIndex] = arr[leftIndex];
        tIndex++;
        leftIndex++;
    }

    while (rightIndex<=right){
        temp[tIndex] = arr[rightIndex];
        tIndex++;
        rightIndex++;
    }
    //todo 将临时数组复制给arr  可以直接遍历赋值,因为原数组的这部分元素都在临时数组中

    int arrIndex = left;
    tIndex = 0;
    while (arrIndex<=right){
        //   System.out.println(tIndex+"-----------"+arrIndex);
        arr[arrIndex] = temp[tIndex];
        arrIndex++;
        tIndex++;
    }
}

归并思想:

将一组数据拆分至最小单元,在将最小单元合并,合并的过程中进行排序,合并直到最小单元长度等于元数据长度。

性能分析

内存消耗

O(n)
实际上,尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)。

稳定性

是否稳定决定于代码21行
if (arr[leftIndex]<=arr[rightIndex])
如果判断条件是<=那就是稳定的,如果判断条件是<而不是<=就是不稳定的。

执行效率

最好,最坏,平均都是O(nlogn)

这里分析时间复杂度,和之前有点不同,因为通过递归实现的,所以这里的时间复杂度即分析递归的时间复杂度。

递归代码时间复杂度推导过程

首先先看一下递归的使用场景
一个问题 a 可以分解为多个子问题 b、c,那求解问题 a 就可以分解为求解问题 b、c。问题 b、c 解决之后,我们再把 b、c 的结果合并成 a 的结果。

递归代码的时间复杂度:

如果我们定义求解问题 a 的时间是 T(a),求解问题 b、c 的时间分别是 T(b) 和 T( c),那我们就可以得到这样的递推关系式:
T(a) = T(b) + T(c) + K
其中 K 等于将两个子问题 b、c 的结果合并成问题 a 的结果所消耗的时间。

mergeSort的时间复杂度:

我们假设对 n 个元素进行归并排序需要的时间是 T(n),那分解成两个子数组排序的时间都是 T(n/2)。我们知道,merge() 函数合并两个有序子数组的时间复杂度是 O(n)。所以,套用前面的公式,归并排序的时间复杂度的计算公式就是:

T(1) = C;   n=1时,只需要常量级的执行时间,所以表示为C。
T(n) = 2*T(n/2) + n; n>1

2*T(n/2)为每个子数组排序的时间
n为合并两个子数组的时间,merge() 函数合并两个有序子数组的时间复杂度是 O(n)
以上只是一次merge的时间函数,如果在以上的基础上再次拆分:

T(n) = 2*T(n/2) + n
     = 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n       //进行两次合并
     = 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n
     = 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n
     ......
     = 2^k * T(n/2^k) + k * n
     ......

当 T(n/2^k)=T(1) 时,也就是 n/2^k=1,我们得到 k=log2n 。我们将 k 值代入上面的公式,得到 T(n)=Cn+nlog2n 。
大O分析,则得出时间复杂度为:

O(nlogn)

归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn),这种排序算法,和原数组是否有序无关。

快速排序

排序逻辑

通过不断递归二分,不断将数组分成两组,左侧组所有元素都比右侧组所有元素小,在和中间值比较的过程中,先到达中间位置的一侧负责控制另一侧的索引,让另一组合中间值比较与交换中间值,也就是另一组和中间值排序。

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归公式

quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1… r)

终止条件:
p >= r

code

public static void quickSort(int[] arr,int left,int right){
        //todo 首次分组通过中间值,制造两组,左面小于中间值,右面大于中间值,后面每次都在各自组中再次分组,直到中间值等于最小值或者等于最大值。
        int leftIndex = left;
        int rightIndex = right;
        int middleValue = arr[(leftIndex + rightIndex)/2];
        //左少 -1,1,2,0,3,4,6  l1 r3  l2 r2  l3 r1  -1,0,2,1,3,4,6    右少  -1,-2,-3,0,-4,-5,6    -1,1,2,0,-3,4,5  -1,-3,2,0,1,4,5   -1,-3,0,2,1,4,5
        while (leftIndex < rightIndex){
            int temp = 0;
            while (arr[leftIndex] < middleValue){
                leftIndex++;
            }
            while (arr[rightIndex] > middleValue){
                rightIndex--;
            }
            if (leftIndex == rightIndex){
                break;
            }
            temp = arr[leftIndex];
            arr[leftIndex] = arr[rightIndex];
            arr[rightIndex] = temp;
            //每次中轴两侧比较,左右互补越过中轴,左先到,等右,右先到,等左
            if (arr[rightIndex] == middleValue){
                leftIndex++;
            }
            if (arr[leftIndex] == middleValue){
                rightIndex--;
            }
        }
        //每次出循环,必然leftIndex = rightIndex
        if (leftIndex == rightIndex){
            leftIndex++;
            rightIndex--;
        }

        if (leftIndex<right ){
            quickSort(arr,leftIndex,right);
        }

        if (rightIndex>left){
            quickSort(arr,left,rightIndex);
        }
    }

性能分析

内存消耗

O(1)

稳定性

不稳定

执行效率

最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(nlogn)

总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容