排序算法总结

本文主要学习了《算法》(Robert Sedgewick Kevin Wayne)中的排序章节,并对所有的重要知识点进行总结

1、选择排序

算法:从数组中找到最小的元素,和第一个元素交换;在剩余N-1个数中找到最小的元素,和第二个元素交换。

特点:1) 运行时间和输入无关,需要N^2/2次比较和N次交换;2) 数据移动是最小的,交换次数和数组大小是线性关系。

void sort(int[] a){
    int n = a.length;
    for(int i=0; i<n;i++){
        int min = i;
        for(int j=i+1; j<n; i++){
            if(a[j]<a[min])
                min = j;
        }
        int temp = a[i];
        a[i] = a[min];
        a[min] = temp;
    }
}

2、插入排序

算法:从第二个元素开始,将元素和其前一个元素进行比较、交换,直至其找到合适位置。

特点:1) 对有序数据进行排序时达到最好情况,需要N-1次比较和0次交换;2) 最坏情况为原始完全倒序时,需要大约N^2/2次比较和交换;3) 对部分有序的数组较有效,也很适合小规模数组。

void sort(int[] a){
    int n = a.length;
    for(int i=1; i<n;i++){
        for(int j=i; j>0 && a[j]<a[j-1]; j--){
            int temp = a[j];
            a[j] = a[j-1];
            a[j-1] = temp;
        }
    }
}

3、希尔排序

算法:这是基于插入排序的改进,插入排序只能和相邻的数进行比较。希尔排序引入参数h,使得数组中任意间隔为h的元素都有序,从而保证随着h值的递减,最终整个数据都是有序的。

特点:1)权衡了子数组的规模和有序性;2) 算法性能取决于h;3) 适用于大型数组,但具体性能难以评价。代码量小,不需要额外内存空间,运行时间可接受。

void sort(int[] a){
    int n = a.length;
    int h=1;
    while(h < n/3)
        h = 3*h+1;
    while (h >= 1){
        for(int i=1; i<n;i++){
            for(int j=i; j>0 && a[j]<a[j-h]; j-=h){
                int temp = a[j];
                a[j] = a[j-h];
                a[j-h] = temp;
            }
        }
        h = h/3;
    }
}

4、归并排序

算法:递归地将数组分成两半分别排序,然后将结果归并起来。算法实现分为递归调用sort对分割的两个子数组进行排序,然后使用merge函数将两个数组进行合并。初始调用时使用sort(a,0,a.length-1)表示整个数组。

特点:1) 能够保证排序时间和NlogN成正比;2) 但是需要的额外空间也和N成正比;3) 缺点是辅助数组使用的空间和原始数组大小成正比。

下面给出自顶向下递归调用的归并排序。

void merge(int[] a, int lo, int mid, int hi){
    //将a[lo:mid]和a[mid:hi]两个已经排序好的子数组归并为一整个数组
    int i = lo,j=mid+1;
    for(int k=lo;k<=hi;k++)
        aux[k] = a[k]; //辅助数组aux,初始化时分配和a一样大的空间以供之后使用
    //开始进行归并
    for(int k=lo;k<=hi;k++){
        if(i>mid)   a[k] = aux[j++];    //左侧子数组中所有元素均已汇入大数组时,直接取右半边数组中的元素
        else if(j>hi) a[k] = aux[i++];  //右侧子数组中所有元素均已汇入大数组时,直接取左半边数组中的元素
        else if(a[j]<a[i])  a[k] = aux[j++]; //当前两个子数组索引所指位置,将较小值先加入到大数组中
        else    a[k] = aux[i];
    }
}
void sort(int[] a, int lo, int hi){
    if(hi<=lo)  return;
    int mid = lo + (hi - lo)/2;
    //递归调用sort函数,分别对左半边数组和右半边数组进行排序
    sort(a,lo,mid);
    sort(a,mid,hi);
    //调用merge函数进行合并
    merge(a,lo,mid,hi);
}

优化点:1) 对小规模的数组使用插入排序,而不是递归调用sort函数;2) 通过a[mid]和a[mid+1]的大小比较来测试数组是否已经有序,如果有序可提前停止递归调用;3) 不将元素复制到辅助数组?


还有一种自底向上的归并,先解决小数组问题,然后慢慢整合成一个大的数组,这样的方法不需要递归,对应的merge并未改变,而sort函数改变如下:

void sort(int[] a){
    for(int sz=1; sz<a.length;sz=sz+sz)
        for(int lo=0;lo<a.length-sz;lo+=sz+sz)
            merge(a,lo,lo+sz-1,Math.min(lo+sz+sz-1,a.length-1));
}
如果所有元素相同,归并的运行时间是线性的。


5、快速排序

算法:先使用partition找到pivot所处位置,保证其之前的数字均小于pivot,其之后的数字均大于pivot;然后递归调用将其他位置的元素进行排序。

和归并算法的区别:和归并是互补关系。1) 归并算法将数组分为子数组进行排序然后合并进行排序,递归调用在整个数组合并之前调用;快速排序将数组变为前半部分中数字均小于后半部分,从而使得子数组排序后直接整个数组排序完毕,递归调用在处理整个数组之后调用。2) 归并算法的切分点为1/2处,快排的切分点取决于pivot位置。

