数据结构之排序【归并排序和快排的顶级优化和快排的三种原理的实现及分析】 内含动态演示图

简介: 引言:1.归并排序(MergeSort)2.快速排序的优化(顶级优化)3.快速排序的三种思路的代码实现及分析4.归并排序和快排第3原理的测试

引言:

刚刚去回顾了一下递归实现的几个小代码,感觉递归真的是很神奇的一个东西,现在感觉递归确实也是比较不好理解的东西,所以这边我提供几个好的学习和理解递归的方法,递归无非就是两种,一种是求出递归的递归,另一种是起作用的递归函数的递归,两种是有一定的区别的。我们先讲一下要求出递归的递归,这种就类似于用递归求一个数的阶乘,必须要从头开始算,算出了头才可以算尾,这个就叫要求出递归的递归,学习这种递归首先就是要弄明白这个递归的原理,然后再在纸上把每一个递归的值给算出来,当然是要从最头上的那个开始算,另一种就是不需要算的递归,只要可以发挥出函数的作用就可以了,这种递归是比较的抽象的,因为这种作用型递归函数都是非常长的一串代码,所以当我们要理解这种递归的时候,我们可以画出它的递归展开图,就是一幅图一幅图的画,递归一次你就画一幅图,并且在画图理解的同时可以调试代码,按照图来调试代码,一遍不行再一遍,直到理解(这时想要理解这种递归的唯一方法),所以递归可以说是非常的不好搞。现在北京时间 2022/12/27/0:43 ,今天可以看出是超出了平常写博客的时间,但是我们还是要坚持写博客,并且我刚刚看了一下我最开始写的博客,我觉得当时的博客写的很有味道,现在的博客总感觉没什么味道了,以前写博客每一个字,都是肺腑之言一般,现在的博客除了引言中的内容,好像大部分都有一些不够活灵活现的感觉,以前的每一句话写的真的都很好玩的感觉,就算是那种死板的语言也可以被我们自己理解然后写的非常的有趣搞笑,不想现在的,有一些刻板,就像是以前都在写真心话,现在写的都是客套话的感觉,并且有一种以前写的博客是真真实实的写给自己看的,而现在的博客更不像是不是写给自己看的;总感觉现在的博客和以前区别很大,可能是我真的经历了吧!经历了改变了,以前刚开始写博客,写完之后睡觉好像都是想住别人在浏览你的博客,然后你的解释是如何如何的好,语句是多么多么的清楚明了,自己在句里行间写了多少多少的细节在里面,然后别人发现这些细节的时候是怎样的感觉,之类的想法,当时好像都会有,但是现在的我,好像……,可能是明白了CSDN的推荐和不推荐,关注和不关注之类的问题和当今时代文章好像确实是没有人看,更没有人会一个字一个字的仔细看,并且自己作为一个小白,写的内容也都是最基本的,好像确实是不需要看之类的这一系列额想法在里面,最终导致了我们博客风格的改变吧!行文来到这个位置,我接上一篇博客分享一下我的昨晚睡觉体验(如我意料之中,是我许久以来睡的最舒服的一次),缩小了位置,并不会影响我的睡觉体验,但是却可以让我睡的暖和,…… ,天大寒,码字不易,引言就这样吧!今天我们讲一下什么是归并排序和快速排序的另外两种思路。


1.归并排序(MergeSort)

时间复杂度:O(N*logN)


1.1 归并排序原理:我们可以假设一个数组它的左半区间有序 右半区间也有序 ,然后在这个前提之下,然后我们使用归并算法:依次比较左半区间和右半区间中的元素然后取小的元素放到新创建的临时数组中。


按照归并排序的原理,此时我需要有两个新的个区间,如何获得两个区间呢?就是将int mid = (left + right) / 2; 这样我们就可以利用这个中间值进行一个数组分成两个区间,有了区间我们此时就假设左半区间[ left , mid ] 和右半区间 [ mid+1 ,right ] 是有序的,那么我们就可以进行归并,让我的左半区间和右半区间有序(只有这样,才可以进行我的归并算法实现),分治递归实现。


图示如下:

36.png


问题:归并之前,左右区间没有序?怎么办?

此时这个问题就涉及到我们归并排序中的递归问题

所以想要解决这个问题就需要我们进行一个分治递归的思想,从而实现我们的代码

所以我们此时如何使我的左半区间和右半区间有序呢?

分治递归:

  _MergeSort(arr, left, mid, tmp);//假设我使用归并算法已经把左半区间排成有序了
  _MergeSort(arr, mid + 1, right, tmp);//假设我使用归并算法已经把半右区间排成有序了

这样我们就可以让我们的一个数组的左半区间和右半区间都有序了,此时就可以进行归并排序了

具体动图如下:

32.gif

1.2 归并排序代码实现

