天空一声巨响,迪宝闪亮登场。哈哈哈数据结构刷题又来了。实践是检验真理的唯一标准,最近感觉到自己的代码功底越来越深了,看来多写代码确实很有帮助。这次又带来最新的三道题目,来跟我一起去看看把。(顺便提一下,现在我在PTA上浙大《数据结构》这个题库已经排名1/2883了哦!!!)

排序1 排序

这道题主要是用来巩固各种排序算法的代码基础,基本的排序算法有如下几种:

  • 冒泡排序
  • 插入排序
  • 堆排序/选择排序
  • 希尔排序
  • 归并排序
  • 拓扑排序

下面我想用各种排序算法完成此题,然后对比下不同排序算法的:1)代码量、2)时间和空间复杂度以及3)各种排序算法在不同数据集上的适用性

冒泡排序

冒泡排序代码如下:

  1. #include<iostream>
  2. using namespace std;
  3. int N,tempN;
  4. long int *H;
  5. int flag = 1;
  6. void Swap(long int &a, long int &b);
  7. int main(){
  8. cin >> N;
  9. H = new long int[N];
  10. for(int i = 0;i < N;i++)
  11. cin >> H[i];
  12. while(flag == 1){
  13. flag = 0;
  14. for(int i = 0;i < N - 1;i++){
  15. for(int j = i;j < N - 1;j++){
  16. if(H[j] > H[j+1]){
  17. Swap(H[j],H[j+1]);
  18. if(flag == 0)
  19. flag = 1;
  20. }
  21. }
  22. }
  23. }
  24. cout<<H[0];
  25. for(int i = 1;i < N;i++)
  26. cout<<' '<<H[i];
  27. return 0;
  28. }
  29. void Swap(long int &a, long int &b){
  30. long int temp1;
  31. temp1 = a;
  32. a = b;
  33. b = temp1;
  34. }

选择排序

选择排序代码如下:

  1. #include<iostream>
  2. using namespace std;
  3. int N;
  4. long int *H;
  5. long int temp;
  6. int main(){
  7. cin >> N;
  8. H = new long int[N];
  9. for(int i = 0;i < N;i++){
  10. cin >> temp;
  11. if(i == 0){
  12. H[i] = temp;
  13. continue;
  14. }
  15. for(int j = 0;j < i;j++){
  16. if(temp < H[j]){
  17. for(int k = i;k > j ;k--)
  18. H[k] = H[k-1];
  19. H[j] = temp;
  20. break;
  21. }
  22. else if(j == i-1)
  23. H[i] = temp;
  24. }
  25. }
  26. cout<<H[0];
  27. for(int i = 1;i < N;i++)
  28. cout<<' '<<H[i];
  29. return 0;
  30. }

堆排序/选择排序

堆排序代码如下:

#include<iostream>

using namespace std;
int N,tempN;
long int *H;

void Swap(long int &a, long int &b);
void CreateHeap(int index);
void DeleteHeap(int index);

int main(){
    cin >> N;
    tempN = N;
    H =  new long int[N+1];
    for(int i = 1;i <= N;i++){
        cin >> H[i];
        CreateHeap(i);
    }
    tempN = N;
    int count = 0;
    while(tempN != 0){
        if(count == 0)
            count++;
        else
            cout<<' ';
        cout<<H[1];
        DeleteHeap(1);
    }
    return 0;
}

void Swap(long int &a, long int &b){
    long int temp1;
    temp1 = a;
    a = b;
    b = temp1;
}

void CreateHeap(int index){
    //建堆,插入新元素后调整成堆
    int temp = index;
    for(;temp != 1;temp = temp/2){
        if(H[temp] < H[temp/2])
            Swap(H[temp],H[temp/2]);
        else
            break;
    }
}

//堆的删除,用来实现堆排序
void DeleteHeap(int index){
    int Ms = tempN;
    if(Ms == 0)
        return;
    else if(Ms == 1){
        tempN--;
        return;
    }
    if(index == 1){
        H[1] = H[Ms];
        tempN--;
    }
    //从两个子节点中找出比现在这个元素小的,交换位置并迭代函数
    //下面考虑了两种节点的多种情况(可能为空)
    if(2*index <= tempN && 2*index+1 <= tempN){
        if(H[2*index] <= H[2*index+1] && H[2*index] < H[index]){
            Swap(H[index],H[2*index]);
            DeleteHeap(2*index);
        }
        else if(H[2*index] >= H[2*index+1] && H[2*index+1] < H[index]){
            Swap(H[index],H[2*index+1]);
            DeleteHeap(2*index+1);
        }
        else
            return;
    }
    else if(2*index <= tempN && 2*index+1 > tempN){
        if(H[2*index] < H[index]){
            Swap(H[index],H[2*index]);
            DeleteHeap(2*index);
        }
        else
            return;
    }
    else
        return;
}

