七大排序(二)

之前讲的快速排序都是递归版本,递归是有缺陷的:如果递归的深度太深,会出现爆栈的问题。
这是就需要递归改非递归了,递归改非递归需要用到栈,数据结构中的栈。
二叉树的递归转非递归是高阶数据结构的内容了。
原来的递归是通过函数栈帧的方式存数据的,具体包括:参数,返回值,局部变量等等。
核心是数据排序的那个局部变量区间。所以要转为非递归就要存储局部变量的区间。这里用栈来存储控制区间的变量。
C语言阶段栈还是要自己写的,已经写过了就找到写过的栈,将stack.c和stack.h复制,在vs2019编译器的资源管理器中选中头文件,右击->添加->现有项->双击->粘贴->选中stack.c和stack.h->添加,然后将stack.c拖动到源文件中。然后想用栈只需要包含一下头文件就可以了。
下面直接实现,在实现中看是如何非递归实现的。
void QuickSort3(int* a, int begin, int end)
{
    stack st;
    StackInit(&st);
    StackPush(&st, begin);
    StackPush(&st, end); //这里在栈中存储了首元素下标,尾元素下标,相当于把要进行单趟排序的区间储存起来。
//单趟排序函数: int PartSort( int * a , int left , int right ),利用栈储存要排序的区间,在合适的时候拿出来作单趟排序的参数,就是非递归实现排序的核心。
    while(!StackEmpty(&st))
    {
        int right = StackTop(&st); //栈结构后进先出,后进的是尾,所以先保存给right
        StackPop(&st);
        int left = StackTop(&st);
        StackPop(&st);
        int keyi = PartSort3(a, left, right); //单趟排序后keyi位置的值已经不需要改了,所以要排序的区间变成了[left, keyi-1],[keyi+1, right],在left >= keyi-1,right <= keyi+1的情况下区间不需要入栈。
        if(left < keyi-1)
        {
            StackPush(&st, left); //入栈顺序不能改变
            StackPush(&st, keyi-1);
        }
        if(keyi+1 < right)
        {
            StackPush(&st, keyi+1); //入栈顺序不能改变
            StackPush(&st, right);
        }
    }
    StackDestroy(&st);
}
下图是栈空间数据的变化。
对数据的非递归排序有二叉树遍历的思想(根,右子树,左子树),如果想要改为根,左子树,右子树只需要把上下两个if互换就可以了。数据结构的栈即使要排序的数据再多也不会爆的,原因在于内存空间区域划分,栈空间和堆空间的大小是不一样的,32位Linux下(总共4GB),栈空间的大小是8MB,堆空间的大小是2GB左右。非递归排序单趟排序的函数栈帧一次循环只开辟一个,而且循环结束函数栈帧都会销毁。
并不是能不用递归就不用递归,如果代码很容易写递归和非递归,那么优先选非递归;但是像二叉树(遍历),快排,写递归很简单,就优先选择递归。只是有爆栈风险的情况下,会把递归改非递归。
如果用队列实现非递归,就有点像层序遍历了,开始入0,9,单趟排序第一层,然后入0,2,4,9,出0,2,单趟排序第二层第一部分,出4,9,单趟排序第二层第二部分。但是结构较栈就比较复杂了。
下面讲解归并排序
在前面的顺序表,链表章节已经接触到归并了,将两个有序数组合并:单链表修改方向即可,数组这里就要额外开辟空间了,在原来的空间上容易造成数据覆盖。
基本思想:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and  Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤(如图所示):将一组数据分解成最小的有序段,即只有1个,然后归并。(归并的前提是数据有序)分割的部分由递归来完成,分割完后进行归并。
代码如下:
void _MergeSort(int* a, int begin, int end, int* tmp)
{
    if(begin >= end)
        return ;
    int mid = (begin + end)/2;
    _MergeSort(a, begin, mid, tmp);
    _MergeSort(a, mid+1, end, tmp); //不能使用[begin, mid -1],[mid, end]进行分解,可能会出现死循环,如mid为2,end为3,mid = (2+3)/2 = 2,mid = 2,end = 3
    //归并部分(如果归并部分用printf("归并[%d][%d] [%d][%d]",begin, mid, mid+1, end);来分析归并过程的话,结果如下图)
    
}
void MergeSort(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if(tmp == NULL)
        return;
    _MergeSort(a, 0, n-1, tmp); //子函数一般在前面加一个'_'
    free(tmp);
}
过程稍稍复杂,画一下递归展开图:
归并部分:由上图已知,开始是[0,0],[1,1]的归并。将归并后的数组放到tmp中,归并结束后,在放回数组a中。
void _MergeSort(int* a, int begin, int end, int* tmp)
{
    if(begin >= end)
        return ;
    int mid = (begin + end)/2;
    _MergeSort(a, begin, mid, tmp);
    _MergeSort(a, mid+1, end, tmp);
    int begin1 = begin, end1 = mid;
    int begin2 = mid+1, end2 = end;
    int index = begin; //这里index也可以定义为0,tmp空间可以恰好存储数组a,定义为0就是将数据储存在tmp开头,定义为begin就是将数据储存在tmp中对应数组a的位置,具体情况如下图。和定义为0的区别在于memcpy拷贝,tmp传入位置不同
    while(begin1 <= end1 && begin2 <= end2)
    {
        if(a[begin1] < a[begin2])
        {
            tmp[index++] = a[begin1++];
        }
        else
        {
            tmp[index++] = a[begin2++];
        }
    }
    while(begin1 <= end1)
    {
        tmp[index++] = a[begin1++];
    }
    while(begin2 <= end2)
    {
        tmp[index++] = a[begin2++];
    }
    memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
     //如果index开始是0,那么修改为: memcpy(a + begin, tmp, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if(tmp == NULL)
        return;
    _MergeSort(a, 0, n-1, tmp); //子函数一般在前面加一个'_'
    free(tmp);
}
下面进行归并排序的非递归写法
能不能用栈来实现非递归呢?能,但是很麻烦,快排用栈来实现非递归是快排是从上往下的,二叉树逻辑是根,左子树,右子树;而归并是自下而上的,二叉树逻辑是左子树,右子树,根。用栈来实现很麻烦。其实归并的非递归只需要循环就可以解决了
直接将数组一个一个的部分进行归并。第一次归并的区间大小为1,第二次为2,第三次为4,当归并的区间大于等于数组长度的时候循环结束。如图所示:
看起来似乎很简单,很简单为什么不用呢?
因为循环区间边界的控制相当麻烦。而且排序的数组如果不是2的次方个都会有问题。因为在不是2的次方个的情况下,不能满足gap*2恰好等于排序数组的个数,如果不能满足,后面的归并就会出现这种情况:数组的边界是[0, 5],归并的区间为:[0,3],[4,7](当gap为4时)毫无疑问数组越界了。
void MergeSortNoRecursion(int* a, int n)
{
        int* tmp = (int*)malloc(sizeof(int) * n);
        if (tmp == NULL)
        {
               return;
        }
        int gap = 1;
        while (gap < n)
        {
               int i = 0;
               for (i = 0; i < n; i+=gap * 2)
               {
                       int begin1 = i, end1 = i + gap - 1; //这里是控制边界的部分,是难点之一
                       int begin2 = i + gap, end2 = i + gap * 2 - 1;
                       int index = i;
                       while (begin1 <= end1 && begin2 <= end2)
                       {
                              if (a[begin1] < a[begin2])
                              {
                                      tmp[index++] = a[begin1++];
                              }
                              else
                              {
                                      tmp[index++] = a[begin2++];
                              }
                       }
                       while (begin1 <= end1)
                       {
                              tmp[index++] = a[begin1++];
                       }
                       while (begin2 <= end2)
                       {
                              tmp[index++] = a[begin2++];
                       }
               }
               memcpy(a, tmp, n * sizeof(int));
               gap *= 2;
        }
}
所以需要对代码进行改进:
void MergeSortNoRecursion(int* a, int n)
{
        int* tmp = (int*)malloc(sizeof(int) * n);
        if (tmp == NULL)
        {
               return;
        }
        int gap = 1;
        while (gap < n)
        {
               int i = 0;
               for (i = 0; i < n; i+=gap * 2)
               {
                       int begin1 = i, end1 = i + gap - 1;
                       int begin2 = i + gap, end2 = i + gap * 2 - 1;
                       int index = i;
                       if (begin2 >= n) //通过修改越界的区间来控制变量
                              begin2 = n - 1;
                       if (end2 >= n)
                              end2 = n - 1;
                       if (end1 >= n)
                              end1 = n - 1;
                       while (begin1 <= end1 && begin2 <= end2 && index < n)
                         //只修改区间是不够的,因为区间变成了[i, n-1],[n-1, n-1],本来有n-i个数,因为 [n-1, n-1]的存在变成了n-i+1个数,tmp空间有限,就造成了数组越界,所以加上一个条件index < n,下面的while循环也一样。
                       {
                              if (a[begin1] < a[begin2])
                              {
                                      tmp[index++] = a[begin1++];
                              }
                              else
                              {
                                      tmp[index++] = a[begin2++];
                              }
                       }
                       while (begin1 <= end1 && index < n)
                       {
                              tmp[index++] = a[begin1++];
                       }
                       while (begin2 <= end2 && index < n)
                       {
                              tmp[index++] = a[begin2++];
                       }
               }
               memcpy(a, tmp, n * sizeof(int));
               gap *= 2;
        }
}
int main()
{
        int a[] = { 1,5,8,7,4,6,9,3, 11 };
        MergeSortNoRecursion(a, sizeof(a) / sizeof(a[0]));
        for (int i = 0; i < 9; i++)
               printf("%d ", a[i]);
        return 0;
}
这种方法会有如下情况的警告,但不影响执行结果,应该是vs上的bug。
处理增加index < n的条件,还有一种处理方法:如果begin2也越界了,说明归并的第二个数组不存在,将数组区间修改为不存在的区间,再对if条件进行一些修正即可,如下:
if (begin2 >= n)
{
      begin2 = n;
      end2 = n - 1;
}
if (end2 >= n && begin2 < n)
{
      end2 = n - 1;
}
if (end1 >= n)
      end1 = n - 1;
