快排&超详细,Leetcode排序数组题目带你升华掌握(下)

简介: 快排&超详细,Leetcode排序数组题目带你升华掌握(上)

优化

借助题目:排序数组

给一个数组,要求给他排序,要求很简单,却只有50%的通过率,力扣标记简单不一定简单,标记中等那一定是有点难。

这题很显然,普通的排序比如冒泡排序,插入排序,选择排序是过不了的,我们刚刚学习了快排,何不尝试一波。

void Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}
 void QuickSort1(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  int keyi = PartSort1(a, begin, end);
  QuickSort1(a, begin, keyi - 1);
  QuickSort1(a, keyi + 1, end);
}
//hoare版本
int PartSort1(int* a, int left, int right)
{
  int keyi = left;
  while (left < right)
  {
    while (left < right && a[right] >= a[keyi])
    {
      --right;
    }
    //找大
    while (left < right && a[left] <= a[keyi])
    {
      ++left;
    }
    Swap(&a[left], &a[right]);
  }
  Swap(&a[keyi], &a[left]);
  return left;
}
int* sortArray(int* nums, int numsSize, int* returnSize){
    QuickSort1(nums, 0, numsSize-1);
    *returnSize=numsSize;
    return nums;
}

快排解题的代码如下

然而却发现

 用hoare大佬的方法走一遍,发现时间复杂度在这组用例上为O(N^2),right一直向右移,不会分成左右两组,一直递归n次,right向前的次数从n到1,这样的话花费的时间太多了,针对这种情况进行优化,就要介绍三数取中。

三数取中

如何防止上面最坏的情况发生?

只需要防止最左边的数为该组数里最小的数即可。

 可以找出left位置的值和right位置的值及(left+right)/2数组中间下标的值,找出他们三个中间大小的一个,与a[left]进行交换,这样就可以防止left是数组中最小的元素这种情况的发生。

将三数取中抽离成函数

//三数取中
int GetMidi(int* a, int left, int right)
{
  int mid = (left + right) / 2;
  //left mid right
  if (a[left] < a[mid])
  {
    if (a[mid] < a[right])
    {
      return mid;
    }
    else if (a[left] > a[right])
    {
      return left;
    }
    else
    {
      return right;
    }
  }
  else
  {
    if (a[mid] > a[right])
    {
      return mid;
    }
    else if (a[left] > a[right])
    {
      return left;
    }
    else
    {
      return right;
    }
  }
}

在每次partsort前搞来中间值的坐标,与left进行交换

int midi=GetMidi(a,left,right)

