数据结构之排序【快速排序和归并排序的非递归代码实现及分析】

简介: 数据结构之排序【快速排序和归并排序的非递归代码实现及分析】

引言:

今天因为要写论文,所以现在有点迟了,并且此时是北京时间:2022/12/28/1:41 ,我发现晚睡我真的是专业的,当然睡觉我也是专业的,懂的都懂,现在有点迟加上天大寒,手指不可屈伸,所以我们的引言就这样啦!但是这个位置我还想要记录一下:今天我的搜狗输入法成功进入20万字了,电脑上自带的键盘都要给我敲烂了,我已经能听出来空格键的声音跟以前不一样了,但是还可以用,本来是打算在20万字之时就换一个键盘的,但是……一言难尽,所以暂时我们继续用电脑上的键盘,等我们到40万字的时候再换吧(想象一下,等40万字,可能……)。上篇博客我们讲到了快速排序的递归方法实现,今天我们就讲一下快速排序的非递归实现。


1.快排实现(非递归)

有的人会问,我们不是已经实现了快排吗?快排我已经学会了啊,为什么还要区分递归和非递归呢?

原因就是递归的缺陷,递归是有一定的缺陷的。


递归的缺陷:建立栈帧,效率低(但是这个对于递归基本不算是缺陷) ,在一些极端情况下,栈帧深度(因为调用一个函数就会创建一个栈帧)太深,会导致栈溢出(这个就是递归的真正的大缺陷)


所以我们就要想一些方法来解决这个缺陷,当然解决方法有很多,比如增加栈的空间,使它不会栈溢出,但是这好像是不可能的,所以最好的方法就是不使用递归(当然是在某些特定的情况下不使用)

所以我们就可以想到递归的替换方法,我相信大多数人第一时间都会想到的是用循环来替代递归,但是今天我们不使用循环来替代递归,今天我们使用数据结构中的栈来模拟递归的过程,当然前提是有一个栈,所以我们要先自己实现一个栈。


代码如下:

void Swap(int* child, int* parent)
{
  int tmp = *child;
  *child = *parent;
  *parent = tmp;
}
int PartQuickSort1(int* arr, int left, int right)
{
  int index = GetMidIndex(arr, left, right);
  Swap(&arr[left], &arr[index]);
  int begin = left;
  int end = right;
  int pivot = begin;
  int key = arr[begin];
  while (begin < end)
  {
    while (begin < end && arr[end] >= key)
    {
      end--;
    }
    arr[pivot] = arr[end];
    pivot = end;
    while (begin < end && arr[begin] <= key)
    {
      begin++;
    }
    arr[pivot] = arr[begin];
    pivot = begin;
  }
  pivot = begin;
  arr[pivot] = key;
  return pivot;
}
void QuickSortNonR(int* arr, int n)
{
  //此时因为这个需要有一个栈需要我们去拿以前文件中的栈的实现的代码
  //所以为了方便,我们把这个非递归的快排的实现给拿到外面去
  ST st;
  StackInit(&st);
  //栈实现的原理:栈里面的区间就是需要被单趟排序的
  StackPush(&st, n - 1);//根据栈的原理(后进先出),当我们想要先出我们的左,我们就不应该先入左,而是应该要先入右,此时的右也就是我们的n-1(传参传的是闭区间的话)
  StackPush(&st, 0);//根据栈的原理,并且同理的情况下,这边我们要出的是右,所以我们就应该要入左,也就是入0
  while (!StackEmpty(&st))//只要栈里面有数据就不结束,只有当我的栈里面的数据为空的时候才停止
  {
    //此时如上面两步,我已经把我的左子数组和右子数组给放进去了
    int left = StackTop(&st);//数组放进去之后,我就开始出数组中的元素(并且此时因为先进后出原理,所以此时我们栈中出的第一个元素就是我的要放在arr[0]的元素了)
    StackPopt(&st);//出完元素要删除掉,因为是栈,只有这样我们才可以出剩下的元素
    int right = StackTop(&st);
    StackPopt(&st);
    int keyIndex = PartQuickSort1(arr, left, right);
    //此时有了上面这个挖坑法的代码,我们此时就可以拿到中间的那个key了
    //所以我们此时就把这个数组给分为了左子区间和右子区间
    //[left,keyIndex-1][keyIndex][keyIndex+1,right]
    if (keyIndex + 1 < right)
    {
      //此时就是表示右半区间还存在,元素没有排序完,我们就还需要入栈
      StackPush(&st, right);
      StackPush(&st, keyIndex + 1);
    }
    if (left < keyIndex - 1)//这个表示此时的栈
    {
      //因为上述的代码left是会++的,所以只要left没有走到key的位置处,就是表示此时left中还有元素没有出完
      StackPush(&st, keyIndex-1);
      StackPush(&st, left);
    }
  }
  StackDestory(&st);
}

此时的void QuickSortNonR(int* arr, int n)这个代码中的内容就是不使用递归实现快排的代码,而int PartQuickSort1(int* arr, int left, int right)而这个函数就是一个普通的挖坑法获取key的代码而已,所以重点是void QuickSortNonR(int* arr, int n)这个函数中的内容,但是前提是我们要有一个数据结构中的栈,所以我们此时就还要实现一个栈出来(C语言的不好的地方,假如是C++或者Java就可以不用,因为它们有自己独立的栈可以供我们直接调用),所以关于栈的知识可以看我以前的博客,讲解的都是非常的到位的。


2.归并排序(非递归)

原理如图:

38.png


代码实现:

