数据结构(初阶)—— 排序算法(下)(2)

简介: 数据结构(初阶)—— 排序算法(下)(2)

2.非递归实现

1.基本思路

快排的递归实现,由于存在栈溢出的可能性;往往需要用模拟栈来实现非递归;


       给定一个N个数据的数组,其下标范围为[ 0,N-1 ],将这段区间入栈,然后出栈去找key,划分左右区间,因为栈的特点是后进先出,将这两个区间看做两个的整体,那么这两个区间入栈的顺序就是先入右子区间,再入左子区间;(详解如下)

1ecd1b2606ed46e9956a89f231c9802c.png

其实整体的思路和递归很类似,但本质上是循环迭代的过程; 也是通过找key划分区间,每次划分出来的区间都去找key,每个区间key的左边都比key小,右边都比key大,直到不可划分为止,那么整个数组就排序完成了;

2.代码实现

//快排(非递归)
void QuickSortNonR(int* a, int left, int right)
{
  ST st;
  StackInit(&st);
    //先入0-9区间
  StackPush(&st, left);
  StackPush(&st, right);
  while (!StackEmpty(&st))
  {
        //取出0-9区间
    int end = StackTop(&st);
    StackPop(&st);
    int begin = StackTop(&st);
    StackPop(&st);
        //找中间基准值、划分左右区间
    int keyi = Partion3(a, begin, end);
    //[begin,keyi-1] keyi [keyi+1,end]
        //先入右区间
    if (keyi + 1 < end)
    {
      StackPush(&st, keyi + 1);
      StackPush(&st, end);
    }
        //再入左区间
    if (begin < keyi - 1)
    {
      StackPush(&st, begin);
      StackPush(&st, keyi - 1);
    }
  }
  StackDestroy(&st);
}

三、归并排序

1.递归实现

        归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

1ecd1b2606ed46e9956a89f231c9802c.png

代码实现 

//归并排序(递归)
//时间复杂度:O(N*logN)
//空间复杂度:O(N)
void _MergeSort(int* a, int left, int right, int* tmp)
{
  if (left >= right)
  {
    return;
  }
  int mid = (left + right) / 2;
  //如果[left,mid]  [mid+1,right]有序
  _MergeSort(a, left, mid, tmp);//归并左区间
  _MergeSort(a, mid+1, right, tmp);//归并右区间
  int begin1 = left, end1 = mid;
  int begin2 = mid + 1, end2 = right;
  int i = left;
    //进行合并
  while (begin1 <= end1 && begin2 <= end2)
  {
    if (a[begin1] < a[begin2])
    {
      tmp[i++] = a[begin1++];
    }
    else
    {
      tmp[i++] = a[begin2++];
    }
  }
    //在合并的时候,存在某个区间数组未合并完的情况
  while (begin1 <= end1)
  {
    tmp[i++] = a[begin1++];
  }
  while (begin2 <= end2)
  {
    tmp[i++] = a[begin2++];
  }
  //tmp数组的值拷贝回a数组中
  for (int j = left; j <= right; ++j)
  {
    a[j] = tmp[j];
  }
}
void MergeSort(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  if (tmp == NULL)
  {
    printf("malloc fail\n");
    exit(-1);
  }
  _MergeSort(a, 0, n - 1, tmp);
  free(tmp);
  tmp = NULL;
}

2.非递归实现

1.方法1:分组归并、整体拷贝——边界控制的阐述

1.基本思路:

1ecd1b2606ed46e9956a89f231c9802c.png 2.边界控制


       上图进行gap分组依次归并,可以看出:两个数据归并——>四个数据归并——>八个数据归并——>归并结束,排序完成;也就说明这种非递归的思想只适用于数组元素个数是2^n;


       针对数组元素个数非2^n时,要对其边界进行控制,这也是归并排序的一个难点;整体的框架可能大家都理解,利用临时的tmp数组,然后把待排序数组拆分成两个数组,依次取遍历两个数组,哪个小就先入tmp数组,可能存在没入完的数组,再将剩下的全部入tmp数组;


假设现有一个9个元素的数组,如何对其进行边界控制?


以gap=gap*2(gap>=1)的增长方式进行每组的归并操作,首先将数组分成两个区间,分别表示为[ begin1,end1 ]和[ begin2,end2 ];


如图所示:

