排序算法总结

来源:互联网 发布:快压 mac版 怎么用 编辑:程序博客网 时间:2024/06/01 12:17

排序算法学习总结

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

冒泡排序法

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

代码实现:

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 0