这样就不用加上index < n的条件了。上面的那个警告还是有的,不影响结果就是了。
归并排序的时间复杂度是经典的O(N*logN)(一共有logN层,每层O(N)),空间复杂度O(N)
排序的核心部分到此就结束了,下面介绍一下补充部分。
排序有时会被分为两类:内排序和外排序
内排序是数据在内存中排序,外排序是数据量很大,比如10亿个整数(大概4GB),数据在磁盘中的排序。
内排序可以用学到的几种排序,外排序就只能使用归并排序了。
数据在内存中有数组形式,可以支持下标随机访问;数据在磁盘中,就是文件形式,支持串行访问(gets,getc)
当有10亿个数的文件,只有1GB的内存,如何对文件中的数进行排序?
将10亿的文件分成两个5亿文件,假设5亿的文件已经排好了,那么每次从文件中读取数据,比较,小的存入10亿数据的文件即可。当然这只是理论上可行,实际中要创建太多文件,效率低。
实际上会选择将4GB的文件划分成8等份,然后在内存中对每份文件中的数据进行堆排序/希尔排序/快速排序/归并排序,排成有序后,再通过归并排序合并成4GB的文件。这种方法对文件的操作要求非常高。
下面介绍一下计数排序
计数排序是非比较排序的一种,非比较排序还有一种叫基数排序,也叫桶排序。桶排序复杂且不实用(桶排序只能作用于整数)。
计数排序也不实用,但是思想很有意思,一些题会出现计数排序的变种。
计数首先是统计数据个数。开辟适合(数组中最大值的大小加1)大小的空间,初始化为0,利用哈希中映射的概念,遍历数组,在对应的位置处++,如遍历的数据为2,就在开辟数组中下标为2的位置++。这样统计每个数出现的次数。然后在根据开辟数组中的信息排序。如下图:
当然这样有缺陷的,比如排序的数据是:100000  9999  5000  4444   30000,有必要开100001个空间吗?
这里采用上面的映射方式(绝对映射(数据是几就映射到几的位置))显然是不合适的。这里选择了相对映射,将数据中最小的值作为开头,最后的位置为最大值。每次数据映射到val-min的位置。当然这种情况用比较排序肯定更好。计数排序适用于一些数据比较集中的情况。
排序中最后一点:稳定性
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
稳定性的意义:单单进行整数排序确实没有意义,但是在结构体中,按值进行排序的时候,就有意义了。比如在考研录取上,发现最后一个名额,有两个相同成绩的人,这时先交卷的录取。稳定性的意义就体现出来了。或者成绩相同,按数学分数高的优先录取,可以先按数学排,然后用具有稳定性的排序排总分。
现在来探讨一下这些排序的稳定性。
直接插入排序:稳定
希尔排序:不稳定——相同的数可能会分到不同的gap组中,然后按各自组的大小进行排序
选择排序:不稳定——一些书上甚至会说稳定,举个例子就能说明不稳定:一组数 :3  3  1  1  5,进行选择排序是选最小值,为1,与第一位交换:1  3  3  1  5,这种情况下,1是稳定的,但是3不稳定,所以选择排序不稳定。
堆排序:不稳定——向下调整,向上调整会换来换去。
冒泡排序:稳定
快速排序:不稳定
归并排序:稳定——需要保证if条件中相等的话,前面的先放入。
计数排序:一般情况下不稳定,但是是可以做到稳定的,比如按下图思路,不过计数排序的稳定性没有意义,计数排序对结构体是没有办法排序的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值