希尔排序

希尔排序代码如下所示:

#include<iostream>
#include<cmath>

using namespace std;
int N,D;
long int *H;
long int temp;

int main(){
    cin >> N;
    H = new long int[N];
    int count = 1;
    while((int)pow(2,count) - 1 < N)
        count++;
    count--;
    for(int i = 0;i < N;i++)
        cin >> H[i];
    for(int i = count;i > 0;i--){
        D = (int)pow(2,i) - 1;
        for(int j = 0;j < D;j++){
            for(int k = j;k < N;k += D){
                if(k == j)
                    continue;
                temp = H[k];
                for(int p = j;p < k;p += D){
                    if(temp < H[p]){
                        for(int s = k;s > p;s -= D)
                            H[s] = H[s-D];
                        H[p] = temp;
                        break;
                    }
                }
            }
        }
    }
    cout<<H[0];
    for(int i = 1;i < N;i++)
        cout<<' '<<H[i];
    return 0;
}

归并排序

归并排序代码如下所示:

#include<iostream>

using namespace std;
int N;
int length = 1;
long int *H,*tempH;
long int temp;

void Mergesort(long int *a,long int*b);
void Merge(int start,int end,long int *a,long int*b);

int main(){
    cin >> N;
    H = new long int[N];
    tempH = new long int[N];
    for(int i = 0;i < N;i++)
        cin >> H[i];
    while(length <= N){
        Mergesort(H,tempH);
        length *= 2;
        Mergesort(tempH,H);
        length *= 2;
    }
    cout<<H[0];
    for(int i = 1;i < N;i++)
        cout<<' '<<H[i];
    return 0;
}

void Mergesort(long int *a,long int*b){
    int i = 0;
    for(;i < N - 2 * length;i += 2 * length)
        Merge(i,i+2*length-1,a,b);
    Merge(i,N-1,a,b);
    //cout<<b[0];
    //for(int i = 1;i < N;i++)
    //    cout<<' '<<b[i];
    //cout<<endl;
}

void Merge(int start,int end,long int *a,long int*b){
    if(end - start + 1 <= length){
        for(int i = start;i <= end;i++)
            b[i] = a[i];
        return;
    }
    int leftend = start + length -1;
    int l = start;
    int r = start + length;
    while(l <= leftend && r <= end){
        if(a[l] < a[r])
            b[start++] = a[l++];
        else
            b[start++] = a[r++];
    }
    while(l <= leftend)
        b[start++] = a[l++];
    while(r <= end)
        b[start++] = a[r++];
}

不同排序算法的对比

仅仅会写代码怎么行!真正的程序员还要懂得分析,那下面就让我们来分析分析。

代码量

下面是五种排序方法各自的代码量:

代码量(行数)
冒泡排序 38
选择排序 32
堆排序 87
希尔排序 41
归并排序 60

不难看出,越复杂的方法需要的代码量也越多,但是于此同时时间复杂度也越好。但是在这里我们有个极大的例外——希尔排序,已经有研究表明,希尔排序大多数情况下是这五种排序方法最好的之一,基本和堆排序不相上下,尤其是使用Sedgewick增量序列的希尔排序,效果出奇的好。

时间复杂度

时间复杂度这里就直接甩结论把:

最坏时间复杂度
冒泡排序 CS专题——浙大数据结构刷题(六) - 图1
选择排序 CS专题——浙大数据结构刷题(六) - 图2
堆排序 CS专题——浙大数据结构刷题(六) - 图3
希尔排序 CS专题——浙大数据结构刷题(六) - 图4
归并排序 CS专题——浙大数据结构刷题(六) - 图5

空间复杂度

空间复杂度的分析较为复杂,因为每种排序方法自己可能也有多种实现方式,例如说归并排序就有递归和非递归两种方法,多种方法的空间复杂度各不相同,所以很难进行分析。这里就把每种排序算法的运行图放一下把:
冒泡排序:
image.png
选择排序:
image.png
堆排序:
image.png
希尔排序:
image.png
归并排序:
image.png

