各种排序总结

    本文对各种排序做一个总结(这里不简述具体的过程),包括冒泡排序、选择排序、快速排序、归并排序、堆排序。分别从时间复杂度、空间复杂度、算法的最坏情况以及稳定性方面分析,递归的尽量会附带非递归实现。

1. 冒泡排序

    冒泡排序是一种很简单的排序算法,这里直接上代码(从小到大排序):

bool flag = true;//如果在某一趟排序中数列已经有序,则结束排序过程
for(int i = 0;i < n-1 && flag; ++i){
    flag = false;
    for(int j = 0; j < n-i-1; ++j){//减一是为了实现g[j+1]
        if(g[j] > g[j+1]){
            swap(g[j], g[j+1]);
            flag = true;
        }
    }
}

时间复杂度:时间复杂度是O(n^2),假设排序的数有n个,遍历一趟的复杂度是O(n),需要遍历n-1次,所以时间复杂度为O(n^2)。

空间复杂度:O(1),因为不需要额外的存储空间。

最坏情况:要排序的数是逆序排好序的。

稳定性:冒泡排序是稳定的算法,因为它实现相邻两个元素之间的交换,没有改变元素的相对位置,所以满足稳定性。

2. 选择排序

    选择排序是一种比较好理解的排序,可以说是冒泡排序的兄弟了,只是排序方式不一样,下面直接给出代码:

//选择排序
void Select_Sort(int a[], int n){
    int Min;
    for(int i = 0;i < n-1; ++i){//n-1次选择就可以了
        Min = i;
        for(int j = i+1; j < n; ++j){//从i - n之间选择一个最小的放在 i 处。
            if(a[j] < a[Min]){
                Min = j;
            }
        }
        if(Min != i){
            swap(a[i], a[Min]);
        }
    }
}

时间复杂度:O(n^2),因为每次遍历的复杂度是O(n)的,一共遍历n-1次,所以复杂度是O(n^2)。

空间复杂度:O(1)

最坏情况:貌似排序的数和复杂度没关系,属于佛系。

稳定性:选择排序是不稳定的排序,例如有5 8 5 2 9 ,5个元素按从小到大排序,当第一趟排序后形成2 8 5 5 9 ,两个5的相对位置最终排好序后发生了变化。

3. 快速排序

    快速排序(Quick Sort)使用分治法策略它的基本思想是:选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。快速排序经常容易被改造用在其它的一些方面,如求数组中第k大的数。快排的基准值的选取会对排序结果有一定影响。快速排序是面试中常考的类型,需要关注非递归的方式。

    这里不对算法进行相应描述,直接看代码(其中基准值是随机选取的):

/*快速排序
 * l ----- 序列左边下标
 * t ----- 序列右边下标
 * a[] --- 数组
 */
int Partition(int a[], int l, int t){//划分
    int i = l;
    int j = t;
    int x = a[i];
    while(i < j){
        while(i < j && a[j] > x)
            j--;
        if(i < j)
            a[i++] = a[j];
        while(i < j && a[i] < x)
            i++;
        if(i < j)
            a[j--] = a[i];
    }
    a[i] = x;
    return i;
}
void Quick_Sort(int a[], int l, int t){
    if(l < t){
        int rm = l + rand()%(t - l + 1);//基准值是在随机选取的
        swap(a[l], a[rm]);
        int mid = Partition(a, l, t);
        Quick_Sort(a, l, mid-1);//递归调用
        Quick_Sort(a, mid+1, t);//递归调用
    }
}

非递归代码:

typedef pair<int, int> Pair;
int Partition(int a[], int lt, int rt){//划分
    int i = lt;
    int j = rt;
    int rm = i + rand()%(j - i + 1);//基准值是在随机选取的
    swap(a[i], a[rm]);
    int x = a[i];
    while(i < j){
        while(i < j && a[j] > x)
            j--;
        if(i < j)
            a[i++] = a[j];
        while(i < j && a[i] < x)
            i++;
        if(i < j)
            a[j--] = a[i];
    }
    a[i] = x;
    return i;
}
void Quick_Sort(int a[], int l, int t){
    stack<Pair>S;
    S.push(Pair(l, t));
    while(!S.empty()){
        int lt = S.top().first;
        int rt = S.top().second;
        S.pop();
        int mid = Partition(a, lt, rt);
        if(mid+1 < rt)
            S.push(Pair(mid+1, rt));
        if(lt < mid-1)
            S.push(Pair(lt, mid-1));
    }
}

