时间复杂度为O(nlogn)的排序算法(归并排序、快速排序),比时间复杂度O(n²)的排序算法更适合大规模数据排序。

归并排序

归并排序的核心思想

采用“分治思想”,将要排序的数组从中间分成前后两个部分,然后对前后两个部分分别进行排序,再将排序好的两部分合并在一起,这样数组就有序了。

分治是一种解决问题的思想,递归是一种编程技巧,使用递归的技巧就是,先找到递归公式和终止条件,然后将递归公式翻译成递归代码。

归并排序的递推公式和终止条件:

//递归公式
merge_sort(p...r) = mege(merge_sort(p...q),merge_sort(q+1,r)); //终止条件
p >= r,不再继续分解

归并排序代码

public class MergeSort {

    public static void main(String[] args) {
int[] a = {4, 3, 2, 1, 6, 5};
mergeSort(a,0,a.length - 1);
for (int i : a) {
System.out.println(i);
}
} public static void mergeSort(int[] a, int p, int r) {
//终止条件
if (p >= r) return; int q = (r - p) / 2 + p;
//递归公式
mergeSort(a, 0, q);
mergeSort(a, q + 1, r); //到这里递归结束,可以假设[0,q],[q + 1,r]已经排好序了
merge(a, p, q, r); } private static void merge(int[] a, int p, int q, int r) {
int i = p;
int j = q + 1;
int k = 0;
int[] temp = new int[r - p + 1];
while (i <= q && j <= r) {
if (a[i] <= a[j]) {
temp[k++] = a[i++];
} else {
temp[k++] = a[j++];
}
} //判断哪个子数字中有数据,判断依据必须是 <=
int start = i;
int end = q;
if (j <= r) {
start = j;
end = r;
} //将剩余数据拷贝到临时数组temp中
while (start <= end) {
temp[k++] = a[start++];
} //将temp数组中数据[0,r-p],拷贝至a数组中原来位置
//可以直接使用数组复制函数
for (int n = 0; n <= r - p; n++) {
a[p + n] = temp[n];
}
}
}

优化

可以利用哨兵节点对merge方法进行优化,将数组分配两部分,并将Integer.MAX_VALUE添加到每个数组的最后一位,就可以一次性将两个数组中数据全部比较完,不会剩余数据

//优化merge代码
private static void mergeBySentry(int[] a, int p, int q, int r) {
int[] leftArr = new int[q - p + 2];
int[] rightArr = new int[r - q + 1]; for (int i = 0; i <= q - p; i++) {
leftArr[i] = a[p + i];
}
leftArr[q - p + 1] = Integer.MAX_VALUE; for (int i = 0; i < r - q; i++) {
rightArr[i] = a[q + i + 1];
}
rightArr[r - q] = Integer.MAX_VALUE; int i = 0;
int j = 0;
int k = p;
while (k <= r) {
if (leftArr[i] <= rightArr[j]) {
a[k++] = leftArr[i++];
} else {
a[k++] = rightArr[j++];
}
}
}

稳定性

归并排序是稳定的排序算法,是否稳定取决于合并merge方法,当两个数组有相同数据合并时,可以先将左边的数据先存入temp中,这样就可以保证稳定性

时间复杂度

最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)。推导过程待补充...

空间复杂度

归并排序不是原地排序算法。

递归代码的空间复杂度并不能像时间复杂度那样累加。刚刚我们忘记了最重要的一点,那就是,尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)

快速排序

快速排序核心思想

对数组p到r进行排序,从数组中从中取出一个数据作为pivot(分区点),将小于pivot的放在左边,大于pivot的放在右边,之后利用分治、递归思想,再对左右两边的数据进行排序,直到区间缩小为1,说明数据有序了

递归公式和终止条件

//递归公式
quick_sort(p...r) = quick_sort(p...q) + quick_sort(q + 1 ... r) //终止条件
p >= r

快速排序代码