1ecd1b2606ed46e9956a89f231c9802c.png

       这两个区间,在gap的以2倍的变化中且与元素个数存在一定的关系,我们需要利用这两个区间加上tmp数组的利用,进行归并操作;那么就必须保证这两个区间是有效的(区间一定要有数据);因为在遍历数组的过程中,两个区间时时发生变化,就存在以下三种情况:

1ecd1b2606ed46e9956a89f231c9802c.png

具体情况具体分析:

情况1:

1ecd1b2606ed46e9956a89f231c9802c.png

        此时[ begin2, end2 ]=[ 9, 9 ],此区间是不存在元素的;根据归并的思想:把数据入tmp数组,再拷贝回原数组;就入数据而言,是对这两个区间的元素大小进行判断,既然此时只有一个区间有元素,那么就可以把没有元素的区间进行修正,修正为不存在的区间;元素个数n=9,将end2修正为8(end=n-1),那么就不存在这样的区间,就会把[ begin1, end1 ]=[ 8, 8 ]的数据入到tmp数组中去;能不能

情况2:

1ecd1b2606ed46e9956a89f231c9802c.png

  此时[ begin1, end1 ]=[ 8, 9 ],[ begin2, end2 ]=[ 10, 11 ];[ begin2, end2 ]区间不存在元素,虽然在情况1的基础上将其修改为不存在的区间,但是[ begin1, end1 ]有两个元素,一个是2,一个是随机值,当数据入tmp数组时,会把这个随机值带入,并拷贝回原数组;此时数组已经越界,一定会存在错误;此时还要将end1修正为8(end=n-1),那么此区间就只有一个元素了,就不会出现数组越界的情况;


情况3:

1ecd1b2606ed46e9956a89f231c9802c.png

此时[ begin2, end2 ]=[ 8, 15 ],此区间是不存在元素的;还需要将end2进行修正,修正为8(end=n-1);也是由于此区间存在3个随机值,修正的目的就是让此区间只有一个明确的值存在的值,确保数组不越界;


以上就是针对数组元素个数非2^n时的边界控制,只要掌握归并排序的边界控制,其代码实现就变得容易许多;

代码实现

//归并排序(非递归)----适用于2的n次方
void MergeSortNonR(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  if (tmp == NULL)
  {
    printf("malloc fail\n");
    exit(-1);
  }
  int gap = 1;
  while (gap < n)
  {
    for (int i = 0; i < n; i += 2 * gap)
    {
      //区间范围[i,i+gap-1] [i+gap,i+2*gap-1]
      int begin1 = i, end1 = i + gap - 1;
      int begin2 = i + gap, end2 = i + 2 * gap - 1;
      /*******************************重要*****************************/
      //end1越界 [begin2,end2]不存在
      if (end1 >= n)
      {
        //进行修正
        end1 = n - 1;
      }
      // [begin1,end1]存在,[begin2,end2]不存在
      if (begin2 >= n)
      {
        //进行修正
        begin2 = n;
        end2 = n - 1;
      }
      //end2越界
      if (end2 >= n)
      {
        end2 = n - 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++];
      }
    }
    //printf("\n");
    //把归并好的数据拷贝回原数组
    for (int i = 0; i < n; ++i)
    {
      a[i] = tmp[i];
    }
    gap *= 2;
  }
  free(tmp);
  tmp = NULL;
}

2.方法2:分组归并、逐次归并拷贝——边界控制的阐述

1.基本思路:

1ecd1b2606ed46e9956a89f231c9802c.png

2.边界控制

假设现有一个9个元素的数组,如何对其进行边界控制?

以gap=gap*2(gap>=1)的增长方式进行每组的归并操作,首先将数组分成两个区间,分别表示为[ begin1,end1 ]和[ begin2,end2 ];

如图所示:

1ecd1b2606ed46e9956a89f231c9802c.png

       与方法一不同的是,此方法不需要整体归并完后拷贝到原数组中去,而是归并一次拷贝一次,所有并不需要考虑原数组中数字2会被覆盖为随机值的情况;但是对于边界的问题在循环迭代的过程中,依然存在如下几中情况:

1ecd1b2606ed46e9956a89f231c9802c.png

情况1:

1ecd1b2606ed46e9956a89f231c9802c.png

因为是归并一次数据就拷贝回原数组,所以并不会影响到原数组末尾的2;此时end越界,将end修正,修正为8(end=n-1);

情况2与情况3:

1ecd1b2606ed46e9956a89f231c9802c.png

 这两种情况在end2修正后,end1和begin2存在越界,因为是归并一次数据就拷贝回原数组,所以并不会影响到原数组末尾的2;所以只要当end1和begin2越界时,让其不执行拷贝的操作就可以了(即代码实现时让其遇到此情况就跳出循环,去执行下一次gap分组)

