快排&超详细,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);
}

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

加油!

目录
相关文章
|
1月前
|
算法
Leetcode 初级算法 --- 数组篇
Leetcode 初级算法 --- 数组篇
38 0
|
1月前
|
程序员 C语言
【C语言】LeetCode(力扣)上经典题目
【C语言】LeetCode(力扣)上经典题目
|
1月前
【LeetCode-每日一题】 删除排序数组中的重复项
【LeetCode-每日一题】 删除排序数组中的重复项
20 4
|
1月前
|
索引
Leetcode第三十三题(搜索旋转排序数组)
这篇文章介绍了解决LeetCode第33题“搜索旋转排序数组”的方法,该问题要求在旋转过的升序数组中找到给定目标值的索引,如果存在则返回索引,否则返回-1,文章提供了一个时间复杂度为O(logn)的二分搜索算法实现。
19 0
Leetcode第三十三题(搜索旋转排序数组)
|
1月前
|
算法 C++
Leetcode第53题(最大子数组和)
这篇文章介绍了LeetCode第53题“最大子数组和”的动态规划解法,提供了详细的状态转移方程和C++代码实现,并讨论了其他算法如贪心、分治、改进动态规划和分块累计法。
60 0
|
1月前
|
C++
【LeetCode 12】349.两个数组的交集
【LeetCode 12】349.两个数组的交集
17 0
|
3月前
|
存储 算法
LeetCode第83题删除排序链表中的重复元素
文章介绍了LeetCode第83题"删除排序链表中的重复元素"的解法,使用双指针技术在原链表上原地删除重复元素,提供了一种时间和空间效率都较高的解决方案。
LeetCode第83题删除排序链表中的重复元素
|
2月前
|
SQL Oracle 关系型数据库
CASE WHEN 语句的语法及示例,LeetCode 题目 “确认率” 练习
本文介绍了SQL中CASE语句的两种形式和语法,并通过LeetCode题目“确认率”的SQL查询示例展示了CASE语句在实际问题中的应用,解释了如何使用CASE语句计算特定条件的比率。
|
2月前
|
Unix Shell Linux
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
本文提供了几个Linux shell脚本编程问题的解决方案,包括转置文件内容、统计词频、验证有效电话号码和提取文件的第十行,每个问题都给出了至少一种实现方法。
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行