排序大全(思路+代码+分析)

文章内容繁多,且混有代码,记得配合目录观看,达到最佳学习体验!

前言

1.分析标准:性质+时间复杂度

性质分类:
就地排序/非就地排序
就地:只用到存储数据的空间,没有额外开辟空间

内部排序/外部排序
内部:待排序的数据能一次性放到内存中(就是内存16g够不够用)
(不能的话需要和外存交互,外部的这里只有归并)

稳定排序/不稳定排序
稳定:大小相同的数据,排序前后相对位置不变

2.统一的标准

数组存储 统一由下标1开始存储

数组排序 统一为排成从小到大

语言实现是C++

个别排序因为很少见,没有代码,只提供了思路,当然,绝大多数是有代码哒。

//基于交换的排序

//1.冒泡排序:

不断比较相邻两个元素的,如果这两个元素是乱序的,

//则交换位置,从而实现把(这一趟)(乱序区中)最大的数据放在最后面

//重复上面的过程(等于是从后向前变得有序,有序区在后方,随趟数增加而增加)

性质:就地的 稳定的 内部的

时间复杂度:O(n^2)

#include<iostream>

using namespace std;

int main()

{

    int n; cin >> n;

    int data[105];

    for (int i = 1; i <= n; i++)

    {

        cin >> data[i];

    }

    for (int i = 1; i <= n - 1; i++)//遍历趟数

    {

        int flag = 0;//优化

        for (int j = 1; j <= n - i; j++)

        {

             if (data[j] > data[j + 1])

             {

                 swap(data[j], data[j + 1]);

                 flag = 1;//如果一趟没有发生交换,说明已经排好了

             }

        }

        if (flag == 0)

        {//排好了就直接输出

             break;

        }

        for (int i = 1; i <= n; i++)

        {

             cout << data[i] << " ";

        }

        cout << endl;

    }

   

    return 0;

}

/*

8

3 1 8 4 2 6 1 7

*/

//2.快速排序:

先选一个基准数x,将比它小的数据放在它的前面,将比它大的数据放在它的后面,

//排好一趟后,x把序列分为两部分,这两部分内部可能还是乱序的,所以分别对这两部分进行快速排序......

// (递归的思路)

//基准数选择:(1)排序区间第一个(2)排序区间最后一个(3)中间位置(这里代码选第一个啦)

//代码思路:皮课51:00

//性质:就地的 内部的 不稳定的

时间复杂度:每趟划分等于是遍历一遍整个数列,一共排logn趟,所以时间复杂度为O(nlogn)

#include<iostream>

using namespace std;

void quick_sort(int* data, int l, int r)

{

    if (l < r)

    {

        int i, j, x;//两个指向,一个基准数

        i = l; j = r;

        x = data[l];

        while (i < j)//记住时刻要判断i<j

        {

             while (i<j && data[j]>x)j--;

             if (i < j)

             {

                 data[i] = data[j];

                 i++;

             }

             while (i < j && data[i] < x)i++;

             if (i < j)

             {

                 data[j] = data[i];

                 j--;

             }

        }//这个大循环进行完表示一次左右划分好了

        data[i] = x;//此时i==j

        quick_sort(data, l, i - 1);//左边再排一次

        quick_sort(data, i + 1, r);//右边再排一次

    }

    return;//当传参的i==j时,说明已经排好了,无法再分着排了

}

int main()

{

    int n; cin >> n;

    int data[105];

    for (int i = 1; i <= n; i++)

    {

        cin >> data[i];

    }

    quick_sort(data,1,n);//数列的第一个和最后一个

    for (int i = 1; i <= n; i++)

    {

        cout << data[i] << " ";

    }

    return 0;

}

//基于选择的排序

//1.选择排序:

每次从待排序区中选择一个最小的数,放在待排序区中的第一个位置,

//重复以上过程,待排序区递减,有序区递增,直到排序完成。

//性质:就地的 不稳定的 内部的

时间复杂度:O(n^2)

#include<iostream>

using namespace std;

int main()//这个代码和冒泡一样,可以用flag进行优化

{

    int n; cin >> n;

    int data[105];

    for (int i = 1; i <= n; i++)

    {

        cin >> data[i];

    }

    for (int i = 1; i <= n - 1; i++)

    {

        int minn = i;//记录一趟中最小元素的下标

        for (int j = i + 1; j <= n; j++)

        {

             if (data[minn] > data[j])

                 minn = j;

        }

        //将每次最小的元素放在每趟无序区的一个的位置 i 处

        swap(data[minn], data[i]);

    }

    for (int i = 1; i <= n; i++)

    {

        cout << data[i] << " ";

    }

    return 0;

}