适用场景

先来说说冒泡排序和选择排序,这两个排序算法确实比较低效,但是他们的好处是没有用到很多额外的存储空间;再来看看堆排序,堆排序是个非常好用的算法,而且有堆这种数据结构作为基础,堆排序的不好之一是需要比较多的额外空间(尤其没有处理好的时候);希尔排序是个不错的方法,效率仅次于堆排序,而且不需要多余的额外存储空间,代码量也比较少,代码较好理解;而归并排序的话常常用于外排序,内排序不经常用到。

Insert or Merge

这道题主要是需要大家发现插入排序和归并排序的特点,我推荐:因为是做二元判决,所以可以判断是不是插入排序,这个比较容易判断,如果不是插入的话就是归并,然后用上一题写的排序代码再进行一轮就行了。我大概是55分钟AC了,最后一些边界测试需要小心。
此题代码如下:

#include<iostream>

using namespace std;
int N;
int *a,*b;
int length;

int checkd();
void Mergesort(int *a,int*b);
void Merge(int start,int end,int *a,int*b);

int main(){
    cin >> N;
    a = new int[N];
    b = new int[N];
    int pos1,pos2,temp;
    //插入排序中前面是有序的,后面是相同的
    pos1 = pos2 = -1;
    for(int i = 0;i < N;i++)
        cin >> a[i];
    for(int i = 0;i < N;i++){
        cin >> b[i];
        if(i != 0 && b[i] < b[i-1] && pos1 == -1)
            pos1 = i - 1;
        if(b[i] == a[i] && pos2 == -1 && pos1 != -1)
            pos2 = i;
        if(pos2 != -1 && b[i] != a[i])
            pos2 = -1;
    }
    //cout<<pos1<<" "<<pos2<<endl;
    if(pos1 + 1 == pos2){
        cout<<"Insertion Sort"<<endl;
        //再进行一轮插入排序
        temp = b[pos2];
        for(int i = 0;i < pos2;i++){
            if(temp < b[i]){
                for(int j = pos2;j > i;j--)
                    b[j] = b[j-1];
                b[i] = temp;
                break;
            }
        }
        cout<<b[0];
        for(int i = 1;i < N;i++)
            cout<<' '<<b[i];
    }
    else{
        cout<<"Merge Sort"<<endl;
        //先判断进行到第几轮
        int D = checkd();
        int *tempb;
        tempb = new int[N];
        length = D;
        //cout<<length<<endl;
        //然后再进行一轮归并排序
        Mergesort(b,tempb);
        cout<<tempb[0];
        for(int i = 1;i < N;i++)
            cout<<' '<<tempb[i];
    }
    return 0;
}

//判断归并排序的当前增量D
int checkd(){
    int gap = -1;
    int tempgap = 0;
    int i = 0;
    for(;i < N;i++){
        if(tempgap == 0){
            tempgap++;
            //cout<<gap<<endl;
            continue;
        }
        if(b[i] >= b[i-1])
            tempgap++;
        else{
            if(gap == -1)
                gap = tempgap;
            else if(tempgap < gap)
                gap = tempgap;
            tempgap = 1;
        }
        //cout<<gap<<endl;
    }
    return gap;
}

//归并排序
void Mergesort(int *a,int*b){
    int i = 0;
    for(;i < N - 2 * length;i += 2 * length)
        Merge(i,i+2*length-1,a,b);
    Merge(i,N-1,a,b);
    //cout<<b[0];
    //for(int i = 1;i < N;i++)
    //    cout<<' '<<b[i];
    //cout<<endl;
}

//两个有序子列的归并
void Merge(int start,int end,int *input1,int*input2){
    if(end - start + 1 <= length){
        for(int i = start;i <= end;i++)
            input2[i] = input1[i];
        return;
    }
    int leftend = start + length -1;
    int l = start;
    int r = start + length;
    while(l <= leftend && r <= end){
        if(input1[l] < input1[r])
            input2[start++] = input1[l++];
        else
            input2[start++] = input1[r++];
    }
    while(l <= leftend)
        input2[start++] = input1[l++];
    while(r <= end)
        input2[start++] = input1[r++];
}

Insertion or Heap Sort