Swap(&a[left[,&a[midi]);

加上三数取中再次运行

然而。。。。

 再次出乎意料,如果是全部数字都一样或者有大量重复数据,right还是像刚才有序数组的情况一样,right一直向左,时间复杂度为O(N^2),此时三数取中也没用了,无论怎么取,取到的都是2。针对这种情况怎么办?

三路分化

将hoare大佬的方法和双指针法结合起来,将数组分为三个部分,左边小于key,中间的区段是等于key的,右边的部分是大于key的。

设置三个指针,两个指针用于位置的交换是数组划分为三个区间,一个指针用于遍历。

看思路

最后在left和right之间的部分就是等于5的部分,left之前皆小于key,right之后皆大于key。

总结下来只有以下三种情况

  1. cur的值小于key,交换cur的值和left的值,cur++,left++。
  2. cur的值等于key,直接++cur
  3. cur的值大于key,交换cur和right的值,cur不能动。

代码实现

void QuickSort1(int* a, int left, int right)
{
  if (left >= right)
  {
    return;
  }
  int begin = left;//记录左右边界
  int end = right;
  int midi = GetMidi(a, left, right);
  Swap(&a[left], &a[midi]);
  int key = a[left];
  int cur = left + 1;
  while (cur <= right)
  {
    if (a[cur] < key)//重点理解
    {
      Swap(&a[cur], &a[left]);
      ++left;
      ++cur;
    }
    else if (a[cur] > key)
    {
      Swap(&a[cur], &a[right]);
    }
    else
    {
      ++cur;
    }
  }
  QuickSort1(a, begin, left-1);
  QuickSort1(a, right+1, end);
}

这次我们信心满满,如果数组的元素全部为2的话,遍历一遍,right和left直接相遇,甚至只需要O(N)的时间就可以完成。

更改代码后运行。

 还是超出时间限制,这又是为什么呢?这道题对快速排序做了针对性的处理,普通的快排很难过这道题,他会检测我们的三数取中,然后搞出对应的一串数字,让每次取出的数都是数组中倒数第二大,这样的话时间复杂度还是很大。

解决方法:

 选取数组中任意位置的数和left位置和right作比较选出keyi,而不是一直取中间坐标与他们相比较,用rand函数进行操作,使我们选数位置没有规律可循,这样编译器就不能根据我们所写的代码搞出针对性用例。

代码如下:

int GetMidi(int* a, int left, int right)
{
  //int mid = (left + right) / 2;
  int mid =left+(rand()%(right-left));
  //left mid right
  if (a[left] < a[mid])
  {
    if (a[mid] < a[right])
    {
      return mid;
    }
    else if (a[left] > a[right])
    {
      return left;
    }
    else
    {
      return right;
    }
  }
  else
  {
    if (a[mid] > a[right])
    {
      return mid;
    }
    else if (a[left] > a[right])
    {
      return left;
    }
    else
    {
      return right;
    }
  }
}
void Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}
void QuickSort1(int* a, int left, int right)
{
  if (left >= right)
  {
    return;
  }
  int begin = left;//记录左右边界
  int end = right;
  int midi = GetMidi(a, left, right);
  Swap(&a[left], &a[midi]);
  int key = a[left];
  int cur = left + 1;
  while (cur <= right)
  {
    if (a[cur] < key)
    {
      Swap(&a[cur], &a[left]);
      ++left;
      ++cur;
    }
    else if (a[cur] > key)
    {
      Swap(&a[cur], &a[right]);
      --right;
    }
    else
    {
      ++cur;
    }
  }
  QuickSort1(a, begin, left-1);
  QuickSort1(a, right+1, end);
}
int* sortArray(int* nums, int numsSize, int* returnSize){
  srand(time(NULL));
    QuickSort1(nums, 0, numsSize-1);
    *returnSize=numsSize;
    return nums;
}

运行后如图

可以发现所用时间还是很长,这道题如果用堆排序和shell排序都是很好过的,放在这里是为了提升我们对快排的理解和掌握。

小区间优化

 在上边显示递归流程的图中,我们可以看到递归的过程,类比树的结构,树的每一个节点都是一次递归,树的最后一排的节点个数占全部节点的50%,倒数第二行占总个数的25%,如果分成的数组的量的变小到一定的个数的时候可以不再使用递归,而是选择其他的方法进行排序,可以减少大量的递归次数。

这种方法叫做小区间优化,使用什么排序比较好呢?冒泡排序和选择排序的时间复杂度为O(N²),选择排序的时间复杂度最高为O(N²),可以使用选择排序进行小区间优化,从而减少大量的递归次数

插入排序及小区间优化后的代码如下

//插入排序
void InsertSort(int* a, int n)
{
  //[0,end]有序,把end+1的位置插入到前序序列
  //控制[0,end+1]有序
  for (int i = 0; i < n - 1; i++)
  {
    int end = i;
    int tmp = a[end + 1];
    while (end >= 0)
    {
      if (tmp < a[end])
      {
        a[end + 1] = a[end];
      }
      else
      {
        break;
      }-+
      --end;
    }
    a[end + 1] = tmp;
  }
}
//加上小区间优化
void QuickSort2(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  if ((end - begin + 1) > 10)//如果一个数组的元素小于10,就进行插入排序
  {
    int keyi = PartSort2(a, begin, end);
    QuickSort2(a, begin, keyi - 1);
    QuickSort2(a, keyi + 1, end);
  }
  else
  {
    InsertSort(a + begin, end - begin + 1);
  }
}
//加上小区间优化
void QuickSort2(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  if ((end - begin + 1) > 10)
  {
    int keyi = PartSort2(a, begin, end);
    QuickSort2(a, begin, keyi - 1);
    QuickSort2(a, keyi + 1, end);
  }
  else
  {
    InsertSort(a + begin, end - begin + 1);
  }
}

快排非递归版

复习一下:它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归 进行,以此达到整个数据变成有序序列。

将一整个数组不断细分,在不断划分的过程中进行有序化,最终完成排序的功能。在前边partsort函数中,我们只要通过某种方法得到分成一组一组的小数组段的left和right即可。

上边的说明递归的流程图是直接全部展开的,实际上是一直向左划分,直到分出的数组left和right相等,然后回到上步。

如图所示

结合代码就很容易明白。

在非递归版本我们想和递归的步骤相同,如何得到上述递归过程中的left和right?这是问题之所在,而且在递归过程中向partsort1传入不同的left和right。

可以借助栈先进先出的功能来实现,push起始的left和right,进行第一次partsort,知道了left和right后,将他们再pop掉。

void QuickSortNonR(int* a, int begin, int end)
{
  ST st;
  STInit(&st);
  STPush(&st, end);
  STPush(&st, begin);
  while (!STEmpty(&st))
  {
    int left = STTop(&st);
    STPop(&st);
    int right = STTop(&st);
    STPop(&st);
    int keyi = PartSort1(a, left, right);
    if (keyi + 1 < right)
    {
      STPush(&st, right);
      STPush(&st, keyi + 1);
    }
    if (left < keyi - 1)
    {
      STPush(&st, keyi - 1);
      STPush(&st, left);
    }
  }
  STDestroy(&st);
}

快排讲到这里就结束啦,如果你有耐心看完的话你一定会对快排的掌握有所提升哒。

加油!

目录
相关文章
|
2月前
|
算法
LeetCode第53题最大子数组和
LeetCode第53题"最大子数组和"的解题方法,利用动态规划思想,通过一次遍历数组,维护到当前元素为止的最大子数组和,有效避免了复杂度更高的暴力解法。
LeetCode第53题最大子数组和
LeetCode------找到所有数组中消失的数字(6)【数组】
这篇文章介绍了LeetCode上的"找到所有数组中消失的数字"问题,提供了一种解法,通过两次遍历来找出所有未在数组中出现的数字:第一次遍历将数组中的每个数字对应位置的值增加数组长度,第二次遍历找出所有未被增加的数字,即缺失的数字。
|
2月前
|
存储 算法
LeetCode第83题删除排序链表中的重复元素
文章介绍了LeetCode第83题"删除排序链表中的重复元素"的解法,使用双指针技术在原链表上原地删除重复元素,提供了一种时间和空间效率都较高的解决方案。
LeetCode第83题删除排序链表中的重复元素
|
2月前
|
算法
LeetCode第81题搜索旋转排序数组 II
文章讲解了LeetCode第81题"搜索旋转排序数组 II"的解法,通过二分查找算法并加入去重逻辑来解决在旋转且含有重复元素的数组中搜索特定值的问题。
LeetCode第81题搜索旋转排序数组 II
|
7天前
|
SQL Oracle 关系型数据库
CASE WHEN 语句的语法及示例,LeetCode 题目 “确认率” 练习
本文介绍了SQL中CASE语句的两种形式和语法,并通过LeetCode题目“确认率”的SQL查询示例展示了CASE语句在实际问题中的应用,解释了如何使用CASE语句计算特定条件的比率。
|
2月前
|
算法 索引
LeetCode第34题在排序数组中查找元素的第一个和最后一个位置
这篇文章介绍了LeetCode第34题"在排序数组中查找元素的第一个和最后一个位置"的解题方法,通过使用双指针法从数组两端向中间同时查找目标值,有效地找到了目标值的首次和最后一次出现的索引位置。
LeetCode第34题在排序数组中查找元素的第一个和最后一个位置
|
2月前
|
算法
LeetCode第33题搜索旋转排序数组
这篇文章介绍了LeetCode第33题"搜索旋转排序数组"的解题方法,通过使用二分查找法并根据数组的有序性质调整搜索范围,实现了时间复杂度为O(log n)的高效搜索算法。
LeetCode第33题搜索旋转排序数组
|
2月前
|
算法
LeetCode第12题目整数转罗马数字
该文章介绍了 LeetCode 第 12 题整数转罗马数字的解法,通过使用 TreeMap 按照整数从大到小排序,先使用大的罗马数字表示整数,再用小的,核心是先表示完大的罗马数字,想通此点该题较简单。
LeetCode第12题目整数转罗马数字
|
2月前
|
算法
LeetCode第13题目罗马数字转整数
该文章介绍了 LeetCode 第 13 题罗马数字转整数的解法,通过从大到小解析罗马数字,根据罗马数字的特点,按照从大到小的顺序匹配罗马数字和整数的关系,从而解决该问题,同时强调要注意观察题目考查的知识点特征。
|
6天前
|
Unix Shell Linux
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
本文提供了几个Linux shell脚本编程问题的解决方案,包括转置文件内容、统计词频、验证有效电话号码和提取文件的第十行,每个问题都给出了至少一种实现方法。
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行