特点:1)能够保证排序时间和NlogN成正比;2) 原地排序,只需要一个很小的辅助栈;3) 内循环短小;4) 但是比较脆弱,需要避免低劣的性能。

int partition(int[] a, int lo, int hi){
    int i = lo, j=hi+1;
    int pivot = a[lo];
    while(true){ //找到比pivot大的值和比pivot小的值并互换位置,保证前面的值小于后面的值
        while(a[++i]<pivot)
            if(i==hi)
                break;
        while(pivot<a[--j])
            if(j==lo)
                break;
        if(i>=j)
            break;
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }    //最终跳出循环时1) i==j指向同一个数字;2)j=i-1,此时a[:j]均小于等于pivot,a[i:]均大于等于pivot
    int temp = a[lo];
    a[lo] = a[j];
    a[j] = temp;
    return j;
}
void sort(int[] a, int lo, int hi){
    if(hi<=lo)  return;
    int j = partition(a, lo, hi);
    //递归调用sort函数,分别对左半边数组和右半边数组进行排序
    sort(a,lo, j-1;
    sort(a,j+1,hi);
}

6、优先队列

算法:主要目的不是全排序,而是从N个事物中找到最大(或最小)的M个事物,N值可能很大或者在不断扩大,全排序的计算压力过大。所以使用优先队列,基于删除最大(最小)元素和插入元素两种操作,来维护这M个事物。堆排序是其中的重要算法。

基础的优先队列可以通过数组(有序或无序)、链表来实现插入和删除操作。二叉堆是能很好地实现优先队列的基本操作。

二叉堆:基于完全二叉树,从根节点往下,一层一层由上向下、从左至右,每个节点下方连接两个更小的节点。这样的完全二叉树也可以存储在数组中,根节点置于位置1,其子节点置于位置2,3,依次往下,从而保证位置k的节点的父节点在位置k/2上,其子节点在2k和2k+1位置处。使用数组时,不使用下标为0处的位置,而从1开始存储根节点,从而保证上述性质的满足,所以存储N个元素需要N+1大小的数组。

基于二叉堆的两个基础操作为上浮和下沉,上浮是指在末端添加一个节点时,需要不断和其父节点进行比较、交换,从而将其置于合适位置,这是从末端向根节点的上浮过程;而下沉是对中间某个节点进行改变,变成一个较小值时,需要不断往下搜索,找到合适位置,是从根节点出发的下沉过程。对应的代码如下:

void swim(int k){
    while(k>1 && a[k/2]<a[k]){
        int temp = a[k];
        a[k] = a[k/2];
        a[k/2] = temp;
        k = k/2;
    }
}

void sink(int k){
    while(2*k<=N){
        int j = k*k;
        if(j<N && a[j]<a[j+1]) j++;  //下沉时,将子节点中较大的节点和父节点进行交换
        if(!less(k,j)) break;
        int temp = a[k];
        a[k] = a[j];
        a[j] = temp;
        k = j;
    }
}

这样的基本操作就能够实现上文提到的优先队列中的两个基本操作:1) 插入元素:在末端插入元素,并使用swim函数进行上浮操作至合适位置;2) 删除最大元素:从顶端删除最大元素,并将数组的最后一个元素放到顶端,调用sink函数进行下沉操作至合适位置。

void insert(int v){
    a[++N] = v;
    swim(N);
}

int delMax(){
    int max = a[1];
    //N为所有节点的个数,将最末端的元素调至根节点,并将最后一个置为null,对应元素个数减一
    a[1] = a[N];
    a[N] = null;
    N--;
    sink(1);
    return max;
}

Example: multiway(有多个输入流,将其归并成一个有序的输出流)也可以使用优先队列解决:首先将每个输入流的第一个数取出,每次执行insert操作从而构成最小堆,调用delMin删除最小数,并读入这个最小数对应流中的下一个数,调整堆;以此类推,每一都可以得到一个最小数,并读入一个新数。。。

而堆排序也可以使用这两个基本操作完成,这里我们将sink函数的参数改变为sink(a,N,k),第二个参数代表当前数组的大小,k代表需要下沉的元素的下标。

void sort(int[] a){
    int N = a.length;
    //先使用下沉构造堆
    for(int k=N/2,k>0;k--)
        sink(a,N,k);
    //每次将根节点置于当前的末端,也就是将最大元素放在最后,不断调整堆,最后数组中即为排序后的数组
    while(N>1){
        int temp = a[1];
        a[1] = a[N];
        a[N] = temp;
        N--;
        sink(a,N,1);
    }
}

稳定性:能够保证重复元素在数组中的相对位置。  插入排序和归并排序可以保证稳定性,其他的不能。


Example:找到数组中的第K小的元素

1) 全排序,然后找到第K个

2) 使用堆排序,依次找到最小、第2小。。。。。第K小,相当于topK的变形

3) 使用快速排序中的partition函数的变形可以在线性时间内完成。每一次找到的pivot的索引j,那么前j个数一定是是top-j,所以需要在后面的数组中找到第(K-j-1)个,所以我们只需要找到索引为K位置对应的数值即可。对应代码如下:

int select(int[] a, int k){
    int lo = 0, hi = a.length-1;
    while(hi>lo){
        int j = partition(a,lo,hi);
        if (j==k) 
            return a[k];
        else if (j > k){
            hi = j-1;
        } else if (j < k){
            lo = j+1;
        }
    }
    return a[k];
}






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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值