时间复杂度:快速排序最坏时间复杂度是O(n^2),在(1)数组中的元素全部相同,(2)数组已经排好序或逆序(这要看选取的基准值,如果总是选取最左边的作为基准值则是O(n^2))这两类情况下快速排序会达到最坏的情况。从这里可以看出选取基准值很重要,如果基准值随机选取,快排很难达到最坏情况。平均时间复杂度是O(n*log2(n)),可以这样理解:快排是采用分治的策略,可以把这个过程看成一个二叉树,则遍历一层的时间复杂度是O(n),树的高度为log2(n+1),则复杂度为O(n*log2(n))。

空间复杂度:O(logn)~O(n),这里看的是递归的深度。

稳定性:快排是一种不稳定的算法,因为在选取基准值相互交换的时候,元素的相对位置会变化。

4. 归并排序

    归并排序算法是典型的分治算法,其算法思想是先将数组对半分开,然后将得到的两个数组再对半分开,一直到只有一个元素为止,然后再进行两两合并,最后合并为一个大的数组,具体思路可看下图。


代码实现:

void Merge(int a[], int lt, int rt, int p[]){//p是临时数组
    int mid = (rt - lt)/2 + lt;
    int i = lt, j = mid + 1;
    int k = 0;
    while(i <= mid && j <= rt){
        if(a[i] <= a[j]){
            p[k++] = a[i++];
        }else {
            p[k++] = a[j++];
        }
    }
    while(i <= mid){
        p[k++] = a[i++];
    }
    while(j <= rt){
        p[k++] = a[j++];
    }
    for(i = 0; i < k; ++i){
        a[lt+i] = p[i];
    }
}
void MergeSort(int a[], int lt, int rt, int p[]){
    if(lt < rt){
        int mid = (rt - lt)/2 + lt;
        MergeSort(a, lt, mid, p);
        MergeSort(a, mid+1, rt, p);
        Merge(a, lt, rt, p);
    }
}

时间复杂度:归并排序最好、最坏、平均时间复杂度都是O(nlogn),同样可以把归并排序的过程看成一个完全二叉树(这里要和快速排序区分),划分的时间复杂度为O(logn),每层合并的时间复杂度为O(n),所以总的时间复杂度为O(nlogn)。

最坏情况:复杂度和排序的数的顺序无关,佛系。

空间复杂度:O(n),用到一个n的辅助数组。

稳定性:归并排序是稳定的。

5. 堆排序

    堆排序(Heap_Sort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利数组特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。常用于求前1-k的所有数字。

 堆排序的代码如下:

//向下调整
void Adjust_Heap(int a[], int idx, int Len){
    while(idx*2+1 < Len){
        int temp = idx*2+1;
        if(temp+1 < Len && a[temp+1] > a[temp]){
            temp++;
        }
        if(a[idx] < a[temp]){
            swap(a[idx], a[temp]);
            idx = temp;
        }else break;
    }
}

void Heap_Sort(int a[], int n){
    //建立大顶堆
    for(int i = (n-1)/2; i >= 0; --i){
        Adjust_Heap(a, i, n);
    }
    //排序
    for(int i = 0;i < n; ++i){
        swap(a[0], a[n-i-1]);
        Adjust_Heap(a, 0, n-i-1);
    }

}

时间复杂度:建堆的复杂度是O(n)(这个需要计算一下),排序的复杂度是n * lgn 所以是O(nlog2(n))。

空间复杂度:O(1)

稳定性:堆排序是不稳定的。举个栗子:1 2 2 的情况就不满足(从小到大排序),因为排序后两个2的相对位置变了。

                 

Reference :

https://blog.csdn.net/jiajing_guo/article/details/69388331



猜你喜欢

转载自blog.csdn.net/u011074149/article/details/80401979
今日推荐