排序算法

Posted thetinkerj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排序算法相关的知识,希望对你有一定的参考价值。

排序算法

排序算法两阶段

第一阶段(比较排序)

  • 插入排序
  • 合并排序
  • 堆排序
  • 快速排序

第二阶段(非比较排序)

  • 计数排序
  • 基数排序
  • 桶排序

第一阶段:比较排序

插入排序

插入排序的主要思想

将当前的元素放入前面合适的位置

插入排序的实现细节

public int[] insert_sort(int[]a){
    int len = a.length;
    for(int i = 0;i<len;i++){
        for(int j = 0;j<i;j++){
            if(a[i]<a[j]){
                int tmp = a[i];
                a[i] = a[j];
                a[j]=tmp;
            }
        }
    }
    return a;
}

插入排序的小结

插入排序是原地排序最多只需要一个额外空间,用于中间的交换

插入排序的时间复杂度是(Theta(n^2))

合并排序

合并排序的主要思想

使用分而治之的思想,分治法的核心在

分:将一个需要排序的数组,分成对称的左右两个

合:将两段升序数组合并

合并排序的实现细节

public void merge(int[]a,int p,int q,int m){
    // [p-----m][-----q]
    int len_l = m-p+2;
    int len_r = q-(m+1)+2;// ****容易出错的部分!***
    int[] left = new int[len_l];
    int[] right= new int[len_r];
    for(int i=p;i<=m;i++)    left[i-p]      = a[i];
    for(int i=m+1;i<=q;i++)  right[i-(m+1)] = a[i];
    left[len_l-1]  = Integer.MAX_VALUE;
    right[len_r-1] = Integer.MAX_VALUE;

    //合并的思考角度有三个
    //x,y,本身,这里采用的是本身策略
    int i = p;
    int idl = 0;
    int idr = 0;
    while(i<=q){
        if(left[idl]<right[idr]){
            a[i] = left[idl];
            i++;idl++;
        }else{
            a[i] = right[idr];
            i++;idr++;
        }
    }
    //return a;此处的修改涉及到java引用传递的问题
}

public  int[] merge_sort(int[]a,int p,int q) {
    if(p<q){
        int m = (p+q)/2;
        merge_sort(a, p, m);
        merge_sort(a, m+1, q);
        merge(a, p, q, m);
    }
    return a;
}

合并排序的小结

合并排序需要最多的额外空间是数组一倍的空间

算法的复杂度是(Theta(nlogn))

个人认为:该算法设计的精妙之处在于每一次的修改都是直接作用在数组本身,这样可以是分合过程十分自然的连为一体!

堆排序

堆排序所使用到的数据结构:最大堆/最小堆

形式化的结构:

a[15] : {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}

抽象式的结构:完全二叉树

            0

    1           2

3       4   5       6

7 8 9 10 11 12 13 14

注:在实现过程中发现诸多计算机数学中的细节问题

  • 数组下标为(0)带来的问题:

最大(最小)堆的性质:

对于任意一个(node[i]),其父亲节点(node[frac{i-1}{2}]),孩子节点分别为(node[2 imes i])(node[2 imes i+1]) 。(以最大堆为例)每个节点必然满足大于其左右任意孩子节点。

堆排序涉及到的三个过程以及具体实现

public int[]  max_heapify(int[]a,int i,int size){
    /*
        一个基本假设:
            a[i]是待插入元素,其左右两边的堆是最大堆
            a[i]有可能比左右两个最大堆的子堆还要小
    */
    //对输入参数的解释
    // a 输入数组 i 需要调整的根节点
    // 其左儿子 left = a[2×i]
    // 其右儿子 right = a[2×i+1]
    // 目标是使元素a[i]贪心下沉到一个合适的位置
    // 使合并后的堆任然是一个 max_heap
    // size参数的设计是为了方便配合heap_sort
    // 由于heap_sort过程中需要在衰减的空间中进行原地排序
    int max_idx = i;
    int left  = 2*(i+1)-1;
    int right = 2*(i+1);
    if(left<size){
        if(a[left]>a[max_idx]){
            max_idx = left;
        }
    }
    if(right<size){
        if(a[right]>a[max_idx]){
            max_idx = right;
        }
    }
    if(max_idx!=i){
        int tmp = a[i];
        a[i] = a[max_idx];
        a[max_idx] = tmp;
        max_heapify(a, max_idx,size);
    }
    return a;
}

public int[] build_max_heap(int[]a){

    int len = a.length;
    for(int i = (len-1)/2;i>=0;i--)
        a = max_heapify(a, i,a.length);
    return a;
}