//2.堆排序

// 基础概念:

//堆(Heap)是类基于完全叉树的特殊数据结构。通常将堆分为两种类型:

//1顶堆(Max Heap):在顶堆中,根结点的值必须于他的孩结点的值,对于叉树

//中所有树都满此规律

//2顶堆(Min Heap):在顶堆中,根结点的值必须于他的孩结点的值,对于叉树

//中所有树都满此规律

//思路:

//1.建小顶堆(两种方式)

// (1)自我初始化:先将原来的存到数组中,再从下到上,

// 先找只有一个或两个孩子结点的根结点(先找最小的有孩子结点的子树),

// 确定该子树的父亲结点比它的孩子节点小后,再向上判断调整更大子树的父亲节点

// 注意如果发生交换,还要判断交换后的下面的子树是否还符合上小下大的性质,判断到无孩子的结点为止

// 简述:自下而上判断,发生交换还要自上而下检查。

// (2)插入建堆:整一个空数组,从上到下插入数据,

// 插入同时,调整,使每次插入的数据小于其父亲节点的数据,如果大,就将其和父亲交换,

//每次交换后,从下往上查找,判断交换后会不会影响上面数据的平衡,一直到查找到根节点为止

//2.堆排序

//循环n次,每次输出最小的数

//每次输出根节点,根节点位置补上堆结尾的数据,再次堆排序

//以上过程循环n次

//性质:就地的 内部的 不稳定的

时间复杂度:O(nlogn)//建堆和排序时间复杂度都是这个

#include<iostream>

#include<cstdlib>

using namespace std;

void downheap(int* data, int i, int n)

{

    int fa = i; int child;

    while (fa * 2 <= n)

    {

        child = fa * 2;

        if (child + 1 <= n && data[child + 1] < data[child])

        {//找左右节点中最小的一个

             child = child + 1;

        }

        if (data[child] < data[fa])

        {//比较父亲节点和较小孩子节点的大小,通过选择,交换确定父亲为较小的那一个

             swap(data[child], data[fa]);

             fa = child;//这一步是让child作为根节点,

             //再次循环,判断下面的子树是否因为上面结点的交换

             // 而不满足小顶堆的条件

        }

        else

        {

             break;//要是fa结点才是三个节点中最小的

        }//就没必要交换了,已经满足条件了

    }

}

void insertheap(int* data, int i)

{

    int child = i; int fa;//这里让child=i;

    while (child > 1)

    {

        fa = child / 2;//思路和初始化的很像

        if (data[fa] > data[child])

        {

             swap(data[fa], data[child]);

             child = fa;

        }

        else

        {

             break;

        }

    }

}

int main()

{

    int n; cin >> n;

    int data[1005];

    for (int i = 1; i <= n; i++)

    {

        cin >> data[i];

        insertheap(data, i);

    }

    //根据完全二叉树的性质,n/2前的结点都是有孩子结点的根节点

    //画一个完全二叉树试试就知道了

    //for (int i = n / 2; i >= 1; i--)

    //{//自我初始化小顶堆

    //  downheap(data, i, n);

    //}

    cout << "遍历小顶堆" << endl;

     for (int i = 1; i <= n; i++)

    cout << data[i] << " ";

    //堆排序

     cout << "\n" << "堆排序:" << endl;

    int size = n;//实际长度

    for (int i = 1; i <= n; i++)

    {

        cout << data[1] << " ";

        data[1] = data[size]; //将头节点赋值为最后一个结点

        size--; //删去最后一个结点

        downheap(data, 1, size); //调整,建成小顶堆

    }

    return 0;

}

/*

实验数据:

8

53 17 78 9 45 65 87 32

小顶堆遍历输出:

9 17 65 32 45 78 87 53

堆排序输出:

9 17 32 45 53 65 78 87

*/

//基于插入的排序

(代码默认从1下标开始存数据)

//1.直接插入排序:插入时,遍历查找插入的位置,再插入(顺序表插入)

//-----析插入排序

//-----二路插入排序

//2.希尔排序

//1.直接插入排序

(从小到大排序)

// 逻辑:将数组分为有序的和无序的两部分,从前到后,一开始有序区只有一个元素,

// 每次将无序区的一个元素移入有序区,和有序区已存在的元素比较,如果有比它小的元素,

// 记录下标ind,和顺序表插入的思路一样,将其插入到ind位置

