重点算法排序之快速排序、归并排序(上篇)

简介: 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

一、排序的概念及常见的排序算法


 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。


 在排序中我们还经常讨论这个排序是否稳定?那到底怎么来判断一个潘旭是否稳定呢?我们先看一下排序稳定的概念。


 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。


  我们比较常见的排序有六种:插入排序、希尔排序、选择排序、堆排序、快速排序、归并排序。应用较多的,也是相对来说比较重要的有快速排序和归并排序。我们本篇文章先来详细介绍一下快速排序和归并排序的思想及代码详解,由于插入排序、希尔排序、选择排序相对较为简单,所以我们就不在详解。下篇文章会写出堆排序的思想及代码详解。



二、快速排序的思想及代码详解

2、1 快速排序的思想



任取待排序元素序列中的某元素作为基准值(我们这里一般选择数组的第一个元素或者最后一个元素或者中间的元素),将数组的元素与基准值比较分成左、右子序列,左子序列中所有元素均小于或等于基准值,右子序列中所有元素均大于或等于基准值,然后对左右子序列重复该过程(递归实现排序),直到所有元素都排列在相应位置上为止。


 我们举一个例子一起理解一下。加入我们给出如下数组:

int arr[]={49,38,65,97,76,13,27,49};

 对上面的数组进行快排,我们按照快排的思想分为三步骤进行排序,如下:

  1. 我们先选出基准值,我们在这里选择数组的最左边的数。
  2. 按照基准直对数组进行初次排序,分成左、右子序列。左子序列中所有元素均小于或等于基准值,右子序列中所有元素均大于或等于基准值。


cb6eb295ff964cf4ae69a45b5ba770ea.png


  1. 递归排序左右子序列,当子序列中只有一个元素时我们就不在递归(递归的停止条件)。

  当递归结束时,此时的序列已经为有序的了。我们不难发现,快速排序递归实现有序是用了分治的思想。我们刚开始是把一个无序的数组分成了左、右子序列数组,进而分别对左、右子序列再分,直到分成左、右子序列只有一个数据时(我们可以把一个数据看成有序的),就不会再分。这时递归结束后就已经是有序的了。


  那么具体显现的思路有几种呢?在这里我给出三种是实现方法:挖坑法、左右指针法、前后指针法。

2、2 挖坑法

2、2、1 挖坑法实现思想


我们先来了解一下挖坑法的主要思想:


先选出一个坑的位置(基准值),我们选择坑的位置一般会选最左边或者最右边。

第一次排序:当我们选择坑的位置为最左边时,我们再从数组的最右边依次往前找找比基准直小的数放到坑的位置。更新坑的位置为从右边找到比基准直小的位置。我们再从左边依次找比基准直大的数字放到新坑的位置。反复此操作,当左右相遇时(此时该地方为坑),把基准值放到坑里就行。

递归重复第二步骤的操作达到有序。


2、2、2 挖坑法举例

 通过上述思想我们举一个例子具体看一下怎么实现的。我们给出如下数组:

  我们按照上述挖坑法思想来看一下具体实现:

  1. 选出坑的位置(基准直);



ead910d872ee4f92b96aab6d6ae4d36d.png


  1. 第一次排序;
  2. 递归排序左、右子序列。





90bc79f6f3664850b9f1c4eefb3be30b.png


2、2、3 挖坑法代码实现

void Swap(int* p1, int* p2)
{
  int tmp = *p1;
  *p1 = *p2;
  *p2 = tmp;
}
void QuickSort(int arr[], int left, int right)
{
  if (left >= right)
  {
    return;
  }
  int index = GetMid(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;
  }
  arr[begin] = key;
  QuickSort(arr, left, begin - 1);
  QuickSort(arr, begin + 1, right);
}
void Print(int arr[], int n)
{
  int i = 0;
  for (i = 0; i < n; i++)
  {
    printf("%d ", arr[i]);
  }
}
void TestSort()
{
  int arr[10] = { 0,9,8,7,6,5,4,3,2,1 };
  int n = 10;
  QuickSort(arr, 0, n - 1);
  Print(arr, n);
}
int main()
{
  TestSort();
  return 0;
}



2、3 左右指针法

2、3、1 左右指针法实现思想


我们先来了解一下左右指针法的主要思想:


先选出一个基准值,我们选择坑的位置一般会选最左边或者最右边或者中间的数据。

第一次排序:我们定义两个指针。初始时,一个指针(begin指针)指向最左边的数据,另一个指针(end指针)指向最后一个数据。begin指针向右找比基准值大的数据,找到了就停下来。end指针向左找比基准值小的数据,找到了就停下来。然后交换两个指针的指向的数据,直到begin指针与end指针相遇,最后把基准值与begin与end相遇位置的数据交换即可。

递归左右子区重复第二步骤的操作达到有序。