public int[] heap_sort(int[]a){
    a = build_max_heap(a);
    int size = a.length;
    for(int i = a.length-1;i>0;i--){
        int tmp = a[i];
        a[i] = a[0];
        a[0]=tmp;
        a =max_heapify(a, 0, size-(a.length-i)) ;  
    }
    return a;
}

heap_sort小结:

heap_sort中最重要的过程是max_heapify过程,对该过程进行适当修改可以适应不同算法的要求。

build_max_heap算法采用自底向上的策略,首先构造满足条件左右子树,最后在层层推进将待插入元素使用heap_sort方法,插入到合适的位置。

heap_sort方法:初始状态-过程状态-结束状态。

快速排序

快速排序的主要思想是分治法

将一个数放在合适的位置上,然后左右递归。

([ dots forall m<i , x_m le x_i dots][x_i][dots forall n>i , x_n ge x_i dots])

算法实现细节:

public int[] quick_sort(int[]a,int p,int q){
    if(p<q){
        int i = partition(a, p, q);
        quick_sort(a, p, i-1);
        quick_sort(a, i+1, q);
    }
    return a;
}
public int partition(int[]a,int p,int q){
    int x = a[q];
    int i = p-1;
    for(int j=p;j<q;j++){
        if(a[j]<=x){
            i = i+1;
            int tmp = a[i];
            a[i] = a[j];
            a[j] = tmp; 
        }
    }
    i++;
    a[q] = a[i];
    a[i] = x;
    return i;
} 

对递归过程的文字描述:

每次等待排序的是数组最后一个元素,从头到尾对数组进行一次遍历,再标记一个分割线,如果当前元素小于待安插元素,将分界线右移一位,将该元素放进左边,若该元素大于待安插元素,则进行下一次判断。

本算法的初始状态为在没有开始之前,没有元素在左边。

过程演示:

|]*****^**************$ // |分割线,*非末尾元素,$待安插元素
|*]****^**************$ // ]算法当前遍历位置
|**]***^**************$ // *表示大于$的数,^表示小于$的数
|***]**^**************$
|****]*^**************$
|*****]^**************$
|*****]^**************$
---------------------------过程当放大
*|****]^**************$
^|****]***************$
---------------------------
^|*****]**************$
...
^|*******************]$
^$|*******************]

个人认为本算法中唯一难点就在于当前元素小于待安插元素时,分割线右移,小于元素与分割线占位元素交换,当然将两个操作顺序交换也是完全没有问题的。

第二阶段:非比较排序

  • 计数排序
  • 基数排序
  • 桶排序

首先,比较算法的最坏下界一定是(Theta (nlgn)),关于这点可以使用决策树来解释(此处不做解释)。所以非比较算法一定是在特定的问题条件下,牺牲空间或者其它非时间因素,完成对算法的优化。

计数排序

使用条件:待排序样本满足 (X = {x|ale x le b,a、bin Z })

使用思想: hash 映射

注意数组下标的调试

public int[] count_sort(int[]a,int min,int max){
    int[]b =  new int[a.length];
    int[]count = new int[max-min+1];
    /*
    基数排序的主要流程
    第一轮 计数
    第二轮 累加
    第三轮 排序 
     */
    for(int i = 0;i<count.length;i++){
        count[i]=0;
    }
    for(int v : a){
        count[v-min]++;
    }
    int cur_num  = 0;
    for (int i = 0;i<count.length;i++) {
        cur_num = count[i]+cur_num;
        count[i] = cur_num;
    }
    for(int i = a.length-1;i>=0;i--){
        b[count[a[i]-min]-1] = a[i];
        count[a[i]-min]--;
    }
    return b;
}

基数排序

基数排序还是非常有意思的过程下面模拟下:

原始 个位 十位 百位
329 720 720 329
457 355 329 355
657 436 436 436
839 457 839 457
436 657 355 657
720 329 457 720
355 839 657 839

其伪代码

for i  最小比较位 to 最大比较位
    使用合适稳定的比较排序对目标数组进行排序

桶排序

桶排序的使用场景比较特殊:

桶排序适用于均匀分布的数组,期望与最终待分类的样本会被均匀的分布到一个个小桶中(这貌似有点经典统计学派)。所以就认为事先将其划分出一个个大小相等的桶,然后将待分配元素放在合适桶中,最终整个数组就是一个天然有序!

以上是关于排序算法的主要内容,如果未能解决你的问题,请参考以下文章

算法排序之堆排序

快速排序-递归实现

从搜索文档中查找最小片段的算法?

在第6731次释放指针后双重免费或损坏

TimSort算法分析

以下代码片段的算法复杂度