public static void quickSort(int[] a, int n){
quickSortInternally(a,0,n - 1);
} private static void quickSortInternally(int[] a,int p, int r){
if (p >= r) return; int q = partition(a, p, r);
quickSortInternally(a,p,q - 1);
quickSortInternally(a,q + 1,r);
} //p:起始位置,r:终止位置
private static int partition(int[] a, int p, int r) {
//取出中间点
int pivot = a[r]; //i、j为双指针,i始终指向大于中间点的第一个元素,j不断遍历数组,最终指向最后一个元素即中间点
int i = p;
//比较从p开始,到r-1结束
for(int j = p; j < r; ++j) {
//如果小于中间点
if (a[j] < pivot) {
if (i == j) {
//如果i和j相等,说明之前没有大于中间点的元素,i和j都加1
// j在进行下一轮循环的时候会自动加1,所以在这里只加i
++i;
} else {
//如果不相等,说明i已经指向第一个大于中间点的元素
// 需要将小于中间的的a[j]与a[i]交换位置,然后都加1
int tmp = a[i];
a[i++] = a[j];
a[j] = tmp;
}
}
} //循环结束,i指向大于中间点a[r]的第一个元素
//将a[i]与a[r]交换位置
int tmp = a[i];
a[i] = a[r];
a[r] = tmp; System.out.println("i=" + i);
//返回交换后中间点坐标位置
return i;
}

性能分析

快速排序是原地、不稳定的排序算法,时间复杂度在大部分情况下的时间复杂度都可以做到 O(nlogn),只有在极端情况下,才会退化到 O(n²)

原地:空间复杂度为O(1),不需要占用额外存储空间

不稳定:因为分区的过程涉及交换操作,如果数组中有两个相同的元素,比如序列 6,8,7,6,3,5,9,4,在经过第一次分区操作之后,两个 6 的相对先后顺序就会改变。所以,快速排序并不是一个稳定的排序算法

时间复杂度:待补充

思考

O(n) 时间复杂度内求无序数组中的第 K 大元素。比如,4, 2, 5, 12, 3 这样一组数据,第 3 大元素就是 4。

思路:

  • 选择数组A[0,n-1]的最后一个元素A[n-1]作为中间点pivot

  • 对数组A[0,n-1]原地分区,分为[0,p-1],[p],[p+1,n-1],此时[0,p-1]这个分区中虽然可能无序,但是全部是比中间点小的元素,所以[p]为这群数中的第p+1大元素(下标为p,所以共有p+1个元素,应该是p+1大)

  • 比较p+1和K,如果p+1 = K,说明A[p]就是求解元素,如果K > p+1,说明求解元素出现在A[p+1,n-1]中,则按照上面方法递归对A[p+1,n-1]进行分去查找,同理,如果K < p+1,则对A[0,p-1]进行分区查找

时间复杂度:O(n)。第一次分区查找,我们需要对大小为 n 的数组执行分区操作,需要遍历 n 个元素。第二次分区查找,我们只需要对大小为 n/2 的数组执行分区操作,需要遍历 n/2 个元素。依次类推,分区遍历元素的个数分别为、n/2、n/4、n/8、n/16.……直到区间缩小为 1。如果我们把每次分区遍历的元素个数加起来,就是:n+n/2+n/4+n/8+…+1。这是一个等比数列求和,最后的和等于 2n-1。所以,上述解决思路的时间复杂度就为 O(n)。

笨方法:每次取数组中的最小值,将其移动到数组的最前面,然后在剩下的数组中继续找最小值,以此类推,执行 K 次,也可以找到第K大元素。但这种方法的时间复杂度为O(K*n),在K值比较小时,时间复杂度为O(n),当K为n/2或n时,时间复杂度就为O(n²)了

思考2

现在你有 10 个接口访问日志文件,每个日志文件大小约 300MB,每个文件里的日志都是按照时间戳从小到大排序的。你希望将这 10 个较小的日志文件,合并为 1 个日志文件,合并之后的日志仍然按照时间戳从小到大排列。如果处理上述排序任务的机器内存只有 1GB,你有什么好的解决思路,能“快速”地将这 10 个日志文件合并吗

