跳转:排序算法
在看这个学习,有些没讲全,以下为补充。
快速排序优化
根据快速排序的4种优化整理
枢轴的优化
1、固定枢轴
基本算法本身每次切分是用的第一个,就叫固定枢轴,但如果序列本身有序或基本有序,则会使时间复杂度提升到O(N²),因为每次切分都用的第一个,要切分N次,每第 i 次比较 N - i 次。
2、随机枢轴
随机取枢轴,枢轴选的最差的概率就是N分之一,当然,选择最优枢轴的概率也是N分之一。避免了最坏情况的多次发生,算法的平均性能较好。
3、三数取中
选取数组开头,中间和结尾的元素,通过比较,选择中间的值作为快排的枢轴。更加稳定,但是开销较大
算法的优化
1、插入排序
快排在处理长度较小的数组时,效率会比较低。当待排序列长度为5~20之间,此时使用插入排序能避免一些有害的退化情形。
class T extends Comparable<T>{//Override compareTo method}
public void QSort(T arr[], int low, int high){
if (high - low + 1 < 10){
InsertSort(arr, low, high);
return;
}
if(low < high){
int pivotPos = Partition(arr, low, high);
QSort(arr, low, pivotPos - 1);
QSort(arr, pivotPos + 1, high);
}
}
public void insertSort(T[] arr) {
int len = nums.length;
for (int i = 1; i < len; i++) {
for (int j = i; j > 0 && arr[j].compareTo(arr[j - 1]) < 0; j--) {
swap(nums, j, j - 1);
}
}
}
2、尾递归优化
尾递归概念:
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。
如果一个函数中所有递归形式的调用都出现在函数的末尾,当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。
例:
public int fun(int n){
if(n == 1) return 1;
return n * fun(n - 1);//×
}
public int fun(int n, int total){
if(n == 1) return total;
return fun(n - 1, total * n);//√
}
尾递归原理:
当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
如何写尾递归:
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数
快排的尾递归优化:
(这也行,学习)
public void QSort(T arr[],int low,int high)
{
int pivotPos;
if (high - low + 1 < 10)
{
InsertSort(arr,low,high);
return;
}
while(low < high)
{
pivotPos = Partition(arr,low,high);
QSort(arr,low,pivotPos-1);
low = pivotPos + 1;
}
}
特别注意:对递归的优化,主要是为了减少栈深度。在处理随机数组时,(三数取中+插排+尾递归)的组合并不一定比(三数取中+插排)的效率高。
排序算法的稳定性
稳定性通俗地讲就是,能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。
不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序
稳定的排序算法:冒泡排序、插入排序、归并排序、基数排序
基数排序
这个排序算法简直太神奇了!比桶算法都神奇。
基数排序是稳定的排序算法。