#include<stdlib.h>
void _MergeSort(int* arr, int left, int right, int* tmp)
{
  if (left >= right)//递归条件
  {
    return;
  }
  int mid = (left + right) >> 1;
  //分治递归
  _MergeSort(arr, left, mid, tmp);
  _MergeSort(arr, mid + 1, right, tmp);
  //归并算法的代码实现:
  int begin1 = left;
  int end1 = mid;
  int begin2 = mid + 1;
  int end2 = right;
  int index = left;
  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 i;
  for (i = left; i <= right; i++)
  {
    arr[i] = tmp[i];
  }
}
void MergeSort(int* arr, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  _MergeSort(arr, 0, n - 1, tmp);
  free(tmp);
}

注意点: 为了防止待会递归这个函数的时候每一次都还要重新malloc一块空间,所以我们就要把这个函数的作用外包到外面去,不然每次都要malloc一块空间效率下降,所以我就可以在外部重新写一个这个函数的子函数void _MergeSort(int* arr, int left, int right, int* tmp) 并且此时的这个函数其实还是MergeSort函数,只是多了一个 _ 而已,_ 表示的就是子函数的意思


2.快速排序的优化(顶级优化)

(注释详细)

学到这个位置我们意识到了,快排是有一定的缺陷的,当我转参的数组是一个有序的数组的时候(例:1 2 3 4 5 6 7 8 9 ),想要使用快排进行排序(因为快排的原理,每次都是取arr[0]或arr[end-1]),所以就会导致第一次是N,第二次是N-1,……,最后0,然后等差数列相加(就会导致快排的时间复杂度是:O(N^2))

所以快排是有一定的缺陷的,所以为了避免这个缺陷(我们就要对key的值进行优化),就会使用一个叫三数取中的方法来获取key的值:所以我就要对这个快排的代码就行改进


第一个优化:快排的三数取中法优化, 函数如下int GetMidIndex(int* arr, int left, int right)

第二个优化:小区间优化


int GetMidIndex(int* arr, int left, int right)
{
  //三数取中法代码
  int mid = (left + right) >> 1;//这句代码=> int mid = (left + right)/2; 只是移位的效率会高一点点
  //下面的这些if条件的判断就是为了取到三个数中不是最大也不是最小的那个数而已
  if (arr[left] < arr[mid])
  {
    if (arr[mid] < arr[right])
    {
      return mid;
    }
    else if(arr[left]>arr[right])//程序来到这个位置就是说明,此时我的mid是我的最大值,然而我的目的是为了找到中间的那个值,所以这边我就比较一下left和right就行,大的那个就是中间值
    {
      return left;
    }
    else
    {
      return right;//程序来到这里就说明right大,那么right就是中间值,是中间值就return
    }
  }
  else  //此时这个else的意思就是:arr[left] > arr[mid]
  {
    if (arr[left] < arr[right])//这边三数取中的写法很多,可以有不一样的写法,不怕
    {
      return left;
    }
    else if (arr[mid] < arr[right])
    {
      return right;
    }
    else
    {
      return mid;
    }
  }
}
void QuickSort(int* arr, int left, int right)
{
  if (left >= right)
  {
    return;
  }
  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;
  //此时代码来到这个位置就有一些不好的地方(比如刚刚我们使用了三数取中的函数,如果此时需要排序的数据非常大(1000000),有这么多个数据,此时如果要一直调用三数取中的函数就会导致效率问题)
  //并且当我们需要排序的数据剩下的不是非常多的时候(此时正是调用次数最多的时候因为快排是一个logN的排序 越到后面,执行次数越多(例:2^19 2^18)),所以此时我们可以来一个小区间优化
  //就是当需要排序的数据没有那么多时,我们就不使用快速排序,我们使用直接插入排序就行
  //原递归代码:
  //QuickSort(arr, left, pivot - 1);
  //QuickSort(arr, pivot + 1, right);
  //所以优化代码如下:
  if (pivot - 1 - left > 10)
  {
    QuickSort(arr, left, pivot - 1);
  }
  else
  {
    InsertSort(arr + left, pivot - 1 - left + 1);
  }
  if (right - (pivot + 1) > 10)
  {
    QuickSort(arr, pivot + 1, right);
  }
  else
  {
    InsertSort(arr + pivot + 1, right - (pivot + 1) + 1);
  }
}

3.快速排序的三种思路的代码实现及分析

3.1 快排的第一种思想挖坑法

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;
}

3.2 快排的第二种思路的实现(前后指针法)

int PartQuickSort3(int* arr, int left, int right)
{
  int index = GetMidIndex(arr, left, right);
  Swap(&arr[left], &arr[index]);
  int keyi = left;
  int prev = left;
  int cur = left + 1;
  while (cur <= right)
  {
    if (arr[cur] < arr[keyi])
    {
      prev++;
      Swap(&arr[prev], &arr[cur]);
    }
    cur++;
  }
  Swap(&arr[keyi], &arr[prev]);
  return prev;
}

3.3 快排的第三种方法的实现(挖坑法的变形(左右指针法))

理解:注释很详细