时间复杂度为O(nlogn)的排序算法的更多相关文章

  1. 平均时间复杂度为O(nlogn)的排序算法

    本文包括 1.快速排序 2.归并排序 3.堆排序 1.快速排序 快速排序的基本思想是:采取分而治之的思想,把大的拆分为小的,每一趟排序,把比选定值小的数字放在它的左边,比它大的值放在右边:重复以上步骤 ...

  2. 备战秋招之十大排序——O(nlogn)级排序算法

    时间复杂度O(nlogn)级排序算法 五.希尔排序 首批将时间复杂度降到 O(n^2) 以下的算法之一.虽然原始的希尔排序最坏时间复杂度仍然是O(n^2),但经过优化的希尔排序可以达到 O(n^{1. ...

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

    时间复杂度为 n*logn的LIS算法是用一个stack维护一个最长递增子序列 如果存在 x < y 且  a[x] > a[y],那么我们可以用a[y]去替换a[x] 因为a[y]比较小 ...

  4. JavaScript 数据结构与算法之美 - 十大经典排序算法汇总(图文并茂)

    1. 前言 算法为王. 想学好前端,先练好内功,内功不行,就算招式练的再花哨,终究成不了高手:只有内功深厚者,前端之路才会走得更远. 笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 ...

  5. 11.经典O(n²)比较型排序算法

    关注公号「码哥字节」修炼技术内功心法,完整代码可跳转 GitHub:https://github.com/UniqueDong/algorithms.git 摘要:排序算法提多了,很多甚至连名字你都没 ...

  6. Python版常见的排序算法

    学习笔记 排序算法 目录 学习笔记 排序算法 1.冒泡排序 2.选择排序 3.插入排序 4.希尔排序 5.快速排序 6.归并排序 7.堆排序 排序分为两类,比较类排序和非比较类排序,比较类排序通过比较 ...

  7. C#中常用的排序算法的时间复杂度和空间复杂度

    常用的排序算法的时间复杂度和空间复杂度   常用的排序算法的时间复杂度和空间复杂度 排序法 最差时间分析 平均时间复杂度 稳定度 空间复杂度 冒泡排序 O(n2) O(n2) 稳定 O(1) 快速排序 ...

  8. 排序—时间复杂度为O(n2)的三种排序算法

    1 如何评价.分析一个排序算法? 很多语言.数据库都已经封装了关于排序算法的实现代码.所以我们学习排序算法目的更多的不是为了去实现这些代码,而是灵活的应用这些算法和解决更为复杂的问题,所以更重要的是学 ...

  9. 八大排序算法Java

    目录(?)[-] 概述 插入排序直接插入排序Straight Insertion Sort 插入排序希尔排序Shells Sort 选择排序简单选择排序Simple Selection Sort 选择 ...

随机推荐

  1. redis高级命令2

    主服务负责数据的写,从服务器负责客户端的高并发来读 创建主从复制 clone不能让上面的mac地址不能重复,IP地址也不能重复 122和123是从服务器,我们修改二者的配置文件 其中 192.168. ...

  2. nmap二层发现

    使用nmap进行arp扫描要使用一个参数:-sn,该参数表明屏蔽端口扫描而只进行arp扫描. nmap支持ip段扫描,命令:nmap -sn 192.168.1.0/24 nmap速度比arping快 ...

  3. Pytorch入门——手把手带你配置云服务器环境

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天这篇是Pytorch专题第一篇文章. 大家好,由于我最近自己在学习Pytorch框架的运用,并且也是为了响应许多读者的需求,推出了这个P ...

  4. yum本地源创建

    1 安装yum-utils包,yum-utils可以将需要的包下载在本地,安装后可以使用yumdownloader   yum -y install yum-utils* 2 建立目录/yum/yum ...

  5. STL初步学习(queue,deque)

    4.queue queue就是队列,平时用得非常多.栈的操作是只能是先进先出,与栈不同,是先进后出,与之后的deque也有区别.个人感觉手写队列有点麻烦,有什么head和tail什么的,所以说 STL ...

  6. 小白写了一堆if-else,大神实在看不下去了,竟然用策略模式直接摆平了

    这里涉及到一个关键词:策略模式,那么到底什么是策略模式呢?本文就来好好给大家讲讲策略模式,大家可以带着如下几个问题来阅读本文:   1. 如何通过策略模式优化业务逻辑代码(可以根据自己从事的工作思考) ...

  7. C++ 半同步半异步的任务队列

    代码已发布至 HAsyncTaskQueue

  8. Oracle IO性能测试

    Oracle IO性能测试 前言 最近发生了迁移测试库后(单节点迁移RAC)因为IO性能问题导致迁移后性能非常差的问题. 原本想在创建ASM磁盘组之前用Orion做测试,但是忘了做就没做结果出了这档子 ...

  9. (私人收藏)清新文艺唯美PPT模板

    清新文艺唯美PPT模板 https://pan.baidu.com/s/12hP5pT2KfPGCgOnvp0rOIAf0dj

  10. (私人收藏)python学习(游戏、爬虫、排序、练习题、错误总结)

    python学习(游戏.爬虫.排序.练习题.错误总结) https://pan.baidu.com/s/1dPzSoZdULHElKvb57kuKSgl7bz python100经典练习题python ...