2、3、2 左右指针法举例

 同样,我们先给出一个数组,如下:

 我们用上述的左右指针法的思想来实现一下:


  1. 选出一个基准直(数组最左边的值);
  2. 定义左右指针,依次往中间找;
  3. 递归左右子区间重复第二步骤达到有序。


2、3、3  左右指针法代码实现

#include<stdio.h>
void Print(int arr[], int n)
{
  int i = 0;
  for (i = 0; i < n; i++)
  {
    printf("%d ", arr[i]);
  }
}
void Swap(int* p1, int* p2)
{
  int tmp = *p1;
  *p1 = *p2;
  *p2 = tmp;
}
void QuickSort(int arr[], int left, int right)
{
  if (left >= right)
    return;
  int begin = left;
  int end = right;
  int keyi = begin;
  while (begin < end)
  {
    while (begin < end && arr[end] >= arr[keyi])
    {
      end--;
    }
    while (begin < end && arr[begin] <= arr[keyi])
    {
      begin++;
    }
    Swap(&arr[begin], &arr[end]);
  }
  Swap(&arr[begin], &arr[keyi]);
  QuickSort(arr, left, begin - 1);
  QuickSort(arr, begin + 1, right);
}
void TestSort()
{
  int arr[] = { 3,5,1,2,7,6,4,5,8,0,9,-1 };
  int n = 12;
  QuickSort(arr, 0, n - 1);
  Print(arr,n);
}
int main()
{
  TestSort();
  return 0;
}

2、4 前后指针法

2、4、1 前后指针发实现思想


 我们先来了解一下前后指针法的主要思想:


先选出一个基准值,我们一般只会选择第一个数据作为基准值;

第一次排序:我们定义两个指针(前后指针)。初始时,前指针(prev指针)指向最左边的数据,后指针(cur指针)指向第二个数据。cur指针向右找比基准值小的数据,找到了就停下来。然后perv指针加一,cur指针与prev指针指向的数据交换。直到cur指针找到最右边停止。最后再把基准值与prev数据交换一下即可。

递归左右子区重复第二步骤的操作达到有序。


2、4、2  前后指针法举例

老样子,我们先给出一个数组,如下:

 我们用上述的前后指针法的思想来实现一下:

  1. 先选出一个基准值;
  2. 定义前后指针,进行第一次排序;


4fc16f10b72d499bb4ab9658860ac261.png


  1. 递归实现左右子序列。

2、4、3 前后指针代码实现

#include<stdio.h>
void Print(int arr[], int n)
{
  int i = 0;
  for (i = 0; i < n; i++)
  {
    printf("%d ", arr[i]);
  }
}
void Swap(int* p1, int* p2)
{
  int tmp = *p1;
  *p1 = *p2;
  *p2 = tmp;
}
void QuickSort(int arr[], int left, int right)
{
  int keyi = left;
  int prev = left;
  int cur = left + 1;
  while (cur <= right)
  {
    while (arr[cur] < arr[keyi] && ++prev != cur)
    {
      Swap(&arr[cur], &arr[prev]);
    }
    cur++;
  }
  Swap(&arr[keyi], &arr[prev]);
  QuickSort(arr, left, prev - 1);
  QuickSort(arr, prev + 1, right);
}
void TestSort()
{
  int arr[] = { 3,5,1,2,7,6,4,5,8,0,9,-1 };
  int n = 12;
  QuickSort(arr, 0, n - 1);
  Print(arr,n);
}
int main()
{
  TestSort();
  return 0;
}



 以上就是快速排序的三种不同的实现方法,但是实现的基本思想是大同小异的。接下来我们看一下归并排序

三、归并排序的思想及代码详解

 归并排序的思想与快速排序的思想有一点相似之处,但又有些不同。当我们理解了快速排序后,归并排序理解起来也就是跟简单了。我们来看一下归并排序的思想实现。



3、1 归并排序的思想实现


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


 我们同样将归并排序分为三大步骤:


找出数组个数的中间值,将数组非为左右子区间;

递归找到最小的左右区间;

依次将最小的左右(有序)区间进行归并操作得到有序数组。

 我们结合下面的举例图片更容易理解。

a0b1ea182e0848839d5b70f329a6b8f6.png



3、2 归并排序的代码实现

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++];
  }
  for (int i = left; i <= right; i++)
  {
    arr[i] = tmp[i];
  }
}
void TestSort()
{
  int arr[] = { 3,5,1,2,7,6,4,5,8,0,9,-1 };
  int n = 12;
  int tmp[12];
  QuickSort(arr, 0, n - 1,tmp);
  Print(arr,n);
}
int main()
{
  TestSort();
  return 0;
}