// 重复上两行的程序(n -1)次

//时间复杂度:O(n^2)

//性质:就地的 稳定的 内部的

#include<iostream>

using namespace std;

int main()

{

    int data[105];

    int n; cin >> n;

    for (int i = 1; i <= n; i++)

    {//输入数据

        cin >> data[i];

    }

    int d, ind;//临时存数据  存确定位置下标

    for (int i = 2; i <= n; i++)//第一次有序区只有一个数据,不用进行

    {

        d = data[i];

        ind = i;

        for (int j = 1; j < i; j++)//找出需要插入的下标ind

        {

             if (data[j] > d)

             {

                 ind = j;

                 break; //记得加 前面可能有多个比d大的,取靠前的

             }

        }

             for (int j = i; j > ind; j--)//将ind后的元素整体后移

             {

                 data[j] = data[j - 1];

             }

             data[ind] = d;

    }

    for (int i = 1; i <= n; i++)

        {

             cout << data[i] << " ";

        }

    return 0;

}

/*

测试 数据:

6

3 1 7 5 2 4

*/

//改良后的直接插入排序:

(插入排序基本都用这个)

//不再先确定下标,再移动插入,而是从后往前和有序去对比

//前者大的话将前面的这个元素后移,不大的话就取消比较,确定位置下标,直接赋值

//上述两行运行n-2遍  (等于是把查找确定下标和插入两步合成了一步)

//时间复杂度:O(n^2)

//性质:就地的 稳定的 内部的

#include<iostream>

using namespace std;

int main()

{

    int data[105];

    int n; cin >> n;

    for (int i = 1; i <= n; i++)

    {//输入数据

        cin >> data[i];

    }

    int d;//临时存数据 

    for (int i = 2; i <= n; i++)

    {

        d = data[i];

        int j;//存确定位置下标

        for (j = i; j >= 2; j--)

        {

             if (data[j - 1] > d)

                 data[j] = data[j - 1];

             else

                 break;

        }

        data[j] = d;

    }

/*for (int i = 2; i <= n; i++)

{//思路相同,代码第二种写法(注:dàt)

    int j, t = data[i];

    for ( j = i - 1; j >= 1; j--)

    {

        if (data[j] > t)

        {

             data[j + 1] = data[j];

        }

        else

             break;

    }

    data[j+1] = t;

}*/

    for (int i = 1; i <= n; i++)

        {

             cout << data[i] << " ";

        }

    return 0;

}

/*

测试 数据:

6

3 1 7 5 2 4

*/

// ---- - 析半插入排序:

将直接插入排序中查找确定插入位置ind的遍历查找部分,

// 优化为二分查找,for循环两个O(n)的循环,第一个O(n)变为了O(log2n),

// 但由于下面插入部分的循环仍为O(n),等于说是优化了个寂寞,总时间复杂度仍为O(n^2),

// 所以这里代码不展示。

//-----二路插入排序:

创建一个环形数组,先将从下标1部分·存进数组中,

//这里等于是将数组分为前后两部分,比下标1小的存它右边,比它小的存它左边,

//分为两部分多次进行如顺序表插入的操作,这样是将复杂度降低为原来的1/2,

//时间复杂度依旧是O(n^2),也是优化了个寂寞,这里代码就不展示了。

//2.希尔排序(shell)

(缩小增量排序):

//在直接插入排序基础上,对待排序数据进行分组,对每组进行排序,

//不断缩小组数并排序,最终缩小为1组

//每次分为(n/(2^m))组,m为分组次数 eg: n/2  n/4  n/8...(一种增量选择,这是希尔增量序列吗,还有别的选择,如hibbard增量序列)

//性质:就地的 不稳定的 内部的

时间复杂度:跟增量选择方式有关,希尔最差为O(n^2)

#include<iostream>

using namespace std;

int main()

{

    int n; cin >> n;

    int data[105];

    for (int i = 1; i <= n; i++)

    {

        cin >> data[i];

    }

    int k = 0;//计算趟数

    int t;

    for (int d = n / 2; d >= 1; d /= 2)//遍历增量/组数

    {

        k++;//标记趟数

        //以d为增量直接进行插入排序(代码把各组的插入排序放在了一起,更简便)

//这个就是变形的插入排序的代码

        for (int i = d + 1; i <= n; i++)

        {//这个插入在用的是上面改良部分第二种代码表示

             t = data[i];

             int j;

             for (j = i-d; j >= 1; j-=d)

             {

                 if (data[j] > t)

                 {

                     data[j+d] = data[j];

                 }

                 else

                 {

                     break;

                 }

             }

             data[j+d] = t;

        }

        cout << "第" << k << "趟 " << "增量为" << d <<    <<endl;

    for (int i = 1; i <= n; i++)

    {

        cout << data[i] << " ";

    }

cout<<endl;

    }

   

    return 0;

}