这道题基本是上一题的翻版,而且更加简单,判断出是堆排序后直接再进行一次删除最大堆的堆顶元素+调整堆即可,23分钟左右一次就AC。
此题代码如下:

#include<iostream>

using namespace std;
int N,num,tempN;
int *a,*b,*H;

int checknum();
void Swap(int &a, int &b);
void CreateHeap(int index);
void DeleteHeap(int index);

int main(){
    cin >> N;
    tempN = N;
    a = new int[N];
    b = new int[N];
    H = new int[N+1];
    int pos1,pos2,temp;
    pos1 = pos2 = -1;
    for(int i = 0;i < N;i++)
        cin >> a[i];
    for(int i = 0;i < N;i++){
        cin >> b[i];
        if(i != 0 && b[i] < b[i-1] && pos1 == -1)
            pos1 = i - 1;
        if(b[i] == a[i] && pos2 == -1 && pos1 != -1)
            pos2 = i;
        if(pos2 != -1 && b[i] != a[i])
            pos2 = -1;
    }
    //cout<<pos1<<" "<<pos2<<endl;
    if(pos1 + 1 == pos2){
        cout<<"Insertion Sort"<<endl;
        temp = b[pos2];
        for(int i = 0;i < pos2;i++){
            if(temp < b[i]){
                for(int j = pos2;j > i;j--)
                    b[j] = b[j-1];
                b[i] = temp;
                break;
            }
        }
        cout<<b[0];
        for(int i = 1;i < N;i++)
            cout<<' '<<b[i];
    }
    else{
        cout<<"Heap Sort"<<endl;
        num = checknum();
        tempN = N - num;
        for(int i = 1;i <= tempN;i++)
            H[i] = b[i-1];
        DeleteHeap(1);
        cout<<H[1];
        for(int i = 2;i <= tempN;i++)
            cout<<' '<<H[i];
        cout<<' '<<b[0];
        for(int i = N - num;i < N;i++)
            cout<<' '<<b[i];
    }
    return 0;
}

int checknum(){
    int i = 1;
    for(;i < N;i++){
        if(b[i] > b[0])
            break;
    }
    return N - i;
}

void Swap(int &a, int &b){
    int temp1;
    temp1 = a;
    a = b;
    b = temp1;
}

void CreateHeap(int index){
    //建堆,插入新元素后调整成堆
    int temp = index;
    for(;temp != 1;temp = temp/2){
        if(H[temp] > H[temp/2])
            Swap(H[temp],H[temp/2]);
        else
            break;
    }
}

//堆的删除,用来实现堆排序
void DeleteHeap(int index){
    int Ms = tempN;
    if(Ms == 0)
        return;
    else if(Ms == 1){
        tempN--;
        return;
    }
    if(index == 1){
        H[1] = H[Ms];
        tempN--;
    }
    //从两个子节点中找出比现在这个元素小的,交换位置并迭代函数
    //下面考虑了两种节点的多种情况(可能为空)
    if(2*index <= tempN && 2*index+1 <= tempN){
        if(H[2*index] >= H[2*index+1] && H[2*index] > H[index]){
            Swap(H[index],H[2*index]);
            DeleteHeap(2*index);
        }
        else if(H[2*index] <= H[2*index+1] && H[2*index+1] > H[index]){
            Swap(H[index],H[2*index+1]);
            DeleteHeap(2*index+1);
        }
        else
            return;
    }
    else if(2*index <= tempN && 2*index+1 > tempN){
        if(H[2*index] > H[index]){
            Swap(H[index],H[2*index]);
            DeleteHeap(2*index);
        }
        else
            return;
    }
    else
        return;
}

总结

这次的刷题记录是排序专场,我们学习到了五种排序方法,五种方法各有千秋,强烈建议五种排序方法大家都手写一次,这样子以后用到的时候直接CV就行(这也是为自己的代码库增加新代码的方式嘛!)。通过这次刷题,我又获得了新的体悟:1)你现在写的代码有可能可以为将来节省不少时间,之后再遇到直接CV即可;2)在实现算法之前多想想,不要只是急于实现,刷题到这个地步时也要想想如何高效实现,包括时间空间复杂度和代码量等等
欢迎对ECE/CS/AI感兴趣的小伙伴关注我,如果你对我的内容有什么建议的话,或者你也对算法和数据结构感兴趣的话,可以单独找我讨论,也欢迎在评论区留下你的声音。