总结


 通过上面的学习,我们发现快速排序和归并排序都用了分治的思想来实现的。分支的思想就是把一个复杂的问题分解成很多相对较容易的问题,通过实现相对较容易的小问题后,一步一步返回完成复杂的大问题。 分支的思想在快速排序和归并排序中就得到了很好的体现。快速排序和归并排序在排序中相对来说是十分重要的,我们应该反复练习,达到熟能生巧的地步。


 快速排序和归并排序就讲到这里,希望本篇文章对你有所帮助,后续会持续更新堆排序的详解。


 感谢阅读ovo~


相关文章
|
1月前
|
算法
【算法】二分查找——在排序数组中查找元素的第一个和最后一个位置
【算法】二分查找——在排序数组中查找元素的第一个和最后一个位置
|
1月前
|
搜索推荐 算法 Java
现有一个接口DataOperation定义了排序方法sort(int[])和查找方法search(int[],int),已知类QuickSort的quickSort(int[])方法实现了快速排序算法
该博客文章通过UML类图和Java源码示例,展示了如何使用适配器模式将QuickSort类和BinarySearch类的排序和查找功能适配到DataOperation接口中,实现算法的解耦和复用。
17 1
现有一个接口DataOperation定义了排序方法sort(int[])和查找方法search(int[],int),已知类QuickSort的quickSort(int[])方法实现了快速排序算法
|
30天前
|
算法 搜索推荐 Java
算法实战:手写归并排序,让复杂排序变简单!
归并排序是一种基于“分治法”的经典算法,通过递归分割和合并数组,实现O(n log n)的高效排序。本文将通过Java手写代码,详细讲解归并排序的原理及实现,帮助你快速掌握这一实用算法。
37 0
|
1月前
|
算法 关系型数据库 MySQL
揭秘MySQL中的版本号排序:这个超级算法将颠覆你的排序世界!
【8月更文挑战第8天】在软件开发与数据管理中,正确排序版本号对软件更新及数据分析至关重要。因MySQL默认按字符串排序版本号,可能出现&#39;1.20.0&#39;在&#39;1.10.0&#39;之前的不合理情况。解决办法是将版本号各部分转换为整数后排序。例如,使用`SUBSTRING_INDEX`和`CAST`函数从`software`表的`version`字段提取并转换版本号,再按这些整数排序。这种方法可确保版本号按逻辑正确排序,适用于&#39;major.minor.patch&#39;格式的版本号。对于更复杂格式,需调整处理逻辑。掌握此技巧可有效应对版本号排序需求。
86 3
|
30天前
|
数据采集 搜索推荐 算法
【高手进阶】Java排序算法:从零到精通——揭秘冒泡、快速、归并排序的原理与实战应用,让你的代码效率飙升!
【8月更文挑战第21天】Java排序算法是编程基础的重要部分,在算法设计与分析及实际开发中不可或缺。本文介绍内部排序算法,包括简单的冒泡排序及其逐步优化至高效的快速排序和稳定的归并排序,并提供了每种算法的Java实现示例。此外,还探讨了排序算法在电子商务、搜索引擎和数据分析等领域的广泛应用,帮助读者更好地理解和应用这些算法。
21 0
|
1月前
|
算法 搜索推荐
算法设计 (分治法应用实验报告)基于分治法的合并排序、快速排序、最近对问题
这篇文章是关于分治法应用的实验报告,详细介绍了如何利用分治法实现合并排序和快速排序算法,并探讨了使用分治法解决二维平面上的最近对问题的方法,包括伪代码、源代码实现及时间效率分析,并附有运行结果和小结。
|
1月前
|
存储 算法 Java
LeetCode初级算法题:反转链表+统计N以内的素数+删除排序数组中的重复项Java详解
LeetCode初级算法题:反转链表+统计N以内的素数+删除排序数组中的重复项Java详解
18 0
|
2月前
|
存储 算法 搜索推荐
算法进阶之路:Python 归并排序深度剖析,让数据排序变得艺术起来!
【7月更文挑战第12天】归并排序是高效稳定的排序算法,采用分治策略。Python 实现包括递归地分割数组及合并已排序部分。示例代码展示了如何将 `[12, 11, 13, 5, 6]` 分割并归并成有序数组 `[5, 6, 11, 12, 13]`。虽然 $O(n log n)$ 时间复杂度优秀,但需额外空间,适合大规模数据排序。对于小规模数据,可考虑其他算法。**
65 4
|
2月前
|
算法 搜索推荐 编译器
算法高手养成记:Python快速排序的深度优化与实战案例分析
【7月更文挑战第11天】快速排序是编程基础,以O(n log n)时间复杂度和原址排序著称。其核心是“分而治之”,通过选择基准元素分割数组并递归排序两部分。优化包括:选择中位数作基准、尾递归优化、小数组用简单排序。以下是一个考虑优化的Python实现片段,展示了随机基准选择。通过实践和优化,能提升算法技能。**
47 3
|
2月前
|
算法 搜索推荐 C#