int PartQuickSort2(int* arr, int left, int right)
{
  int index = GetMidIndex(arr, left, right);
  Swap(&arr[left], &arr[index]);
  int begin = left;
  int end = right;
  int keyi = begin;
  //左右指针法的快排
  while (begin < end)//表示两指针找元素,直到相遇为止
  {
    while (begin < end && arr[end] >= arr[keyi])//就是在右边找比key小的数
    {
      end--;        //注意点:记得要有等号,因为如果没有等号就可能会死循环,因为如果左边和右边,两边同时找到一个和keyi相等的数(进不去循环,就导致end和begin不变),此时就会导致这两个数相等的数一直换过来换过去,就会导致死循环
    }
    while (begin < end && arr[begin] <= arr[keyi])
    {
      begin++;
    }
    //程序来到这个位置就说明我已经找到了比key大的数和比key小的数了,此时只要把这两个数给交换一下,然后我就完成了快排的单趟排序了(剩下的就交给递归去搞定就行了)
    Swap(&arr[begin], &arr[end]);//交换完,我就完成了快排的单趟排序了(剩下的就交给递归去搞定就行了)
  }
  Swap(&arr[begin], &arr[keyi]);//这步就是为了把我的keyi这个关键位置给放到中间的那个位置(同时也是正确的那个位置)
  return begin;//就是返回中间位置(虽然可能不是中间,但是就是要中间的意思)
}

2.1 快排的三种原理实现测试

void QuickSort2(int* arr, int left, int right)
{
  if (left >= right)
  {
    return;
  }
    //三种思路的测试(不可以同时调用)
  int keyIndex = PartQuickSort1(arr, left, right);
  int keyIndex = PartQuickSort2(arr, left, right);
  int keyIndex = PartQuickSort3(arr, left, right);
  if (keyIndex - 1 - left > 10)
  {
    QuickSort2(arr, left, keyIndex - 1);
  }
  else
  {
    InsertSort(arr + left, keyIndex - 1 - left + 1);
  }
  if (right - (keyIndex + 1) > 10)
  {
    QuickSort2(arr, keyIndex + 1, right);
  }
  else
  {
    InsertSort(arr + keyIndex + 1, right - (keyIndex + 1) + 1);
  }
}

4.归并排序和快排第3原理的测试

37.png



相关文章
|
11天前
|
存储 人工智能 算法
【C++数据结构——内排序】二路归并排序(头歌实践教学平台习题)【合集】
本关任务是实现二路归并算法,即将两个有序数组合并为一个有序数组。主要内容包括: - **任务描述**:实现二路归并算法。 - **相关知识**: - 二路归并算法的基本概念。 - 算法步骤:通过比较两个有序数组的元素,依次将较小的元素放入新数组中。 - 代码示例(以 C++ 为例)。 - 时间复杂度为 O(m+n),空间复杂度为 O(m+n)。 - **测试说明**:平台会对你编写的代码进行测试,提供输入和输出示例。 - **通关代码**:提供了完整的 C++ 实现代码。 - **测试结果**:展示代码运行后的排序结果。 开始你的任务吧,祝你成功!
30 10
|
11天前
|
搜索推荐 算法 数据处理
【C++数据结构——内排序】希尔排序(头歌实践教学平台习题)【合集】
本文介绍了希尔排序算法的实现及相关知识。主要内容包括: - **任务描述**:实现希尔排序算法。 - **相关知识**: - 排序算法基础概念,如稳定性。 - 插入排序的基本思想和步骤。 - 间隔序列(增量序列)的概念及其在希尔排序中的应用。 - 算法的时间复杂度和空间复杂度分析。 - 代码实现技巧,如循环嵌套和索引计算。 - **测试说明**:提供了测试输入和输出示例,帮助验证代码正确性。 - **我的通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了代码运行的测试结果。 通过这些内容,读者可以全面了解希尔排序的原理和实现方法。
42 10
|
11天前
|
搜索推荐 C++
【C++数据结构——内排序】快速排序(头歌实践教学平台习题)【合集】
快速排序是一种高效的排序算法,基于分治策略。它的主要思想是通过选择一个基准元素(pivot),将数组划分成两部分。一部分的元素都小于等于基准元素,另一部分的元素都大于等于基准元素。然后对这两部分分别进行排序,最终使整个数组有序。(第一行是元素个数,第二行是待排序的原始关键字数据。本关任务:实现快速排序算法。开始你的任务吧,祝你成功!
31 7
|
2月前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
2月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
80 1
|
2月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
62 6
|
3月前
|
算法 搜索推荐 Java
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
基数排序是一种稳定的排序算法,通过将数字按位数切割并分配到不同的桶中,以空间换时间的方式实现快速排序,但占用内存较大,不适合含有负数的数组。
50 0
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
|
3月前
|
Java C++
【数据结构】探索红黑树的奥秘:自平衡原理图解及与二叉查找树的比较
本文深入解析红黑树的自平衡原理,介绍其五大原则,并通过图解和代码示例展示其内部机制。同时,对比红黑树与二叉查找树的性能差异,帮助读者更好地理解这两种数据结构的特点和应用场景。
51 0
|
2月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
284 9
|
2月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
44 1

热门文章

最新文章