void MergeSortNonR(int* arr, int n)
{
  //归并的思想,我肯定是要有一个临时数组的
  int* tmp = (int*)malloc(sizeof(int) * n);
  int gap = 1;//表示每一组需要进行归并的数据的个数
  while (gap < n)
  {
    int i;
    for (i = 0; i < n; i += 2 * gap)//由图得到
    {
      //[i,i+gap-1] [i+gap,i+gap*2-1]
        //归并算法的代码实现:
      int begin1 = i;
      int end1 = i + gap - 1;
      int begin2 = i + gap;
      int end2 = i + gap * 2 - 1;
      int index = i;
      //此时因为数据的个数,不一定按照整数倍,所以在划分组的时候就可能导致越界,或者不存在
      //所以需要修正一下
      if (begin2 >= n)
      {
        begin2 = n + 1;
        end2 = n;
      }
      //end2越界也要修正一下
      if (end1 >= n)
      {
        end1 = n - 1;
      }
      //end2越界,需要修正后再归并
      if (end2 >= n)
      {
        end2 = n - 1;
      }
      while (begin1 <= end1 && begin2 <= end2)
      {
        if (arr[begin1] < arr[begin2])
        {
          tmp[index++] = arr[begin1++];
        }
        else
        {
          tmp[index++] = arr[begin2++];
        }
      }
      while (begin1 <= end1)
      {
        tmp[index++] = arr[begin1++];
      }
      while (begin2 <= end2)
      {
        tmp[index++] = arr[begin2++];
      }
    }
    //将新数组中的元素拷贝回原数组
    int j;
    for (j = 0; j < n; j++)
    {
      arr[j] = tmp[j];
    }
    gap *= 2;
  }
  free(tmp);
}


3.测试代码

39.png


时间有点急了,已经快3点多了,所以就这样吧!我要睡了。

相关文章
|
前端开发 Java
java实现队列数据结构代码详解
本文详细解析了Java中队列数据结构的实现,包括队列的基本概念、应用场景及代码实现。队列是一种遵循“先进先出”原则的线性结构,支持在队尾插入和队头删除操作。文章介绍了顺序队列与链式队列,并重点分析了循环队列的实现方式以解决溢出问题。通过具体代码示例(如`enqueue`入队和`dequeue`出队),展示了队列的操作逻辑,帮助读者深入理解其工作机制。
716 1
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
439 59
|
算法 搜索推荐
快速排序-数据结构与算法
快速排序(Quick Sort)是一种基于分治法的高效排序算法。其核心思想是通过选择基准(pivot),将数组划分为左右两部分,使得左侧元素均小于基准,右侧元素均大于基准,然后递归地对左右两部分进行排序。时间复杂度平均为 O(n log n),最坏情况下为 O(n²)(如数组已有序)。空间复杂度为 O(1),属于原地排序,但稳定性不佳。 实现步骤包括编写 `partition` 核心逻辑、递归调用的 `quickSort` 和辅助函数 `swap`。优化方法有随机化基准和三数取中法,以减少最坏情况的发生。
806 13
|
搜索推荐 C语言
数据结构(C语言)之对归并排序的介绍与理解
归并排序是一种基于分治策略的排序算法,通过递归将数组不断分割为子数组,直到每个子数组仅剩一个元素,再逐步合并这些有序的子数组以得到最终的有序数组。递归版本中,每次分割区间为[left, mid]和[mid+1, right],确保每两个区间内数据有序后进行合并。非递归版本则通过逐步增加gap值(初始为1),先对单个元素排序,再逐步扩大到更大的区间进行合并,直至整个数组有序。归并排序的时间复杂度为O(n*logn),空间复杂度为O(n),且具有稳定性,适用于普通排序及大文件排序场景。
|
存储 人工智能 算法
【C++数据结构——内排序】二路归并排序(头歌实践教学平台习题)【合集】
本关任务是实现二路归并算法,即将两个有序数组合并为一个有序数组。主要内容包括: - **任务描述**:实现二路归并算法。 - **相关知识**: - 二路归并算法的基本概念。 - 算法步骤:通过比较两个有序数组的元素,依次将较小的元素放入新数组中。 - 代码示例(以 C++ 为例)。 - 时间复杂度为 O(m+n),空间复杂度为 O(m+n)。 - **测试说明**:平台会对你编写的代码进行测试,提供输入和输出示例。 - **通关代码**:提供了完整的 C++ 实现代码。 - **测试结果**:展示代码运行后的排序结果。 开始你的任务吧,祝你成功!
529 10
|
搜索推荐 C++
【C++数据结构——内排序】快速排序(头歌实践教学平台习题)【合集】
快速排序是一种高效的排序算法,基于分治策略。它的主要思想是通过选择一个基准元素(pivot),将数组划分成两部分。一部分的元素都小于等于基准元素,另一部分的元素都大于等于基准元素。然后对这两部分分别进行排序,最终使整个数组有序。(第一行是元素个数,第二行是待排序的原始关键字数据。本关任务:实现快速排序算法。开始你的任务吧,祝你成功!
450 7
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
707 1
|
存储 Java 开发者
Java中的Map接口提供了一种优雅的方式来管理数据结构,使代码更加清晰、高效
【10月更文挑战第19天】在软件开发中,随着项目复杂度的增加,数据结构的组织和管理变得至关重要。Java中的Map接口提供了一种优雅的方式来管理数据结构,使代码更加清晰、高效。本文通过在线购物平台的案例,展示了Map在商品管理、用户管理和订单管理中的具体应用,帮助开发者告别混乱,提升代码质量。
260 1
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
1171 77
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
281 0
栈区的非法访问导致的死循环(x64)

热门文章

最新文章