面试总结1——排序算法

 

排序算法

比较排序和非比较排序

  1. 常见的排序算法都是比较排序,非比较排序包括计数排序、桶排序和基数排序,非比较排序对数据有要求,因为数据本身包含了定位特征,所有才能不通过比较来确定元素的位置。
  2. 比较排序的时间复杂度通常为O(n^或者O(nlogn),比较排序的时间复杂度下界就是O(nlogn),而非比较排序的时间复杂度可以达到O(n),但是都需要额外的空间开销。

为什么要分稳定排序和非稳定排序

稳定排序课以让第一个关键字排序的结果服务与第二个关键字排序中数值相等的那些数。

所有相等的数经过某种排序方法后,仍能保持他们在排序之前的相对次序,我们就说这种排序方法是稳定的。反之,就是非稳定的。

内排序和外排序

  1. 在排序过程中,所有需要排序的数都在内存,并在内存中调整它们的存储顺序,称为内排序;
  2. 在排序过程中,只有部分数被调入内存,并借助内存调整数在外存中的存放顺序排序方法称为外排序。

稳定排序

  1. 冒泡: O(n)(加标记)~O(n^2),空间O(
  2. 插入:O(n)~O(n^,空间O(1)
  3. 归并:O(nlogn),空间O(n)
  4. 基数:O(nK),空间O(n),K为特征个数。

不稳定排序

  1. 选择:O(n^2),空间O(,需要移动的数据次数比较少,优于冒泡排序。
  2. 快排:O(nlogn)~ O(n^,空间O(logn)~O(n)(递归调用使用的空间)
  3. 堆排序:O(nlogn),空间O(1)
  4. 希尔:O(nlogn)

其他比较

  1. 插入排序的速度较慢,但参加排序的序列局部或整体有序时,这种排序能达到较快的速度,在这种情况下,快速排序、归并反而慢了。
  2. 当n较小时,对稳定性不作要求时宜用选择排序,对稳定性有要求时宜用插入或冒泡排序。若待排序的记录的关键字在一个明显有限范围内时,且空间允许是用桶排序。
  3. 当n较大时,关键字元素比较随机,对稳定性没要求宜用快速排序。
  4. 当n较大时,关键字元素可能出现本身是有序的,对稳定性有要求时,空间允许的情况下,宜用归并排序。对稳定性没有要求时宜用堆排序。

实际应用中,快排优于堆排?

  1. 虽然都是O(nlogn)级别,但是时间复杂度是近似得到的,快排前面的系数更小,所以性能更好些。
  2. 堆排比较交换次数更多。
  3. 也是最主要的原因,和cpu缓存(cache)有关。堆排序要经常处理距离很远的数,不符合局部性原理,会导致cache命中率降低,频繁读写内存。

排序算法详细

冒泡排序

相邻节点进行比较,最大的元素移动到后面;时间复杂度O(n^2)。

优化

优化1: 如果某一轮两两比较中没有任何元素交换,这说明已经都排好序了,算法结束。用flag标记。

优化2: 某一轮结束位置为i,但是这一轮的最后一次交换发生在lastSwap的位置,则lastSwap到i之间是排好序的,下一轮的结束点就不必是i--了,而直接到lastSwap。

插入排序

对于完全有序的数组,性能优势明显,O(n)

遍历到i时,[0,i]的数组是排好序的,取出Ai,从i-1向前遍历,如果小于,交换直到最后一个位置。

优化1:将之前的所有的比它大的元素进行两两交换(从小到大排列的排序),会增加一些交换时间,降低运行效率,优化:不是进行两两交换,而是把当前待插入的元素取出,让当前元素与之前的所有元素进行一一比较,前一个元素大于当前元素直接覆盖,而到了最后当找到合适位置,插入即可。

归并排序

归并排序是采用分治法。将它分成两半分别排序,然后将结果归并起来。时间复杂度O(nlogn),空间复杂度O(n),需要一个辅助数组存放排好序的数据。

也分为递归(自顶向下,不断调用自身,多了入栈和出栈的操作)和迭代方法(自底而上,先将序列每相邻两个数字进行归并操作,再将相邻4个,一次类推,直到整个数组)。

优化

优化1:直接将辅助数组作为参数传入,而不是合并时分配。

优化2:对小规模子数组使用插入排序而不是mergeSort,一般可以将归并排序的时间缩短 10% ~ 15%;

优化3:判断测试数组是否已经有序,如果 arr[mid] <= arr[mid+1],我们就认为数组已经是有序的并跳过merge() 方法,可以是任意有序的子数组算法的运行时间变为线性的。

优化4:merge() 方法中不将元素复制到辅助数组,节省数组复制的时间。调用两种排序方法,一种:将数据从输入数组排序到辅助数组;另一种:将数据从辅助数组排序到输入数组。重点:在每个层次交换输入数组和辅助数组的角色。

选择排序

遍历数组,遍历到i时,a0,a1...ai-1是已经排好序的,然后从i到n选择出最小的,记录下位置,如果不是第i个,则和第i个元素交换。此时第i个元素可能会排到相等元素之后,造成排序的不稳定。

优化

优化1:每次查找时不仅找出最小值,还找出最大值,分别插到前面和后面,可以减少一半的查询时间。(注意要防止找到的最大值在left位置,并且left已经被交换的情况)

快排

快速排序首先找到一个基准,下面程序以第一个元素作为基准(pivot),然后先从右向左搜索,如果发现比pivot小,则和pivot交换,然后从左向右搜索,如果发现比pivot大,则和pivot交换,一直到左边大于右边,此时pivot左边的都比它小,而右边的都比它大,此时pivot的位置就是排好序后应该在的位置,此时pivot将数组划分为左右两部分,可以递归采用该方法进行。

!!  while中的第一个while一定要先遍历j(从后开始遍历),因为最后和基准值交换的时候交换的是j的位置,否则最后如果i==j的话会出错。

基准的选取:

  1. 选取第一个元素。
  2. 随机选取一个元素
  3. 使用左端、右端和中心位置上的三个元素的中值作为“基准”。

优化1:当快排达到一定深度后,划分的区间很小时,再使用快排的效率不高。由《数据结构与算法分析》(Mark Allen Weiness所著)可知,当待排序列长度为5~20之间,此时使用插入排序能避免一些有害的退化情形。

优化2:在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割。

优化3:优化递归操作(主要是为了减少栈深度,效率不一定会提高)。快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化。使用尾递归优化后,可以缩减堆栈的深度,由原来的O(n)缩减为O(logn)。

优化4:使用并行或多线程处理子序列

 

尾递归概念:

如果一个函数中所有递归形式的调用都出现在函数的末尾,当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。

尾递归原理:

当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。

堆排序

过程:

  1. 建堆。
  2. 将当前无序区的堆顶元素arr[0]同该区间的最后一个记录交换,然后将无序区长度减一,调整为新的堆。
  3. 依此类推。知道所有元素都排好序。

建堆:两种方法

  • 1. 调整法(下沉):自底向上,从最后一个结点直到第一个结点,依次对每个结点进行调整(下沉)来调整堆;

  • 2. 插入法(上浮):每次向已经有序的区间添加一个元素,上浮这个元素,有序区间长度加1,直到全部元素都插入堆为止,O(nlogn)。

堆排序

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值