代码实现

/*第二种*/
void MergeSortNonR2(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  if (tmp == NULL)
  {
    printf("malloc fail\n");
    exit(-1);
  }
  int gap = 1;
  while (gap < n)
  {
    for (int i = 0; i < n; i += 2 * gap)
    {
      //[i,i+gap-1] [i+gap,i+2*gap-1]
      int begin1 = i, end1 = i + gap - 1;
      int begin2 = i + gap, end2 = i + 2 * gap - 1;
      int index = i;
      //核心思想:end1、begin2、end2都有可能越界
      //end1越界
      if (end1 >= n || begin2 >= n)
      {
        break; 
      }
      //end2越界,需要归并,修正end2
      if (end2>=n)
      {
        end2 = n - 1;
      }
      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++];
      }
      //把归并好的数据拷贝回原数组
      for (int j = i; j <= end2; ++j)
      {
        a[j] = tmp[j];
      }
    }
    gap *= 2;
  }
  free(tmp);
  tmp = NULL;
}
目录
相关文章
|
2月前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
49 1
|
3月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
99 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
2月前
|
存储 算法 搜索推荐
Python 中数据结构和算法的关系
数据结构是算法的载体,算法是对数据结构的操作和运用。它们共同构成了计算机程序的核心,对于提高程序的质量和性能具有至关重要的作用
|
2月前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
2月前
|
算法
数据结构之路由表查找算法(深度优先搜索和宽度优先搜索)
在网络通信中,路由表用于指导数据包的传输路径。本文介绍了两种常用的路由表查找算法——深度优先算法(DFS)和宽度优先算法(BFS)。DFS使用栈实现,适合路径问题;BFS使用队列,保证找到最短路径。两者均能有效查找路由信息,但适用场景不同,需根据具体需求选择。文中还提供了这两种算法的核心代码及测试结果,验证了算法的有效性。
112 23
|
2月前
|
算法
数据结构之蜜蜂算法
蜜蜂算法是一种受蜜蜂觅食行为启发的优化算法,通过模拟蜜蜂的群体智能来解决优化问题。本文介绍了蜜蜂算法的基本原理、数据结构设计、核心代码实现及算法优缺点。算法通过迭代更新蜜蜂位置,逐步优化适应度,最终找到问题的最优解。代码实现了单链表结构,用于管理蜜蜂节点,并通过适应度计算、节点移动等操作实现算法的核心功能。蜜蜂算法具有全局寻优能力强、参数设置简单等优点,但也存在对初始化参数敏感、计算复杂度高等缺点。
62 20
|
2月前
|
机器学习/深度学习 算法 C++
数据结构之鲸鱼算法
鲸鱼算法(Whale Optimization Algorithm,WOA)是由伊朗研究员Seyedali Mirjalili于2016年提出的一种基于群体智能的全局优化算法,灵感源自鲸鱼捕食时的群体协作行为。该算法通过模拟鲸鱼的围捕猎物和喷出气泡网的行为,结合全局搜索和局部搜索策略,有效解决了复杂问题的优化需求。其应用广泛,涵盖函数优化、机器学习、图像处理等领域。鲸鱼算法以其简单直观的特点,成为初学者友好型的优化工具,但同时也存在参数敏感、可能陷入局部最优等问题。提供的C++代码示例展示了算法的基本实现和运行过程。
58 0
|
3月前
|
机器学习/深度学习 存储 缓存
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
文章主要介绍了排序算法的分类、时间复杂度的概念和计算方法,以及常见的时间复杂度级别,并简单提及了空间复杂度。
50 1
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
|
2月前
|
算法 vr&ar 计算机视觉
数据结构之洪水填充算法(DFS)
洪水填充算法是一种基于深度优先搜索(DFS)的图像处理技术,主要用于区域填充和图像分割。通过递归或栈的方式探索图像中的连通区域并进行颜色替换。本文介绍了算法的基本原理、数据结构设计(如链表和栈)、核心代码实现及应用实例,展示了算法在图像编辑等领域的高效性和灵活性。同时,文中也讨论了算法的优缺点,如实现简单但可能存在堆栈溢出的风险等。
59 0
|
3月前
|
存储 算法 Java
Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性
Java Set因其“无重复”特性在集合框架中独树一帜。本文解析了Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性,并提供了最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的hashCode()与equals()方法。
55 4