排序算法总结

排序算法学习总结

本文中的思路代码皆借鉴于《大话数据结构》

冒泡排序法

算法描述:
给一个待排序的数组,固定其中一端,从另一端开始两两比较,如果反序则交换,直到没有反序的记录为止。在此过程中,数字较小(或较大)的慢慢从数组的一端向另一端移动,如同气泡慢慢浮上水面。

代码实现:

boolean flag = true;
public void sort() {
    for (int i = 0; i < array.length - 1 && flag; i++) {
        //每次循环初始为false,若在一个循环里没有发生数据交换,则排序结束。
        flag = false;
        for (int j = array.length - 1; j > i; j--) {
            if (array[j - 1] > array[j]) {
                Swap.swap(array, j - 1, j);
                flag = true;
            }
        }
    }
}

复杂度分析:
- 对于长度为N的数组,在初始有序的情况下,循环次数为N-1,交换次数为0;
- 初始反序的情况下,循环次数为(N-1)N/2次,交换次数为(N-1)N/2;


简单选择排序

算法描述:
每一次循环在n-i+1个记录里选取最小的记录,并和第i个记录交换,作为有序序列的第i个记录。

代码实现:

public void sort( ){
    int min = 0;
    for(int i = 0;i<length();i++){
        min = i;
        for(int j = i+1;j<length();j++){
            if(array[min]>array[j]){
                min = j;
            }
        }
        if(min!=i){
            Swap.swap(array, min, i);
        }
    }
}

复杂度分析:
- 给定一个长为N的数组,若初始为有序,则循环次数为(N-1)N/2,交换次数为0;
- 若初始反序,则循环次数为(N-1)N/2,交换次数为N/2;


直接插入排序

算法描述:
将一个数据插入到已经排序好的有序数组中。

代码实现:

public void sort(){
    int temp = 0;
    int i,j=0;
    for(i = 1;i<length();i++){
        if(array[i]<array[i-1]){
            temp = array[i];//记录待插入的数
            for(j = i-1;j>=0&&array[j]>temp;j--){
                array[j+1] = array[j];//比待插入的数大的往后移
            }
            array[j+1] = temp;//将待插入的数插入正确的位置
        }
    }
}

复杂度分析:
- 给定一个长为N的数组,若初始为有序,则比较次数为N-1,移动次数为0;
- 若初始反序,则比较次数为(N-1)+(N-1)N/2=(N+2)(N-1)/2次,移动次数为(N+2)(N-1)/2次。


希尔排序

算法描述:
将待排序数组分成几块,对每一块进行直接插入排序,当整个序列基本有序后,再进行一次总的直接插入排序。

代码实现:

public void sort() {
    int i, j = 0;
    int temp = 0;
    //increment为增量,初始为数组长度,代表将每个数据看做一个子块,再在每次循环中减小increment的大小,即将数组看成若干块,直至为1循环结束,排序结束
    int increment = length();
    do {
        increment = increment / 3 + 1;//经验公式
        for (i = increment ; i < length(); i++) {
            if (array[i - increment] > array[i]) {
                temp = array[i];
                for (j = i - increment; j >= 0 && array[j] > temp; j -= increment) {
                    array[j + increment] = array[j];
                }
                array[j + increment] = temp;
            }
        }
    } while (increment > 1);
}

复杂度分析:
时间复杂度为O(n^(3/2))


堆排序

算法分析:
堆排序就是利用堆(如大顶堆)进行排序的方法。它的基本思想是将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将其与序列最后一个数交换,此时最后一个数就是最大值。然后将剩余n-1个数构造成一个大顶堆,重复上述过程,完成排序。

代码实现:

/***
 * 堆调整
 * @param node 待调整的节点
 * @param end 堆的最后一个节点
 */
private void heapAdjust(int node , int end){
    int temp = array[node];
    //对于节点i,若有子节点,则第2i+1个节点为其左节点,2(i+1)个节点为其右节点
    for(int i = node*2+1;i<end;i=2*i+1){
        if(i<end-1&&array[i]<array[i+1]) i++;//若右节点较大,则与右节点互换
        if(temp>array[i]) break;//若根节点更大,则不调整,结束循环
        array[node] = array[i];
        node = i;
    }
    array[node] = temp;
}

public void sort(){
    //构建大顶堆
    for(int i = length()/2-1;i>=0;i--){
        heapAdjust(i,length());
    }
    //排序
    for(int j = length()-1;j>0;j--){
        Swap.swap(array, j, 0);
        heapAdjust(0,j);
    }
} 

复杂度分析:
- 构建堆时,每个非终端节点最多进行两次比较和互换,因此构建堆的时间复杂度为O(n);
- 排序时,第i次取堆顶记录重建堆要用O(logi)的时间(完全二叉树的某节点i到根节点的距离为logi+1),并且需要取n-1次堆顶数据,因此时间复杂度为O(nlogn);
- 故总体来说,堆排序的时间复杂度为O(nlogn)。


归并排序

算法分析:
假设初始序列有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到n/2个长度为2或1的有序子序列,再两两归并……直至得到一个长度为n的有序序列为止,此为2路归并排序。

代码实现:

public void sort() {
    mergeSort(array, array, 0, length() - 1);
}

//将sr[s..t]归并排序为tr1[s..t]
public void mergeSort(int[] sr, int[] tr1, int s, int t) {
    int m = (s + t) / 2;//将sr[s..t]平分为sr[s..m]和sr[m+1..t]
    int[] tr2 = new int[length()];
    if (s == t) {
        tr1[s] = sr[s];
    } else {
        mergeSort(sr, tr2, s, m);
        mergeSort(sr, tr2, m + 1, t);
        merge(tr2, tr1, s, m, t);//将tr[s..m]和sr[m+1..t]归并待tr1[s..t]
    }
}