//基于归并的排序

//这里是2-路归并(简称归并排序)

//思路:先将数组不断二分,一直到变为n个单个元素,

// 再将其按照二分的路径逆着合并过来,合并的同时,遍历两组元素,

//将其按照从大到小的顺序排好,由此直到合并完成为止。

//代码思路:1.递归,输入左右下标 l , f ,不断二分,直到 l == f;

//2.创建一个临时数组t,将两组元素遍历按从小到大顺序存入t中,再将t的值赋给data数组

//性质:非就地的 稳定的 外部的

时间复杂度:O(nlogn)

#include<iostream>

#include<cstdlib>

using namespace std;

void merge(int* data, int l, int mid, int r)

{

    int i = l;

    int j = mid + 1;

    int t[105], k = 0;//临时存储数据 控制该数组存入的下标

    //将两组元素遍历按从小到大顺序存入t中

    while (i <= mid && j <= r)

    {//搞完之后,i , j有一个会越界,证明它所在的那半部分遍历完成

        if (data[i] <= data[j])//注意,t数组是从下标0开始存的

        {

             t[k] = data[i];

             k++;

             i++;

        }

        else

        {

             t[k] = data[j];

             k++;

             j++;

        }

    }

    while (i <= mid)

    {//两个while循环,将另一半没遍历保存完的遍历完

        t[k] = data[i];

        k++;

        i++;

    }

    while (j <= r)

    {

        t[k] = data[j];

        k++;

        j++;

    }

    for (int i = 0; i < k; i++)//将t中排好的数据赋给data

    {

        data[l + i] = t[i];

    }

}

void merge_sort(int* data, int l, int r)

{//递归的思想

    if (l < r)

    {

        int mid = (l + r) / 2;

        //左半部分继续分

        merge_sort(data, 1, mid);

        //右半部分继续分

        merge_sort(data, mid + 1, r);

        //分完之后就和起来

        merge(data, l, mid, r);

    }

}

int main()

{

    int n; cin >> n;

    int data[105];

    for (int i = 1; i <= n; i++)

    {

        cin >> data[i];

    }

    merge_sort(data, 1, n);

    for (int i = 1; i <= n; i++)

        cout << data[i] << " ";

    return 0;

}

基于统计的排序

1.计数排序:

 思路:统计数组中最大值到最小值之间所有数,每个数出现的次数,后直接按次数输出即可

eg : 原数组a[105],声明cut[...],注意全部初始化为0; ,将其中按照min -> max依次递增赋值(从下标min -> max赋值),

遍历执行cut[a[i]]++; 的程序,统计好个数,按顺序最后输出相应个数cut[a[i]]即可。

 优点:时间复杂度O(n);

 缺点:(1)空间换时间,(max-min)差太多时,浪费空间

              (2)正常无法对负数和小数排序(负数要用偏移量改造,小数乘成整数)(记得改完最后还要改回来)

 性质:内部的 非就地 稳定?->不一定(想要稳定加一个数组记录cut数组前缀和-->皮课1:00:00)

2.桶排序:

思路:将所有数'/10',根据结果将数组中元素分成若干组,再分别对每组进行以前学过的别的排序,后合并

分成若干组,每组有一个数组存储,这样一个数组可以看作一个桶。

时间复杂度:将数据分组->O(n)+后面桶内排序的时间复杂度

优缺点:比起后采取的排序,时间复杂度更低;比起计数排序,空间关键复杂度更低,算是一种居中和妥协。

3.基数排序:

将数组中所有的数据的位数补成和其中最高位的数一致,eg:    1->001   12->012   100->100

按照个位->十位->...的顺序进行计数排序,先对十位上的数字进行计数排序,依次到最高位

高位排序要使用低位排序的结果,eg: 021 025 十位排序一致,细分先后时需要根据个位排序021<025的结果将025排到021前面

 直到排完最高位,得到结果。(思路讲解:皮课1:18:00)

性质:非就地 内部 稳定?->看计数排序怎么写

 时间复杂度:O(d*(n+b))  d->最高位数  n->数组长度  b->进制数(eg : 十进制 b=10)

  • 24
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值