public void merge(int[] sr, int[] tr, int s, int m, int t) {
    int j, k, l;
    //将sr中记录由小到大归并到tr
    for (j = m + 1, k = s; s <= m && j <= t; k++) {
        if (sr[s] < sr[j]) {
            tr[k] = sr[s++];
        } else {
            tr[k] = sr[j++];
        }
    }
    //将剩余的sr[s..m]复制到tr
    if (s <= m) {
        for (l = k ; l <= t; l++) {
            tr[l] = sr[s++];
        }
    }
    //将剩余的sr[j..n]复制到tr
    if (j <= t) {
        for (l = k ; l <= t; l++) {
            tr[l] = sr[j++];
        }
    }
}

复杂度分析:

  • 一趟归并需要将sr[0]~sr[n-1]中相邻的长度为h的有序序列进行两两归并。并将结果放到tr[0]~tr[n-1]中,这需要将待排序列中的所有记录扫描一遍,因此耗费O(n)时间;
  • 由完全二叉树的深度可知,整个归并排序需要进行logn次;
  • 故总的时间复杂度为O(nlogn)。
  • 归并过程中需要与待排序序列同样大小的存储空间以及递归深度为logn的栈空间,因此空间复杂度为O(n+logn)。
  • 此外,因为需要两两比较,不存在跳跃,因此是一种稳定的排序算法。

归并排序的非递归实现:

public void sort(int[] tr) {
    int k = 1;
    while (k < length()) {
        mergePass(array, tr, k, length() - 1);
        k = k << 1;
        mergePass(tr, array, k, length() - 1);
        k = k << 1;
    }
}

public void mergePass(int[] sr, int[] tr, int s, int n) {
    int i = 0;
    while (i < n - 2 * s + 1) {
        merge(sr, tr, i, i + s - 1, i + 2 * s - 1);//两两归并
        i = i + 2 * s;
    }

    if (i <= n - s + 1) {//归并最后两个序列
        merge(sr, tr, i, i + s - 1, n);
    } else {//最后只剩一个序列
        for (int j = i; j < n; j++) {
            tr[j] = sr[j];
        }
    }

}

复杂度分析:

  • 非递归的迭代方法,避免了递归时深度为logn的栈空间,因此空间复杂度为O(n),避免地柜时间性能上也有一定的提升(递归时函数进出栈需要消耗时间)。

快速排序

算法分析:
通过一趟排序将待排序记录分割成独立的两部分,其中一部分均比另一部分小,则可分别对这两部分继续进行排序,直至整个序列有序。

代码实现:

public void sort(){
    qSort(0, length()-1);
}
protected void qSort(int low , int high){
    int pivot;
    if(low<high){
        pivot = partition(low,high);//将array[low..high]分为两部分,在枢纽值pivot之前的都比其小,在其后的都比其大
        qSort(low, pivot-1);
        qSort(pivot+1, high);
    }
}
protected int partition(int low , int high){
    int pivotKey = array[low];//将待排序部分的第一个值作为枢纽值
    while(low<high){//从两端交替向中间扫描
        while(low<high&&array[high]>pivotKey)
            high--;
        Swap.swap(array, low, high);//将比枢纽值小的交换到前端
        while(low<high&&array[low]<=pivotKey)
            low++;
        Swap.swap(array, low, high);//将比枢纽值大的交换到后端
    }
    //最终low=high,返回枢纽值
    return low;
}

复杂度分析:

  • 在最优情况下,即每次枢纽值都能将待排序序列平分两半,时间复杂度为O(nlogn),递归造成的栈空间为递归树的深度logn,即空间复杂度为O(logn);
  • 在最坏情况下,即待排序序列为正序或倒序,时间复杂度为O(n^2),需要进行n-1次调用,空间复杂度为O(n);
  • 平均情况下,时间复杂度为O(nlogn),空间复杂度为O(logn);
  • 由于关键字的比较和交换是跳跃进行的,所以快速排序是一种不稳定的排序方法。

优化:

  • 枢纽选取优化:取头、尾和中间值中的中间值
int mid = low+(high-low)/2;
if(array[low]>array[high])//左值较小
    Swap.swap(array, low, high);
if(array[mid]>array[high])//右值最大
    Swap.swap(array, mid, high);
if(array[mid]>array[low])//中值最小
    Swap.swap(array, mid, high);

int pivotKey = array[low];
  • 不必要的交换优化:替换操作取代交换操作
int pivotKey = array[low];
while(low<high){
    while(low<high&&array[high]>pivotKey)
        high--;
    array[low] = array[high];
    while(low<high&&array[low]<=pivotKey)
        low++;
    array[high] = array[low];
}
array[low] = pivotKey;
  • 优化递归操作:尾递归
protected void qSort(int low , int high){
    int pivot;
    while(low<high){//if改成了while
        pivot = partition(low,high);
        qSort(low, pivot-1);
        low = pivot+1;//尾递归
        //qSort(pivot+1, high);
    }
}

7种算法的指标:

排序方法平均情况最好情况最坏情况辅助空间稳定性
冒泡排序O(n^2)O(n)o(n^2)O(1)稳定
简单选择排序O(n^2)O(n^2)o(n^2)O(1)稳定
直接插入排序O(n^2)O(n)o(n^2)O(1)稳定
希尔排序O(nlogn)~O(n^2)O(n^1/3)o(n^2)O(1)不稳定
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定
快速排序O(nlogn)O(nlogn)o(n^2)O(logn)~O(n)不稳定
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值